Shells

Layout wrappers that surround route content. Shells are decoupled from URL structure — a flat route like /settings can share a shell with /dashboard without nesting.

Defining a Shell

Shell modules live in src/shells/ and export a Shell component:

src/shells/app.tsx
import type { ShellProps } from "@pracht/core";

export function Shell({ children }: ShellProps) {
  return (
    <div class="app-layout">
      <nav class="sidebar">
        <a href="/dashboard">Dashboard</a>
        <a href="/settings">Settings</a>
      </nav>
      <main>{children}</main>
    </div>
  );
}

Shell Head Metadata

Shells can contribute to <head> by exporting a head function. Shell metadata merges with route-level metadata:

// Shell exports head() for shared metadata
export function head() {
  return {
    title: "My App",
    meta: [{ name: "viewport", content: "width=device-width, initial-scale=1" }],
  };
}

// Route can override title, arrays are concatenated
export function head() {
  return { title: "Dashboard — My App" };
}
💡

Route title overrides shell title. Array fields like meta and link are merged.

Shell Document Headers

Shells can contribute HTTP headers to page documents by exporting a headers function:

export function headers() {
  return {
    "content-security-policy": "default-src 'self'",
  };
}

Shell headers merge with route-level headers exports. Route headers override shell headers with the same name. These headers apply to HTML document responses and prerendered SSG/ISG HTML, not API routes or route-state JSON fetches.

Shell Error Boundary

Shells can export ErrorBoundary to provide a shared fallback for routes that do not define their own boundary:

import type { ErrorBoundaryProps } from "@pracht/core";

export function ErrorBoundary({ error }: ErrorBoundaryProps) {
  return <p>Something went wrong: {error.message}</p>;
}

Route-level ErrorBoundary exports take precedence over the shell boundary.

Assigning Shells

Register shells by name in defineApp, then reference them in routes or groups:

src/routes.ts
export const app = defineApp({
  shells: {
    public: "./shells/public.tsx",
    app: "./shells/app.tsx",
  },
  routes: [
    // Per-route
    route("/", "./routes/home.tsx", { shell: "public" }),

    // Per-group — all children inherit
    group({ shell: "app" }, [
      route("/dashboard", "./routes/dashboard.tsx"),
      route("/settings", "./routes/settings.tsx"),
    ]),
  ],
});

Client-Side Navigation

When navigating between routes that share the same shell, pracht preserves the shell and only re-renders the route content. When crossing shell boundaries, the full page tree is re-rendered.