The Index Pattern
Caspian routes are HTML-first. Use
index.html
for the visible page and add
index.py
only when the same route needs metadata, server-side context, auth,
caching, redirects, or route-owned @rpc() actions.
index.py
The backend companion. Use it when the route needs metadata,
page(),
auth checks, redirects, route-level caching, or route-owned
@rpc()
actions.
-
Imports
render_pagefromcasp.layout -
Returns
render_page(__file__, ...)for UI routes -
Receives path params as a single
paramsdict
index.html
The authored page template. Keep the visible markup here, import Python
components with top-of-file @import comments, and keep any
owned plain <script> inside the same single root.
-
Uses HTML-first
x-*component tags - Owns PulsePoint markup and local state
- Must keep exactly one authored top-level root
Current behavior when both files exist
index.py
is not a replacement for index.html.
For UI routes, it is the backend companion that prepares context and then
renders the sibling template with
render_page(__file__, ...).
File-System Routing
Folders under src/app
define URLs directly.
Route Logic Example
Keep metadata and server-side preparation in
index.py.
For a route that renders UI, call
render_page(__file__, ...)
so Caspian renders the sibling index.html.
from typing import Optional from casp.layout import Metadata, render_page metadata = Metadata( title="Dashboard | Caspian", description="Overview page for the dashboard.", ) async def page(params: dict, search: Optional[str] = None, request=None): slug = params.get("slug") return render_page(__file__, "slug": slug, "search": search, "has_request": request is not None, )
params
dict. Query params are injected by name, and
request
is injected by keyword when declared.
Template Example
Author the page in index.html.
Keep the import directive above the root and keep the owned script inside
the same root.
<!-- @import Button from "../../../components/Button.py" --> <section class="space-y-4 p-8"> <h1 class="text-2xl font-bold">My Todos</h1> <ul class="space-y-2"> <template pp-for="todo in todos"> <li key="{todo.id}" class="flex items-center gap-2"> <span>{todo.title}</span> </li> </template> </ul> <x-button onclick="setFilter('open')"> Show Open </x-button> <script> const [todos] = pp.state({{ todos | tojson }}); const [filter, setFilter] = pp.state("all"); </script> </section>