feat(site): phase 1 — the foundation
Some checks failed
PR checks / checks (pull_request) Failing after 4m19s
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:
40
src/lib/brand.mjs
Normal file
40
src/lib/brand.mjs
Normal file
@@ -0,0 +1,40 @@
|
||||
import brandDefault from '../../brand-default/brand.json' with { type: 'json' };
|
||||
|
||||
/**
|
||||
* The single accessor for brand text (§7). Every template reads brand through here and
|
||||
* never imports `brand.json` directly, so phase 2 can change WHERE the values come from
|
||||
* without touching a single call site.
|
||||
*
|
||||
* ---------------------------------------------------------------------------
|
||||
* A tension phase 2 has to resolve, recorded here so it is not discovered late
|
||||
* ---------------------------------------------------------------------------
|
||||
* §7 promises that changing the site name, the Discord invite or the contact address is a
|
||||
* file edit on the bind mount plus a restart — the same class of change as swapping a
|
||||
* logo. But §6 prerenders the pages at build time, and a value read at build time is baked
|
||||
* into the HTML, where no mounted file can reach it.
|
||||
*
|
||||
* Assets are fine: they are served by `GET /brand/*` at runtime, which reads the mount per
|
||||
* request. Text is not, and phase 2 owns the fix. The options, in the order they are worth
|
||||
* trying:
|
||||
*
|
||||
* 1. A response-time rewrite in the Node adapter's middleware, substituting a small set
|
||||
* of placeholder tokens in the prerendered HTML. Keeps every page static and the
|
||||
* mount authoritative. Costs one pass over the response body.
|
||||
* 2. Mark the handful of pages that show brand text as `prerender = false`. Simple, but
|
||||
* it spreads: the footer is on every page, so "the handful" is all of them.
|
||||
* 3. Accept that text is build-time and only assets are mounted. Cheapest, and it
|
||||
* contradicts the sentence in §7 that says otherwise — so it needs the org lead's
|
||||
* agreement, not a quiet decision here.
|
||||
*
|
||||
* Until then this returns the stock values, which is the correct behaviour for an empty
|
||||
* mount either way.
|
||||
*/
|
||||
export const brand = Object.freeze({ ...brandDefault });
|
||||
|
||||
/**
|
||||
* `brand.json` carries `$comment` keys for the operator who opens the mounted copy. They
|
||||
* are documentation, not fields, and must never reach a template.
|
||||
*/
|
||||
export function brandFields() {
|
||||
return Object.fromEntries(Object.entries(brand).filter(([k]) => !k.startsWith('$')));
|
||||
}
|
||||
46
src/lib/tokens.mjs
Normal file
46
src/lib/tokens.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
// Vite inlines the file's text at build time. This is deliberately NOT a `readFileSync`
|
||||
// against `import.meta.url`: that works in dev and then throws ENOENT during prerender,
|
||||
// because the bundled chunk sits in `dist/server/.prerender/` and the CSS does not follow
|
||||
// it there. `?raw` puts the bytes in the bundle, where they are needed.
|
||||
import tokensCss from '../styles/tokens.css?raw';
|
||||
|
||||
/**
|
||||
* Reads `tokens.css` at build time and exposes its custom properties to JavaScript.
|
||||
*
|
||||
* This exists because a few values have to leave CSS: `<meta name="theme-color">`, the OG
|
||||
* card's background, an SVG diagram's stroke. Copying them into a template would be
|
||||
* exactly the drift §7 warns about — "one CSS file changes most of the appearance, and
|
||||
* then there is a hardcoded #0e1318 in the footer" — and `checkTokens.mjs` would fail the
|
||||
* build for it, correctly.
|
||||
*
|
||||
* So the token file stays the single source and this reads it, rather than the other way
|
||||
* round. Deliberately a plain regex over `--name: value;` and not a CSS parser: the file
|
||||
* it reads is one we own and keep flat, and a dependency here would be a dependency in the
|
||||
* build of every page.
|
||||
*
|
||||
* Note that this resolves the STOCK values. A bind-mounted `theme.css` overrides tokens in
|
||||
* the browser, at runtime, which is the whole point — anything derived through this module
|
||||
* is therefore build-time and will not follow a mounted theme. Keep that list short.
|
||||
*/
|
||||
function readTokens() {
|
||||
const withoutComments = tokensCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
const out = {};
|
||||
for (const match of withoutComments.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/gi)) {
|
||||
out[match[1]] = match[2].trim();
|
||||
}
|
||||
return Object.freeze(out);
|
||||
}
|
||||
|
||||
export const tokens = readTokens();
|
||||
|
||||
/** Throws rather than emitting `undefined` into a template. */
|
||||
export function token(name) {
|
||||
const value = tokens[name];
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
`Unknown design token "${name}". Every token is defined in src/styles/tokens.css; ` +
|
||||
`add it there rather than inlining a value at the call site.`
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user