diff --git a/.env.example b/.env.example index 0f743466..40a3d2df 100644 --- a/.env.example +++ b/.env.example @@ -107,6 +107,10 @@ NEXT_PUBLIC_API_BASE_EXTERNAL= # [Optional] Direct API base URL (alternative to above) NEXT_PUBLIC_API_BASE= +# [Optional] Application Base Path (if deployed under a subpath) +# Example: /deep-tutor +NEXT_PUBLIC_APP_BASE_PATH= + # ============================================================================== # Debug & Development # ============================================================================== diff --git a/.env.example_CN b/.env.example_CN index 8a6dae19..af011a12 100644 --- a/.env.example_CN +++ b/.env.example_CN @@ -106,7 +106,9 @@ NEXT_PUBLIC_API_BASE_EXTERNAL= # [可选] 直接 API 基础地址(上述配置的替代方案) NEXT_PUBLIC_API_BASE= - +# [可选] 应用基础路径(如果部署在子路径下) +# 示例:/deep-tutor +NEXT_PUBLIC_APP_BASE_PATH= # ============================================================================== # 调试与开发配置 # ============================================================================== diff --git a/Dockerfile b/Dockerfile index 036c487d..069d0778 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,7 @@ COPY web/ ./ # Create .env.local with placeholder that will be replaced at runtime # Use a unique placeholder that can be safely replaced RUN echo "NEXT_PUBLIC_API_BASE=__NEXT_PUBLIC_API_BASE_PLACEHOLDER__" > .env.local +RUN echo "NEXT_PUBLIC_APP_BASE_PATH=/__APP_BASE_PATH_PLACEHOLDER__" >> .env.local # Build Next.js for production with standalone output # This allows runtime environment variable injection @@ -236,15 +237,48 @@ else echo "[Frontend] Example: -e NEXT_PUBLIC_API_BASE_EXTERNAL=https://your-server.com:${BACKEND_PORT}" fi +# Get App Base Path +APP_BASE_PATH=${NEXT_PUBLIC_APP_BASE_PATH:-} +# Remove trailing slash if present +APP_BASE_PATH=${APP_BASE_PATH%/} +if [ -n "$APP_BASE_PATH" ]; then + echo "[Frontend] 📌 Using App Base Path: ${APP_BASE_PATH}" +fi + echo "[Frontend] 🚀 Starting Next.js frontend on port ${FRONTEND_PORT}..." # Replace placeholder in built Next.js files # This is necessary because NEXT_PUBLIC_* vars are inlined at build time -find /app/web/.next -type f \( -name "*.js" -o -name "*.json" \) -exec \ - sed -i "s|__NEXT_PUBLIC_API_BASE_PLACEHOLDER__|${API_BASE}|g" {} \; 2>/dev/null || true +# We must include html and css files as they may contain hardcoded paths +# Use grep first to speed up and ensure we only touch files with the placeholder +echo "[Frontend] 🔍 Scanning and replacing placeholders..." + +# 1. Replace API Base +grep -lR "__NEXT_PUBLIC_API_BASE_PLACEHOLDER__" /app/web/.next | while read file; do + sed -i "s|__NEXT_PUBLIC_API_BASE_PLACEHOLDER__|${API_BASE}|g" "$file" +done + +# 2. Replace App Base Path +# We need to handle the leading slash in the placeholder /__APP_BASE_PATH_PLACEHOLDER__ +if [ -z "$APP_BASE_PATH" ]; then + # If empty, replace placeholder with empty string + echo "[Frontend] 🧹 Removing base path placeholder..." + grep -lR "/__APP_BASE_PATH_PLACEHOLDER__" /app/web/.next | while read file; do + sed -i "s|/__APP_BASE_PATH_PLACEHOLDER__||g" "$file" + done +else + # If set, replace placeholder with actual path + echo "[Frontend] 🔄 Replacing base path placeholder with ${APP_BASE_PATH}..." + grep -lR "/__APP_BASE_PATH_PLACEHOLDER__" /app/web/.next | while read file; do + sed -i "s|/__APP_BASE_PATH_PLACEHOLDER__|${APP_BASE_PATH}|g" "$file" + done +fi # Also update .env.local for any runtime reads echo "NEXT_PUBLIC_API_BASE=${API_BASE}" > /app/web/.env.local +if [ -n "$APP_BASE_PATH" ]; then + echo "NEXT_PUBLIC_APP_BASE_PATH=${APP_BASE_PATH}" >> /app/web/.env.local +fi # Start Next.js cd /app/web && exec node node_modules/next/dist/bin/next start -H 0.0.0.0 -p ${FRONTEND_PORT} diff --git a/docker-compose.yml b/docker-compose.yml index b7a3c0bb..88249759 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,6 +74,8 @@ services: - NEXT_PUBLIC_API_BASE_EXTERNAL=${NEXT_PUBLIC_API_BASE_EXTERNAL:-} # Alternative: Direct API base URL (same priority as NEXT_PUBLIC_API_BASE_EXTERNAL) - NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE:-} + # Application base path (if deployed in a subdirectory) + - NEXT_PUBLIC_APP_BASE_PATH=${NEXT_PUBLIC_APP_BASE_PATH:-} volumes: # Mount local config directory (read-only) diff --git a/scripts/start_web.py b/scripts/start_web.py index dd5ba6b6..89a19a12 100644 --- a/scripts/start_web.py +++ b/scripts/start_web.py @@ -329,6 +329,11 @@ def start_frontend(): print_flush(f"📌 Using default API URL: {api_base_url}") print_flush(" 💡 For remote access, set NEXT_PUBLIC_API_BASE in .env file") + # Get App Base Path + app_base_path = os.environ.get("NEXT_PUBLIC_APP_BASE_PATH", "") + if app_base_path: + print_flush(f"📌 Using App Base Path: {app_base_path}") + # Generate/update .env.local file with port configuration # This ensures Next.js can read the backend port even if environment variables are not passed env_local_path = os.path.join(web_dir, ".env.local") @@ -346,6 +351,7 @@ def start_frontend(): f.write("# NEXT_PUBLIC_API_BASE=http://your-server-ip:8001\n") f.write("# ============================================\n\n") f.write(f"NEXT_PUBLIC_API_BASE={api_base_url}\n") + f.write(f"NEXT_PUBLIC_APP_BASE_PATH={app_base_path}\n") print_flush(f"✅ Updated .env.local with API base: {api_base_url}") except Exception as e: print_flush(f"⚠️ Warning: Failed to update .env.local: {e}") @@ -355,6 +361,9 @@ def start_frontend(): env = os.environ.copy() env["PORT"] = str(frontend_port) env["NEXT_PUBLIC_API_BASE"] = api_base_url + if app_base_path: + env["NEXT_PUBLIC_APP_BASE_PATH"] = app_base_path + # Set encoding environment variables for Windows env["PYTHONIOENCODING"] = "utf-8" env["PYTHONUTF8"] = "1" diff --git a/web/.env.local b/web/.env.local index 8840a64d..7aa4e9ff 100644 --- a/web/.env.local +++ b/web/.env.local @@ -9,3 +9,4 @@ # ============================================ NEXT_PUBLIC_API_BASE=http://localhost:8001 +NEXT_PUBLIC_APP_BASE_PATH= \ No newline at end of file diff --git a/web/components/Sidebar.tsx b/web/components/Sidebar.tsx index 0fd8fe4a..b56f1aa6 100644 --- a/web/components/Sidebar.tsx +++ b/web/components/Sidebar.tsx @@ -27,6 +27,7 @@ import { LucideIcon, } from "lucide-react"; import { useGlobal } from "@/context/GlobalContext"; +import { appPath } from "@/lib/path"; const SIDEBAR_EXPANDED_WIDTH = 256; const SIDEBAR_COLLAPSED_WIDTH = 64; @@ -228,7 +229,7 @@ export default function Sidebar() {
{t("DeepTutor { + if (typeof window !== 'undefined') { + return process.env.NEXT_PUBLIC_APP_BASE_PATH?.replace(/\/$/, '') || ''; + } + return process.env.NEXT_PUBLIC_APP_BASE_PATH?.replace(/\/$/, '') || ''; +}; + +export const appPath = (path: string) => { + const basePath = getBasePath(); + if (!path) return basePath; + + // If path is absolute URL, return as is + if (path.startsWith('http://') || path.startsWith('https://')) { + return path; + } + + const cleanPath = path.startsWith('/') ? path : `/${path}`; + + if (!basePath) return cleanPath; + + return `${basePath}${cleanPath}`; +}; diff --git a/web/next.config.js b/web/next.config.js index 67c6891c..5ecf93d7 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -1,34 +1,39 @@ /** @type {import('next').NextConfig} */ +// Ensure basePath doesn't have a trailing slash, as Next.js doesn't like it +const basePath = process.env.NEXT_PUBLIC_APP_BASE_PATH?.replace(/\/$/, '') + +if (basePath) { + console.log(`> [DeepTutor] Applying basePath: ${basePath}`) +} + const nextConfig = { + ...(basePath ? { basePath } : {}), // Move dev indicator to bottom-right corner devIndicators: { - position: "bottom-right", + position: 'bottom-right', }, // Transpile mermaid and related packages for proper ESM handling - transpilePackages: ["mermaid"], + transpilePackages: ['mermaid'], // Turbopack configuration (Next.js 16+ uses Turbopack by default for dev) turbopack: { resolveAlias: { // Fix for mermaid's cytoscape dependency - use CJS version - cytoscape: "cytoscape/dist/cytoscape.cjs.js", + cytoscape: 'cytoscape/dist/cytoscape.cjs.js', }, }, // Webpack configuration (used for production builds - next build) - webpack: (config) => { - const path = require("path"); + webpack: config => { + const path = require('path') config.resolve.alias = { ...config.resolve.alias, - cytoscape: path.resolve( - __dirname, - "node_modules/cytoscape/dist/cytoscape.cjs.js", - ), - }; - return config; + cytoscape: path.resolve(__dirname, 'node_modules/cytoscape/dist/cytoscape.cjs.js'), + } + return config }, -}; +} -module.exports = nextConfig; +module.exports = nextConfig