# Filters
URL: /chirp/docs/build-apps/html-fragments/filters/
Section: html-fragments
Tags: templates, filters, globals, kida
--------------------------------------------------------------------------------
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 with form_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 for href 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. sanitized markdown output, CMS blocks, server-generated HTML). Never use on raw user input — that enables XSS. {{ content | markdown }} {{ cms_block | safe(reason="admin-only CMS") }} Chirp's markdown filter sanitizes unsafe HTML and URLs by default and returns template-safe markup. Pass sanitize=False to register_markdown_filter() only for trusted markdown where raw HTML is intentional. The reason argument is for code review and audit; it is not used at runtime. URL attributes — When building href 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's chirp 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>© {{ current_year() }} {{ site_name() }}</footer> Registration Timing Filters and globals must be registered during the setup phase (before app.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 Rendering -- How templates are rendered App Lifecycle -- When filters are registered API Reference -- Complete API surface
--------------------------------------------------------------------------------
Metadata:
- Word Count: 850
- Reading Time: 4 minutes