Rendering, head, and hydration
Choose SSR or CSR, compose escaped route metadata, understand head cleanup, and keep private values out of browser-readable hydration state.
Treat SSR as the default
The route chain resolves to one PageRenderMode. Omitting the named render export selects SSR; only render = "csr" selects client-only route rendering.
| Mode | Route declaration | Server | Browser |
|---|---|---|---|
| SSR | No render export | Middleware, eligible loaders, head, layouts, and the page render for the request. | The same tree hydrates, then later navigation resolves in the browser. |
| CSR | export const render = "csr" | Tavo.js renders the configured CSR fallback and static head contributions only. | The route resolves and renders after client boot. |
A CSR selection anywhere in the route module chain makes the resolved route CSR.
Static generation, revalidation, vary, cache tags, and static params are incompatible with CSR and are ignored with manifest diagnostics.
Use CSR only when the route cannot produce useful request HTML. Client interactivity does not require CSR; SSR pages hydrate into interactive components.
Dynamic head functions do not run on the server for CSR routes because route data is not resolved there.
Return escaped TSX from head
A named head export returns PageHeadExport: escaped TSX for normal metadata or a PageHead object for response status and document attributes.
tsximport { Seo } from "@tavojs/core";
type Project = {
name: string;
summary: string;
};
export function head({
data
}: {
data: Project | null;
}) {
const title = data ? `${data.name} · Projects` : "Project";
return (
<>
<title>{title}</title>
<Seo
title={title}
description={data?.summary}
robots="index,follow"
/>
</>
);
}TSX children and attributes are escaped by the renderer. Prefer TSX and
Seofor every normal metadata contribution.The hard raw-string boundary is
unsafeHeadHtmlin aPageHeadobject orHeadcomponent. There is no raw head string alias.unsafeHeadHtmlis inserted without escaping. Never concatenate user, loader, request, database, translation, or CMS values into it unless a trusted sanitizer establishes the complete HTML policy.head may also return title, status,
htmlAttributes, andbodyAttributesin aPageHeadobject.
Predict head precedence and browser cleanup
Headcontributions resolve from outer layouts to inner layouts, then the page.A dynamic layout head receives that layout's own loader data and error. The page head receives page data and the page-loader error.
Later title, status,
htmlAttributes, andbodyAttributesvalues win for the same field.Managed
Seoentries are deduplicated by their framework key with the later value winning. Unkeyed head nodes preserve contribution order.On browser navigation,
Tavo.jsremoves nodes owned by the previous route, applies the next route's nodes and attributes, and restores the document fallback title when the next route has no title.
Assume hydration state is public
SSR sends enough state for the browser to adopt the server-rendered tree. Anyone who receives the HTML can read this serialized state, including values that are not visibly rendered. Its ordered route data entries describe the resolved layout and page layers.
| State | Browser exposure |
|---|---|
| Page data | Serialized for the resolved page. |
| layers and layerData | Serialized layout and page loader results, both ordered and keyed. |
| Store snapshots | Serialized when included in the document hydration state. |
| Plugin state | Serialized when a plugin contributes hydration state. |
| Errors | Hydration error details are redacted to a generic internal-server message. |
Return the minimum browser-safe shape from loaders. Keep tokens, session internals, credentials, private profile fields, and database records on the server.
Server-only execution does not make a returned loader value secret; SSR loader output can still be serialized.
Review nested layout data as carefully as page data because
layerDataexposes successful results by ID.Redaction is a failure safeguard, not a reason to pass rich server exceptions into view props or custom error output.
Keep the server and client trees compatible
Render deterministic initial output from the same route data and serialized store state on both sides.
Move DOM reads, browser storage, timers, observers, and subscriptions into client lifecycle hooks.
Do not branch initial markup on
Date.now(),Math.random(), locale defaults, viewport measurements, or undocumented globals.A hydration mismatch is a correctness failure: fix the divergent input instead of suppressing the warning.
Clean up head ownership, controller work, and subscriptions when navigation replaces the route.
Verify source HTML and hydrated behavior
bashnpx tavo inspect route /projects/example --json
npx tavo build
PORT=4174 node .tavo/build/server/start.mjsInspect the raw document response to confirm SSR content, status, escaped metadata, and the absence of private values.
Inspect the live head before and after client navigation to confirm stale title, meta, html attributes, and body attributes are removed or replaced.
Hydrate with browser console errors treated as test failures.
Test a CSR route separately: the raw response should contain the chosen fallback and only static head contributions.
Look up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.