Concepts
Shell
The shell is the one function that wraps every route body in a full HTML document. It owns the head, the persistent chrome, and the content slot — everything that survives navigation.
What the shell owns
A route renders the part of the page that changes. The shell renders
everything around it: the doctype, the <head> built
from route metadata, stylesheets and scripts, persistent chrome like
the site header, and the slot the body lands in. It runs once per
document request — fragment navigation swaps the slot's content and
leaves the rest of the shell untouched.
A minimal shell
A shell is a plain function from { body, meta, nonce } to a
document string. Interpolate body directly as
${body} — the adapter hands it in as already-rendered,
trusted HTML, so it composes into the document with no
raw call.
// site/shell.js
import { html } from "@nativefragments/core/server";
export const shell = ({ body, meta }) => html`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${meta.title}</title>
<meta name="description" content="${meta.description}" />
<link rel="canonical" href="${meta.canonical}" />
<link rel="stylesheet" href="/app/styles.css" />
<script type="module" src="/app/client.js"></script>
</head>
<body>
<header>…site chrome…</header>
<main id="content-slot">${body}</main>
</body>
</html>`;
The content slot
#content-slot is the default target for
fragment navigation: link clicks fetch
the next route's body and replace the slot's children. Anything outside
the slot persists across navigation — the header stays mounted,
components in the chrome keep their state, and module scope survives.
That is why persistent UI belongs in the shell, not in route bodies.
Metadata
meta arrives normalized from the route's
meta() function —
title, description, and
canonical always exist (empty string or the pathname when
unset). On fragment navigation the browser router updates the document
head from the fragment response, so the shell's head markup stays
correct without re-rendering.
The ${body} contract
The adapter renders the route body and passes it to the shell as trusted
RawHtml under the
body key. Interpolate it verbatim as
${body}. Do not wrap it in
raw() and do not escape it — that is also how streaming
finds the body's position. If the shell escapes or transforms
body, the adapter falls back to buffered rendering and logs
a warning.
Streaming shells
For streamed documents the adapter
needs the document split around the body. Return
{ before, after } when called without a body;
keep the string form for everything else.
export const shell = ({ body, meta, nonce }) => {
const parts = {
before: html`<!doctype html>
<html lang="en">
<head><title>${meta.title}</title></head>
<body><main id="content-slot">`,
after: html`</main></body></html>`,
};
if (body === undefined) return parts;
return html`${parts.before}${body}${parts.after}`;
};
The CSP nonce
The Cloudflare adapter passes a per-request nonce. Put it
on inline scripts and styles with
attrs if you enable a strict
contentSecurityPolicy —
the framework's own streaming bootstrap uses the same nonce.
export const shell = ({ body, meta, nonce }) => html`<!doctype html>
<html lang="en">
<head>
<title>${meta.title}</title>
<script${attrs({ nonce })}>
document.documentElement.classList.add("js");
</script>
</head>
<body><main id="content-slot">${body}</main></body>
</html>`;