SSR-First Rendering, With Static Output When You Want It
Need this adapted for your organization? Work with AstroWeb.dev — Astro services by NViewsWEB.
Feature

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:

  1. creates a random nonce and stores it in locals.nonce, where the base layout reads it for inline script and style tags,
  2. builds the policy for the route from src/site/config/security.mjs,
  3. sets Content-Security-Policy on HTML responses (in development it sends Content-Security-Policy-Report-Only),
  4. adds X-Content-Type-Options: nosniff and 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

  1. Set serverStatus in functions.mjs.
  2. Run pnpm run build (it regenerates manifests, CSS and the Cloudflare config first).
  3. For server mode, check a page response for content-security-policy and x-content-type-options.
  4. Deploy with Wrangler. See the Cloudflare deployment checklist.

For how the policy itself is built, read How Response-Header CSP Works.