Skip to content
Built with

Layouts and shared data ​

Build & Deploy

Use layouts for navigation, headers and other UI shared by several pages. Layouts nest with your folders and keep their state as you navigate.

On this pageAdd a layout · Use named layouts · Share data across pages

Layouts ​

This step wraps pages in a layout. Put a layout file in a pages/ directory.

  • pages/
    • layout.tsxRoot layout (nav, footer)
    • index.tsx
    • users/
      • layout.tsxNested layout for /users/*
      • index.tsx
      • [id].tsx
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>
    </>
  );
}
vue
<!-- 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>
svelte
<!-- 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>
tsx
// 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>
    </>
  );
}

The layout wraps each page it covers. The page is passed as children in React, Solid and Svelte, and as a slot in Vue.

Layout nesting diagram: pages/layout wraps pages/users/layout wraps pages/users/[id] page component

Named layouts ​

Named layouts let a page use a different layout without changing its URL. This step defines them in _layouts/ directories inside 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 ​

This step selects a named layout. Export a layout constant from the page.

tsx
export const layout = 'landing';

export default function Page() {
  return <div>This page uses the landing layout</div>;
}
vue
<script>
export const layout = 'landing';
</script>

<template>
  <div>This page uses the landing layout</div>
</template>
svelte
<script context="module">
export const layout = "landing";
</script>

<div>This page uses the landing layout</div>
tsx
export const layout = 'landing';

export default function Page() {
  return <div>This page uses the landing layout</div>;
}

For Markdown pages, set it in frontmatter:

md
---
layout: post
---

# My Blog Post

Layout modes ​

ValueBehavior
"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.
falseNo 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.

The SDK uses the closest matching _layouts/ file in the page's directory or an ancestor. A missing named layout fails the build.

Shared data ​

This step shares data with every page. Middleware sets it with c.set("shared", {...}). Augment CloudContextVariables to type it, and useShared() infers the type.

ts
// 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. Shared data comes from middleware. Page props come from the loader. See Context variables.

Next steps ​

implemented.stream AI