Building dynamic web apps with Chirp? This track covers block rendering, regions, OOB updates, and framework integration—everything you need for HTMX partials and layout composition.

Tip

Duration: ~45 min | Prerequisite: Python 3.14+, basic Chirp or HTMX familiarity

1

Framework Integration

Block rendering, introspection, and composition APIs for Chirp, Bengal, and custom adapters

Framework Integration

Kida provides block-level rendering, introspection, and composition helpers for frameworks that need fragments (HTMX, Turbo), layout assembly, or template validation. Use these APIs when building adapters, validating routes, or composing layouts programmatically.

Overview

API Purpose
render_block(name, **ctx) Render a single block — HTMX partials, cached nav
render_with_blocks(overrides, **ctx) Compose layout with pre-rendered HTML in blocks
list_blocks() Discover block names for validation
template_metadata() Full analysis — blocks, extends, dependencies
validate_block_exists() Check block exists beforerender_block
get_structure() Lightweight manifest for composition planning

Block Rendering

render_block

Render a single block from a template. Supports inherited blocks: when the template extends a parent, you can render parent-only blocks by name (e.g.render_block("sidebar") on a child that extends a base defining sidebar).

template = env.get_template("page.html")

# Render content block (HTMX partial response)
html = template.render_block("content", title="Hello", items=items)

# Render parent-only block from descendant
html = template.render_block("sidebar", site=site)

Raises KeyErrorif the block does not exist in the template or any parent.

render_with_blocks

Render a template with pre-rendered HTML injected into blocks. Enables programmatic layout composition without{% extends %}in the template source.

layout = env.get_template("_layout.html")
inner_html = "<h1>Hello</h1><p>Content here.</p>"

# Inject inner_html as the "content" block
html = layout.render_with_blocks({"content": inner_html}, title="Page Title")

Each key in block_overridesnames a block; the value is a pre-rendered HTML string that replaces that block's default content.

list_blocks

List all blocks available forrender_block(), including inherited blocks.

blocks = template.list_blocks()
# ['title', 'nav', 'content', 'footer']

Introspection

template_metadata

Get full template analysis including inheritance info, block metadata, and dependencies. ReturnsNone if AST was not preserved (preserve_ast=Falseor loaded from bytecode cache without source).

meta = template.template_metadata()
if meta:
    print(meta.extends)              # Parent template name
    print(list(meta.blocks.keys()))  # Block names
    print(meta.all_dependencies())   # Context paths accessed

block_metadata

Get per-block analysis: purity, cache scope, inferred role.

blocks = template.block_metadata()
nav = blocks.get("nav")
if nav and nav.cache_scope == "site":
    # Safe to cache nav across all pages
    html = cache.get_or_render("nav", ...)

validate_context

Check a context dict for missing variables before rendering.

missing = template.validate_context(user_context)
if missing:
    raise ValueError(f"Missing template variables: {missing}")

Composition Module

Thekida.compositionmodule provides validation helpers for frameworks:

from kida import Environment, FileSystemLoader
from kida.composition import validate_block_exists, get_structure

env = Environment(loader=FileSystemLoader("templates/"))

validate_block_exists

Check if a block exists before callingrender_block:

if validate_block_exists(env, "skills/page.html", "page_content"):
    html = env.get_template("skills/page.html").render_block("page_content", ...)
else:
    # Handle missing block
    ...

Returns Falseif the template is not found or the block is missing.

get_structure

Get a lightweight structure manifest (block names, extends parent, dependencies). Cached by Environment for reuse.

struct = get_structure(env, "page.html")
if struct and "page_root" in struct.block_names:
    # Template has page_root block — suitable for layout composition
    ...

block_role_for_framework

Classify block metadata into framework-relevant roles ("fragment", "page_root", or None). Useful for frameworks that need to distinguish content blocks from layout roots.

from kida.composition import block_role_for_framework

meta = template.template_metadata()
for name, block in meta.blocks.items():
    role = block_role_for_framework(block)
    if role == "fragment":
        # Suitable for HTMX partial
        ...

Adapter Pattern

A minimal template adapter wraps Kida's APIs:

from kida import Environment
from typing import Any

class KidaAdapter:
    """TemplateAdapter implementation using Kida's block/layout APIs."""

    def __init__(self, env: Environment) -> None:
        self._env = env

    def render_template(self, template: str, context: dict[str, Any]) -> str:
        return self._env.get_template(template).render(context)

    def render_block(self, template: str, block: str, context: dict[str, Any]) -> str:
        return self._env.get_template(template).render_block(block, context)

    def compose_layout(
        self,
        template: str,
        block_overrides: dict[str, str],
        context: dict[str, Any],
    ) -> str:
        return self._env.get_template(template).render_with_blocks(
            block_overrides, **context
        )

    def template_metadata(self, template: str) -> object | None:
        from kida.environment.exceptions import (
            TemplateNotFoundError,
            TemplateSyntaxError,
        )
        try:
            return self._env.get_template(template).template_metadata()
        except (TemplateNotFoundError, TemplateSyntaxError):
            return None

Chirp uses this pattern in KidaAdapter.

Case Studies

Bengal (Static Site Generator)

  • Full renderrender()for page output
  • Bytecode cache — Persistent.bengal/cache/kida/for cold-start
  • Fragment cache{% cache %}with site-scoped TTL
  • Analysisblock_metadata(), is_cacheable()for incremental builds

Chirp (Web Framework)

  • Full renderrender()for full-page responses
  • Block renderrender_block()for HTMX fragments, partial updates
  • Layout compositionrender_with_blocks()for programmatic layout assembly
  • Streamingrender_stream(), render_stream_async()for chunked HTTP
  • Introspectiontemplate_metadata() for composition planning, validate_block_exists() before render_block
  • AdapterKidaAdapter implements Chirp's TemplateAdapterinterface

AST-driven OOB discovery: Chirp usestemplate_metadata()to discover OOB regions at build time — it never hard-codes which blocks to render.build_layout_contract() callsmeta.regions() (or filters meta.blocks by *_oobsuffix), extracts cache_scope and depends_on from each block's BlockMetadata, and builds a LayoutContractcached per template. On boosted navigation, Chirp renders the page fragment plus the OOB regions ashx-swap-oobupdates. See Chirp's Kida Integration for the full flow.

Chirp + Regions: Step-by-Step

  1. Define regions in layout templates — Use{% region %}for blocks that should update out-of-band (sidebar, breadcrumbs, title):

       {% region sidebar_oob(current_path="/") %}
         <nav>{{ current_path }}</nav>
       {% end %}
    
  2. Discover at build time — Chirp callsmeta = template.template_metadata() and meta.regions() to get region blocks. Each has region_params, depends_on, and cache_scope.

  3. Render on navigation — For HTMX boosted requests, Chirp renders the main content block plus each OOB region viarender_block(), passing the region's params from the request context.

  4. No hard-coding — The layout contract is built from AST analysis. Add or remove regions in templates; Chirp adapts automatically.

See Regions for syntax and Chirp Kida Integration for the full Chirp flow.

See Also

2

Functions

Define reusable template functions and macros

Functions

Define reusable template functions within your templates.

Defining Functions

Use{% def %}to create a function:

{% def greet(name) %}
    <p>Hello, {{ name }}!</p>
{% end %}

{{ greet("Alice") }}
{{ greet("Bob") }}

Parameters

Required Parameters

{% def button(text, url) %}
    <a href="{{ url }}" class="button">{{ text }}</a>
{% end %}

{{ button("Click Me", "/action") }}

Default Values

{% def button(text, url, style="primary") %}
    <a href="{{ url }}" class="button button-{{ style }}">{{ text }}</a>
{% end %}

{{ button("Save", "/save") }}
{{ button("Cancel", "/cancel", style="secondary") }}

Keyword Arguments

{% def card(title, content="", footer=none) %}
    <div class="card">
        <h3>{{ title }}</h3>
        {% if content %}
            <p>{{ content }}</p>
        {% end %}
        {% if footer %}
            <footer>{{ footer }}</footer>
        {% end %}
    </div>
{% end %}

{{ card(title="Hello", content="World") }}
{{ card("Title Only") }}

Typed Parameters

Parameters can carry optional type annotations following Python syntax:

{% def card(title: str, items: list, footer: str | None = none) %}
    <h3>{{ title }}</h3>
    {% for item in items %}<p>{{ item }}</p>{% end %}
    {% if footer %}<footer>{{ footer }}</footer>{% end %}
{% end %}

Annotations are optional per-parameter — you can mix typed and untyped:

{% def mixed(name: str, options, count: int = 0) %}
    ...
{% end %}

Supported Syntax

Syntax Meaning
x: str Simple type
x: int Simple type
x: list Generic without params
x: dict[str, int] Generic with params
x: str | None Union (PEP 604 style)
x: MyModel Custom type name

What Annotations Do

Annotations are documentation and validation hints, not enforced at runtime. The template engine does not performisinstancechecks. Their value is:

  1. Compile-time call-site validation — wrong parameter names are caught immediately whenvalidate_calls=Trueis set on the Environment
  2. IDE support — annotations flow into the generated Python code, enabling autocomplete in tooling
  3. Self-documenting — makes component interfaces explicit

Call-Site Validation

Enablevalidate_callson the Environment to catch parameter errors at compile time:

from kida import Environment

env = Environment(validate_calls=True)

# This emits a warning: 'titl' is not a param of 'card'
env.from_string("""
    {% def card(title: str) %}{{ title }}{% end %}
    {{ card(titl="oops") }}
""")

Validation checks:

  • Unknown parameters — keyword args not in the definition
  • Missing required parameters — params without defaults not provided
  • *args / **kwargsin the definition relax validation accordingly

See Static Analysis for the programmatic API.


Capturing Content (Default Slot)

Functions can capture call-block content usingcaller():

{% def wrapper(title) %}
    <section>
        <h2>{{ title }}</h2>
        <div class="content">
            {{ caller() }}
        </div>
    </section>
{% end %}

{% call wrapper("Section Title") %}
    <p>This content is passed to the wrapper.</p>
    <ul>
        <li>Item 1</li>
        <li>Item 2</li>
    </ul>
{% end %}

caller()without arguments reads the default slot.

Named Slots

Kida also supports named slots for multi-region components:

{% def card(title) %}
    <article>
        <h2>{{ title }}</h2>
        <div class="actions">{% slot header_actions %}</div>
        <div class="body">{% slot %}</div>
    </article>
{% end %}

{% call card("Settings") %}
    {% slot header_actions %}<button>Save</button>{% end %}
    <p>Body content.</p>
{% end %}

How it works:

  • {% slot %} in a defis the default placeholder.
  • {% slot name %} in a defis a named placeholder.
  • Inside{% call %}, use {% slot name %}...{% end %}to provide named slot content.
  • caller("name") retrieves a named slot from inside a def.

Slot Forwarding with{% yield %}

When composing macros, you often need to forward the outer caller's slot content into a nested{% call %}. The {% slot %} tag has context-dependent meaning: inside {% call %} it defines content (a SlotBlock), not a render reference. Use {% yield %}when you want to render the enclosing def's caller slot regardless of block context:

{% def selection_bar() %}<bar>{{ caller() }}</bar>{% end %}
{% def resource_index() %}
  {% call selection_bar() %}
    {% yield selection %}
  {% end %}
{% end %}
{% call resource_index() %}
  {% slot selection %}Badges{% end %}
  Cards
{% end %}
  • {% yield %} — render the caller's default slot (same as {% slot %}inside a def).
  • {% yield name %} — render the caller's named slot name.

{% yield %} is self-closing (no {% end %}) and always produces a render reference, even inside {% call %}blocks. It resolves to the nearest enclosing def's caller, regardless of nesting depth. No caller means no output (silent no-op).

When to use: Prefer{% yield %} over the double-nesting workaround {% slot x %}{% slot x %}{% end %}when forwarding slots through nested calls.

Slot Context Inheritance

Slot content is rendered in the caller's context. Variables from the page or render context are available in slot content without| default():

{% def form(action, method="get") %}
<form action="{{ action }}" method="{{ method }}">
    {% slot %}
</form>
{% end %}

{% block page_content %}
{% call form("/search") %}
    {{ search_field("q", value=q) }}
    {% if selected_tags %}
        {{ hidden_field("tags", value=selected_tags | join(",")) }}
    {% end %}
{% end %}
{% end %}

When render_block("page_content", q="...", selected_tags=["a","b"]) is called, q and selected_tags are available inside the form slot because the slot body inherits the caller's render context. This works for both render() and render_block().

Slot Detection

When a function is called via{% call %}, it receives slot content accessible through caller(). Use the built-in has_slot() helper inside a {% def %}body to detect whether any call slot content was provided:

{% def card(title) %}
    <div class="card">
        <h3>{{ title }}</h3>
        {% if has_slot() %}
            <div class="card-body">
                {{ caller() }}
            </div>
        {% end %}
    </div>
{% end %}

When called directly, has_slot() returns false:

{{ card("Simple Card") }}
{# Output: <div class="card"><h3>Simple Card</h3></div> #}

When called with {% call %}, has_slot() returns true:

{% call card("Rich Card") %}
    <p>This content appears in the card body.</p>
    <button>Action</button>
{% end %}
{# Output includes the card-body wrapper #}

This pattern is useful for components that should adapt their markup depending on whether slot content is provided, for example rendering a wrapper <div>only when there is something to wrap.


Islands Wrapper Pattern

Functions are a good fit for reusable island mount wrappers in server-rendered apps:

{% def island_shell(name, attrs="") %}
<section{{ attrs }}>
    <div class="island-fallback">
        {% slot %}
    </div>
</section>
{% end %}

{% call island_shell("editor", attrs=island_attrs("editor", {"doc_id": doc.id}, "editor-root")) %}
    <p>Server-rendered fallback editor UI.</p>
{% end %}

Guidelines:

  • Keep fallback slot content usable without JavaScript.
  • Pass serialized props via helper globals (avoid manual inline JSON).
  • Prefer stable mount IDs for deterministic remount behavior.
  • Use explicit wrapper signatures when creating state primitives:
{% def grid_state_shell(state_key, columns, attrs="") %}
<section{{ attrs }}>
    {% slot %}
</section>
{% end %}

Regions

Regions are parameterized blocks that work as both blocks (forrender_block()) and callables (for {{ name(args) }}). Use them when you need parameterized fragments for HTMX partials, OOB updates, or layout composition.

Syntax

{% region name(param1, param2=default) %}
    ...body...
{% end %}

{{ name(value1, value2) }}

Block and Callable

A region compiles to both:

  • A block — calltemplate.render_block("name", param1=..., param2=...)
  • A callable — use{{ name(args) }}in the template body
{% region sidebar(current_path="/") %}
  <nav>{{ current_path }}</nav>
{% end %}

{% block content %}
  {{ sidebar(current_path="/about") }}
{% endblock %}
# From Python: render the region as a block
html = template.render_block("sidebar", current_path="/settings")

Outer Context

Region bodies can read variables from the outer render context (not just parameters):

{% region crumbs(current_path="/") %}
{{ breadcrumb_items | default([{"label":"Home","href":"/"}]) | length }}
{% end %}

{{ crumbs(current_path="/x") }}

When render_block("crumbs", ...) or {{ crumbs(...) }} is called, the region receives its params plus the caller's context. breadcrumb_itemscomes from the outer context.

Region default expressions

Optional parameters can use any expression as a default, not just simple variable names. Defaults are evaluated at call time from the caller's context:

{% region sidebar(section, meta=page.metadata) %}
  <nav>{{ meta.title }}</nav>
{% end %}

{% region stats(count=items | length) %}
  {{ count }} items
{% end %}

{% region header(title=page?.title ?? "Default") %}
  <h1>{{ title }}</h1>
{% end %}

Supported expressions include:

  • Simple namescurrent_page=page(zero-overhead inline lookup)
  • Attribute accessmeta=page.metadata
  • Filterscount=items | length
  • Optional chainingtitle=page?.title ?? "Default"
  • Null coalescingmeta=data?.info ?? {}

Static analysis (depends_on) correctly captures context paths from complex defaults for incremental build and cache scope inference.

Regions vs Defs

Use case Region Def
render_block() ✅ Yes — region is a block ❌ No — def is not a block
{{ name(args) }} ✅ Yes ✅ Yes
Slots /{% call %} ❌ No ✅ Yes
Outer-context access ✅ Yes ✅ Yes (via caller context)
Framework OOB discovery meta.regions() ❌ N/A

Use regions when you need parameterized blocks forrender_block(), HTMX OOB, or framework layout composition. Use defs when you need slots, {% call %}, or component composition.

Framework Integration

Frameworks like Chirp usetemplate_metadata().regions() to discover OOB regions at build time. Each region's BlockMetadata includes is_region, region_params, and depends_onfor cache scope inference. See Framework Integration.


Macros

Kida also supports the{% macro %}syntax:

{% macro input(name, value="", type="text") %}
    <input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}

{{ input("username") }}
{{ input("password", type="password") }}

Importing Functions

Import functions from other templates:

{% from "macros.html" import button, input %}

{{ button("Submit", "/submit") }}
{{ input("email", type="email") }}

Import all with a namespace:

{% import "forms.html" as forms %}

{{ forms.input("name") }}
{{ forms.textarea("bio") }}

Best Practices

Single Responsibility

Each function should do one thing:

{# Good: Single purpose #}
{% def user_avatar(user, size=32) %}
    <img src="{{ user.avatar_url }}"
         alt="{{ user.name }}"
         width="{{ size }}"
         height="{{ size }}">
{% end %}

{# Avoid: Too much logic #}
{% def user_card_with_everything(user, show_bio, show_posts, ...) %}
    ...
{% end %}

Descriptive Names

{# Good: Clear purpose #}
{% def format_price(amount, currency="USD") %}
{% def user_badge(role) %}
{% def pagination_nav(current, total) %}

{# Avoid: Vague names #}
{% def render(x) %}
{% def do_thing(item) %}

Macro vs Context Variable Naming

When importing macros that render context variables, use different names so the macro does not shadow the variable. Prefer verb-prefixed names for macros and noun-like names for context variables:

{# Good: Macro and variable have different names #}
{% from "_route_tabs.html" import render_route_tabs %}
{% if route_tabs | default([]) %}
    {{ render_route_tabs(route_tabs, current_path) }}
{% end %}

{# Avoid: Same name causes shadowing — route_tabs may resolve to the macro #}
{% from "_route_tabs.html" import route_tabs %}
{% if route_tabs | default([]) %}  {# When route_tabs not in context, this is the macro (truthy) #}
    {{ route_tabs(route_tabs, current_path) }}  {# Passes macro as first arg → "not iterable" #}
{% end %}
Use for Naming Examples
Macros Verb-prefixed render_route_tabs, format_date, render_nav
Context variables Noun-like route_tabs, items, skills

If you seeCannot iterate over macro 'X', a macro is shadowing a context variable. Rename the macro (e.g. render_X) to avoid the collision.

See Also

3

Static Analysis

Analyze templates for dependencies, purity, and caching potential

Static Analysis

Kida can statically analyze compiled templates to extract dependency information, determine output purity, and recommend caching strategies — all without rendering. No other Python template engine provides this capability.

What Analysis Provides

Every compiled template exposes these analysis results:

Capability Method Returns
Context dependencies template.required_context() Top-level variable names the template accesses
Full dependency paths template.depends_on() Dotted paths likepage.title, site.pages
Block metadata template.block_metadata() Per-block purity, dependencies, cache scope
Full metadata template.template_metadata() Complete analysis including inheritance info
Cache check template.is_cacheable("nav") Whether a block can be safely cached
Context validation template.validate_context(ctx) Missing variable names before rendering

Quick Start

from kida import Environment, DictLoader

env = Environment(loader=DictLoader({
    "page.html": """
        {% extends "base.html" %}
        {% block title %}{{ page.title }}{% end %}
        {% block nav %}
            <nav>{% for item in site.menu %}<a href="{{ item.url }}">{{ item.label }}</a>{% end %}</nav>
        {% end %}
        {% block content %}{{ page.content }}{% end %}
    """,
    "base.html": """
        <html>
        <head><title>{% block title %}{% end %}</title></head>
        <body>{% block nav %}{% end %}{% block content %}{% end %}</body>
        </html>
    """,
}))

template = env.get_template("page.html")

Check What Variables a Template Needs

>>> template.required_context()
frozenset({'page', 'site'})

Validate Context Before Rendering

>>> template.validate_context({"page": page_obj})
['site']  # 'site' is missing

>>> template.validate_context({"page": page_obj, "site": site_obj})
[]  # all required variables present

Inspect Block-Level Metadata

>>> meta = template.block_metadata()
>>> nav = meta["nav"]
>>> nav.depends_on
frozenset({'site.menu'})
>>> nav.is_pure
'pure'
>>> nav.cache_scope
'site'
>>> nav.is_cacheable()
True

Determine Caching Strategy

>>> template.is_cacheable("nav")    # site-wide cache
True
>>> template.is_cacheable("content")  # page-specific, still cacheable
True
>>> template.is_cacheable()          # all blocks cacheable?
True

Analysis Concepts

Dependencies

The dependency walker extracts every context variable path a template accesses. Results are conservative: they may include paths not taken at runtime (e.g., both branches of an{% if %}) but never miss a path that is used.

>>> template.depends_on()
frozenset({'page.title', 'page.content', 'site.menu'})

required_context()extracts just the top-level names:

>>> template.required_context()
frozenset({'page', 'site'})

Purity

A block is pure if its output is deterministic given the same inputs. Pure blocks can be cached safely. Impure blocks (usingrandom, shuffle, or similar) must be re-rendered each time.

Purity Meaning Cacheable?
"pure" Deterministic output Yes
"impure" Uses non-deterministic functions No
"unknown" Cannot determine statically Treat as impure

Cache Scope

Cache scope tells you how broadly a block's output can be reused:

Scope Meaning Example
"site" Same output for every page Navigation, footer
"page" Varies per page but stable for a given page Content, title
"none" Cannot cache (impure block) Random quotes
"unknown" Cannot determine Mixed dependencies

Landmarks

Kida detects HTML5 landmark elements (<nav>, <main>, <header>, <footer>, <aside>) in block output and uses them to infer the block's role:

>>> meta["nav"].emits_landmarks
frozenset({'nav'})
>>> meta["nav"].inferred_role
'navigation'

Context Validation

validate_context()is designed for build systems, SSR frameworks, and testing pipelines that need to catch missing variables before rendering:

template = env.get_template("email.html")
missing = template.validate_context(user_context)
if missing:
    raise ValueError(f"Missing template variables: {missing}")
result = template.render(**user_context)

This runs dependency analysis (cached after first call) and compares required top-level variable names against the provided context keys plus environment globals. It returns a sorted list of missing names, or an empty list if everything is present.

Call-Site Validation

Kida can validate{% def %}call sites at compile time, catching parameter errors before any template is rendered:

from kida import Environment

env = Environment(validate_calls=True)

template = env.from_string("""
    {% def button(text: str, url: str, style="primary") %}
        <a href="{{ url }}" class="btn btn-{{ style }}">{{ text }}</a>
    {% end %}

    {{ button(text="Save", urll="/save") }}
""")
# UserWarning: Call to 'button' at <string>:6 — unknown params: urll; missing required: url

What It Checks

Issue Example
Unknown params Callingbutton(labl="X") when param is label
Missing required Callingbutton() when texthas no default
*args / **kwargsrelaxation Definitions with*args or **kwargssuppress unknown-param warnings

Programmatic API

For build systems and CI pipelines, use theBlockAnalyzerdirectly:

from kida import Environment, DictLoader
from kida.analysis import BlockAnalyzer

env = Environment(
    loader=DictLoader({"page.html": src}),
    preserve_ast=True,
)
template = env.get_template("page.html")

analyzer = BlockAnalyzer()
issues = analyzer.validate_calls(template._optimized_ast)

for issue in issues:
    if not issue.is_valid:
        print(f"{issue.def_name} at line {issue.lineno}: "
              f"unknown={issue.unknown_params}, "
              f"missing={issue.missing_required}")

CallValidation

Field Type Description
def_name str Name of the called{% def %}
lineno int Line number of the call site
col_offset int Column offset of the call site
unknown_params tuple[str, ...] Keyword args not in the definition
missing_required tuple[str, ...] Required params not provided
duplicate_params tuple[str, ...] Params passed more than once
Property Description
is_valid Trueif no issues were found

Configuration

UseAnalysisConfigto customize analysis behavior for your framework:

from kida import AnalysisConfig
from kida.analysis import BlockAnalyzer

config = AnalysisConfig(
    # Variables indicating page-specific scope
    page_prefixes=frozenset({"post.", "post", "article.", "article"}),
    # Variables indicating site-wide scope
    site_prefixes=frozenset({"settings.", "settings", "global."}),
    # Additional functions your framework guarantees are pure
    extra_pure_functions=frozenset({"asset_url", "t", "current_lang"}),
    # Filters that produce non-deterministic output
    extra_impure_filters=frozenset({"random_choice"}),
)

analyzer = BlockAnalyzer(config=config)

Kida ships with a default config (DEFAULT_CONFIG) that includes common SSG pure functions likeasset_url, t, canonical_url, etc.

Case Study: Bengal Static Site Generator

Bengal uses Kida's analysis API to implement smart incremental builds:

  1. 1

    Compile all templates

    Enable AST preservation when compiling.

  2. 2

    Analyze each template

    Get block metadata for each compiled template.

  3. 3

    Identify site-cacheable blocks

    Find nav, footer, sidebar usingcache_scope == "site".

  4. 4

    Cache site-scoped blocks

    Cache once per build, reuse across all pages.

  5. 5

    Re-render page-scoped blocks only

    Only when page content changes.

  6. 6

    Track dependencies

    Invalidate caches when upstream data changes.

This reduces full-site rebuild time by 40-60% for sites with shared navigation and footer blocks.

Case Study: Chirp Web Framework

Chirp uses Kida's introspection and block APIs for dynamic web apps:

  1. 1

    Composition planning

    Usetemplate_metadata()to discover blocks and inheritance before rendering.

  2. 2

    Block validation

    Callvalidate_block_exists(env, template, block) before render_block()to avoid KeyError.

  3. 3

    Fragment rendering

    Userender_block()for HTMX partial responses and Turbo Stream updates.

  4. 4

    Layout assembly

    Userender_with_blocks()to inject pre-rendered content into layout templates.

  5. 5

    Adapter pattern

    KidaAdapter implements Chirp's TemplateAdapterinterface, wrapping all Kida APIs.

See Framework Integration for the full adapter pattern and API usage.

API Reference

Template Methods

Method Signature Description
required_context() () -> frozenset[str] Top-level variable names needed
depends_on() () -> frozenset[str] All dotted dependency paths
validate_context() (context: dict) -> list[str] Missing variable names
block_metadata() () -> dict[str, BlockMetadata] Per-block analysis results
template_metadata() () -> TemplateMetadata | None Full template analysis
is_cacheable() (block_name: str | None) -> bool Cache safety check
list_blocks() () -> list[str] Block names in template

BlockMetadata

Field Type Description
name str Block identifier
depends_on frozenset[str] Context paths accessed
is_pure "pure" | "impure" | "unknown" Determinism classification
cache_scope "site" | "page" | "none" | "unknown" Recommended cache level
emits_html bool Whether block produces output
emits_landmarks frozenset[str] HTML5 landmarks detected
inferred_role str Heuristic role classification
is_region bool True if block is a{% region %}
region_params tuple[str, ...] Parameter names (regions only)

TemplateMetadata

Field Type Description
name str | None Template identifier
extends str | None Parent template name
blocks dict[str, BlockMetadata] All block metadata
top_level_depends_on frozenset[str] Dependencies outside blocks
Method Description
all_dependencies() Union of all block and top-level dependencies
get_block(name) Get metadata for a specific block
regions() Return only region-typed blocks (for OOB discovery)
cacheable_blocks() List of blocks whereis_cacheable()is True
site_cacheable_blocks() List of blocks withcache_scope == "site"

CallValidation

Field Type Description
def_name str Name of the called{% def %}
lineno int Line number of the call site
col_offset int Column offset of the call site
unknown_params tuple[str, ...] Keyword args not in the definition
missing_required tuple[str, ...] Required params not provided
duplicate_params tuple[str, ...] Params passed more than once
Property Type Description
is_valid bool Trueif no issues were found

AnalysisConfig

Field Type Default Description
page_prefixes frozenset[str] {"page.", "page", ...} Page-scope variable prefixes
site_prefixes frozenset[str] {"site.", "site", ...} Site-scope variable prefixes
extra_pure_functions frozenset[str] frozenset() Additional pure function names
extra_impure_filters frozenset[str] frozenset() Additional impure filter names

Integration with Frameworks

Frameworks like Chirp use Kida templates and run contract validation (chirp check) to verify that hx-post, hx-get, and action URLs in templates match registered routes. Dynamic URLs built with ~ or {{ var }}are correctly skipped — only literal URLs are validated.

4

render_block and Def Scope

Blocks do not inherit defs from the same template

render_block and Def Scope

When you calltemplate.render_block("block_name", context), the block is rendered in isolation. It does not have access to {% def %}macros defined in the same template.

Inherited blocks:render_block() resolves blocks through the inheritance chain. You can render parent-only blocks (e.g. sidebarfrom a base template) on a descendant template. Child overrides still win. See Inheritance — render_block.

The Problem

{# page.html — def and block in same file #}
{% def helper() %}shared logic{% end %}

{% block content %}
  {{ helper() }}  {# NameError or UndefinedError when using render_block #}
{% end %}

If a framework (e.g. Chirp) calls template.render_block("content", ...), the block cannot see helperbecause blocks are compiled with their own scope.

Fix

Split defs into a separate file and import them:

{# _helpers.html #}
{% def helper() %}shared logic{% end %}
{# page.html #}
{% from "_helpers.html" import helper %}

{% block content %}
  {{ helper() }}
{% end %}

Now both full-page render() and render_block("content", ...) work, because the block imports helperfrom another template.

Regions: An Alternative

{% region %}blocks are compiled as part of the template and have access to defs and imports from the same file. If you need a parameterized block that uses defs, consider a region instead of splitting into a separate template:

{% from "_helpers.html" import helper %}

{% region sidebar(current_path="/") %}
  {{ helper(current_path) }}
{% end %}

Both render_block("sidebar", ...) and {{ sidebar(...) }} work, and the region body can call helper(). See Functions — Regions.

When This Matters

  • Fragment rendering — Chirp and similar frameworks userender_block()to return HTML fragments for htmx, SSE, etc.
  • Block caching — Site-scoped block caching renders blocks individually viarender_block().

See Also

✓ Track Complete