You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Response headers (CSP, HSTS, …) are dropped when a page is prerendered #2
Thanks for the bundle, prerendering from routes rather than content is a great fit for Symfony apps.
What happens
PageRenderer::render() keeps the response body and its format, and RenderedPage only carries content and format. Every other header the application sets on the response is discarded during generation: Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, whether they come from NelmioSecurityBundle or from a kernel.response listener.
So an app that ships a strict policy when served by PHP loses it silently once exported: the "no runtime to exploit" gain comes with losing the defence-in-depth headers. GitHub Pages cannot set custom headers at all, but Cloudflare Pages, Netlify, Caddy and nginx can, and the bundle already knows what they should be.
Proposal (opt-in, backward compatible)
RenderedPage gains an optional array $headers = [], filled by PageRenderer from an allowlist: Content-Security-Policy (and -Report-Only), Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and the three Cross-Origin-* policies. Never Set-Cookie, Cache-Control or Vary, which describe the dynamic response rather than the page.
A headers_file option (default null, e.g. '_headers') writes them at the end of a run, in the format Cloudflare Pages and Netlify read:
Docs: add the headers to the Caddy and nginx examples, and note that GitHub Pages cannot set headers at all. A <meta http-equiv="Content-Security-Policy"> fallback exists, but browsers ignore frame-ancestors, report-uri and sandbox there, and HSTS cannot be set that way.
I'm happy to open a PR along these lines, with tests, if the direction suits you. Two questions first: should the allowlist be configurable, and would you rather keep the headers-file writer as an event listener in the docs than as a bundle option?
Agreed, these headers shouldn't be lost, and RenderedPage is the right place to carry them.
I'd store all the response headers in RenderedPage::$headers though, and filter only when writing them out, as listeners may need others (X-Robots-Tag, Link, ...). No configurable allowlist for now IMO.
Writing the headers file is a common need, so I think it belongs in the bundle, as a listener enabled by your headers_file option. Some things to take care of:
Partial runs: a --route/--path run should merge with the existing file rather than overwrite it.
Rule limit: AFAIK, Cloudflare Pages caps _headers at 100 rules, so shared headers should go into a single /* rule, with per-path rules only for the differing ones.
Caddy and nginx: documenting the headers in the config examples is enough for now.
#5 added a GenerationFinishedEvent, dispatched once every page has been processed. The listener can collect headers on PageRenderedEvent and write the file from there.
Last thing worth documenting: a nonce-based CSP gets frozen at build time, so hashes should be used instead.
Thanks, that all makes sense. I went with your approach:
every header kept in RenderedPage::$headers, filtered only when the file is written (fixed list, no option);
a listener behind headers_file, on PageRenderedEvent + your GenerationFinishedEvent;
shared headers folded into a single /* rule for the 100-rule cap, per-path rules only for what differs;
partial runs merge: the generated rules sit between two comment markers, so a filtered run re-reads the pages already listed and hand-written rules outside the markers are kept;
Caddy/nginx documented, plus the nonce-vs-hash note.
Activity
mtarld commentedon Oct 21, 2026
Agreed, these headers shouldn't be lost, and
RenderedPageis the right place to carry them.I'd store all the response headers in
RenderedPage::$headersthough, and filter only when writing them out, as listeners may need others (X-Robots-Tag,Link, ...). No configurable allowlist for now IMO.Writing the headers file is a common need, so I think it belongs in the bundle, as a listener enabled by your
headers_fileoption. Some things to take care of:--route/--pathrun should merge with the existing file rather than overwrite it._headersat 100 rules, so shared headers should go into a single/*rule, with per-path rules only for the differing ones.#5 added a
GenerationFinishedEvent, dispatched once every page has been processed. The listener can collect headers onPageRenderedEventand write the file from there.Last thing worth documenting: a nonce-based CSP gets frozen at build time, so hashes should be used instead.
WDYT? Your PR is more than welcome 🙂
jnadaud commentedon Oct 21, 2026
Thanks, that all makes sense. I went with your approach:
RenderedPage::$headers, filtered only when the file is written (fixed list, no option);headers_file, onPageRenderedEvent+ yourGenerationFinishedEvent;/*rule for the 100-rule cap, per-path rules only for what differs;PR: #6