SSR-First Rendering, With Static Output When You Want It
One setting chooses server-rendered or static output. SSR is the default because it lets the middleware send a nonce-based CSP header on every page.
AstroWEBTheme builds either as static files or as a server-rendered Cloudflare Worker. The choice is one line, and the two modes use different security header mechanisms, so pick one for the whole site instead of mixing them.
The switch
// src/site/config/functions.mjs
serverStatus: "server", // "static" or "server"
astro.config.mjs reads it: static sets Astro output: "static", anything else sets output: "server" with the @astrojs/cloudflare adapter. wrangler.jsonc points main at the adapter entrypoint and serves ./dist as assets.
Why SSR is the default
A real Content-Security-Policy header with a per-request nonce can only come from code that runs on each request. In src/middleware.ts the middleware:
- creates a random nonce and stores it in
locals.nonce, where the base layout reads it for inline script and style tags, - builds the policy for the route from
src/site/config/security.mjs, - sets
Content-Security-Policyon HTML responses (in development it sendsContent-Security-Policy-Report-Only), - adds
X-Content-Type-Options: nosniffand the extra headers for the route, and applies the auth rules for dashboard and API paths.
Do not prerender in server mode
In server mode a page that exports prerender = true is built to a plain HTML file. With the current wrangler.jsonc, which has no run_worker_first, Cloudflare serves that file as a static asset and the worker, the middleware, the nonce and the CSP header never run for it. For that reason every page in the demo now sets:
---
export const prerender = false;
---
Pages without the export are also server-rendered in server mode. If a page really must be static, accept that it carries no header-based policy, or switch the whole site to static.
Static mode
With serverStatus: "static" the site is plain files. The middleware only adds headers on the dev server. core/scripts/generate/headers.mjs then builds a _headers file (for Cloudflare Pages or a static host) from the same security.mjs config. That policy is fixed at build time, so it cannot carry a nonce. The generator exits early when the site is not static, so a server build never writes one.
server (default) |
static |
|
|---|---|---|
| Output | Worker plus assets | HTML files |
| CSP | Header with per-request nonce, from middleware | Fixed _headers policy |
| D1 live content, auth, dashboard, MCP | Available | Not available |
| Hosting | Cloudflare Workers | Any static host |
Workflow
- Set
serverStatusinfunctions.mjs. - Run
pnpm run build(it regenerates manifests, CSS and the Cloudflare config first). - For server mode, check a page response for
content-security-policyandx-content-type-options. - Deploy with Wrangler. See the Cloudflare deployment checklist.
For how the policy itself is built, read How Response-Header CSP Works.