Rendering & islands
Patties renders pages with React server rendering using renderToReadableStream.
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 thedataprop. Return aResponse(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>.