feat(site): phase 17 - per-game module pages, /runicnpc/, and checkTerms over the built site
All checks were successful
PR checks / checks (pull_request) Successful in 2m37s

PLAN.md §16 phase 17, built on the org lead's answers of 2026-10-22,
recorded as D75-D79 (count of record 79).

- D75: core marketing pages describe what a game module adds in core's
  words. "Game intelligence" became "What a game module adds"; each
  game's own list moved to src/data/games/ and renders only on the new
  /modules/uo/ and /modules/rust/. /modules/ is a card per game.
- The page-side capability check is per game (assertGameCoverage), the
  item phase 13 moved here.
- D76: the homepage keeps its game screenshot, labelled. screens.mjs
  entries may carry `game` (D67); Screenshot.astro and the app grid
  print the game's name and mark the figure data-game.
- D77: /app/ describes the Rust screens (released the same day).
- D78: /runicnpc/, the standalone page, is in the header and footer;
  the narrow header was re-measured so five links keep two rows.
- D79: checkTerms.mjs --built reads every built page's visible text
  after the build; game routes and data-game elements allow their
  game's words. Proven against ten plants. Still the twelfth check.
- Re-read fixes: /architecture/'s "two machines is also the maximum",
  Discord "guild" on /integrations/, and collection.mjs against what
  Steam sign-in and the Rust module keep (PLAY_DATA_SAFETY.md
  regenerated; the Play answers are unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-22 20:10:51 -05:00
parent ec65b2d5aa
commit 56473d4d51
32 changed files with 1924 additions and 451 deletions

View File

@@ -2,26 +2,27 @@
import Base from '../layouts/Base.astro';
import PageHeader from '../components/PageHeader.astro';
import NotBuilt from '../components/NotBuilt.astro';
import SupportedGames from '../components/SupportedGames.astro';
import platform from '../data/platform.json';
import { capabilityGroup } from '../data/capabilities.mjs';
import { games } from '../data/games/index.mjs';
/**
* `/modules/` — PLAN.md §13 phase 4.
* `/modules/` — PLAN.md §13 phase 4, restructured in §16 phase 17.
*
* §10 gives this page four jobs: what a module is, `module-uo` as the worked example,
* writing your own, and the Integration Kit with its draft badge (D8). They are in that
* order because they are increasing commitment — a reader deciding whether to install one,
* a reader wondering what they get, a reader considering building one.
* §10 gave this page four jobs: what a module is, a worked example, writing your own, and
* the Integration Kit with its draft badge (D8). They are in that order because they are
* increasing commitment — a reader deciding whether to install one, a reader wondering what
* they get, a reader considering building one.
*
* ---------------------------------------------------------------------------------------
* THE WORKED EXAMPLE READS ITS OWN CAPABILITIES
* TWO GAMES, EACH ON ITS OWN PAGE
* ---------------------------------------------------------------------------------------
* The `module-uo` section lists what the module publishes, and it takes that list from
* `capabilities.mjs` rather than retyping it — the same list the homepage names and
* `/features/` expands, which is already checked against the module's own manifest through
* `platform.json` (§12). A third hand-maintained copy on this page is exactly the failure
* that machinery exists to prevent, and this is the page where it would be least visible.
* The worked example was the Ultima Online module, listed in that game's words. Since phase
* 17 there are two modules, and D63 keeps a game's vocabulary on its own pages, so this page
* is core: a card per game, in core's words, from `games/index.mjs`, each linking to
* `/modules/uo/` or `/modules/rust/`, where the game's real feature list is (D75). What the
* modules share — a sidecar, their own tables, their own releases — stays here, said once.
*
* ---------------------------------------------------------------------------------------
* THE DRAFT CHIP IS A DECISION, NOT A DISCLAIMER
@@ -34,14 +35,12 @@ import { capabilityGroup } from '../data/capabilities.mjs';
*/
const title = 'Modules';
const description =
'What a module is, what the Ultima Online module publishes, and what it takes to write ' +
'one for another game.';
'What a module is, the two game modules that exist — Ultima Online and Rust — and what it ' +
'takes to write one for another game.';
const gitea = `${platform.gitea.base}/${platform.gitea.org}`;
const docs = `${gitea}/docs/src/branch/main`;
const gameIntelligence = capabilityGroup('game-intelligence');
/** The three ways a module reaches a running deployment. None of them is a build. */
const installPaths = [
{
@@ -81,8 +80,10 @@ const installPaths = [
<p>
That division is not an aspiration bolted on afterwards. The Ultima Online support was
extracted out of the site into a module, and every URL it had before the move it still
has — which is the only version of this claim worth making.
has — which is the only version of this claim worth making. Rust followed, built
against the same seam.
</p>
<SupportedGames class="mod__supported" />
</PageHeader>
<section class="page section mod" id="what">
@@ -146,60 +147,65 @@ const installPaths = [
</ol>
</section>
<section class="page section mod" id="module-uo">
<div class="mod__head">
<p class="eyebrow">The worked example</p>
<h2>module-uo</h2>
<span class="chip chip--version">{platform.releases['Module-uo']}</span>
</div>
<section class="page section mod" id="games">
<p class="eyebrow">The game modules</p>
<h2>Two games, one module each</h2>
<p class="prose mod__lede">
The Ultima Online module, and the reference every module that follows is measured
against. It is what turns a general-purpose community site into something that knows
what a shard is — and it is the proof that the seam described on
<a href="/architecture/">the architecture page</a> is real, because the code on the far
side of it was moved there rather than designed there.
Both were built inside this project, and a deployment runs one of them — the site
refuses to start a second. Each has a page of its own, in its own game's words, and a
documentation group that starts from installing it.
</p>
<div class="mod__example">
<div class="panel mod__caps">
<h3>What it publishes</h3>
<ul>
{gameIntelligence.items.map((item) => <li>{item.label}</li>)}
</ul>
<p class="mod__caps-note">
The same list <a href="/features/">features</a> expands, read from one file that is
checked against the module's own manifest on every build.
</p>
</div>
<ul class="mod__games">
{
games.map((game) => (
<li class="panel mod__game">
<div class="mod__head">
<h3>{game.name}</h3>
<span class="chip chip--version">
{game.release}
</span>
</div>
<p>{game.blurb}</p>
<div class="mod__game-links">
<a class="btn btn--primary" href={game.href}>
What the {game.name} module adds
</a>
<a class="btn btn--ghost" href={game.docs}>
Its documentation
</a>
</div>
</li>
))
}
</ul>
<div class="mod__facts">
<section>
<h3>It connects to a real server</h3>
<p>
The module talks to the sidecar beside your game server, not to the game. You
deploy that side with the installer and paste four values into the admin panel;
nothing here requires the game to exist, and with no server configured the site
renders normally and shows it offline.
</p>
</section>
<section>
<h3>It owns its own tables</h3>
<p>
Its schema is applied by the site on every boot and its data is its own. The
module declares which versions of the core interface it speaks — the site runs
{' '}{platform.moduleApi} — and refuses to load against one it does not.
</p>
</section>
<section>
<h3>It is a separate release</h3>
<p>
Versioned, tagged and published on its own cadence, independently of the site.
Upgrading one does not mean upgrading the other, as long as the declared interface
range still holds.
</p>
</section>
</div>
<div class="mod__facts mod__facts--shared">
<section>
<h3>Each connects to a real server</h3>
<p>
A module talks to the sidecar beside your game server, not to the game. You deploy
that side with the installer and give the admin panel its address and token;
nothing here requires the game to exist, and with no server configured the site
renders normally and shows it offline.
</p>
</section>
<section>
<h3>Each owns its own tables</h3>
<p>
Its schema is applied by the site on every boot and its data is its own. A module
declares which versions of the core interface it speaks — the site runs
{' '}{platform.moduleApi} — and refuses to load against one it does not.
</p>
</section>
<section>
<h3>Each is a separate release</h3>
<p>
Versioned, tagged and published on its own cadence, independently of the site.
Upgrading one does not mean upgrading the other, as long as the declared interface
range still holds.
</p>
</section>
</div>
</section>
@@ -239,9 +245,6 @@ const installPaths = [
<a class="btn btn--ghost" href={`${docs}/website/MODULE_API.md`} rel="noopener noreferrer">
The module contract
</a>
<a class="btn btn--ghost" href={`${docs}/modules/uo/README.md`} rel="noopener noreferrer">
module-uo in depth
</a>
</div>
</section>
@@ -287,7 +290,6 @@ const installPaths = [
.mod__grid h3,
.mod__paths h3,
.mod__caps h3,
.mod__facts h3,
.mod__draft h3 {
margin: 0 0 0.5rem;
@@ -317,55 +319,53 @@ const installPaths = [
font-style: italic;
}
.mod__example {
.mod__games {
display: grid;
gap: 1rem;
margin-top: 2rem;
grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
align-items: start;
}
.mod__caps ul {
margin: 0;
margin: 2rem 0 0;
padding: 0;
list-style: none;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
}
.mod__game {
display: flex;
flex-direction: column;
}
.mod__game h3 {
margin: 0;
color: var(--gold);
font-size: 1.25rem;
}
.mod__game p {
margin: 0.75rem 0 0;
color: var(--muted);
font-size: 0.94rem;
}
.mod__caps li {
position: relative;
padding-left: 1.1rem;
color: var(--text);
.mod__game-links {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin-top: auto;
padding-top: 1.25rem;
}
.mod__caps li + li {
margin-top: 0.3rem;
.mod__supported {
margin-top: 1.5rem;
}
/* The same drawn marker the homepage's capability lists use, so a reader who has
seen this list once recognises it as the same list. */
.mod__caps li::before {
content: '';
position: absolute;
left: 0;
top: 0.62em;
width: 5px;
height: 5px;
border-radius: var(--radius-pill);
background: var(--portal);
opacity: 0.75;
.mod__facts--shared {
display: grid;
gap: 1.4rem;
margin-top: 2.25rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}
.mod__caps-note {
margin: 1rem 0 0;
padding-top: 0.85rem;
border-top: 1px solid var(--line-soft);
color: var(--dim);
font-size: 0.85rem;
}
.mod__facts section + section {
margin-top: 1.4rem;
.mod__facts--shared section + section {
margin-top: 0;
}
.mod__facts p {
@@ -395,10 +395,4 @@ const installPaths = [
gap: 0.7rem;
margin-top: 1.75rem;
}
@media (max-width: 860px) {
.mod__example {
grid-template-columns: minmax(0, 1fr);
}
}
</style>