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 }}→ transformsvalue - Function:
{{ my_function() }}→ performs operation
Prerequisites
- Bengal site initialized
- Kida enabled in
bengal.yaml - Python knowledge
Steps
Step 1: Create Filter Function
Create a Python file for your filters:
mkdir -p python
touch python/filters.py
Define your filter function:
# 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}"
Step 2: Register Filter
Create a registration function to add your filter to the template environment:
mkdir -p python
touch python/filter_registration.py
Register the filter:
# python/filter_registration.py
from bengal.core import Site
from .filters import currency
def register_filters(site: Site) -> None:
"""Register custom Kida filters.
Note: This uses internal APIs that may change in future versions.
A stable plugin API is planned for v0.4.0.
"""
# Access the template engine environment
# The template engine is created during the build process
if hasattr(site, '_template_engine') and site._template_engine:
env = site._template_engine._env
env.add_filter("currency", currency)
Step 3: Call Registration Function
You need to call this function during the build process. Currently, this requires accessing internal APIs. Two approaches:
Option A: Programmatic Build (Recommended)
Create a custom build script that calls your registration function:
# build.py
from pathlib import Path
from bengal.core import Site
from bengal.orchestration.build import BuildOptions
from python.filter_registration import register_filters
# Load site
site = Site.from_config(Path("."))
# Register filters before building
register_filters(site)
# Build site
options = BuildOptions()
site.build(options)
Option B: Internal API Access (Advanced)
::: {warning}
This approach uses internal APIs (site._template_engine) that may change. Use with caution and test after Bengal updates.
:::
If you need to register filters during the build process, you can access the template engine after it's created. The template engine is typically available during the rendering phase of the build.
Step 4: Use Filter in Template
Use your custom filter in templates:
{{ 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/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 and use:
# python/filter_registration.py
from bengal.core import Site
from .filters import truncate_words
def register_filters(site: Site) -> None:
"""Register custom Kida filters."""
if hasattr(site, '_template_engine') and site._template_engine:
env = site._template_engine._env
env.add_filter("truncate_words", truncate_words)
{{ page.content | truncate_words(20, "...") }}
Filter with Context Access
Filters can access template context if needed:
# 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/filter_registration.py
from bengal.core import Site
from .filters import relative_date
def register_filters(site: Site) -> None:
"""Register custom Kida filters."""
if hasattr(site, '_template_engine') and site._template_engine:
env = site._template_engine._env
# Note: Context passing depends on filter implementation
# Kida passes context when filters accept it as a parameter
env.add_filter("relative_date", relative_date)
{{ post.date | relative_date }}
{# Output: "2 days ago" #}
Collection Filter
Create a filter that works on collections:
# 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/filter_registration.py
from bengal.core import Site
from .filters import where_contains
def register_filters(site: Site) -> None:
"""Register custom Kida filters."""
if hasattr(site, '_template_engine') and site._template_engine:
env = site._template_engine._env
env.add_filter("where_contains", where_contains)
{% let matching_posts = site.pages
|> where('type', 'blog')
|> where_contains('title', 'python') %}
Using Decorator Syntax
You can also use decorator syntax for cleaner registration:
# python/filter_registration.py
from bengal.core import Site
def register_filters(site: Site) -> None:
"""Register all custom filters."""
if hasattr(site, '_template_engine') and site._template_engine:
env = site._template_engine._env
@env.filter()
def currency(value: float, symbol: str = "$") -> str:
"""Format a number as currency."""
return f"{symbol}{value:,.2f}"
@env.filter()
def truncate_words(value: str, length: int = 50) -> str:
"""Truncate text to a specific word count."""
words = value.split()
if len(words) <= length:
return value
return " ".join(words[:length]) + "..."
Testing Filters
Test your filters:
# 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
- Type hints: Always include type hints for clarity
- Docstrings: Document parameters and return values
- None handling: Handle None values gracefully
- Error handling: Provide sensible defaults
- Naming: Use descriptive, lowercase names with underscores
- API access: Use
env.add_filter()method (preferred) overenv.filters['name'] = func
Troubleshooting
Filter Not Found
If your filter isn't available in templates:
- Check registration timing: Ensure
register_filters()is called before templates are rendered - Verify template engine: Confirm
site._template_engineexists and has_envattribute - Check filter name: Filter names are case-sensitive and must match exactly
Template Engine Not Available
Ifsite._template_engineisNone:
- The template engine is created during the rendering phase
- Ensure you're calling
register_filters()after the engine is initialized - Consider using a custom build script that registers filters before building
Context Not Passed to Filters
Kida doesn't automatically inject context into filters. If you need template context:
- Accept
context=Noneas a parameter in your filter function - Access context variables through the context parameter when provided
- Note that context passing behavior may vary depending on how the filter is called
::: {warning}
Internal API Usage: Accessingsite._template_engine._envuses internal APIs that may change in future versions. A stable plugin API for filter registration is planned for v0.4.0. Test your filter registration after Bengal updates.
:::
Complete Example
# 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/filter_registration.py
"""Filter registration for custom Kida filters."""
from bengal.core import Site
from .filters import currency, truncate_words, relative_date
def register_filters(site: Site) -> None:
"""Register custom Kida filters.
Call this function before building your site to register all custom filters.
"""
if hasattr(site, '_template_engine') and site._template_engine:
env = site._template_engine._env
env.add_filter("currency", currency)
env.add_filter("truncate_words", truncate_words)
env.add_filter("relative_date", relative_date)
Using in a build script:
# build.py
from pathlib import Path
from bengal.core import Site
from bengal.orchestration.build import BuildOptions
from python.filter_registration import register_filters
site = Site.from_config(Path("."))
register_filters(site)
site.build(BuildOptions())
Next Steps
- Use Pipeline Operator — Chain filters together
- Create Custom Template — Build templates with your filters
- Kida Syntax Reference — Complete syntax documentation
Seealso
- Template Functions Reference — Built-in filters
- Build Hooks — More customization options