Skip to content

Add the website: a guide for people, with running games and compiled snippets - #61

Merged
SandroMaglione merged 7 commits into
mainfrom
claude/project-thread-pi2uzm
Oct 23, 2026
Merged

SandroMaglione merged 7 commits into
mainfrom
claude/project-thread-pi2uzm

Conversation

@SandroMaglione

@SandroMaglione SandroMaglione commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Requested by Sandro · project thread

This PR adds a docs website for Ritmo, written for humans. This description is the hand-off document: an agent picking the work up should be able to finish it from what is written here, the code on this branch, and website/README.md.

1. Requirements (from Sandro, the owner)

These are paraphrased from the original request and have not been relaxed:

  1. A documentation website for humans. It is a place to point real people for a reference to the library. It is not for coding agents: agents keep reading what npm ships (JSDoc, package READMEs, CHANGELOGs, SETUP.md, the skill).
  2. No internals and no full API reference. Leave out quirks, type-level implementation details, and generated docs for every type. Explain what the library does and how to use it.
  3. Brand. Use the official logo and assets everywhere, with the colours of the official branding (brand/README.md).
  4. Embedded games. Games built with the library run directly inside the pages, so a page can point at a real example running in the docs.
  5. Code snippets with type information. Errors are shown inside the snippet, to demonstrate how Ritmo avoids bugs through type safety and compile-time checks.
  6. Lives in the library's main repository (typeonce-dev/ritmo).
  7. Domain: a subdomain of the organisation's existing domain. The choice, accepted by Sandro, is ritmo.typeonce.dev.

Project-wide constraint: Ritmo must never depend on Effect. That applies to the published packages, and it is also why the deploy uses plain wrangler instead of Alchemy, which is Effect-based and what the Spirit Call game uses.

2. Decisions taken (and why)

Decision Why
Plain Astro 7 + MDX, not Starlight or VitePress Starlight renders code through Expressive Code, whose Twoslash plugin only supports TS ^5.5. Plain Astro also gives full control over the brand.
Shiki Twoslash (@shikijs/twoslash 4.5, twoslash 0.3.9) for snippets It runs the real compiler: hover types and JSDoc, ^? queries, // @errors: lines.
The website pins typescript 6.0.3, while the repo is on TS 7.0.2 Twoslash needs the JS compiler API (^5.5 || ^6), which native TS 7 does not have. Ritmo supports TS ≥ 5.9, so the errors match what games see.
Snippets compile against the workspace source (workspace:* deps) A broken docs example fails the build, so an API change must fix the docs in the same PR.
Errors are condensed to Ritmo's own fix message (website/twoslash.mjs) Ritmo embeds the fix as a string at the end of a long type error (Fix<"…">, __fixTickOrder__, __schemaConflicts__, __runtimeRequirementError__). The page shows only that message, and the copy says so honestly. Absolute build paths are stripped from hovers.
Games are embedded through an iframe of /play/<id>/ Keyboard.actions(window, …) and Gamepad.actions(window, …) listen on window. A frame isolates input and crashes, and lets several games share a page. Frames are lazy-loaded and start on click, which also gives the frame focus.
Demos reuse integration/top-down unchanged and add only a browser host and a Pixi renderer The simulation stays the tested, headless one. The page shows real library usage (FixedLoop, BrowserTickSource, Controls, InputCapture, RenderSync, PixiNodeRegistry).
Pagefind for search It is static and needs no service. Code popups and error lines are excluded from the index (pagefind.yml).
Cloudflare static assets via wrangler (website/wrangler.jsonc) No Worker code, no Effect, and the custom domain route is ritmo.typeonce.dev.
Deploy after each npm publish (in release.yml) and by hand from the Website workflow The site documents what is on npm, not unreleased main.
Brand assets are imported from brand/assets, never copied (src/brand.ts) One source of truth. The generators in brand/ stay the only place assets are made.

3. What is done in this PR

Package: website/ is a private workspace package, added to pnpm-workspace.yaml. It is not in the changesets fixed group and is not published. The root tsconfig, vitest, and pnpm check don't include it.

Pages (website/src/pages):

  • /: hero with the running game, install command, three pillars, a compiled tick-order error, and the "Ritmo does not draw" section.
  • /docs/: Getting started (install, the packages table, first world, a system, running a tick).
  • /docs/with-an-agent/: the agent-first pitch, the SETUP.md prompt, the skill and its principles.
  • /docs/concepts/: world, systems, queries, commands and applyDeferred, schedules and the tick, state machines, the fixed step and determinism.
  • /docs/type-safety/: a gallery of compile errors, each with its fix message: undeclared write, undeclared spawn, missing required component, raw id despawn, tick order, missing service, duplicate names, Clock.hz(59.94) and a state typo, and Result from Runtime.make.
  • /docs/browser/: the embedded game, FixedLoop + BrowserTickSource, Controls / Keyboard / Gamepad / InputCapture, and RenderSync with Pixi.
  • /docs/devtools/: a scripted Session run of the top-down game with an invariant, real why() and lint output, a table of session calls, the lint and oxlint checks, and Determinism.check.
  • /examples/: the top-down game, plus links to Spirit Call (spirit-call.typeonce.dev) and Inkline Town.
  • /brand/: logos with downloads, the palette, type, and rules.
  • /play/top-down/: the bare game page that gets embedded.
  • 404.

Components:

  • Header: lockup, nav, search dialog, theme toggle.
  • Docs layout: sidebar, prev/next, "edit on GitHub".
  • Game: the iframe embed.
  • BrandAssets and Palette.
  • TypePopups: places Twoslash hover popups with position: fixed so code blocks that scroll sideways don't clip them.
  • Footer.

Styling:

  • Brand tokens live in src/styles/global.css. Ink is the default; Paper is the light theme, chosen by prefers-color-scheme or the toggle stored in localStorage.
  • Code blocks always sit on a dark surface, with ligatures off.
  • The Shiki theme src/shiki/ritmo.json is built from the palette (Tick for keywords and errors).
  • Twoslash overrides are in src/styles/twoslash.css.
  • Fonts: JetBrains Mono and Inter from Google Fonts.

Demo (src/demos/top-down/):

  • game.ts (mount(host)): Pixi app, world scaled to fit, runtime with input and node-registry services, FixedLoop, and a HUD read through a Game.Inspector.
  • render.ts: one RenderSync.system each for walls, pickups, and the player.
  • Demos are registered in src/demos/index.ts.

CI and deploy:

  • .github/workflows/website.yml runs on Node 24, because Astro needs ≥ 22.23.4. On every PR and push it runs typecheck (astro sync && tsc) and build (astro build && pagefind). Its workflow_dispatch takes a deploy input (main only).
  • release.yml: the changesets step has id: changesets. When published == 'true', the job builds and runs pnpm --dir website run deploy:cloudflare.

Repo rules: AGENTS.md (which CLAUDE.md symlinks to) now names website/ in Repo, adds the website build to Verify, and replaces "the root README is the only doc for humans" with the website rule.

Verified:

  • pnpm check passes.
  • pnpm --dir website typecheck and build pass.
  • CI is green: Build the website, Typecheck and tests, Performance regressions.
  • wrangler deploy --dry-run reads the build.
  • In headless Chromium:
    • the game starts on click and moves with the keyboard inside the iframe;
    • hover popups show JSDoc;
    • error lines wrap;
    • light and dark themes work;
    • no page scrolls horizontally at 390px.

4. What remains (in priority order)

  1. Deploy secrets (Sandro will provide). Add repo secrets CLOUDFLARE_API_TOKEN (Workers Scripts edit on the account, Workers Routes edit on the typeonce.dev zone) and CLOUDFLARE_ACCOUNT_ID. The release job runs in the npm environment, so the secrets must be visible there. Then deploy once by hand: run the Website workflow on main with "Deploy" ticked, and check https://ritmo.typeonce.dev. Note that release automation only runs while the repo variable RITMO_RELEASES_ENABLED is true.

  2. Link the site from the root README.md (and optionally each package's homepage in package.json) once it is live. This was left out so the link doesn't 404 before the first deploy. Changing homepage changes what a package publishes, so it needs a changeset and pnpm pack:check.

  3. Touch controls for the demo. On a phone, the game can be seen but not played: top-down binds only keyboard and gamepad. Add Touch from @typeonce/ritmo-browser (a floating stick and an interact button) to the demo host, merged into the same Controls actions. Keep it in website/src/demos/top-down/ or extend integration/top-down/types.ts if the simulation's bindings should own it. Update the controls text in src/demos/index.ts.

  4. More embedded games. Requirement 4 asks for games in each page where useful. To add a demo:

    • create src/demos/<id>/game.ts exporting mount(host: HTMLElement): Promise<() => void>;
    • register it in src/demos/index.ts (title, description, controls, source, load);
    • embed it with <Game id="<id>" />.

    Good candidates: a tiny demo per concept (commands spawning a wave, a state machine pausing, determinism replaying a recorded input side by side), or integration/algorithms.

  5. Optional: embed Spirit Call on /examples/ (an iframe of spirit-call.typeonce.dev; check that its headers allow framing, and note it pins Ritmo 0.9), a social card image (brand/assets/ritmo-lockup-stacked*.svg rasterised for og:image; it currently uses the 512px app icon), and self-hosted fonts instead of Google Fonts.

5. Gotchas

  • Node: Astro 7 refuses to run below Node 22.23.4. The root CI (ci.yml) stays on Node 22; only the website job uses 24.
  • Twoslash flags must start a line. // @errors: 2345 placed inside an indented block is ignored and the build fails with "errors were not marked as expected". Put flags right after // ---cut--- at column 0. One @errors line can list several codes.
  • Unexpected errors fail the build (throws: true). This is intended: it's the check that docs match the API.
  • Error condensing lives in website/twoslash.mjs (condense). A new Ritmo error shape (a new __fix…__ key) is caught by the __fix\w+__ pattern. Anything else falls back to the full message with paths stripped. Twoslash's result.errors is a getter over result.nodes, so rewrite nodes, not errors.
  • astro sync before tsc. .astro/types.d.ts types the .svg/.png imports. Without it the typecheck fails with TS2307 (this was the first CI failure on this PR).
  • Brand imports go up two levels (../../brand/assets/... from src/). vite.server.fs.allow: [".."] permits that in dev.
  • The pnpm script is deploy:cloudflare, not deploy, because pnpm deploy is a built-in pnpm command.
  • Docs-page "Edit on GitHub" links assume each docs page is src/pages/<path>.mdx (/docs/ maps to docs/index.mdx).
  • Repo writing rules apply to page copy (brand/README.md): say step, tick, order, exact, checked, declared, deterministic, replay; no musical puns. Player-facing and docs copy uses no gendered pronouns.

Commands

pnpm install --frozen-lockfile
pnpm --dir website dev          # http://localhost:4321 (Node ≥ 22.23.4)
pnpm --dir website typecheck
pnpm --dir website build        # dist/ + Pagefind index
pnpm --dir website preview
pnpm check                      # the library's own check, unaffected by the website

🤖 Generated with Claude Code

https://claude.ai/code/session_014No3129r6nXFycnzdAEknt

…snippets

The root README was the only human doc. The website (Astro, MDX) explains the
library with the brand's palette and assets, embeds the top-down game running
in the page, and compiles every `ts twoslash` snippet against the workspace
source, so its types, JSDoc, and errors are the compiler's and a broken
example fails the build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014No3129r6nXFycnzdAEknt
The build is the check that every docs snippet still compiles. Deploying from
the release job keeps the site in step with what is on npm; the Website
workflow can also deploy by hand.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014No3129r6nXFycnzdAEknt
A change that breaks a snippet or demo fixes it in the same change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014No3129r6nXFycnzdAEknt
@SandroMaglione SandroMaglione self-assigned this Oct 23, 2026
claude and others added 4 commits October 23, 2026 00:30
CI runs the typecheck before any build, so .astro/types.d.ts (which types the
brand asset imports) did not exist yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014No3129r6nXFycnzdAEknt
Walk a small town tile by tile, read the signs, and talk to three
villagers. The rules are headless systems with a Mode state machine
(Exploring / Talking); Pixi only draws. check.ts drives it with scripted
input and an invariant checked every frame.

Art and font: the CC0 Ninja Adventure pack by Pixel-boy and AAA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Homepage: the brand animation in the hero, the Town demo below it with
  a "Play the demo" button that opens it full screen, three steps to a
  running game, and a mistakes explorer with compiled errors
- Code blocks: file titles, highlighted and focused lines, diffs, a
  richer theme from the brand palette, and no more empty scroll area
  from hidden type popups (queries render as lines, popups are fixed)
- Docs: shorter prose, more comments and spacing in snippets, and a fix
  diff after each type-safety error
- Full screen enlarges the game in place instead of opening a new page
- Header and footer alignment, no chips above titles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…demo

main (#63) groups devices in Controls: the top-down controls now take a
keyboard, a mouse, and a gamepad. The browser host binds Pointer actions
on its canvas, and the devtools page's scripted run passes a scripted
mouse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SandroMaglione
SandroMaglione merged commit 10c614a into main Oct 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants