/** @type {import('next').NextConfig} */ const nextConfig = { // Standalone output for optimized Docker/HF production builds output: "standalone", // Compress responses compress: true, // Image optimization images: { domains: ["github.com"], formats: ["image/avif", "image/webp"], }, // Security headers async headers() { const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const wsUrl = apiUrl ? apiUrl.replace(/^https/, "wss").replace(/^http/, "ws") : ""; // CSP connect-src: allow same-origin + explicit API URL if set const connectSrc = ["'self'", apiUrl, wsUrl] .filter(Boolean) .join(" "); return [ { source: "/(.*)", headers: [ { key: "X-Content-Type-Options", value: "nosniff" }, { key: "X-Frame-Options", value: "SAMEORIGIN" }, // HF embeds in iframe { key: "X-XSS-Protection", value: "1; mode=block" }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, { key: "Content-Security-Policy", value: [ "default-src 'self'", "script-src 'self' 'unsafe-eval' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https: blob:", "font-src 'self' data:", `connect-src ${connectSrc} ws: wss:`, "worker-src 'self' blob:", ].join("; "), }, ], }, ]; }, // API proxy rewrites // When NEXT_PUBLIC_API_URL is set: proxy to external backend // When empty (HF mode): Nginx handles /api/* routing internally async rewrites() { const apiUrl = process.env.NEXT_PUBLIC_API_URL; if (!apiUrl) { // HF mode: no rewrites needed — Nginx routes /api/* to FastAPI return []; } return [ { source: "/api/:path*", destination: `${apiUrl}/api/:path*`, }, ]; }, // Webpack optimizations webpack: (config, { isServer }) => { if (!isServer) { config.resolve.fallback = { ...config.resolve.fallback, fs: false, net: false, tls: false, }; } return config; }, }; export default nextConfig;