Routing
Routes live under src/app,
folders define URL segments, and layouts nest automatically. Each page is
an index.py
module that returns readable markup with html(...)
and owns its metadata, first-render data, RPC actions, auth, caching, and redirects.
Current Rules
-
Put application routes in
src/app/. -
Every page is an
index.pywith readablehtml(...)markup and any route-owned metadata,page(),@rpc()actions, auth checks, caching, redirects, or other server-side logic. -
layout.pyowns the shared wrapper, synchronous layout props, and metadata for a route subtree. -
A single top-level root element or one imported
x-*root is the default template shape, with any owned plain<script>kept inside the template. Sibling top-level nodes are also valid: a multi-root component becomes a fragment, and a multi-rootindex.pyorlayout.pyis wrapped in a layout-neutraldisplay: contentsboundary host.
Route Shapes
| Route Shape | Files | Typical Use |
|---|---|---|
| Page module | index.py | Markup, metadata, data, and route-owned actions |
| Composed page | index.py + components | Short route assembly with focused Python components |
| Backend only | index.py | Redirect-only or action-oriented route |
| Files | URL |
|---|---|
| src/app/index.py | / |
| src/app/about/index.py | /about |
| src/app/dashboard/index.py | /dashboard |
| src/app/(auth)/signout/index.py | /signout |
Route Modules
Import reusable components with normal Python imports, return one
readable html(...)
root, and render imported components with HTML-first kebab-cased
x-* tags.
from casp.component_decorator import html from src.components.Button import Button def page(): return html(r""" <section class="space-y-4"> <h1>Dashboard</h1> <x-button>Create report</x-button> <script> const [filter, setFilter] = pp.state("all"); </script> </section> """)
Root shape
Prefer one root in the markup passed to
html(...) —
for a component it is the only shape that can receive props. Sibling
top-level nodes are supported: a page or layout gets a
display: contents
boundary host, and a component becomes a fragment. Keep any owned plain
<script>
inside the template, and never handwrite
pp-component
or type="text/pp";
those are injected by the runtime.
The Server Workflow
The same index.py
owns metadata, first-render preparation, and the page markup. Pass
server-known values to html(...)
as keyword context.
from casp.component_decorator import html from casp.layout import Metadata metadata = Metadata( title="Dashboard | Caspian", description="Section overview for the dashboard.", ) async def page(params: dict, request=None): slug = params.get("slug") return html(r""" <main><h1>{{ slug }}</h1></main> """, slug=slug)
params
dict. Query params are injected by name, and
request
is injected by keyword when declared.
async def page(): page_html = html("<main>Reports</main>") return page_html, "dashboard_body_class": "dashboard-shell dashboard-shell--reports"
The 2-item tuple form lets a child route push layout props upward so
parent layouts can consume them as
{{ layout.* }}.
Dynamic Segments
Use square brackets to define dynamic URL segments.
Catch-All Segments
Use an ellipsis inside brackets to match multiple path segments.
Route Groups And Section Layouts
Use a normal folder like dashboard/
when the section name should appear in the URL. Use a parenthesized group
like (marketing)/
when the folder should organize code and own a layout without adding a
URL segment.
src/
app/
(marketing)/
layout.py
pricing/
index.py
about/
index.py
dashboard/
layout.py
index.py
settings/
index.py
Layouts
Keep shared wrapper markup, synchronous props, and metadata in
layout.py.
The returned shell stays native HTML plus Jinja and includes <slot />.
<html> <head> <title>{{ metadata.title }}</title> <meta name="description" content="{{ metadata.description }}" /> </head> <body class="{{ layout.theme_class }}"> <main pp-reset-scroll="true"> <slot /> </main> </body> </html>
from casp.layout import Metadata metadata = Metadata( title="Docs Section | Caspian", description="Shared docs layout metadata.", ) def layout(context): return r"""<div><slot /></div>""", "theme_class": "docs-shell"
Current layout runtime
layout()
is synchronous in the installed runtime. Put async I/O in
page()
or in route-owned @rpc()
actions instead of awaiting inside layout.py.