import type { NextConfig } from 'next'; import { OPTIMIZED_IMAGE_HOSTS } from './src/core/lib/image-hosts'; // Build-time invariant — fail loud if NEXT_PUBLIC_STORE_CURRENCY is missing // for a production build. Next.js inlines NEXT_PUBLIC_* into the client // bundle at `next build` time; once inlined, the value cannot be patched at // deploy time. A missing env here is the root cause of non-USD stores ending // up with `$5,096` baked into their HTML (and indexed by Googlebot). // // In dev (`next dev`) we only warn, so local-only experiments don't break. if (!process.env.NEXT_PUBLIC_STORE_CURRENCY) { const msg = 'NEXT_PUBLIC_STORE_CURRENCY is not set.\n' + 'Next.js inlines NEXT_PUBLIC_* vars into the client bundle at build time;\n' + 'a missing value will silently fall back to USD in storefront price displays\n' + 'and ship that to search-engine crawlers. Set it in .env.local (written by\n' + 'create-brainerce-store) or in your CI / hosting build env (Coolify / Vercel).'; if (process.env.NODE_ENV === 'production') { throw new Error(`[next.config] ${msg}`); } else { console.warn(`[next.config] warning: ${msg}`); } } // Advisory only — NEVER throw here. A missing site URL is normal and expected: // AI builders (ChatGPT, Lovable, Bolt) and container hosts build the project // before the deploy domain exists, and the storefront resolves its own origin // from the request at runtime (src/core/lib/site-url.ts). Failing the build // would break exactly the flow this exists to support. The loud signal lives // at runtime instead, where a genuinely unresolvable origin is detectable. if (!process.env.SITE_URL && !process.env.NEXT_PUBLIC_SITE_URL) { console.warn( [ '[next.config] SITE_URL is not set. Canonical URLs, sitemap.xml and robots.txt', 'will be derived from each request (forwarded host), which works but is', 'request-dependent. Set SITE_URL=https://your-domain.com in your hosting', "provider's environment variables once your domain is known.", ].join('\n') ); } const nextConfig: NextConfig = { // isomorphic-dompurify ships jsdom, which at runtime reads stylesheet files // from its own package directory. Webpack bundling breaks those relative // lookups — loading it externally from node_modules keeps the paths intact. // // brainerce (the SDK) is imported across both the server and client // compilation boundaries in ~60 files, including the [locale]/layout.tsx // route (see generateStaticParams below) whose static params are resolved // by a separate jest-worker child process in `next dev` (webpack). That // worker requires the compiled page against the vendor-chunks manifest // while the main dev server may still be writing it, which can throw // "Cannot find module './vendor-chunks/brainerce.js'". Excluding it from // server bundling (loaded externally from node_modules instead) avoids the // race. serverExternalPackages: ['isomorphic-dompurify', 'brainerce'], images: { // The storefront is a consumer of the Brainerce API — it has to render // whatever image URLs the API returns. In practice those URLs are // usually on cdn.brainerce.com, but a product/variant can still carry a // raw upstream-merchant URL (WooCommerce, Shopify, self-hosted) while // its image-import job is pending or failed. Rather than allowlist every // possible merchant host (or hard-fail on them), only cdn.brainerce.com // goes through the server-side optimizer; components render product/blog // images via `CdnImage` (src/ui/shared/cdn-image.tsx), which // detects any other host and renders it `unoptimized` — an unresized // direct-from-origin fetch, same as this template's prior behavior — // instead of next/image throwing on an unconfigured hostname. No // server-side fetching of unknown hosts → no SSRF/DoS surface on this // Next server. remotePatterns: OPTIMIZED_IMAGE_HOSTS.map((hostname) => ({ protocol: 'https' as const, hostname, pathname: '/**', })), }, async headers() { return [ { source: '/(.*)', headers: [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload', }, { key: 'X-Content-Type-Options', value: 'nosniff' }, // SAMEORIGIN (not DENY) so iframe-based payment providers (e.g. Cardcom) // can redirect the iframe back to /payment-complete on the storefront // itself after a successful charge — the postMessage relay needs the // parent frame to be able to render our own same-origin page. { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()', }, ], }, ]; }, }; export default nextConfig;