Route Directory Contract

Reserved files, inheritance rules, route kinds, and shell contract for filesystem routes

8 min read 1651 words

Overview

The route directory contract defines how Chirp discovers and wires filesystem routes. It specifies reserved files, their scope (inherited vs route-local), and how metadata, context, and layouts combine. Understanding this contract helps you structure route directories correctly and avoid common mistakes.

Reserved Files

File Scope Purpose
page.py route-local Primary route handler. Exportsget, post, etc. or handler. page.py → directory URL; other .pyfiles append their stem.
page.html route-local Primary page template. Sibling ofpage.py. Defines fragment blocks.
_meta.py route-local Route metadata (title, section, breadcrumb_label, shell_mode). ExportsMETA or meta().
_context.py inherited Subtree-scoped context provider. Exportscontext()receiving path params, parent context, and services.
_layout.html inherited Subtree layout wrapper. Declares{# target: element_id #} (and optionally {# outlet: element_id #}, {# swap_scope: #}, {# frames: #}) and {% block content %}.
_actions.py route-local Mutation handlers. Exports@actiondecorated functions.
_viewmodel.py route-local View assembly. Exportsviewmodel()merging data for templates.

RouteMeta

_meta.py provides route metadata via a static META constant or a meta()callable:

from chirp.pages.types import RouteMeta

META = RouteMeta(
    title="Skills",
    section="discover",
    breadcrumb_label="Skills",
    shell_mode="tabbed",
)

Or dynamically:

def meta(name: str) -> RouteMeta:
    return RouteMeta(title=f"Skill: {name}", breadcrumb_label=name)

Fields: title, section, breadcrumb_label, shell_mode, auth, cache, tags.

Sections

Register sections beforemount_pages():

from chirp.pages.types import Section, TabItem

app.register_section(Section(
    id="discover",
    label="Discover",
    tab_items=(TabItem(label="Skills", href="/skills"),),
    breadcrumb_prefix=({"label": "App", "href": "/"},),
))

Routes bind to sections via RouteMeta.section. The framework resolves tab_items and breadcrumb_prefix from the matched section. Tab rows use the same data under route_tabs (an alias of tab_items) for chirp-ui’s render_route_tabs macro. Each item is a dict shaped like TabItem: label, href, optional icon, badge, and optional match ("exact" or "prefix"for nested URLs).

For delivery modes (hx-target, boost vs route-tab clicks) and a full checklist, see the shell, sections, and route tabs contract in the chirp-ui repository.

Context Cascade

_context.py providers run root-first. Each receives path params, accumulated parent context, and service providers. Child output overrides parent. shell_actionsmerges deeply.

Layout Chain

Layouts inherit down the directory tree. Each_layout.html declares {# target: element_id #} (which DOM node the layout fills in a nested chain). Optional {# outlet: element_id #} declares the primary navigation outlet (for example main for chirp-ui app shells). LayoutChain.find_start_index_for_target matches both so boosted HX-Target headers can target #main while the layout’s target remains body. Render depth depends on HX-Target: full page renders all layouts; boosted requests start at the matching layout. See Filesystem routing (persistent app shell pattern).

Shell Context Assembly

The framework provides:page_title, breadcrumb_items, tab_items, route_tabs (same list as tab_items when the section defines tabs), current_path. Resolution order: RouteMeta→ section → handler override.

Imperative routes: For handlers that returnTemplate(...) or Page(...) directly (not using filesystem routing), Chirp auto-injects current_path = request.path into the template context when the handler does not provide it. This ensures ChirpUI navigation macros with match=work for both filesystem and imperative route styles.

Route Kinds

Kind Files Description
page page.py, page.html Standard page with template
detail page.py, page.html in{param}/ Parametrized page
action page.py (no template) Mutation-only route
redirect page.py returning Redirect Redirect route

Actions

_actions.py exports @action decorated handlers. Forms use _actionfield to dispatch. The framework discovers actions at route registration.

Viewmodel

_viewmodel.py exports viewmodel()for complex view assembly. Its output merges after cascade and shell context.

Contract Validation

app.check()validates route contracts: section bindings, shell mode/block alignment, route file consistency, duplicate routes, section tab hrefs, and context provider signatures.

Beyond route-level checks,app.check()also validates hypermedia surface contracts. The table below is a category reference for the checks users most often tune in CI:

Read each issue as a pointer to one concrete fix target. A useful diagnostic names the route, template, block, selector, middleware, config flag, import string, or registration that must change. Terminal output groups categories by concern (Routing, HTMX, Forms, OOB / Suspense / SSE, and so on) so the first header tells you which surface to inspect before you open the file.

Check Severity What it catches
page_handlers ERROR / WARNING page.py defines no recognised HTTP method handler (get/post/… or handler fallback). Handler-shaped typos (def handle, def GET, def index) emit WARNING; an entirely missing handler emits ERROR — the file would register no routes and requests 404/500 at runtime.
route_names ERROR Two routes at different paths claim the same name —app.url_for(name) would ambiguously resolve. Method variants of the same URL (e.g. GET from page.py + POST from _actions.py) are not flagged. Fix by renaming one of the pages or setting a module-level name = "…"override.
mount_app_merge INFO app.mount_app(prefix, sub_app) dropped a sub-app template global, filter, provider, error handler, or severity override because the parent had already registered one. Parent-wins is intentional; promote via override_contract_severity("mount_app_merge", Severity.WARNING)if you want CI to flag them.
dead INFO A template exists in the template directory but no route, include, import, or layout references it. Usually cleanup, not a deploy blocker.
component ERROR / WARNING Component-call validation surfaced by Kida/chirp-ui metadata. The Chirp adapter is wired; full precision depends on typed component metadata from the template package.
unreachable_block WARNING A filesystem page template defines a sibling block that layout composition will never render, such aspage_scripts outside page_content. Move the content inside the rendered page block or make it a real fragment target.
composition_extends WARNING A page template extends a registered layout instead of composing into it. Pages should render into the layout content block viarender_with_blocks; they should not override sibling layout blocks.
hx-target, hx-indicator, hx-boost ERROR / WARNING htmx attributes reference missing selectors, invalid boosted links, or unsafe targets. Fix the selector or target element named in the issue.
fragment_target_orphan ERROR / WARNING A required fragment target registry entry points at a block no template provides, or an optional entry cannot be resolved. Required entries are errors because htmx would otherwise swap nothing.
fragment_target_scan ERROR Chirp could not inspect a page template while checking fragment target registrations. Fix the named template parse/load error first; otherwise orphaned target checks may be incomplete.
oob_registry ERROR / WARNING A registered OOB region references a missing block or mismatched target. Required missing regions fail startup; optional regions can be warnings.
reactive_block ERROR DependencyIndexblock reference points to a non-existent template block (typo or renamed block)
reactive_cycle WARNING Derivation graph contains a cycle (index.derive()forms a loop)
reactive_paths WARNING A declaredChangeEvent.changed_paths value is not registered in the DependencyIndex; the event will not update any block.
reactive_audience WARNING A scope declares audience-filtered events but no connection-awarereactive_stream(..., connection=ConnectionInfo(...)).
oob_target WARNING hx-swap-oob element references an idnot found in any template
fragment_scope WARNING A nested fragment block references an import or binding defined only inside an ancestor block; directrender_block()or block-fetch rendering skips that ancestor scope
sse_self_swap ERROR sse-connect and sse-swap appear on the same element. Put sse-swapon a child sink so htmx can target the update correctly.
sse_scope ERROR An SSE connection sits inside a broad inherited htmx target withouthx-disinheritor another safe scope boundary.
sse_crossref ERROR / INFO sse-swap="name"listens for an event no route declares or infers, or a route emits an event no template listens for.
defer_falsy WARNING A Suspense template checks a deferred key with bare truthiness ({% if key %}), which keeps skeletons visible for empty lists, empty strings, or 0. Use is not none or __chirp_defer_pending__.
alpine_cdn_url ERROR A bare jsDelivr Alpine URL would load the package CommonJS entry instead of the browser CDN build. Use/dist/cdn.min.js.
form ERROR / WARNING A route form contract and the template's actual<input>, <select>, or <textarea>names disagree.
form_contract INFO <form action="/path" method="post"> targets a route with no FormContractdeclaration
csrf_form WARNING CSRFMiddleware is active and a static mutating <form> has no {{ csrf_field() }}, csrf_token(), or _csrf_tokenfield.
a11y_label, a11y_alt, a11y_heading, a11y_landmark WARNING Accessibility checks for missing labels, missing image alt text, skipped heading levels, or missing landmarks.
csrf_session, secret_key, middleware_signature ERROR / WARNING Production-safety checks for security middleware ordering, missing secret keys, and middleware call signatures.

These checks run automatically as part ofchirp check myapp:app. Some categories only activate when the app provides the relevant metadata, such as a DependencyIndex, FormContract, OOB registry entries, or docs plugin collection.

app.check()is not a style linter. It exists to catch wiring that can make the browser swap the wrong thing, silently skip an OOB update, or route a page into the wrong shell. For visual symptoms and browser-side diagnostics, start with Debugging Swaps.

Any category can be tuned withapp.override_contract_severity()— for example, demote the missing-handler ERROR during a migration:

from chirp.contracts.types import Severity

app.override_contract_severity("page_handlers", Severity.WARNING)

Introspection

Whenconfig.debug=True:

  • Debug headers:X-Chirp-Route-Kind, X-Chirp-Route-Files, X-Chirp-Route-Meta, X-Chirp-Route-Section, X-Chirp-Context-Chain, X-Chirp-Shell-Context
  • Route explorer:GET /__chirp/routesshows the full route tree with drill-down
  • HTMX panel: Activity log entries show route metadata when expanded

For htmx request records and Swap Doctor diagnostics, open Chirp DevTools with Ctrl+Shift+Din debug mode.