Add a web page¶
The showcase site in web/ is a SvelteKit app built to static files,
which our own nginx server serves; every page is prerendered, takes its
data from the Python package at build time, and runs the engine in the
visitor's browser only when they interact. This page is for a
contributor adding a page: where the route goes, where its logic goes
(not in the component), how it is tested for errors, accessibility and
performance, and how visitors find it. It assumes make web-setup has
been run and that you have read the architecture standard in
web/README.md.
The worked example is the compare page, /compare/GAME: one
prerendered page per game in the catalogue, whose settings live in the
URL and whose comparison runs on several engines.
The pieces of a page¶
| Piece | Where | /compare/GAME |
|---|---|---|
| Route and build-time data | web/apps/site/src/routes/…/+page.server.ts |
routes/compare/[game]/+page.server.ts |
| Page shell: title, description, layout | +page.svelte |
routes/compare/[game]/+page.svelte |
| Logic: settings, URL, request, what to draw | a DOM-free class in $lib/ or web/packages/models |
CompareSettings, RateBar ($lib/compare.ts), TournamentRun (engine package) |
| Drawing | a component in $lib/components/ |
CompareView.svelte |
| Unit tests | Vitest, beside the class | $lib/compare.test.ts |
| End-to-end tests | web/tests/e2e/*.spec.ts |
phase5.spec.ts |
1. The route¶
Pages are files under web/apps/site/src/routes/. The root layout
(routes/+layout.ts) sets prerender = true for every page, and the
static adapter is strict, so the build fails if a page cannot be
prerendered. Two consequences:
- A dynamic route lists its pages.
/compare/[game]exportsentriesfrom+page.server.ts, one per game in the bundled catalogue, and itsloadreturns that game's catalogue entry as plain data (refusing an unknown name with a 404):
export const entries: EntryGenerator = () =>
SiteCatalogue.bundled().games.map((game) => ({ game: game.name }));
export const load: PageServerLoad = ({ params }) => {
const catalogue = SiteCatalogue.bundled();
const game = catalogue.find(params.game);
if (game === undefined) {
error(404, `There is no game called ${params.game}`);
}
return { game, regimes: catalogue.regimes };
};
- Data comes from files Python wrote, read at build time: the
catalogue (
SiteCatalogue, from the protocol'sdescribefixture), the replays (static/replays/), the lessons (static/lessons/) and the demo previews (static/demos/, which/monitorloads in its+page.server.ts). Return plain objects; the page turns them back into model classes, so no page's script carries more than it needs.
The page sets its own <title> (… · {SITE_TITLE}) and a meta
description in <svelte:head>, uses one <h1>, and lives inside the
shared layout's <main>.
Security headers. Every page carries a Content Security Policy as a
<meta> tag (kit.csp in web/apps/site/svelte.config.js): scripts
and connections only to this site, no inline scripts beyond SvelteKit's
hashed ones. A page needs no change to it; if it needs a different
header (a page meant to be framed, a new kind of static file), add a
rule to web/apps/site/static/_headers, which web/tests/e2e/serve.ts
applies in the tests and docs/how-to/embed.md gives as nginx
configuration. Nothing is loaded from another host.
2. The logic: models, not components¶
A Svelte component composes classes; it does not compute what they own. Put the page's logic in DOM-free classes that follow the architecture standard (readonly values, behaviour with its data, invariants checked at construction, no module-level mutable state):
- in
web/packages/modelswhen another app could use it (the play page'sPlaySpec,LiveSession,MoveTargets; the demos'MonitorSettings,monitorView,DemoRunner), exported fromsrc/index.ts; - in
web/apps/site/src/lib/when only the site needs it (CompareSettingsin$lib/compare.ts).
Settings that change what the page shows belong in the URL, as the
command-line flags do (CompareSettings reads and writes
?agents=…&seeds=…, and gives the identical grounded-decision compare
command for Copy CLI command); per-visitor display preferences use
DisplaySettings ($lib/settings.svelte.ts) and never go in a link.
A page that runs the engine creates it on the visitor's first
interaction, through siteTransport() ($lib/engine.ts), so the
replay or recorded preview paints first without Python. Requests go
through Engine methods; a new kind of request is
a protocol operation.
3. Embeddable elements, if the page should travel¶
If other sites should show the same thing, it belongs in
web/packages/elements as a <gd-*> custom element (decisions D70 to
D76), built by make web-elements and documented in
docs/how-to/embed.md; the site page then uses the element or shares
its models. Most pages do not need this.
4. Tests¶
Unit tests (Vitest). Every model class has a *.test.ts beside it:
URL round trips, defaults, validation errors, the command text. They run
in make web-test and make web-check.
End-to-end tests (Playwright). web/tests/e2e/ runs against the
built site served as our server serves it (serve.ts applies
_headers). For a new page:
- add its route to the
ROUTESlist ofsite.spec.ts(indexes and games) or of the spec that owns its section (demos.spec.tsfor/parameters,/monitorand/runtime;phase5.spec.tsfor compare, audit and the teaching pages): those tests open it in both colour schemes, check it with axe for WCAG 2.2 A and AA (accessibilityViolationsinsupport.ts), and fail on any console error or warning (ConsoleWatch); - test what the page does: the preview's numbers at once, a live run, the URL after a change, the keyboard path, a phone's width;
- if its data changes while it is open (a game playing, a setting
applied), mark its parts with
data-region, build them from fixed slots (the design system inweb/README.md) and add the page tolayout.spec.ts, which fails, in Chromium and Firefox, when any region moves.
make web-e2e builds everything and runs the whole suite. While
working on one spec, run it alone with its own output directory, so it
never collides with another run's reports:
make web-build
pnpm --dir web exec playwright test --config tests/e2e/playwright.config.ts monitor-page.spec.ts --output test-results/monitor-page
Lighthouse. make web-lighthouse runs Lighthouse CI on the pages
listed in collect.url of web/tests/lighthouse/lighthouserc.cjs, and
every category (performance, accessibility, best practices, SEO) must
score at least 95. Add a page there when it is a new kind of page
(a new section's first page); /runtime stands for the demo pages and
/play/tictactoe for the play pages.
5. Visitors find it¶
- The header (
$lib/components/SiteHeader.svelte) links the sections: Play, Gallery and Learn. A new section adds a link witharia-currentfor its routes; a page inside a section is linked from that section's index or from the pages it belongs to (each play page links its compare, audit and teaching pages). web/README.mdlists every route and the classes behind it: add the page to the table of its section.- If the documentation describes the feature, link the page from the docs page, and add a CHANGELOG entry.
Definition of done¶
- [ ] Route under
routes/, prerendered (withentriesfor a dynamic route), build-time data as plain objects, title, description and oneh1. - [ ] Logic in DOM-free classes (models package or
$lib/), following the architecture standard; settings in the URL with the identical CLI command where there is one. - [ ] The engine starts on interaction; nothing loads from another host;
_headersonly if the page needs a different header. - [ ] Vitest tests for every class; the route in an e2e spec's
ROUTES(axe in both schemes, no console errors) and tests of its behaviour. - [ ] In
lighthouserc.cjsif it is a new kind of page. - [ ] Linked from the header or its section, listed in
web/README.md, CHANGELOG entry. - [ ]
make check WEB=1,make web-e2eandmake web-lighthousegreen.