layout()
A layout is a page-shaped value that takes children. Compose them outside-in to share chrome across many pages.
Basic
import { layout } from '@place/component'
export const rootLayout = layout({
meta: { bodyClass: 'bg-bg text-fg' },
view: ({ children }) => (
<div class="min-h-screen flex flex-col">
<Header />
<main class="flex-1">{children}</main>
<Footer />
</div>
),
})
load — server-side data
export const dashLayout = layout({
load: async ({ req }) => ({
user: await getUserFromRequest(req),
}),
view: ({ children, user }) => (
<div>
<UserBadge name={user.name} />
{children}
</div>
),
})
Layout load runs server-side before view(). Its return merges into loadData which flows to every layout and the page in the chain.
Layout chains
// Layouts compose outside-in:
page('/admin/users', {
layout: [rootLayout, adminLayout],
view: ({ children }) => <UserList />,
})
// rootLayout wraps adminLayout wraps the page.
// Both layouts' load() run in chain order; results merge into a single
// loadData passed to every view + meta callback in the chain.
Typed named slots
Layouts can expose typed named slots — regions a page can fill with custom content. The layout's second type parameter declares the slot key union; pages get TypeScript autocomplete on those keys. No file convention, no @-prefixed parallel routes, no <NuxtPage /> magic.
// Typed named slots — let pages customize specific layout regions
// without parallel-route file conventions (Next.js) or magic single
// outlets (Nuxt). The layout's second type parameter declares the
// slot key union; pages get autocomplete on those keys.
const dashboard = layout<{}, 'headerActions' | 'sidebar'>({
view: ({ children, slots }) => (
<div class="grid grid-cols-[240px_1fr]">
<aside class="border-r">
{slots('sidebar') ?? <DefaultSidebar />}
</aside>
<main>
<header class="flex justify-end gap-2 p-2">
{slots('headerActions')}
</header>
{children}
</main>
</div>
),
})
// Each page fills the slots it cares about. Missing slots resolve
// to null; the layout can branch on `slots.has('name')` for fallbacks.
page('/users', {
layout: dashboard,
slots: {
headerActions: () => <NewUserButton />,
sidebar: () => <UserFilters />,
},
view: () => <UserList />,
})
page('/settings', {
layout: dashboard,
// No `headerActions` slot — that region renders null.
slots: { sidebar: () => <SettingsNav /> },
view: () => <SettingsForm />,
})
Slots are functions (lazy), so the layout decides when to render them — branching with slots.has('name') covers the "render a fallback when unfilled" case without cost. Slot fills survive the whole layout chain: an outer layout doesn't need to know what its inner layouts consume.
Compared to other frameworks: Next.js requires parallel routes (file-system magic, @modal/page.tsx directories) for the same UX. Nuxt has one <NuxtPage /> outlet per layout — multi-slot isn't first-class. Remix's <Outlet /> is single-slot too. place's named slots are typed values flowing through props.