Page modules

A page module is expected to export:

  • a default React component
  • optionally meta
  • optionally head()
  • optionally loader(req, ctx) — runs on the server before render; its result reaches the component as the data prop. Return a Response (e.g. a redirect) to skip rendering.
  • optionally hydrate — a literal boolean opting the page into whole-tree hydration (below)

If meta is missing and head() is also absent, Patties emits a default HTML head for the current pathname.

Shell and response shape

The renderer wraps pages with a document shell and returns a standard HTML response with text/html; charset=utf-8.

Islands

Client islands are discovered from app/islands/ during the build. Patties generates a client entry, bundles the island graph for the browser, and writes a manifest that maps islands to emitted chunks.

Projects with zero islands are still valid. In that case Patties keeps the client manifest empty and skips the browser bundle output.

Whole-tree hydration

Islands are the default. A page can instead hydrate its entire React subtree as one root with export const hydrate = true, or every page can by default with render: { hydrate: "pages" } in patties.config.ts (opt out with hydrate = false). Inside such a page, function props and context shared across components work freely — only the page's own props cross the wire.

export const hydrate = true;

export async function loader(req: Request, ctx: PattiesContext) {
  return { user: await db.user(ctx.params.id), seen: new Date() };
}
// Optional: make non-JSON data survive the trip (server → client).
export const serialize = (d) => ({ ...d, seen: d.seen.toISOString() });
export const deserialize = (j) => ({ ...j, seen: new Date(j.seen) });

export default function User({ id, data }) { /* … */ }

The server serializes { params, data } into a <script type="application/json" id="__patties_bootstrap"> outside #root, and the page's own chunk hydrates with exactly those props. loader and serialize — and any imports only they use — are stripped from the browser bundle at build time. Pages without hydrate still ship zero JS. See Using upstream shadcn/ui for the main use case.

Client-side navigation

Between hydrated pages, use Link from patties/client:

import { Link } from "patties/client";

<Link href="/realms/acme/clients">Clients</Link>

A click fetches the target's bootstrap payload (middleware and loader() run as usual, answered as JSON via the x-patties-nav request header), loads its page chunk, pushes history, updates the title, and re-renders the existing React root. Back/forward work the same way. Hover or focus prefetches. When the target isn't a hydrated page — or anything goes wrong — it falls back to a normal page load, and on zero-JS or island pages Link is a plain <a>. navigate(href) and prefetch(href) are exported for programmatic use.

Dev HMR injection

In development, Patties injects the HMR client script into the rendered HTML stream before </body>.

Patties documentation · Licensed under MIT. · Design specs