Chirp + Kida — AST-Driven Partial Rendering

Chirp doesn't hard-code which blocks to render as OOB. It asks the template. Kida's template_metadata() gives Chirp regions, dependencies, and cache scope — all from the AST.

This post is about one of the cleaner interactions in the stack: Chirp does not hard-code which regions to update out-of-band. It asks the template.

When you add {% region sidebar_oob(current_path="/") %}...{% end %} to your layout, Chirp discovers it at build time. No config file. No registry. The template declares; Chirp follows.

Most frameworks require the opposite: you register OOB regions in a config file or a central registry. Add the block to your template, then wire it up elsewhere — in settings.py, a route decorator, or a component registry. Two places to update. Forget one, and the region is invisible to the framework. Chirp inverts that: the template is the source of truth.

That is possible because Kida compiles templates to an AST and exposes template_metadata(): blocks, regions, dependencies, and cache scope. Chirp consumes that metadata to build a LayoutContract per template. On boosted navigation, it renders the page fragment plus the matching OOB regions as hx-swap-oob updates.


The flow

Template source → Kida parser → AST → Analyzer → TemplateMetadata
                                                    ↓
                              Chirp build_layout_contract() → LayoutContract
                                                    ↓
                              OOB region list (cache_scope, depends_on per block)

If you do not care about the internals, the important idea is simple: the template is the source of truth for which fragments exist and when they are safe to render.

  1. Kida parses and analyzes the template. TemplateMetadata has blocks, regions(), and per-block BlockMetadata with depends_on, cache_scope, is_region.

  2. Chirp calls adapter.template_metadata(layout_template) on the root layout.

  3. build_layout_contract() discovers blocks named *_oob (or region blocks with that suffix), extracts cache_scope and depends_on from each, and builds a cached LayoutContract.

  4. On boosted navigation (HTMX hx-boost), Chirp renders the page block and the OOB regions. Each region gets hx-swap-oob="true" and targets the right DOM id.

  5. On full page load, the entire layout renders normally. OOB regions are suppressed — they're only for fragment updates.


Regions: one definition, two uses

Kida's {% region %} compiles to both a block, for render_block(), and a callable, for {{ name(args) }}:

{% region sidebar_oob(current_path="/") %}
{% call sidebar() %}
  {{ sidebar_link("/", "Home", active=current_path == "/") }}
  {{ sidebar_link("/settings", "Settings", active=current_path.startswith("/settings")) }}
{% end %}
{% end %}
  • In the app shell slot: {{ sidebar_oob(current_path=current_path | default("/")) }} — callable, renders inline.
  • For OOB updates: render_block("sidebar_oob", current_path=...) — block, rendered as hx-swap-oob fragment.

One definition. No duplication. Chirp discovers it via meta.regions() and filters by the *_oob suffix.


Metadata in action

BlockMetadata field Chirp use
blocks Block existence, OOB discovery fallback
regions() Prefer region-typed OOB blocks
depends_on Skip OOB when required context missing
cache_scope Future: cache page-level fragments
is_region Filter to region blocks for OOB

When template_metadata is unavailable, such as through a Jinja2 adapter, Chirp falls back to well-known ChirpUI OOB blocks. The adapter contract is intentionally loose, blocks plus optional regions(), so other template engines could plug in.


Why it matters

The reader payoff is straightforward:

Declarative: Add a region to the template and Chirp picks it up. No framework config.

Extensible: New OOB regions require only the template change and a DOM id mapping. The LayoutContract is built from the AST.

Type-safe: Block validation uses meta.blocks before render. Missing block means a KeyError at plan time, not a silent empty fragment.

Zero duplication: Regions serve both app shell slots and OOB updates. One definition, two render paths.


Further reading