feat(site): phase 1 — the foundation
Some checks failed
PR checks / checks (pull_request) Failing after 4m19s

Astro 7 with the Node adapter, Starlight mounted at /docs, the token file, both
self-hosted typefaces, the layout shell, and the two build-time checks from §12.

The palette's gold and cyan are sampled from runic-emblem.png rather than
guessed, per §11: 494,059 opaque pixels binned by hue, each value annotated with
its measured contrast against the ground, and restricted rather than brightened
where a ratio fails.

- checkTokens.mjs fails the build on any colour literal outside tokens.css,
  which is what keeps §7's "recolouring is a file copy" promise true.
- checkFacts.mjs re-reads all 14 externally-sourced facts from their authorities
  over the Gitea API and fails on disagreement. It also enforces D13: no email
  address in the source outside brand-default/brand.json.
- Both were negative-tested; neither has ever been allowed to pass by default.

§6 asks for output:'server' with per-page prerender=true. Astro 7 expresses the
same runtime shape as output:'static' with an adapter, opting individual routes
out — so the default is static rather than accidentally server-rendered.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:08:52 -05:00
parent 650ea21ad4
commit 66187dde5d
25 changed files with 10462 additions and 0 deletions

67
src/layouts/Base.astro Normal file
View File

@@ -0,0 +1,67 @@
---
import '../styles/tokens.css';
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import { brand } from '../lib/brand.mjs';
import { token } from '../lib/tokens.mjs';
interface Props {
title: string;
description: string;
/** Suppress the site name suffix — the homepage sets its own full title. */
bareTitle?: boolean;
}
const { title, description, bareTitle = false } = Astro.props;
const fullTitle = bareTitle ? title : `${title} — ${brand.siteName}`;
const canonical = new URL(Astro.url.pathname, Astro.site);
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{fullTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={brand.siteName} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta name="twitter:card" content="summary_large_image" />
<!--
No analytics, no third-party requests, no cookie banner (D9), and the fonts are
self-hosted (§11) — so there is nothing here to preconnect to, and §6's
`default-src 'self'` holds with no exception to argue about. The CSP header itself
is set at the adapter in phase 10; this comment is here so nobody adds a CDN link
in the meantime and quietly breaks the promise.
Favicons and the OG image are served from the brand mount (`/brand/*`) in phase 2.
-->
<meta name="theme-color" content={token('--bg')} />
<slot name="head" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<div class="site">
<Header />
<main id="main">
<slot />
</main>
<Footer />
</div>
</body>
</html>