All checks were successful
PR checks / checks (pull_request) Successful in 2m10s
PLAN.md §16, D63/D64, plus D67-D69 taken 2026-10-22. - Core docs use core's words: a Team, a game server, the sidecar, the bridge plugin, the game module. Detail that belonged to Ultima Online moved into its pages rather than being dropped. - New "Ultima Online" sidebar group (nine pages): an overview, Connect a ServUO shard, Verify the bridge, The shard connection, Client files, Troubleshooting, Bridge.cfg, sidecar.toml, Shard event catalog. Five moved URLs answer a real 301 (src/config/redirects.mjs, asserted by the served test). - Getting started is game-neutral. "Connect your game server" keeps the old URL and hands off to each game; Rust links docs main:rust-link/INSTALL.md until phase 15. "Verify the whole stack" stays, neutral (D69). - D68: GameExample.astro labels a game's real example on a core page with the game's name; checkTerms allows that game's words inside it. - scripts/checkTerms.mjs, the twelfth check: 16 game words, docs pages and both sidebar trees, one allowlisted phrase (Discord's "Guild (server) ID"). Wired into verify and pr-checks.yml. - canonicalDocs grouped by game; the Rust group carries four documents. - D67: the screenshot rule from #32 is adopted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
136 lines
6.6 KiB
JavaScript
136 lines
6.6 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 { docsRedirects } from './src/config/redirects.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',
|
|
},
|
|
|
|
// Pages that moved into a game's own sidebar group (PLAN.md §16, D60). See the file.
|
|
redirects: docsRedirects,
|
|
|
|
|
|
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',
|
|
},
|
|
});
|