const packageJson = require('./package.json') const { withSentryConfig } = require('@sentry/nextjs') const isProduction = process.env.NODE_ENV === 'production' /** * When the browser talks to the API on a different host than the Next app * (LAN Nest, or https://ghabilee.ir from localhost), CORS / SameSite cookies * break. Point NEXT_PUBLIC_API_URL at this Next origin (e.g. * http://localhost:3008/api/v1) and set API_PROXY_TARGET to the real backend * so /api/v1 stays same-origin for the browser. * * Nest still names/flags the httpOnly refresh cookie from *its* NODE_ENV * (`__Host-ghabilee_refresh`+Secure in production, `ghabilee_refresh` on * local HTTP). Rewriting local HTTP Next to production Nest can therefore * emit a Secure `__Host-` cookie the browser may reject. Prefer local Nest * when testing login/refresh. See docs/reference/environment-variables.md. */ const apiProxyTarget = (process.env.API_PROXY_TARGET || '').replace(/\/+$/, '') /** Path prefix for reverse-proxy deploys (e.g. `/ghabilee`). Empty = site root. */ const rawBasePath = (process.env.NEXT_PUBLIC_BASE_PATH || '').trim() const basePath = rawBasePath.replace(/\/+$/, '') || undefined const swScope = basePath ? `${basePath}/` : '/' // Preserve the search equity of URLs that existed before the public SEO // routes were introduced. Specific article migrations must come before the // generic /blogs/:slug rule so removed articles land on the closest useful // replacement instead of a soft-404. const retiredCategorySlugRedirects = [ ['psychology', 'learning-experience'], ['book-reading', 'conversation-connection'], ['open-conversation', 'conversation-connection'], ['walking', 'sports-adventure'], ['pottery', 'learning-experience'], ['games', 'games-entertainment'], ['educational-workshop', 'learning-experience'], ['art-creativity', 'arts-culture'], ['health-wellness', 'sports-adventure'], ['film-documentary', 'arts-culture'], ['daily-challenges', 'conversation-connection'], ['philosophy-thinking', 'conversation-connection'], ['experience-sharing', 'conversation-connection'], ['futures-studies', 'conversation-connection'], ['board-games', 'games-entertainment'], ['active-team-games', 'games-entertainment'], ['puzzle-mind-games', 'games-entertainment'], ['fun-sensory-games', 'games-entertainment'], ].flatMap(([from, to]) => [ { source: `/category/${from}`, destination: `/category/${to}`, permanent: true }, { source: `/category/${from}/:path*`, destination: `/category/${to}/:path*`, permanent: true }, ]) const retiredCitySlugRedirects = [ ['thran', 'tehran'], ['mshd', 'mashhad'], ['asfhan', 'isfahan'], ['syraz', 'shiraz'], ['tbryz', 'tabriz'], ].flatMap(([from, to]) => [ { source: `/city/${from}`, destination: `/city/${to}`, permanent: true }, { source: `/category/:categorySlug/city/${from}`, destination: `/category/:categorySlug/city/${to}`, permanent: true }, ]) const legacySeoRedirects = [ { source: '/karagah-ravanshenasi', destination: '/category/learning-experience', permanent: true }, { source: '/varzesh-goroohi', destination: '/category/sports-adventure', permanent: true }, { source: '/honar-va-khalaghiat', destination: '/category/arts-culture', permanent: true }, { source: '/bazi-va-sargarmi', destination: '/category/games-entertainment', permanent: true }, { source: '/shabake-sazi-ejtemai', destination: '/category/conversation-connection', permanent: true }, { source: '/blogs/goroohhaye-piyaderoyi-iran', destination: '/blog/hiking-outdoor-group-guide', permanent: true, }, { source: '/blogs/mazayaye-karagah-khodshanasi', destination: '/category/learning-experience', permanent: true }, { source: '/blogs', destination: '/blog', permanent: true }, { source: '/blogs/:slug', destination: '/blog/:slug', permanent: true }, ...retiredCategorySlugRedirects, ...retiredCitySlugRedirects, ] /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, logging: false, output: 'standalone', ...(basePath ? { basePath, assetPrefix: basePath } : {}), allowedDevOrigins: ['192.168.100.51', '192.168.100.52'], transpilePackages: ['mapir-react-component', 'react-mapbox-gl'], env: { NEXT_PUBLIC_MAP_API_KEY: process.env.MAP_API_KEY || process.env.NEXT_PUBLIC_MAP_API_KEY || '', NEXT_PUBLIC_APP_VERSION: packageJson.version, NEXT_PUBLIC_VAPID_PUBLIC_KEY: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || process.env.VAPID_PUBLIC_KEY || '', NEXT_PUBLIC_BASE_PATH: basePath || '', }, images: { remotePatterns: [ { protocol: 'https', hostname: 'ghabilee.ir', pathname: '/uploads/**', }, { protocol: 'https', hostname: 'cdn.ghabilee.ir', pathname: '/**', }, { protocol: 'https', hostname: 'file-dev.ghabilee.org', pathname: '/**', }, { protocol: 'http', hostname: '192.168.100.51', port: '3000', pathname: '/**', }, { protocol: 'http', hostname: '65.21.166.55', pathname: '/**', }, ], }, async redirects() { return legacySeoRedirects }, async rewrites() { if (!apiProxyTarget) return [] return [ { source: '/api/v1/:path*', destination: `${apiProxyTarget}/api/v1/:path*`, }, { source: '/socket.io/:path*', destination: `${apiProxyTarget}/socket.io/:path*`, }, ] }, async headers() { return [ { source: '/(.*)', headers: [ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'X-Frame-Options', value: 'DENY' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(self), payment=()' }, ...(isProduction ? [{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin' }] : []), ...(isProduction ? [{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }] : []), ], }, { source: '/sw.js', headers: [ { key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' }, { key: 'Service-Worker-Allowed', value: swScope }, ], }, { source: '/manifest.webmanifest', headers: [{ key: 'Cache-Control', value: 'public, max-age=0, must-revalidate' }], }, ] }, turbopack: { root: __dirname, }, } const sentryAuthToken = (process.env.SENTRY_AUTH_TOKEN || '').trim() module.exports = withSentryConfig(nextConfig, { org: process.env.SENTRY_ORG || undefined, project: process.env.SENTRY_PROJECT || undefined, authToken: sentryAuthToken || undefined, // Source maps upload only when a CI/build auth token is present. sourcemaps: { disable: !sentryAuthToken, }, silent: true, // Tunnel through our origin so ad-blockers don't drop browser events. tunnelRoute: '/monitoring', widenClientFileUpload: Boolean(sentryAuthToken), // Replay is intentionally disabled in sentry.client.config.ts. Remove its // browser-only helpers plus SDK debug statements while preserving tracing. bundleSizeOptimizations: { excludeDebugStatements: true, excludeReplayIframe: true, excludeReplayShadowDom: true, excludeReplayWorker: true, }, webpack: { treeshake: { excludeReplayCompressionWorker: true, excludeReplayIframe: true, excludeReplayShadowDOM: true, removeDebugLogging: true, }, automaticVercelMonitors: false, }, })