Layouts and error pages
Drop these files in app/routes/ and Chevalier picks them up — no wiring.
The app shell
_app.tsx is the document shell: the single <html>…<body> structure wrapping every page. There's one, app-root-only. Render <Head> for the <head> — it adds charset/viewport meta and your stylesheets, and you put the app-wide <title>, favicon, and any extra tags inside it — and {children} for the page. Omit the file to use the built-in shell.
// app/routes/_app.tsx
import { Head, type LayoutProps } from "chevalier";
export default function App({ children }: LayoutProps) {
return (
<html lang="en">
<Head>
<title>My App</title>
<link rel="icon" href="/favicon.png" />
</Head>
<body>{children}</body>
</html>
);
}Nested layouts
_layout.tsx is body-only chrome (nav, sidebar, footer) wrapping a page. Drop one at any level to wrap that directory and everything under it. Layouts nest: a route gets every ancestor _layout.tsx, outer→inner, each wrapping the next via {children}, all inside the app shell. A route with no _layout.tsx ancestor renders bare in the shell.
// app/routes/_layout.tsx
import type { LayoutProps } from "chevalier";
export default function Layout({ children }: LayoutProps) {
return (
<>
<nav>…</nav>
{children}
</>
);
}The route prop
Every _layout.tsx — and the _app shell — also receives route, so chrome can react to the current page without reaching for the raw request:
route.url— the request path, e.g./docs/layouts. What you match on to highlight the active nav link.route.path— the matched route pattern, e.g./docs/:slug.undefinedon a 404 or error render, since no route matched.route.data— the same{ params, ...loaderData }object the page receives.
// app/routes/docs/_layout.tsx
import type { LayoutProps } from "chevalier";
export default function DocsLayout({ children, route }: LayoutProps) {
return (
<>
<nav>
{links.map((l) => (
<a href={l.href} aria-current={route.url === l.href ? "page" : undefined}>
{l.title}
</a>
))}
</nav>
{children}
</>
);
}Layouts render after the page, so a layout can't contribute to <head>: a <PageHead> rendered from a layout is silently dropped. Keep it in the page.
Per-page head
Render <PageHead> anywhere in a page's JSX to add tags to <head> — a <title>, meta, links. They land in the shell's <Head>, and a page <title> overrides the shell default:
import { PageHead } from "chevalier";
export default function About() {
return (
<>
<PageHead>
<title>About — My App</title>
<meta name="description" content="…" />
</PageHead>
<h1>About</h1>
</>
);
}Error pages
_404.tsxrenders with status 404 for any unmatched route (and for a page's ownc.notFound())._error.tsxrenders with status 500 and receives the thrownerroras a prop. The error is also logged server-side viaconsole.error.
_app, _404, and _error are opt-in and app-root-only; omit any to fall back to the built-in shell / Hono's defaults. _layout.tsx and _middleware.ts are both per-directory.