The Layout Pattern
Layouts wrap groups of routes. A focused
layout.py
owns the shared shell, synchronous layout props, and metadata for its subtree.
Layout Anatomy
Current Rules
-
Keep shared wrapper markup, props, and metadata in
layout.py. -
Child content is injected with
<slot />. -
Shared layout props are consumed with native Jinja such as
layout.*. -
Shared metadata is consumed with native Jinja such as
metadata.*. - Return readable native HTML with Jinja values; do not author runtime-owned component boundary attributes.
Sync or async
layout()
may be a plain function or an
async def;
the engine awaits the result when it is awaitable. Prefer keeping I/O in
route page()
functions or route-owned @rpc()
actions anyway — a layout runs for every page in its subtree, so work
done here is paid on every one of them.
Layout Context
Define metadata defaults, return the shared shell, and provide synchronous
values consumed through the layout Jinja object.
from casp.layout import Metadata metadata = Metadata( title="Docs Section | Caspian", description="Shared metadata for the docs subtree.", ) def layout(context): return r"""<div class=" layout.shell_class "><slot /></div>""", "shell_class": "docs-shell", "content_class": "docs-shell__content",
Return shape
Return shell, props
so the same module owns the wrapper while its props remain explicit.
Metadata scope
Metadata defined here becomes the default for everything below this folder, unless a child layout or page overrides it.
Authoring With html(...)
A layout can author its shell through the same
html(...) entrypoint
pages and components use, so one markup form covers the whole app. The layout
is the one caller that cannot render when it runs, because
children is the page
beneath it and has not been composed yet.
from casp.component_decorator import html def layout(context): return html(r""" <div class=" shell_class "> <aside>Docs navigation</aside> <main><slot /></main> </div> """, shell_class="docs-shell")
Rendering is deferred
Called from a layout(),
html(...) hands
back an unrendered
LayoutTemplate
carrying your context. The engine renders it once, later, with
children,
layout, and
metadata merged
in — those three engine-owned names always win over author context.
Deferral is narrow
Only the layout()
frame itself defers. A component the layout calls, or a helper in the same
file, still renders eagerly. The legacy shapes — a bare template
string, (str, props),
a props dict, or None
— all keep working unchanged.
A layout must place its children
A layout template with no
<slot /> and no
children
raises LayoutChildrenError.
Before that check existed, every page beneath such a layout rendered as an
empty shell with no error and no warning.
Returned Shell
Keep the visible shell as a readable raw string in layout.py.
Child routes render into the
<slot />
outlet and shared values are read from native Jinja objects like
layout and
metadata.
<html> <head> <title> metadata.title </title> <meta name="description" content=" metadata.description " /> </head> <body class=" layout.shell_class "> <aside>Docs navigation</aside> <main class=" layout.content_class " pp-reset-scroll="true"> <slot /> </main> </body> </html>
Nested Layouts And Route Groups
Use nested layouts for shared shells in sections like dashboards, docs, and account areas. Use parenthesized route groups when the layout should organize code without adding a URL segment.
src/app/
dashboard/
layout.py
index.py
settings/
index.py
src/app/
(marketing)/
layout.py
pricing/
index.py
about/
index.py
Scroll behavior
In grouped shells with separate sidebar and content scrolling, keep
persistent shell scrollers unmarked and put
pp-reset-scroll="true"
on the content pane that should reset on child-route navigation.
Page-To-Layout Props
When a single page needs to influence a wrapping layout, return a 2-item
tuple from page()
instead of moving that concern into the layout for the whole subtree.
async def page(): page_html = html(r"""<main>Reports</main>""") return page_html, "dashboard_body_class": "dashboard-shell dashboard-shell--reports"