Files
runicgateway.com/src/pages/architecture.astro
wtclaude 2d19ee4220
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
feat(marketing): phase 4 — the marketing pages
PLAN.md §13 phase 4: /features/, /architecture/, /modules/, /integrations/, and
/community/ — plus the two scope items the phase table never assigned to anyone.

Six decisions taken by the org lead before coding, recorded in PLAN.md §10 as
D20-D25:

- D20 /features/ is the homepage's list with a `detail` line, not a second list.
  One data file, two renderings, so they cannot disagree about what exists.
- D21 /architecture/ draws reasons, not reference: three new inline SVGs, one per
  boundary. No endpoint tables, no config keys — those are phase 8's and stay
  canonical in docs/.
- D22 The deliberate absences of §2 become one tagged data file, rendered on the
  three pages that promise them.
- D23 Phase 4 absorbs /community/ (specified in §10 and §14 N3, linked from the
  header since phase 1, built by no phase) and checkLinks.mjs.
- D24 `needsModule`: writing the Teams detail exposed a false claim phase 3
  shipped. Teams are module-sourced only — teams.module_id is NOT NULL, there is
  no create route, sync is gated on providerModuleId() — so the Community group
  no longer says a bare core does all of it.
- D25 The per-capability demo affordance brand.json had promised since phase 2 is
  a deep link, filled at boot from data-demo-path.

checkLinks.mjs reads the built HTML rather than src/, because half these links
are assembled from data files and template literals. Its PLANNED_ROUTES list is
checked in both directions, so it cannot rot into a permanent exemption.

applyBrand.mjs gained a pass that recomputes deep links from their immutable
path, making it idempotent and reversible; checkBrand.mjs lifts that pattern out
and runs it against the stock markup so the two cannot drift. Both proved
against a real mount, in both directions.

Fixes a cascade bug the checks could not see: [data-demo-url=''] and a scoped
component class are both specificity 0,1,0, so .demo-link's `display` beat the
hide rule and twelve links to a nonexistent demo rendered, each resolving to the
current page. The rule is now !important.

The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css.

Verified from a clean checkout: npm ci, all five checks, astro check (0 errors),
production build, and a live browser pass at desktop and 390px.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 01:53:11 -05:00

173 lines
6.2 KiB
Plaintext

---
import Base from '../layouts/Base.astro';
import PageHeader from '../components/PageHeader.astro';
import TwoHosts from '../components/architecture/TwoHosts.astro';
import Allowlist from '../components/architecture/Allowlist.astro';
import ModuleSeam from '../components/architecture/ModuleSeam.astro';
import platform from '../data/platform.json';
import { brand } from '../lib/brand.mjs';
/**
* `/architecture/` — PLAN.md §13 phase 4, built to D21.
*
* ---------------------------------------------------------------------------------------
* WHAT THIS PAGE IS FOR, AND WHAT IT DELIBERATELY IS NOT
* ---------------------------------------------------------------------------------------
* §10 gives it one audience: "a technical evaluator deciding whether to run it". That is a
* narrower job than "explain the system", and the narrowness is what keeps this page from
* becoming a worse copy of the Architecture section in the documentation, which phases 7
* and 8 write.
*
* So the page answers four questions an evaluator actually has, in the order they have
* them — what am I deploying, what leaves my server, what is core and what is a module,
* and what happens when a part of it dies — and it answers them with drawings and reasons.
* It carries no endpoint tables, no configuration keys, no schema and no event catalog.
* Those exist, they are canonical elsewhere, and a second copy here would be a copy that
* goes stale (§1). Every one of them is a link out.
*
* The three diagrams are §11's motif doing actual work rather than decoration: each one
* draws a boundary, and the boundary is the argument in all three cases. The vocabulary
* they share lives in `src/styles/diagram.css`.
*
* ---------------------------------------------------------------------------------------
* LINKS OUT GO TO `/docs/`, NOT TO A GUESSED SLUG
* ---------------------------------------------------------------------------------------
* The same convention phase 3 set for the homepage: phases 7 and 8 own the documentation
* slugs, so linking `/docs/architecture/the-bridge/` today would put a URL in this file
* that nothing checks and a later phase would have to remember to fix. Links into the
* repositories are different — those are real paths that exist now, and `checkLinks.mjs`
* holds them to a branch path rather than a commit permalink.
*/
const title = 'Architecture';
const description =
'How Runic Gateway is put together: what you deploy, what crosses the network, and where ' +
'the game-specific half stops.';
const docs = `${platform.gitea.base}/${platform.gitea.org}/docs/src/branch/main`;
---
<Base title={title} description={description}>
<PageHeader eyebrow="How it is built" title="The parts, and the lines between them">
<p>
Three boundaries decide almost everything about how this software behaves: the one
between your two machines, the one between what the public sees and what staff see, and
the one between the platform and the game. Each is drawn below, with the reasoning
rather than the reference.
</p>
<p>
Nothing here is a specification. Where a real one exists it is linked — the protocol,
the module contract and the operator guide are all documents in the open, and they are
the authority when this page and one of them disagree.
</p>
</PageHeader>
<TwoHosts />
<Allowlist />
<ModuleSeam />
<section class="page section deeper">
<div class="panel deeper__panel">
<p class="eyebrow">Going deeper</p>
<h2>The documents this page is a summary of</h2>
<p class="prose deeper__lede">
Everything above is an argument about shapes. These are the things that specify them,
and they are what a module author, an integrator or an operator should be reading.
</p>
<ul class="deeper__list">
<li>
<a href={`${docs}/link/INTEGRATION.md`} rel="noopener noreferrer">
The bridge protocol
</a>
<span
>What the game and the sidecar say to each other, and what the sidecar publishes.
Protocol {platform.protocol} today, and versioned so a mismatched pair is refused
rather than misread.</span
>
</li>
<li>
<a href={`${docs}/website/MODULE_API.md`} rel="noopener noreferrer">
The module contract
</a>
<span
>The normative interface between core and a module — currently
{platform.moduleApi}. This is the document that decides whether your module
loads.</span
>
</li>
<li>
<a href={`${docs}/installer/INSTALL.md`} rel="noopener noreferrer">
The operator guide
</a>
<span
>Setting the game side up end to end, including the failure modes and what each
step should look like when it worked.</span
>
</li>
<li>
<a href="/docs/">The documentation on this site</a>
<span
>The same ground as a guided path rather than a specification, starting from an
empty server.</span
>
</li>
</ul>
<div class="deeper__actions">
<a class="btn btn--primary" href="/docs/">Start the install guide</a>
<a class="btn btn--ghost" href="/modules/">How modules work</a>
<a class="btn btn--ghost" href={brand.giteaOrg} rel="noopener noreferrer">
Read the source
</a>
</div>
</div>
</section>
</Base>
<style>
.deeper__panel {
padding: clamp(1.5rem, 4vw, 2.75rem);
}
.deeper h2 {
margin: 0 0 0.75rem;
font-size: clamp(1.5rem, 3vw, 2rem);
}
.deeper__lede {
margin: 0;
color: var(--muted);
}
.deeper__list {
margin: 1.75rem 0 0;
padding: 0;
list-style: none;
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}
.deeper__list li {
display: flex;
flex-direction: column;
gap: 0.3rem;
padding-left: 0.9rem;
border-left: 2px solid var(--gold-deep);
}
.deeper__list span {
color: var(--dim);
font-size: 0.9rem;
}
.deeper__actions {
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
margin-top: 2rem;
}
</style>