All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
PLAN.md §7: swapping a logo or recolouring the site is a file copy and a container restart, never a rebuild. Phase 2 builds the mechanism and the checks that keep it true. GET /brand/* resolves every file against the mount first and the baked-in defaults second, per file, at stable unhashed URLs with an ETag and a five minute TTL. Nothing goes through Vite, which would fingerprint the names out of the mount's reach. An X-Brand-Source header says which step answered. Three decisions were taken with the org lead (recorded as D14-D16 in §7): D14 — one raster in, every size out. brand-default holds a single logo.png; the header mark at three pixel ratios, both install icons, the apple-touch icon, the favicons and a real multi-resolution favicon.ico are derived on request from whichever logo.png is in force, cached, and limited to an allowlist of sizes. Shipping fifteen precomputed files would have meant an operator producing fifteen to change a mark — and getting a new header with the old favicon. D15 — brand text is applied at boot. Pages are prerendered, so §7's promise about the site name, tagline and links could not hold at render time. npm start now runs scripts/applyBrand.mjs first, rewriting the built HTML from what it last applied to what the mount says. It rewrites from a record in dist/.brand-applied.json rather than from the defaults, because the naive version works exactly once and then silently ignores every later edit. An empty mount is a no-op; removing a mount restores the stock build byte for byte. Verified both ways, plus a second rename. D16 — the header shows the real emblem, replacing phase 1's placeholder glyph, so the site, the product and the Android launcher icon are one mark. It is raster art, so theme.css cannot recolour it; replacing logo.png is how the mark changes. Two defects found and fixed while proving it: The mounted theme.css did not win. Astro emits its own stylesheet after the head markup, so linking the operator's last was not enough and every override was silently a no-op. tokens.css now lives in @layer tokens and the mounted file is unlayered, which takes order out of the mechanism entirely. The documentation was a different site. Starlight builds its own head, so the docs linked a Starlight default /favicon.svg that does not exist here, carried no manifest or OG card, and never loaded the brand stylesheet — a mounted theme recoloured the marketing pages and left the docs stock. A Head override fixes it; half a rebrand looks like a product bug rather than a missed step. brand-default/wordmark.svg and og-image.png are generated by scripts/buildBrandAssets.mjs from the emblem and Cinzel's outlines and are committed, so CI needs neither the artwork nor a font. Type is converted to paths, because an SVG in an <img> can see neither the page's @font-face rules nor fontconfig — the same isolation that broke currentColor in phase 1. Its glyphs are drawn at the origin and translated: opentype.js emits NaN coordinates at a non-zero origin for some glyphs, and a path parser stops at the first malformed command, so the first lockup read "Runic Gate" and looked like a typo rather than a bug. scripts/checkBrand.mjs is the mechanism for the two failures that are otherwise silent: it puts every literal /brand/... URL in the source through the route's own classifier, so a size that is not on the allowlist fails the build instead of 404ing in a browser, and it rejects a brand string short enough that a blind replacement at boot could corrupt a page. Negative-tested three ways before being trusted. It runs in CI ahead of the type check. Co-Authored-By: Claude <noreply@anthropic.com>
147 lines
7.7 KiB
Markdown
147 lines
7.7 KiB
Markdown
# runicgateway.com
|
|
|
|
The public marketing and documentation site for [Runic Gateway][org] — the platform that puts a
|
|
private game server's live state on a public website without ever exposing the game to the internet.
|
|
|
|
Two audiences: **server administrators** who want to understand and install the platform, and
|
|
**developers** who want to build modules and integrations for it. A third arrives with the Android
|
|
closed beta: **players**, who want the app.
|
|
|
|
**The design of record is [`PLAN.md`](PLAN.md).** It is not a sketch — it carries the verified
|
|
platform state, the org lead's decisions, the information architecture, and the build phases. Read
|
|
it before changing anything here.
|
|
|
|
**Status: phase 1 of 12 — the foundation.** The scaffold, the token file, the typography, the layout
|
|
shell and the two build-time checks are in place. The homepage is phase 3, the marketing pages
|
|
phase 4, and the documentation — the installation path, which is the priority of the whole project —
|
|
phase 7.
|
|
|
|
---
|
|
|
|
## Running it
|
|
|
|
```bash
|
|
npm install
|
|
npm run dev # http://localhost:4321
|
|
```
|
|
|
|
```bash
|
|
npm run build # → dist/ (prerendered pages + the Node server entry)
|
|
npm start # serve the built site
|
|
```
|
|
|
|
Node 22 LTS or newer.
|
|
|
|
## The checks, and why they are not optional
|
|
|
|
Two of them, both from `PLAN.md` §12. Neither is a linter; each one enforces a promise the site
|
|
makes that would otherwise decay quietly.
|
|
|
|
```bash
|
|
npm run check:tokens # no colour literal outside the token file
|
|
GITEA_TOKEN=<token> npm run check:facts # every version agrees with its authority
|
|
npm run check # astro check
|
|
npm run verify # all of the above, then a production build
|
|
```
|
|
|
|
**`checkFacts.mjs`** re-reads every version, protocol number and bundle tag in
|
|
`src/data/platform.json` from its source of truth over the Gitea API — the sidecar's
|
|
`PROTOCOL_VERSION`, the overlay's `overlay.toml`, the website's `MODULE_API_VERSION`, the installer's
|
|
published bundle manifest, and each repository's latest release — and fails on any disagreement.
|
|
|
|
When the platform moves, this repository goes red so that someone updates the site. **That failure is
|
|
the feature**, the same mechanism and the same intent as the Integration Kit's `checkCoreApi.js`. §1
|
|
of the plan records what it is guarding against: an earlier draft confidently stated the platform was
|
|
on protocol 3, because every checkout in the workspace sat on a feature branch whose local `main` had
|
|
never been fetched.
|
|
|
|
It needs a token — anonymous raw fetches fail on this Gitea instance, and a check that silently skips
|
|
itself is worse than no check at all. It must be able to read the other repositories in the
|
|
organisation, not just this one. CI already has this: the workflow maps the org-level
|
|
`REGISTRY_TOKEN` secret into `GITEA_TOKEN` for that step.
|
|
|
|
**`checkTokens.mjs`** fails the build if a colour literal appears anywhere in `src/` outside
|
|
`src/styles/tokens.css`. §7 promises that recolouring the site is a file copy and a container
|
|
restart; a mounted `theme.css` can only redefine custom properties, so a literal in a component is a
|
|
piece of the site an operator can never reach. Without the check, "one CSS file changes the
|
|
appearance" becomes "one CSS file changes most of the appearance".
|
|
|
|
**`checkBrand.mjs`** guards the two things about the branding pipeline that fail quietly. It puts
|
|
every literal `/brand/...` URL in the source through the route's own classifier, so a template
|
|
asking for a size that is not on the allowlist fails the build rather than 404ing in a browser; and
|
|
it refuses a brand string short enough that replacing it blindly at boot could corrupt a page.
|
|
|
|
## Branding is bind-mounted data
|
|
|
|
`brand-default/` is baked into the image and always complete. `brand/` is the bind mount and may be
|
|
empty, partial or full. **Every file resolves against the mount first and the defaults second, per
|
|
file**, so overriding only `theme.css` leaves every logo stock and an empty mount produces exactly
|
|
the stock site. Nothing here goes through Vite, which would fingerprint the filenames into the build
|
|
and put them out of the mount's reach.
|
|
|
|
**Rebranding is one file.** `brand-default/` holds a single raster — `logo.png` — and `GET /brand/*`
|
|
derives every size the site asks for from whichever `logo.png` is in force: the header mark at three
|
|
pixel ratios, the install icons, the apple-touch icon, the favicons and a real multi-resolution
|
|
`favicon.ico`. Drop in one file, restart, and the browser tab and the installed icon change with the
|
|
header.
|
|
|
|
**Brand text is applied at boot.** Pages are prerendered, so the site name, tagline and links are
|
|
baked into HTML that a mounted file cannot reach. `npm start` runs `scripts/applyBrand.mjs` first,
|
|
which rewrites the built HTML from what it last applied to what the mount now says — recorded in
|
|
`dist/.brand-applied.json`, so the second edit works as well as the first. An empty mount makes it a
|
|
no-op.
|
|
|
|
**A mounted `theme.css` wins by cascade layer, not by link order.** `tokens.css` is inside
|
|
`@layer tokens`; the mounted stylesheet is unlayered and therefore beats it wherever the browser
|
|
encounters it. Do not "fix" this by reordering the links — Astro emits its own stylesheet after the
|
|
head markup, which is what made the ordering approach silently useless.
|
|
|
|
To try it: put a `theme.css`, a `logo.png` or a `brand.json` in `brand/`, run `npm run build` and
|
|
`npm start`. `curl -I` any `/brand/*` URL and the `X-Brand-Source` header says which of mount,
|
|
defaults or derivation answered.
|
|
|
|
Regenerating the stock assets is a separate, manual step — `npm run brand:assets` — because it reads
|
|
the emblem and the Cinzel outlines from the sibling checkouts in the workspace. Its output is
|
|
committed so that CI never needs either.
|
|
|
|
## Layout
|
|
|
|
```
|
|
src/
|
|
data/platform.json Every externally-sourced fact. No version is written in prose.
|
|
styles/tokens.css THE token file — the only place a colour literal may appear.
|
|
styles/global.css The layout shell, built entirely from tokens.
|
|
styles/starlight.css Restates our tokens as Starlight's, so the docs cannot drift.
|
|
layouts/, components/ The marketing chrome.
|
|
pages/ Marketing routes.
|
|
content/docs/docs/ Documentation. The extra level mounts Starlight at /docs.
|
|
pages/brand/ GET /brand/* — the mount, resolved and derived. Runs per request.
|
|
lib/brand.mjs The single accessor for brand text.
|
|
lib/brandAssets.mjs Mount-first resolution and on-demand derivation.
|
|
lib/tokens.mjs Reads tokens.css at build time, for the few values that leave CSS.
|
|
config/sidebar.mjs The documentation journey, and the planned tree behind it.
|
|
brand-default/ The stock brand, baked into the image and always complete.
|
|
scripts/ The build-time checks, plus applyBrand (boot) and brand:assets (manual).
|
|
```
|
|
|
|
Two directories are bind mounts at runtime and are **not** in the repository: `brand/` overrides
|
|
anything in `brand-default/` per file, and `data/` holds the beta signup store. See `PLAN.md` §6
|
|
and §7.
|
|
|
|
## Contributing
|
|
|
|
Branch from `main` (`feature/…`, `fix/…`, `docs/…`, `chore/…`) and use
|
|
[Conventional Commits](https://www.conventionalcommits.org/). Run `npm run verify` before opening a
|
|
pull request.
|
|
|
|
**AI-assisted contributions must be disclosed**, per org policy: tick the box in the pull request
|
|
template naming the tool, and mark AI-authored commits with a trailer such as
|
|
`Co-Authored-By: Claude <noreply@anthropic.com>`. Undisclosed AI-generated contributions may be
|
|
closed.
|
|
|
|
## Licence
|
|
|
|
GPL-3.0-or-later, in common with every repository in the organisation. See [LICENSE](LICENSE).
|
|
|
|
[org]: https://gitea.whitlocktech.com/RunicGateway
|