// @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 `