Layouts

Any layout.html file wraps every page in its directory and in every subdirectory below it.

Put <slot /> in the layout where the page content should appear.

That is the whole rule. Here is a layout:

source
<!-- pages/layout.html -->
<header>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
</header>

<main>
  <slot />  <!-- Page content goes here -->
</main>

<footer>
  <p>© 2024</p>
</footer>

And a page that uses it without knowing it exists:

source
<!-- pages/about.html -->
<h1>About</h1>
<p>No nav, no footer. The layout supplies those.</p>

Visiting /about renders the nav, then this page's markup where <slot /> was, then the footer.

Which pages a layout wraps

Every page below the layout file, however deep:

source
pages/
├── layout.html          ← wraps everything below
├── index.html           ← /
├── about.html           ← /about
└── admin/
    └── dashboard.html   ← /admin/dashboard, same layout

A page uses the nearest layout.html above it, so adding one in pages/admin/ takes over for that folder.

Lowercase. The file has to be layout.html. Layout.html is a component named Layout, and no page will pick it up.

Nested layouts

A layout whose whole content sits inside <Layout> renders inside its parent instead of replacing it:

source
<!-- pages/admin/layout.html -->
<Layout>  <!-- Wraps in parent layout -->
  <aside class="admin-sidebar">
    <a href="/admin">Dashboard</a>
    <a href="/admin/users">Users</a>
  </aside>
  <div class="admin-content">
    <slot />
  </div>
</Layout>

Now /admin/dashboard renders inside the admin sidebar, which renders inside the root layout.

Leave the <Layout> wrapper off and the layout replaces its parent instead. That suits sign-in screens, which usually want the page to themselves:

source
<!-- pages/auth/layout.html -->
<!-- No <Layout> wrapper = replaces root layout -->
<div class="auth-page">
  <div class="auth-card">
    <Logo />
    <slot />
  </div>
</div>

The chain is resolved at build time and goes as deep as the directory tree does. Each <Layout> takes the nearest layout above it; <Layout src="../../layout.html"> names a different parent explicitly.

The wrapper has to be the whole file. Aside from <link>, <style> and <script>, <Layout> must be the entire top-level content. Nested inside an element, or written twice, it inherits from nothing and would mount as a component no build has - so it is refused by name.

A cycle - only reachable through src= - is a build error too, naming the layouts that close the loop.

A layout can carry its own stylesheet

A <link rel="stylesheet"> written in a layout belongs to that layout. It is lifted out of the markup at build time and travels in the layout's compiled record, so it never appears inside the layout element: the sheet is loaded before the layout mounts, and released when the layout leaves the chain and no other layout still claims the same href.

source
<!-- pages/docs/layout.html -->
<link rel="stylesheet" href="/pages/docs/docs.css" />

<div class="docs-shell">
  <Sidebar />
  <slot />
</div>
The href must be a literal. href="{url}" is refused by name. The lift happens before any binding could fill the value in, so the stylesheet would silently never load.

Opting out

An empty layout.html means "no layout here". It breaks the chain for its directory and everything below it:

source
pages/
├── layout.html          ← root layout
└── embed/
    ├── layout.html      ← empty file, no layout
    └── widget.html      ← renders bare

Layouts stay mounted

Navigating swaps the <slot /> content and nothing else.

The layout is never rebuilt, so its state, its animations and any playing media survive the move:

source
<!-- pages/layout.html -->
<script>
let theme = storage('light')
let sidebarOpen = true
</script>

<div class="{theme}">
  <button onclick="sidebarOpen = !sidebarOpen">Toggle Sidebar</button>

  <aside class:collapsed={!sidebarOpen}>
    Navigation...
  </aside>

  <main>
    <slot />
  </main>
</div>

Move between pages and theme and sidebarOpen keep their values.

Layouts are components

A layout is an ordinary component file with a special name, so it can hold state and use other components:

source
<!-- pages/layout.html -->
<script>
let theme = storage('system')
</script>

<div class="app theme-{theme}">
  <Header />

  <div class="main-content">
    <slot />
  </div>

  <Footer />
</div>

Guarding a whole section

A <guard> in a layout covers every page under it, so the check lives in one file:

source
<!-- pages/admin/layout.html -->
<Layout>
  <guard access="admins" redirect="/login">
    <AdminSidebar />
    <slot />
  </guard>
</Layout>