Filters

Register custom template filters and globals

4 min read 885 words

Built-in Filters

Chirp ships with built-in filters that are automatically available in every template. These complement Kida's core filters with web-specific utilities.

field_errors

Extract validation errors for a single form field from an errors dict. Returns a list of error messages.

{% for msg in errors | field_errors("email") %}
    <p class="error">{{ msg }}</p>
{% end %}

If the field has no errors, or errors is None, an empty list is returned — so the loop simply produces nothing.

Typical usage withform_or_errors():

@app.route("/signup", methods=["POST"])
async def signup(request: Request):
    result = await form_or_errors(request, SignupForm, "signup.html", "form")
    if isinstance(result, ValidationError):
        return result  # errors and form values are included
    # ... process valid data
<label>Email</label>
<input name="email" value="{{ form.email ?? "" }}">
{% for msg in errors | field_errors("email") %}
    <span class="field-error">{{ msg }}</span>
{% end %}

qs

Build a URL with query-string parameters. Falsy values are automatically omitted.

<a href="{{ '/search' | qs(q=query, page=page) }}">Search</a>
{# Output: /search?q=hello&page=2 #}

Falsy values (None, "", 0, False) are dropped:

{{ '/items' | qs(q=query, category=category, page=none) }}
{# If category is "", outputs: /items?q=hello #}

Appends to existing query strings:

{{ '/search?q=hello' | qs(page=2) }}
{# Output: /search?q=hello&page=2 #}

Special characters are URL-encoded:

{{ '/search' | qs(q="hello world") }}
{# Output: /search?q=hello%20world #}

attr

Output an HTML attribute when the value is truthy, else nothing. Shorthand for optional attributes without{% if %}blocks.

<a href="{{ href }}"{{ class | attr("class") }}>{{ text }}</a>
{# When class is "active": <a href="/foo" class="active">Foo</a> #}
{# When class is "" or None: <a href="/foo">Foo</a> #}

Useful for optional class, data-*, hx-*, and other attributes. Values are HTML-escaped.

url

Safelist a URL forhref attributes. Validates the scheme (blocks javascript:, data:etc.) and returns the URL or a fallback if unsafe. Use when building links from user or external data.

<a href="{{ user_link | url }}">User link</a>
<a href="{{ external_url | url(fallback='/') }}">External</a>

island_props

Serialize JSON props safely for island mount attributes:

<div data-island="editor"
     data-island-props="{{ state | island_props }}">
  Fallback editor UI.
</div>

Use with the island_attrs(...)global for convenience:

<div{{ island_attrs("editor", props=state, mount_id="editor-root") }}>
  Fallback editor UI.
</div>

Use primitive_attrs(...)when you want stricter primitive metadata:

<div{{ primitive_attrs("grid_state", props={"stateKey": "team", "columns": ["name", "role"]}) }}>
  ...
</div>

Security Filters (from Kida)

Chirp uses Kida's template engine, which provides escape and safe filters. These are critical for preventing XSS.

e / escape — HTML-escape a value. When AppConfig(autoescape=True) (the default), {{ x }} is escaped automatically. Use | e explicitly when chaining filters that might strip escaping (e.g. {{ user_input | upper | e }}).

safe(reason="...") — Mark output as trusted HTML so it is not escaped. Only use for content that is sanitized or from trusted sources (e.g. Patitas markdown output, CMS blocks, server-generated HTML). Never use on raw user input — that enables XSS.

{{ content | markdown | safe(reason="patitas output") }}
{{ cms_block | safe(reason="admin-only CMS") }}

The reasonargument is for code review and audit; it is not used at runtime.

URL attributes — When buildinghref from user data, use the url filter or Kida's url_is_safe() / safe_url()in a custom filter. See Kida security docs for context-specific escaping (JavaScript, CSS).

HTML validation — Validate markup with whatwg.org/validator to catch conformance errors. Chirp'schirp check <app> (and app.check()) validates hypermedia contracts: hx-get/hx-post/hx-put/hx-delete/hx-patch and action URLs must resolve to registered routes with compatible methods. Query params (e.g. ?page=1) are stripped before matching. Use both for full coverage.


Custom Filters

Filters transform values in templates. Register them with@app.template_filter():

@app.template_filter()
def currency(value: float) -> str:
    return f"${value:,.2f}"

@app.template_filter()
def pluralize(count: int, singular: str, plural: str) -> str:
    return singular if count == 1 else plural

Use them in templates with the pipe syntax:

<span class="price">{{ product.price | currency }}</span>
<span>{{ count }} {{ count | pluralize("item", "items") }}</span>

Named Filters

By default, the function name becomes the filter name. Override it with an argument:

@app.template_filter("fmt_date")
def format_date(dt: datetime) -> str:
    return dt.strftime("%B %d, %Y")
<time>{{ post.created_at | fmt_date }}</time>

Template Globals

Globals are functions or values available in every template without being passed in the context:

@app.template_global()
def site_name() -> str:
    return "My App"

@app.template_global()
def current_year() -> int:
    return datetime.now().year
<footer>&copy; {{ current_year() }} {{ site_name() }}</footer>

Registration Timing

Filters and globals must be registered during the setup phase (beforeapp.run()or the first request). They become part of the kida environment at freeze time.

app = App()

# Register during setup
@app.template_filter()
def upper(value: str) -> str:
    return value.upper()

# This works
app.run()

# Registering after freeze would raise an error

Type Safety

Filters are regular Python functions with full type annotations. Your IDE provides autocomplete and type checking for filter arguments.

@app.template_filter()
def truncate(value: str, length: int = 50, suffix: str = "...") -> str:
    if len(value) <= length:
        return value
    return value[:length].rsplit(" ", 1)[0] + suffix

Next Steps