// @ts-check import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; import starlight from '@astrojs/starlight'; import { inlineScriptHashes, inlineStyleHashes } from './src/config/cspHashes.mjs'; import { docsSidebar } from './src/config/sidebar.mjs'; /** * PLAN.md §6 calls this "Astro with the Node adapter, `output: 'server'` with per-page * `prerender = true`". Astro 7 expresses that shape the other way round: `output: 'static'` * with an adapter prerenders everything and lets individual routes opt OUT with * `export const prerender = false`. The runtime result is identical to what §6 describes — * a container serving prerendered HTML, with a handful of routes executing per request — * and this is the direction the framework supports, so the default is the safe one: a page * added without thinking about it is static, not accidentally server-rendered. * * The two routes that will opt out live in phases 2 and 5: `GET /brand/*` (§7) and * `POST /api/beta-signup` (§8). */ export default defineConfig({ site: 'https://runicgateway.com', output: 'static', // `staticHeaders` is what turns §6's CSP from a promise into a response header (D48). // Without it the policy ships as a ``, and a meta CSP silently ignores // `frame-ancestors` — the one directive that stops the site being framed. With it, the // build writes `_headers.json` next to the server entry and the standalone server sends // the policy as a real header on every prerendered route, so the operator's reverse proxy // needs no CSP configuration at all and cannot get it wrong. adapter: node({ mode: 'standalone', staticHeaders: true }), build: { // Directory-style URLs, so every link in prose can end in a slash and mean it. format: 'directory', }, security: { csp: { directives: [ // The whole posture in one line: nothing loads from anywhere but this origin. // §6 could promise this without exceptions because the fonts are self-hosted and // D9 rules out analytics — there is no CDN to whitelist and no beacon to allow. "default-src 'self'", // Not covered by `default-src`, and each one closes a specific door: no injected // `` can re-point every relative URL on the page, the signup form can only // post to us, no plugin content at all, and the site cannot be framed. The last // of those is the reason `staticHeaders` is on. "base-uri 'self'", "form-action 'self'", "object-src 'none'", "frame-ancestors 'none'", // One `url(data:image/svg+xml)` survives bundling into the stylesheet. Data URLs // are a real (if small) exfiltration-free risk surface, so this is the only // relaxation of `default-src` on the image directive and it is scoped to images. "img-src 'self' data:", ], scriptDirective: { resources: [ "'self'", // Pagefind (D47) compiles its index with `WebAssembly.instantiate`, which a // strict `script-src` blocks outright — search silently returns nothing. This // permits WASM compilation *only*; it does not restore `eval`. "'wasm-unsafe-eval'", ], // Starlight's own `is:inline` scripts, which Astro does not hash because it never // parses them. Generated — see src/config/cspHashes.mjs and `npm run check:csp`. hashes: inlineScriptHashes, }, styleDirective: { // No `'self'` here, though `style-src` needs it and gets it: Astro's default // already supplies it, and naming it alongside an `attribute`-kind resource makes // the build warn — browsers do not fall back from `style-src-attr` to `style-src`, // so a `'self'` written here would apply to neither scope the author meant. resources: [ // Starlight and Expressive Code write ~3,700 inline `style` attributes into the // documentation — icon sizing, the theme select's width, and every syntax // colour, which Expressive Code emits as custom properties on the element. They // cannot be hashed (CSP hashes cover `