Dev notes
Why the landing page sections were blank in production, how the CSP got fixed, and how the middleware was restructured to avoid paying Auth0 latency on every request.
A Content Security Policy is an HTTP header that tells the browser which resources it's allowed to load and execute. The policy had script-src 'nonce-{nonce}' 'strict-dynamic' — each request generated a random nonce and any script without that nonce attribute was blocked.
Next.js App Router inlines RSC payload scripts directly into the HTML — they look like self.__next_f.push([...]) and hydrate the React tree on the client. These scripts have no nonce attribute. And 'strict-dynamic' explicitly ignores 'self', so even same-origin scripts are blocked unless they carry the nonce.
Locally this was fine because the dev server always re-renders everything. In production the landing page became fully static after moving the auth redirect to middleware, and Vercel served the CSP from the CDN edge. The clue: HeroSection still animated because its animation is pure CSS @keyframes — everything else uses IntersectionObserver which requires JS. JS was simply not running at all.
The fix is script-src 'self' 'unsafe-inline'. What 'unsafe-inline' actually protects against is reflected XSS — an attacker injecting a <script> tag into the HTML response. The real XSS protection here is React's automatic JSX escaping — any value rendered in JSX gets HTML-escaped before it touches the DOM. The attack surface only opens up if you use dangerouslySetInnerHTML, and there is none of that in this codebase.
To do nonce-based CSP correctly in Next.js you have to make the root layout async and read the nonce from request headers inside it. But reading headers() in any server component opts that route out of static generation — every page goes dynamic. The root layout wraps every page, so every page takes the TTFB hit. It's not worth it here, and it's what the Next.js docs recommend for static apps.
auth0.middleware() makes a network call to Auth0 on every single request — not just auth routes, every page. That showed up in TTFB data immediately and the whole middleware was pulled. Now the proxy runs on every request but auth0.middleware() is only invoked for /auth/* and authenticated /vitals or /settings requests. Everything else hits NextResponse.next() with the CSP header attached and returns immediately.
For protected routes, auth0.getSession(req) runs first — the proxy-safe overload that reads from req.cookies directly, no network call, just a cookie decrypt. If there's no session it redirects to login and auth0.middleware never runs. If there is a session, auth0.middleware runs after to handle rolling session refresh.
The remaining directives do meaningful work: default-src 'self' blocks loading resources from unknown domains, frame-ancestors 'none' blocks clickjacking, object-src 'none' blocks Flash and plugins, base-uri 'self' blocks base tag injection. connect-src is locked down so JS can only make requests to known endpoints — same origin, plus specific external services: Speed Insights, TCGdex, and GitHub raw for Pokémon sprites.