· 5 min read
Behind the screen
How this site is actually built — Astro islands, a Cloudflare Worker, server-rendered panels, and the reason there is no AI anywhere in it.
- astro
- cloudflare
- architecture
A short tour of the machinery, for anyone who opens dev tools before they read the copy.
The shape of it
Astro renders every page to static HTML at build time. The 3D city is a single
island mounted with client:only, which means React, Three.js and the physics
engine are in exactly one bundle that no other page pays for. Navigate to
/about and you download no JavaScript at all.
The whole thing is served from a Cloudflare Worker. Static assets on Workers are free and unlimited, and the two routes that need a server — the contact endpoint and the vitals beacon — run at the edge with a D1 database behind them.
The trick I am most pleased with
The nine district panels are not rendered by the game.
They are prerendered into the homepage’s initial HTML as hidden <section>
elements. When you walk up to Genora Tower and press E, the island removes the
hidden attribute on #panel-genora and traps focus inside it. It does not
fetch anything, it does not render anything, and it costs zero kilobytes of
additional JavaScript.
Three things fall out of that, and all three are the reason it is built this way:
- It works with JavaScript off. The panels are just sections on the page.
- Crawlers see everything. Google is reading the same nodes you are.
- Content parity is physical, not disciplinary. There is no second copy of the project write-ups living in a JS bundle that someone forgets to update. The classic site and the game are the same HTML.
The alternative — fetching panel content when a district is entered — is more obvious, and worse in every one of those three ways.
Two zod instances, on purpose
The MDX frontmatter is validated by Astro’s own bundled zod, via
src/content.config.ts. The structured game data in src/data/ is validated by
a directly-installed zod, via src/data/validate.ts.
They are kept strictly apart. A schema built by one zod instance passed into an API expecting the other produces an error message that will cost you an afternoon.
There is a hard rule attached to this: nothing in src/data/ may import
anything from Astro. Content collections are a build-time API and cannot exist
inside a client:only island, but the zone coordinates and blueprint list must
be readable by both the prerendered pages and the browser bundle. So that
directory is plain TypeScript, and it stays that way.
What the validator catches
npm run validate:data parses every dataset, then checks the invariants a
per-file schema cannot see. The useful ones are the cross-file checks:
- A blueprint pointing at a district that does not exist.
- A district flagged as yielding a blueprint with no blueprint assigned to it — which would ship a collectible that silently never spawns.
- Two districts whose interact sensors overlap, so a single
Epress would be ambiguous. This one is pure geometry over the coordinates, and it is the check most likely to catch a real mistake when the city gets rearranged.
None of these are type errors. All of them would otherwise surface as a confusing gameplay bug weeks later.
The build gates
npm run verify runs the data validator, then a no-AI check, then astro check,
then the build, then a check against the built output.
The no-AI check scans package.json against a blocklist of AI SDK package
names and greps the source for provider endpoint hostnames. It matches
dependency names and hostnames exactly — never prose — so writing “AI-powered
tools” in a project description does not fail the build, but adding openai to
the dependency list does. The constraint is enforced by CI rather than by
memory.
The dist check asserts things about the HTML that actually shipped: zero
<canvas> elements in the static output, every district’s panel id present as a
real element on /, and content parity between a panel and its standalone
project page.
Performance posture
A GPU tier check runs on load and picks a starting quality level. From there, frame timings feed an eight-step bail-out ladder — shadow resolution, then draw distance, then post-processing, and so on down to unmounting the canvas altogether and leaving you with the HTML that was underneath the whole time.
On iOS, device pixel ratio is capped at 1.5 and the texture budget at 64 MB, because Safari will terminate a tab that exceeds its memory allowance without raising anything you can catch.
And no AI
No chatbot. No assistant. No model call at runtime. No AI package in
package.json, and a CI gate that fails the build if one appears.
The guide drone that follows you around is roughly twenty lines: filter the districts you have not visited, sort by distance, fly toward the nearest one, lerp toward the player when you get too far away, and say a line from a hardcoded list when you arrive. It cannot hallucinate, it costs nothing per visitor, and it works with the network disconnected.
The eight questions a recruiter tends to ask are answered in the Dossier as
authored <details> elements. I wrote them. They are correct because I checked
them, not because a model was confident.
Full dependency list and asset credits are in CREDITS.md in the repository.