Add a Custom Filter

Extend Kida with your own template filters

7 min read 1392 words
Edit this page

Was this page helpful?

Learn how to create and register custom filters for Kida templates in Bengal.

Goal

Create a custom filter that formats currency values and use it in templates.

Note

Filters vs Functions: This guide covers filters (transform values with|). For functions (standalone operations), see Add Custom Functions (coming soon).

Quick distinction:

  • Filter:{{ value | my_filter }} → transforms value
  • Function:{{ my_function() }}→ performs operation

Prerequisites

  • Bengal site initialized
  • Kida enabled inbengal.toml
  • Python knowledge
  1. 1

    Create Filter Function

    Create a Python file for your filters:

    BASH
    mkdir -p python
    touch python/filters.py
    

    Define your filter function:

    PYTHON
    # python/filters.py
    def currency(value: float, symbol: str = "$") -> str:
        """Format a number as currency.
    
        Args:
            value: Numeric value to format
            symbol: Currency symbol (default: "$")
    
        Returns:
            Formatted string like "$1,234.56"
        """
        if value is None:
            return f"{symbol}0.00"
        return f"{symbol}{value:,.2f}"
    
  2. 2

    Register the Filter in a Plugin

    Register filters through the supported plugin API. A plugin implements thePluginprotocol and calls registry.add_template_filter() in its register()method:

    PYTHON
    # my_bengal_plugin/__init__.py
    from bengal.plugins.protocol import Plugin
    from bengal.plugins.registry import PluginRegistry
    
    from .filters import currency
    
    
    class MyFiltersPlugin(Plugin):
        name = "my-filters"
        version = "1.0.0"
    
        def register(self, registry: PluginRegistry) -> None:
            registry.add_template_filter("currency", currency)
    

    add_template_filteris a stable, supported entry point: registered filters are applied to every template environment during the build, and the frozen registry is safe to share across parallel render workers.

  3. 3

    Declare the Entry Point and Install

    Expose the plugin through thebengal.pluginsentry point group so Bengal discovers it automatically:

    TOML
    # pyproject.toml
    [project.entry-points."bengal.plugins"]
    my-filters = "my_bengal_plugin:MyFiltersPlugin"
    

    Install the plugin into the same environment as Bengal:

    BASH
    uv pip install -e ./my-bengal-plugin
    

    On the next build, Bengal discovers the plugin and registers your filter — no build script or internal-attribute access required. Verify it is picked up with:

    BASH
    bengal plugin list
    bengal plugin info my-filters
    
  4. 4

    Use Filter in Template

    Use your custom filter in templates:

    KIDA
    {{ 1234.56 | currency }}
    {# Output: $1,234.56 #}
    
    {{ 1234.56 | currency("€") }}
    {# Output: €1,234.56 #}
    
    {{ product.price | currency }}
    {# Output: $29.99 #}
    

Advanced Examples

Filter with Multiple Arguments

PYTHON
# python/filters.py
def truncate_words(value: str, length: int = 50, suffix: str = "...") -> str:
    """Truncate text to a specific word count.

    Args:
        value: Text to truncate
        length: Maximum word count
        suffix: Text to append if truncated

    Returns:
        Truncated text
    """
    if not value:
        return ""

    words = value.split()
    if len(words) <= length:
        return value

    return " ".join(words[:length]) + suffix

Register it in your plugin:

PYTHON
# my_bengal_plugin/__init__.py (inside register())
registry.add_template_filter("truncate_words", truncate_words)
KIDA
{{ page.content | truncate_words(20, "...") }}

Filter with Context Access

Filters can access template context if needed:

PYTHON
# python/filters.py
def relative_date(value, context=None):
    """Format date relative to current date.

    Args:
        value: Date to format
        context: Template context (optional, if provided by template engine)

    Returns:
        Relative date string like "2 days ago"
    """
    from datetime import datetime

    if not value:
        return ""

    if isinstance(value, str):
        value = datetime.fromisoformat(value)

    now = datetime.now()
    delta = now - value

    if delta.days == 0:
        return "Today"
    elif delta.days == 1:
        return "Yesterday"
    elif delta.days < 7:
        return f"{delta.days} days ago"
    elif delta.days < 30:
        weeks = delta.days // 7
        return f"{weeks} week{'s' if weeks > 1 else ''} ago"
    else:
        months = delta.days // 30
        return f"{months} month{'s' if months > 1 else ''} ago"
PYTHON
# my_bengal_plugin/__init__.py (inside register())
# Kida passes template context when the filter accepts it as a parameter.
registry.add_template_filter("relative_date", relative_date)
KIDA
{{ post.date | relative_date }}
{# Output: "2 days ago" #}

Collection Filter

Create a filter that works on collections:

PYTHON
# python/filters.py
def where_contains(items, key, value):
    """Filter items where key contains value.

    Args:
        items: List of dictionaries
        key: Key to check
        value: Value to search for

    Returns:
        Filtered list
    """
    if not items:
        return []

    return [
        item for item in items
        if value.lower() in str(item.get(key, "")).lower()
    ]
PYTHON
# my_bengal_plugin/__init__.py (inside register())
registry.add_template_filter("where_contains", where_contains)
KIDA
{% let matching_posts = site.pages
  |> where('type', 'blog')
  |> where_contains('title', 'python') %}

Registering Multiple Filters

A single plugin can register any number of filters in oneregister()call:

PYTHON
# my_bengal_plugin/__init__.py
from bengal.plugins.protocol import Plugin
from bengal.plugins.registry import PluginRegistry

from .filters import currency, truncate_words, where_contains


class MyFiltersPlugin(Plugin):
    name = "my-filters"
    version = "1.0.0"

    def register(self, registry: PluginRegistry) -> None:
        registry.add_template_filter("currency", currency)
        registry.add_template_filter("truncate_words", truncate_words)
        registry.add_template_filter("where_contains", where_contains)

Testing Filters

Test your filters:

PYTHON
# python/test_filters.py
from .filters import currency, truncate_words

def test_currency():
    assert currency(1234.56) == "$1,234.56"
    assert currency(1234.56, "€") == "€1,234.56"
    assert currency(None) == "$0.00"

def test_truncate_words():
    text = "This is a very long text that needs to be truncated"
    result = truncate_words(text, 5)
    assert result == "This is a very long text..."

Best Practices

  1. Type hints: Always include type hints for clarity
  2. Docstrings: Document parameters and return values
  3. None handling: Handle None values gracefully
  4. Error handling: Provide sensible defaults
  5. Naming: Use descriptive, lowercase names with underscores
  6. Registration: Register filters through a plugin'sregistry.add_template_filter()— the supported, thread-safe entry point

Troubleshooting

Filter Not Found

If your filter isn't available in templates:

Plugin Not Discovered

Ifbengal plugin listdoes not show your plugin:

  • Reinstall the plugin after editingpyproject.toml: uv pip install -e ./my-bengal-plugin
  • Confirm the entry point group is exactlybengal.plugins
  • Make sure the class implementsname, version, and register()

Context Not Passed to Filters

Kida doesn't automatically inject context into filters. If you need template context:

Tip

Supported API:registry.add_template_filter()is the stable, supported way to register filters. Avoid reaching into internal attributes such as site._template_engine._env— those are private and may change between releases.

Complete Example

PYTHON
# python/filters.py
"""Custom Kida template filters."""

def currency(value: float | None, symbol: str = "$") -> str:
    """Format a number as currency."""
    if value is None:
        return f"{symbol}0.00"
    return f"{symbol}{value:,.2f}"

def truncate_words(value: str | None, length: int = 50, suffix: str = "...") -> str:
    """Truncate text to a specific word count."""
    if not value:
        return ""

    words = value.split()
    if len(words) <= length:
        return value

    return " ".join(words[:length]) + suffix

def relative_date(value, context=None) -> str:
    """Format date relative to current date."""
    from datetime import datetime

    if not value:
        return ""

    if isinstance(value, str):
        value = datetime.fromisoformat(value)

    now = datetime.now()
    delta = now - value

    if delta.days == 0:
        return "Today"
    elif delta.days == 1:
        return "Yesterday"
    elif delta.days < 7:
        return f"{delta.days} days ago"
    else:
        weeks = delta.days // 7
        return f"{weeks} week{'s' if weeks > 1 else ''} ago"
PYTHON
# my_bengal_plugin/__init__.py
"""Plugin that registers custom Kida filters."""
from bengal.plugins.protocol import Plugin
from bengal.plugins.registry import PluginRegistry

from .filters import currency, truncate_words, relative_date


class MyFiltersPlugin(Plugin):
    name = "my-filters"
    version = "1.0.0"

    def register(self, registry: PluginRegistry) -> None:
        registry.add_template_filter("currency", currency)
        registry.add_template_filter("truncate_words", truncate_words)
        registry.add_template_filter("relative_date", relative_date)

Declaring the entry point:

TOML
# pyproject.toml
[project.entry-points."bengal.plugins"]
my-filters = "my_bengal_plugin:MyFiltersPlugin"

Install the plugin and build — Bengal discovers it automatically and your filters are available in every template:

BASH
uv pip install -e ./my-bengal-plugin
bengal build

Next Steps

Seealso