runicgateway.com

The public marketing and documentation site for Runic Gateway — 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. 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

npm install
npm run dev          # http://localhost:4321
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.

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. 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.

Description
No description provided
Readme GPL-3.0 4.3 MiB
Languages
JavaScript 45.3%
MDX 26.7%
Astro 22.4%
CSS 4%
TypeScript 0.8%
Other 0.8%