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_page from casp.layout
  • Returns render_page(__file__, ...) for UI routes
  • Receives path params as a single params dict

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 Mapping
src/app/index.html
/
|- src/app/dashboard/index.html
/dashboard
|- src/app/dashboard/settings/index.html
/dashboard/settings

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,
    )
Path params arrive as a single 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>