For AI agents: fetch /llms.txt first for the curated documentation index, then use same-host Markdown pages when available.

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>`;

See also