The Layout Pattern
Layouts wrap groups of routes. Keep the shared shell in
layout.html
and add layout.py
only when that shell needs shared synchronous props or metadata.
Layout Anatomy
Current Rules
-
Keep shared wrapper markup in
layout.html. -
Use
layout.pyonly for shared synchronous props or metadata. -
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.* }}. -
Caspian is HTML-first now, so the removed
[[ ... ]]placeholders should not appear in new route templates.
Important runtime detail
layout()
is synchronous in the installed runtime. Put async I/O in route
page()
functions or in route-owned @rpc()
actions instead of awaiting inside layout.py.
layout.py
Use the Python companion for metadata defaults and shared layout props. The layout companion returns the rendered sibling template plus a props dict.
from casp.layout import Metadata, render_layout metadata = Metadata( title="Docs Section | Caspian", description="Shared metadata for the docs subtree.", ) def layout(): return render_layout(__file__), "shell_class": "docs-shell", "content_class": "docs-shell__content",
Return shape
Return render_layout(__file__), props
so the sibling layout.html
stays the authored wrapper while Python supplies data.
Metadata scope
Metadata defined here becomes the default for everything below this folder, unless a child layout or page overrides it.
layout.html
Keep the visible shell in the HTML file. 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.html
layout.py
index.html
settings/
index.html
src/app/
(marketing)/
layout.html
pricing/
index.html
about/
index.html
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 = render_page(__file__, ) return page_html, "dashboard_body_class": "dashboard-shell dashboard-shell--reports"