From 84959637db2b9eadf27759f9e20a0788a61633d2 Mon Sep 17 00:00:00 2001 From: Biki Kalita <86558912+Biki-dev@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:58:55 +0530 Subject: [PATCH] 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 --- app/manifest.ts | 8 +++---- components/chat-example-panel.tsx | 7 +++--- components/chat-message-display.tsx | 3 ++- components/chat-panel.tsx | 5 ++-- components/settings-dialog.tsx | 16 ++++++++----- contexts/diagram-context.tsx | 3 ++- docker-compose.yml | 5 ++++ env.example | 6 +++++ lib/base-path.ts | 37 +++++++++++++++++++++++++++++ next.config.ts | 3 +++ 10 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 lib/base-path.ts diff --git a/app/manifest.ts b/app/manifest.ts index 4716200..411107c 100644 --- a/app/manifest.ts +++ b/app/manifest.ts @@ -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", diff --git a/components/chat-example-panel.tsx b/components/chat-example-panel.tsx index f350825..984601d 100644 --- a/components/chat-example-panel.tsx +++ b/components/chat-example-panel.tsx @@ -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", diff --git a/components/chat-message-display.tsx b/components/chat-message-display.tsx index 58f2aaf..68d72ae 100644 --- a/components/chat-message-display.tsx +++ b/components/chat-message-display.tsx @@ -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({ diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 941769f..b7a1929 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -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) { diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx index 720e039..1d9f710 100644 --- a/components/settings-dialog.tsx +++ b/components/settings-dialog.tsx @@ -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 = { @@ -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", { - method: "POST", - headers: { - "x-access-code": accessCode.trim(), + const response = await fetch( + getApiEndpoint("/api/verify-access-code"), + { + method: "POST", + headers: { + "x-access-code": accessCode.trim(), + }, }, - }) + ) const data = await response.json() diff --git a/contexts/diagram-context.tsx b/contexts/diagram-context.tsx index 5563e8b..a43aeaf 100644 --- a/contexts/diagram-context.tsx +++ b/contexts/diagram-context.tsx @@ -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 }), diff --git a/docker-compose.yml b/docker-compose.yml index 84970bc..89e3a86 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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] diff --git a/env.example b/env.example index 4661bd0..bc03d32 100644 --- a/env.example +++ b/env.example @@ -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. diff --git a/lib/base-path.ts b/lib/base-path.ts new file mode 100644 index 0000000..61278c7 --- /dev/null +++ b/lib/base-path.ts @@ -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}` +} diff --git a/next.config.ts b/next.config.ts index a9579a2..e10ab30 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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, },