Layouts & Shared Data
Use layouts for navigation, headers, and other UI shared by several pages. Layouts nest with your directories and keep their component state as you navigate.
Layouts
Place a layout file in a pages/ directory to wrap its pages:
- pages/
- layout.tsxRoot layout (nav, footer)
- index.tsx
- users/
- layout.tsxNested layout for /users/*
- index.tsx
- [id].tsx
// pages/layout.tsx
import { useShared, Link } from '@void/react';
export default function Layout({ children }: { children: React.ReactNode }) {
const { auth } = useShared();
return (
<>
<nav>
<Link href="/">Home</Link>
<Link href="/users">Users</Link>
{auth?.user && <span>{auth.user.name}</span>}
</nav>
<main>{children}</main>
</>
);
}<!-- pages/layout.vue -->
<script setup lang="ts">
import { useShared, Link } from '@void/vue';
const { auth } = useShared();
</script>
<template>
<nav>
<Link href="/">Home</Link>
<Link href="/users">Users</Link>
<span v-if="auth?.user">{{ auth.user.name }}</span>
</nav>
<main>
<slot />
</main>
</template><!-- pages/layout.svelte -->
<script>
import { useShared, Link } from "@void/svelte";
let { children } = $props();
const { auth } = useShared();
</script>
<nav>
<Link href="/">Home</Link>
<Link href="/users">Users</Link>
{#if auth?.user}<span>{auth.user.name}</span>{/if}
</nav>
<main>
{@render children()}
</main>// pages/layout.tsx
import { useShared, Link } from '@void/solid';
import type { JSX } from 'solid-js';
export default function Layout(props: { children: JSX.Element }) {
const shared = useShared();
return (
<>
<nav>
<Link href="/">Home</Link>
<Link href="/users">Users</Link>
{shared.auth?.user && <span>{shared.auth.user.name}</span>}
</nav>
<main>{props.children}</main>
</>
);
}// pages/layout.tsx
import { useShared, Link } from '@void/hono';
import type { Child } from 'hono/jsx';
export default function Layout(props: { children?: Child }) {
const shared = useShared();
return (
<>
<nav>
<Link href="/">Home</Link>
<Link href="/users">Users</Link>
{shared.auth?.user && <span>{shared.auth.user.name}</span>}
</nav>
<main>{props.children}</main>
</>
);
}When a page renders, the layout wraps it. The page component is injected as the layout's children in React, Solid, Svelte, and Hono, or as a slot in Vue:
Named Layouts
Named layouts let individual pages opt into a different layout without changing the URL structure. Define them in _layouts/ directories within pages/:
- pages/
- _layouts/
- landing.tsxnamed "landing"
- post.tsxnamed "post"
- layout.tsxdefault root layout
- index.tsx
- blog/
- hello.mdlayout: post
- archive.mdlayout: landing
- pricing.tsxlayout: !landing (exclusive)
Selecting a named layout
Export a layout constant from any page:
export const layout = 'landing';
export default function Page() {
return <div>This page uses the landing layout</div>;
}<script lang="ts">
export const layout = 'landing';
</script>
<template>
<div>This page uses the landing layout</div>
</template><script context="module">
export const layout = "landing";
</script>
<div>This page uses the landing layout</div>export const layout = 'landing';
export default function Page() {
return <div>This page uses the landing layout</div>;
}export const layout = 'landing';
export default function Page() {
return <div>This page uses the landing layout</div>;
}For markdown pages, use frontmatter:
---
layout: post
---
# My Blog PostLayout modes
| Value | Behavior |
|---|---|
"landing" | Replace the innermost layout in the chain. Outer ancestor layouts still apply. |
"!landing" | Replace the entire chain. Only the named layout wraps the page. |
false | No layout wrapping at all. Page renders standalone. |
A named layout replaces the deepest default layout: in [root, docs/layout], it replaces docs/layout; in [root], it replaces the root.
Void uses the closest matching _layouts/ file in the page's directory or an ancestor. A missing named layout fails the build.
Shared Data
Middleware can inject data available on every page via c.set("shared", {...}). Augment CloudContextVariables to type the shared data, and useShared() will infer the type automatically:
// middleware/01.auth.ts
import { defineMiddleware } from 'void';
import { getUser, type AuthUser } from 'void/auth';
declare module 'void' {
interface CloudContextVariables {
shared: { auth: { user: AuthUser | null } };
}
}
export default defineMiddleware(async (c, next) => {
const user = getUser();
c.set('shared', { auth: { user } });
await next();
});Call useShared() from your framework adapter, as in the layout examples above. Its return type is inferred from CloudContextVariables. See Context variables.
Middleware sets the starting value. A loader or action can update it before the page is sent:
// pages/profile.server.ts
import { defineHandler } from 'void';
export const action = defineHandler(async (c) => {
const user = await updateProfile(c);
c.set('shared', { ...c.get('shared'), auth: { user } });
});Middleware runs before the action. If an action changes data that shared data is built from, update shared data in the action, or redirect with c.redirect() so middleware runs again on the next page load.