Prevents baking self-proxy /api and /chat rewrites that pin a CPU core inside Docker.
245 lines
8.5 KiB
JavaScript
245 lines
8.5 KiB
JavaScript
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.
|
|
*
|
|
* Never point API_PROXY_TARGET at loopback in Docker/production: inside the
|
|
* container 127.0.0.1:3000 is this Next process, so /api and /chat rewrites
|
|
* become a self-proxy CPU loop. Leave empty when the browser uses a public
|
|
* NEXT_PUBLIC_API_URL, or use the backend service hostname on a shared network.
|
|
*/
|
|
const resolveApiProxyTarget = (raw) => {
|
|
const trimmed = (raw || '').trim().replace(/\/+$/, '')
|
|
if (!trimmed) return ''
|
|
|
|
let hostname = ''
|
|
try {
|
|
hostname = new URL(trimmed).hostname
|
|
} catch {
|
|
console.warn(`[next.config] Ignoring invalid API_PROXY_TARGET=${JSON.stringify(raw)}`)
|
|
return ''
|
|
}
|
|
|
|
const isLoopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
|
|
if (isLoopback) {
|
|
console.warn(
|
|
`[next.config] Ignoring loopback API_PROXY_TARGET=${trimmed} (points at this Next process in Docker and causes a CPU loop)`
|
|
)
|
|
return ''
|
|
}
|
|
|
|
return trimmed
|
|
}
|
|
|
|
const apiProxyTarget = resolveApiProxyTarget(process.env.API_PROXY_TARGET)
|
|
|
|
/** 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: '/chat',
|
|
destination: `${apiProxyTarget}/chat`,
|
|
},
|
|
{
|
|
source: '/chat/:path*',
|
|
destination: `${apiProxyTarget}/chat/: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,
|
|
},
|
|
})
|