graph-igo

Client-side routing

Three URLs, one router — running in the browser. Each is still its own static file.

server-rendered — the file the build wrote, before any JavaScript ran

run() owns the whole body

The counter above is the evidence: click between the views with the network panel open and nothing goes out. The URL still changes, and back and forward still work, because the runtime drives all of it through the browser's Navigation API.

The part to know before copying this: run() renders into <body> and clears what was there. So the site's shell — the header you are looking at, and the footer — is rendered by this router too, through the transform passed to render(). And its links carry data-rmx-document: without it a click on Blog would be routed by this router, which has never heard of that URL.

The same rule runs the other way, and that half is easier to miss: the SPA link in the header is a document load from every page on this site. A document gets one runtime and the first one to start keeps it, so arriving here by a soft navigation would drop this router into a document hydration.ts already owns — run() would never take over, and every link on this page would quietly go back to fetching whole pages. This demo shipped with exactly that bug.