When a strict CSP turned our static Next.js site dynamic
A stricter security header can make a site slower without changing a single visible component. We learned that while hardening wedece.com: a nonce-based Content Security Policy
A stricter security header can make a site slower without changing a single visible component. We learned that while hardening wedece.com: a nonce-based Content Security Policy looked like the obvious upgrade, but on our Next.js deployment it quietly changed how every page was rendered. The eventual fix was not to abandon CSP. It was to choose a policy that matched the site’s actual risk and hosting model.
The policy that changed the rendering model
Our first implementation generated a fresh nonce in middleware for each request. The nonce was added to the CSP header and then read in the shared locale layout so that Next.js scripts, analytics and structured data could receive the same value. In security terms, this is a strong pattern: only scripts carrying the unpredictable, request-specific nonce may run.
The architectural consequence is just as important. Next.js cannot put a per-request nonce into HTML created at build time. Its documentation therefore requires dynamic rendering for nonce-based CSP. Because our shared layout read request headers, that decision applied to the whole site, including pages whose content was otherwise static. Static generation and normal CDN caching disappeared, and each visit required a React server render.
Why this mattered on Cloudflare Workers
wedece.com runs on Next.js through OpenNext on Cloudflare Workers. The Workers Free plan allows 10 ms of CPU time per HTTP request. Cloudflare notes that heavier workloads such as server-side rendering commonly use 10–20 ms. That does not make SSR unsuitable for Workers, but it leaves little room when a mostly static marketing site renders every route on every request.
In our case, the nonce implementation produced Worker error 1102 under modest concurrency. The security control was valid, but its operational cost did not fit this deployment. This was the useful lesson: a header is part of the rendering architecture, not a line to paste at the end of a project.
The policy we ship now
We moved CSP generation out of middleware and into the static headers() configuration in next.config.ts. The production policy keeps a narrow default-src 'self', explicit hosts for analytics and reCAPTCHA, and restrictive directives including object-src 'none', frame-ancestors 'none', base-uri 'self' and form-action 'self'. The application no longer reads a nonce from request headers, so public pages can be prerendered and cached again.
The deliberate compromise is unsafe-inline for scripts and styles. It weakens protection against an injected inline script, so it is not a universal recommendation. It is acceptable for the current scope of wedece.com: a brochure site with no authentication, user accounts or user-generated content rendered into pages. If any of those conditions changes, the decision must be reviewed.
We also left strict-dynamic out. Under CSP Level 3, it causes browsers to ignore script host allowlists, 'self' and 'unsafe-inline' when a valid nonce or hash is present. Combining it casually with the static allowlist would not make the policy stricter; it would make the intended loading model inconsistent and could break third-party scripts.
Verify the result, not the configuration file
After changing CSP, we check the production response, not just source code. On 23 September 2026, the homepage returned the enforced static policy together with x-nextjs-prerender: 1 and cache-control: s-maxage=31536000. Our Playwright smoke suite also visits core routes and checks the blog, sitemap, RSS and structured data. CSP reports are accepted by a dedicated endpoint and forwarded to Sentry in production.
The practical rule is simple: start with the strongest policy your threat model requires, then test its effect on rendering, caching and third-party scripts. A policy that fails under real traffic is not finished, and a faster policy is not automatically safe. The engineering work is in making the trade-off explicit, measurable and easy to revisit.