Support subdirectory deployment and fix API path handling (#311)

* feat: support subdirectory deployment (NEXT_PUBLIC_BASE_PATH)

* removed unwanted check and fix favicon issue

* Use getAssetUrl for manifest assets to avoid undefined NEXT_PUBLIC_BASE_PATH

* Add validation warning for NEXT_PUBLIC_BASE_PATH format

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
This commit is contained in:
Biki Kalita
2025-12-22 19:58:55 +05:30
committed by GitHub
parent 9e9ea10beb
commit 84959637db
10 changed files with 76 additions and 17 deletions

View File

@@ -1,24 +1,24 @@
import type { MetadataRoute } from "next"
import { getAssetUrl } from "@/lib/base-path"
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Next AI Draw.io",
short_name: "AIDraw.io",
description:
"Create AWS architecture diagrams, flowcharts, and technical diagrams using AI. Free online tool integrating draw.io with AI assistance for professional diagram creation.",
start_url: "/",
start_url: getAssetUrl("/"),
display: "standalone",
background_color: "#f9fafb",
theme_color: "#171d26",
icons: [
{
src: "/favicon-192x192.png",
src: getAssetUrl("/favicon-192x192.png"),
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/favicon-512x512.png",
src: getAssetUrl("/favicon-512x512.png"),
sizes: "512x512",
type: "image/png",
purpose: "any",

View File

@@ -9,6 +9,7 @@ import {
Zap,
} from "lucide-react"
import { useDictionary } from "@/hooks/use-dictionary"
import { getAssetUrl } from "@/lib/base-path"
interface ExampleCardProps {
icon: React.ReactNode
@@ -79,7 +80,7 @@ export default function ExamplePanel({
setInput("Replicate this flowchart.")
try {
const response = await fetch("/example.png")
const response = await fetch(getAssetUrl("/example.png"))
const blob = await response.blob()
const file = new File([blob], "example.png", { type: "image/png" })
setFiles([file])
@@ -92,7 +93,7 @@ export default function ExamplePanel({
setInput("Replicate this in aws style")
try {
const response = await fetch("/architecture.png")
const response = await fetch(getAssetUrl("/architecture.png"))
const blob = await response.blob()
const file = new File([blob], "architecture.png", {
type: "image/png",
@@ -107,7 +108,7 @@ export default function ExamplePanel({
setInput("Summarize this paper as a diagram")
try {
const response = await fetch("/chain-of-thought.txt")
const response = await fetch(getAssetUrl("/chain-of-thought.txt"))
const blob = await response.blob()
const file = new File([blob], "chain-of-thought.txt", {
type: "text/plain",

View File

@@ -27,6 +27,7 @@ import {
ReasoningTrigger,
} from "@/components/ai-elements/reasoning"
import { ScrollArea } from "@/components/ui/scroll-area"
import { getApiEndpoint } from "@/lib/base-path"
import {
applyDiagramOperations,
convertToLegalXml,
@@ -291,7 +292,7 @@ export function ChatMessageDisplay({
setFeedback((prev) => ({ ...prev, [messageId]: value }))
try {
await fetch("/api/log-feedback", {
await fetch(getApiEndpoint("/api/log-feedback"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({

View File

@@ -29,6 +29,7 @@ import {
import { useDiagram } from "@/contexts/diagram-context"
import { useDictionary } from "@/hooks/use-dictionary"
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
import { getApiEndpoint } from "@/lib/base-path"
import { findCachedResponse } from "@/lib/cached-responses"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
@@ -172,7 +173,7 @@ export default function ChatPanel({
// Check config on mount
useEffect(() => {
fetch("/api/config")
fetch(getApiEndpoint("/api/config"))
.then((res) => res.json())
.then((data) => {
setDailyRequestLimit(data.dailyRequestLimit || 0)
@@ -243,7 +244,7 @@ export default function ChatPanel({
setMessages,
} = useChat({
transport: new DefaultChatTransport({
api: "/api/chat",
api: getApiEndpoint("/api/chat"),
}),
async onToolCall({ toolCall }) {
if (DEBUG) {

View File

@@ -22,6 +22,7 @@ import {
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { useDictionary } from "@/hooks/use-dictionary"
import { getApiEndpoint } from "@/lib/base-path"
import { i18n, type Locale } from "@/lib/i18n/config"
const LANGUAGE_LABELS: Record<Locale, string> = {
@@ -77,7 +78,7 @@ function SettingsContent({
// Only fetch if not cached in localStorage
if (getStoredAccessCodeRequired() !== null) return
fetch("/api/config")
fetch(getApiEndpoint("/api/config"))
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
@@ -142,12 +143,15 @@ function SettingsContent({
setIsVerifying(true)
try {
const response = await fetch("/api/verify-access-code", {
const response = await fetch(
getApiEndpoint("/api/verify-access-code"),
{
method: "POST",
headers: {
"x-access-code": accessCode.trim(),
},
})
},
)
const data = await response.json()

View File

@@ -5,6 +5,7 @@ import { createContext, useContext, useEffect, useRef, useState } from "react"
import type { DrawIoEmbedRef } from "react-drawio"
import { STORAGE_DIAGRAM_XML_KEY } from "@/components/chat-panel"
import type { ExportFormat } from "@/components/save-dialog"
import { getApiEndpoint } from "@/lib/base-path"
import { extractDiagramXML, validateAndFixXml } from "../lib/utils"
interface DiagramContextType {
@@ -329,7 +330,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
sessionId?: string,
) => {
try {
await fetch("/api/log-save", {
await fetch(getApiEndpoint("/api/log-save"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename, format, sessionId }),

View File

@@ -7,6 +7,11 @@ services:
context: .
args:
- NEXT_PUBLIC_DRAWIO_BASE_URL=http://localhost:8080
# Uncomment below for subdirectory deployment
# - NEXT_PUBLIC_BASE_PATH=/nextaidrawio
ports: ["3000:3000"]
env_file: .env
environment:
# For subdirectory deployment, uncomment and set your path:
# NEXT_PUBLIC_BASE_PATH: /nextaidrawio
depends_on: [drawio]

View File

@@ -97,6 +97,12 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# NEXT_PUBLIC_DRAWIO_BASE_URL=https://embed.diagrams.net # Default: https://embed.diagrams.net
# Use this to point to a self-hosted draw.io instance
# Subdirectory Deployment (Optional)
# For deploying to a subdirectory (e.g., https://example.com/nextaidrawio)
# Set this to your subdirectory path with leading slash (e.g., /nextaidrawio)
# Leave empty for root deployment (default)
# NEXT_PUBLIC_BASE_PATH=/nextaidrawio
# PDF Input Feature (Optional)
# Enable PDF file upload to extract text and generate diagrams
# Enabled by default. Set to "false" to disable.

37
lib/base-path.ts Normal file
View File

@@ -0,0 +1,37 @@
/**
* Get the base path for API calls and static assets
* This is used for subdirectory deployment support
*
* Example: If deployed at https://example.com/nextaidrawio, this returns "/nextaidrawio"
* For root deployment, this returns ""
*
* Set NEXT_PUBLIC_BASE_PATH environment variable to your subdirectory path (e.g., /nextaidrawio)
*/
export function getBasePath(): string {
// Read from environment variable (must start with NEXT_PUBLIC_ to be available on client)
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""
if (basePath && !basePath.startsWith("/")) {
console.warn("NEXT_PUBLIC_BASE_PATH should start with /")
}
return basePath
}
/**
* Get full API endpoint URL
* @param endpoint - API endpoint path (e.g., "/api/chat", "/api/config")
* @returns Full API path with base path prefix
*/
export function getApiEndpoint(endpoint: string): string {
const basePath = getBasePath()
return `${basePath}${endpoint}`
}
/**
* Get full static asset URL
* @param assetPath - Asset path (e.g., "/example.png", "/chain-of-thought.txt")
* @returns Full asset path with base path prefix
*/
export function getAssetUrl(assetPath: string): string {
const basePath = getBasePath()
return `${basePath}${assetPath}`
}

View File

@@ -4,6 +4,9 @@ import packageJson from "./package.json"
const nextConfig: NextConfig = {
/* config options here */
output: "standalone",
// Support for subdirectory deployment (e.g., https://example.com/nextaidrawio)
// Set NEXT_PUBLIC_BASE_PATH environment variable to your subdirectory path (e.g., /nextaidrawio)
basePath: process.env.NEXT_PUBLIC_BASE_PATH || "",
env: {
APP_VERSION: packageJson.version,
},