Client-side navigation between hydrated pages
Move between hydrated pages without a full
Purpose
Move between hydrated pages without a full document load (GitHub issue #37: console screens realm → clients → client). Builds on the page loader() and bootstrap payload — no second data API.
Public surface
import { Link, navigate, prefetch } from "patties/client";
<Link href="/realms/acme/clients">Clients</Link> // prefetch="intent" by default
await navigate("/realms/acme", { replace: true });
Link renders a plain <a>, so it SSRs and degrades to normal navigation on zero-JS and island pages.
Behavior
- A request carrying
x-patties-nav: 1to a page route runs middleware andloader()as usual but answers JSON instead of HTML:{ hydrated: true, route, script, bootstrap: { params, data? }, title? }, or{ hydrated: false }for a route without a page bundle. Linkintercepts unmodified primary clicks (no modifier keys, notarget/download) when the document booted a hydrated page and the target is same-origin. Hover/focus warms the payload and chunk (prefetch).navigatefetches the payload (following redirects — the final URL goes in the address bar),import()s the page chunk unless already registered, pushes history, setsdocument.titlefrommeta.title, and callsroot.render(<Page key={route} {...props} />)on the existing root. Moving between routes remounts; a param change within one route keeps state.popstatere-runsnavigatewithout touching history; scroll goes to the hash target or the top on push.- Anything unhandled —
hydrated: false, a non-JSON or non-OK response, a chunk load failure — falls back tolocation.assign, so navigation always succeeds.
Non-goals
head() nodes other than the title are not swapped on soft navigation; nested layouts and parallel data loading are future work.
Tests
tests/client/page-runtime.test.tsx, tests/integration/hydrate.test.ts (nav payloads on bun + edge).