All checks were successful
PR checks / checks (pull_request) Successful in 1m26s
The checks were green before this phase started and are green now. What found anything was the part no script does: fifty pages at three widths in a real browser, a signup walked against its store, and a full brand mount applied and restarted. D51 — the chrome and the head follow the mount; the consent sentence does not. With a complete brand.json mounted, forty-nine pages came back rebranded and /beta did not. applyBrand.mjs rewrites files in dist/client and /beta renders per request, so its HTML never exists as a file to rewrite; liveBrand() was there for exactly that and was used for betaOptInUrl alone. Everything around the form — title, OG tags, header lockup, footer Source and Discord links — came from the shared chrome, and the shared chrome was baked. renderBrand() picks by Astro.isPrerendered, in one place, so the other forty-nine keep taking the value the boot rewrite will replace. CONSENT_TEXT stays a constant: it is stored verbatim in a person's row, so following a mounted name would change the recorded text of a consent already given. D52 — the documentation half gets phase 10's skip-link fix. Starlight's skip link targets the page <h1>, which is no more focusable than the <main> phase 10 fixed, so following it moved the viewport and not the focus on forty pages. A PageTitle override adds tabindex="-1". D53 — no twelfth check. The external-link sweep (73 of 74 alive) and the brand-mount walk stay throwaway scripts: one would make the build depend on other people's uptime and the other needs Chrome on the runner. Also recorded and deliberately not fixed: Starlight's heading anchor links measure under 24px at 390, and are exempt under SC 2.5.8's Equivalent clause because the mobile table of contents links to every one of the same anchors. npm run verify green — fourteen steps, both suites, all eleven checks. Co-Authored-By: Claude <noreply@anthropic.com>
131 lines
6.5 KiB
JavaScript
131 lines
6.5 KiB
JavaScript
// @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 `<meta http-equiv>`, 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
|
|
// `<base>` 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 `<style>` elements, never attributes), and
|
|
// Astro's own docs record Shiki as incompatible with CSP for exactly this
|
|
// reason. Scoped to `style-src-attr` deliberately: a style attribute cannot
|
|
// execute script, so this leaves the directive CSP exists for — `script-src` —
|
|
// untouched. The marketing pages emit zero inline style attributes.
|
|
{ resource: "'unsafe-inline'", kind: 'attribute' },
|
|
],
|
|
hashes: inlineStyleHashes,
|
|
},
|
|
},
|
|
},
|
|
|
|
integrations: [
|
|
starlight({
|
|
title: 'Runic Gateway',
|
|
// Marketing owns the 404 (§10); a Starlight-chrome 404 on `/features/` would be wrong.
|
|
disable404Route: true,
|
|
// Not a file in `public/`: the brand route derives this from whichever `logo.png` is
|
|
// mounted (§7), so the docs' tab icon changes with a rebrand like everything else.
|
|
// Starlight's default is `/favicon.svg`, which does not exist here — every docs page
|
|
// was requesting a 404 for it.
|
|
favicon: '/brand/favicon.ico',
|
|
// Starlight's own light/dark switch is deliberate: §11 keeps marketing single-theme
|
|
// but has the docs honour the reader's preference.
|
|
customCss: ['./src/styles/tokens.css', './src/styles/starlight.css'],
|
|
components: {
|
|
// Not the `logo` option: that takes an asset imported through Vite, which
|
|
// fingerprints the filename into the build — and a fingerprinted logo is one the
|
|
// bind mount can never replace (§7). The override points at the stable
|
|
// `/brand/*` URL instead.
|
|
SiteTitle: './src/components/DocsSiteTitle.astro',
|
|
// Starlight builds its own head, so the docs otherwise miss the brand stylesheet,
|
|
// the manifest and the OG card entirely. See the component.
|
|
Head: './src/components/DocsHead.astro',
|
|
// One attribute, for one reason: Starlight's skip link targets the `<h1>`, and an
|
|
// `<h1>` is not focusable. See the component (phase 11).
|
|
PageTitle: './src/components/DocsPageTitle.astro',
|
|
},
|
|
credits: false,
|
|
sidebar: docsSidebar,
|
|
pagination: true,
|
|
lastUpdated: false,
|
|
editLink: {
|
|
baseUrl: 'https://gitea.whitlocktech.com/RunicGateway/runicgateway.com/_edit/main/',
|
|
},
|
|
}),
|
|
],
|
|
|
|
prefetch: {
|
|
prefetchAll: true,
|
|
defaultStrategy: 'hover',
|
|
},
|
|
});
|