Learn how to convert existing Jinja2 templates to Kida syntax for unified block endings, pattern matching, and pipeline operators.
Goal
Migrate your Jinja2 templates to Kida syntax while maintaining functionality.
Prerequisites
- Existing Jinja2 templates
- Bengal site configured
- Understanding of template syntax
Migration Strategy
Phase 1: Compatibility Mode (Zero Changes)
Kida is Bengal's default engine and can parse Jinja2 syntax. Your existing Jinja2 templates work without changes.
Tip
Already using Kida: Since Kida is the default engine, new Bengal sites already use Kida. Your Jinja2 templates work, but you can migrate to Kida syntax to use pattern matching, pipeline operators, and unified{% end %}blocks.
Phase 2: Gradual Migration
Migrate templates incrementally, starting with:
- New templates (use Kida syntax)
- Frequently-used templates (biggest performance gain)
- Simple templates (easiest to migrate)
- Complex templates (last)
Syntax Changes
Block Endings
Jinja2:
{% if condition %}
Content
{% endif %}
{% for item in items %}
{{ item }}
{% endfor %}
Kida:
{% if condition %}
Content
{% end %}
{% for item in items %}
{{ item }}
{% end %}
Migration: Replace{% endif %},{% endfor %},{% endblock %}, etc. with{% end %}.
Template Variables
Jinja2:
{% set site_title = site.config.title %}
{# Available in current block only #}
Kida:
{% let site_title = site.config.title %}
{# Available throughout template #}
Migration: Replace{% set %}with{% let %}for template-wide variables. Keep{% set %}for block-scoped variables (variables that should only exist within a specific block).
Scoping Differences:
{% let %}: Template-scoped (available throughout template){% set %}: Block-scoped (only available within the block){% export %}: Promotes variables from inner scopes to template scope
Pattern Matching
Jinja2:
{% if page.type == "blog" %}
Blog post
{% elif page.type == "doc" %}
Documentation
{% elif page.type == "tutorial" %}
Tutorial
{% else %}
Default
{% endif %}
Kida:
{% match page.type %}
{% case "blog" %}
Blog post
{% case "doc" %}
Documentation
{% case "tutorial" %}
Tutorial
{% case _ %}
Default
{% end %}
Migration: Replace longif/elifchains with{% match %}...{% case %}.
Pipeline Operator
Jinja2:
{{ items | selectattr('published') | sort(attribute='date') | first }}
Kida (Jinja2-compatible filters):
{{ items |> selectattr('published') |> sort(attribute='date') |> first }}
Kida (Bengal template functions - default):
{{ items |> where('published', true) |> sort_by('date') |> first }}
Migration:
- Replace
|with|>for the pipeline operator - Kida supports Jinja2 filters (
selectattr,sort) for compatibility - Bengal provides simpler alternatives (
where,sort_by) that are recommended for new code
Fallback Values
Jinja2:
{{ items | default([]) | length }}
{{ name | default('Anonymous') | upper }}
Kida (simple fallbacks):
{{ user.name ?? 'Anonymous' }}
{{ config.timeout ?? 30 }}
Kida (fallbacks with filter chains):
{# Use | default() when applying filters after the fallback #}
{{ items | default([]) | length }}
{{ name | default('') | upper }}
Warning
Precedence gotcha: The??operator has lower precedence than|, so filters bind to the fallback, not the result:
{# ❌ Parses as: items ?? ([] | length) — returns list, not length! #}
{{ items ?? [] | length }}
{# ✅ Correct: use | default() for filter chains #}
{{ items | default([]) | length }}
Migration: Keep using| default()when you need to apply filters after the fallback. Use??for simple direct output.
Fragment Caching
Jinja2 (requires extension):
{% cache "key" %}
Expensive content
{% endcache %}
Kida (built-in):
{% cache "key" %}
Expensive content
{% end %}
Migration: Replace{% endcache %}with{% end %}.
Note
Cache TTL: Fragment cache TTL is configured at the environment level inbengal.yaml, not per-key. Setkida.fragment_ttl(in seconds) to control cache expiration for all fragments.
Step-by-Step Migration
Step 1: Identify Template
Choose a template to migrate:
# List your templates
find templates/ -name "*.html"
Step 2: Create Backup
# Backup original
cp templates/blog/single.html templates/blog/single.html.jinja2
Step 3: Replace Block Endings
# Replace all block endings with {% end %}
sed -i 's/{% endif %}/{% end %}/g' templates/blog/single.html
sed -i 's/{% endfor %}/{% end %}/g' templates/blog/single.html
sed -i 's/{% endblock %}/{% endblock %}/g' templates/blog/single.html # Keep endblock for compatibility
Step 4: Replace Template Variables
Find{% set %}used for template-wide variables:
{# Before #}
{% set site_title = site.config.title %}
{# After #}
{% let site_title = site.config.title %}
Step 5: Convert Pattern Matching
Find longif/elifchains:
{# Before #}
{% if page.type == "blog" %}
Blog
{% elif page.type == "doc" %}
Doc
{% else %}
Default
{% endif %}
{# After #}
{% match page.type %}
{% case "blog" %}
Blog
{% case "doc" %}
Doc
{% case _ %}
Default
{% end %}
Step 6: Update Filter Chains
Replace filter syntax (two options):
Option 1: Keep Jinja2 filters, change operator:
{# Before #}
{{ items | selectattr('published') | sort(attribute='date') }}
{# After (Jinja2-compatible) #}
{{ items |> selectattr('published') |> sort(attribute='date') }}
Option 2: Use Bengal template functions (recommended):
{# Before #}
{{ items | selectattr('published') | sort(attribute='date') }}
{# After (Bengal functions) #}
{{ items |> where('published', true) |> sort_by('date') }}
Step 7: Add Fragment Caching
Add caching to expensive operations:
{# Before #}
{% for post in expensive_calculation() %}
{{ post.title }}
{% endfor %}
{# After #}
{% cache "posts-list" %}
{% for post in expensive_calculation() %}
{{ post.title }}
{% end %}
{% end %}
Step 8: Test
# Build and test
bengal build
bengal serve
Functions vs Filters
Bengal distinguishes between filters (transform values) and functions (standalone operations).
Jinja2 mixes both:
{{ items | selectattr('published') }} {# Filter #}
{{ range(10) }} {# Function #}
Bengal separates them:
{{ items |> where('published', true) }} {# Filter #}
{{ get_page('path') }} {# Function #}
When migrating:
- Jinja2 filters → Bengal filters (use
|or|>) - Jinja2 functions → Bengal functions (direct call)
See Functions vs Filters for complete explanation.
Filter Name Mapping
Kida supports Jinja2 filters for compatibility, and Bengal provides additional template functions:
Jinja2-Compatible Filters
Kida uses the same filter names as Jinja2. The main difference is the pipeline operator|>instead of|:
| Jinja2 Filter | Kida Filter | Notes | |
|---|---|---|---|
selectattr('key') |
selectattr('key') |
Same name, use ` | >` operator |
selectattr('key', 'eq', val) |
selectattr('key', 'eq', val) |
Same name, use ` | >` operator |
sort(attribute='key') |
sort(attribute='key') |
Same name, use ` | >` operator |
sort(attribute='key', reverse=true) |
sort(attribute='key', reverse=true) |
Same name, use ` | >` operator |
Bengal Template Functions (Recommended)
Bengal provides simpler alternatives that work with both|and|>:
| Jinja2 Filter | Bengal Function | Notes | |
|---|---|---|---|
selectattr('key', 'eq', val) |
where('key', val) |
Simpler syntax, supports operators ('eq','ne','gt','in', etc.) |
|
sort(attribute='key') |
sort_by('key') |
Simpler syntax, supportsreverse=true |
|
first(n) |
take(n) |
Different name: usetakeinstead offirst |
|
last(n) |
`take(n) | > reverse` | Different name: usetakewithreverse |
Note
Template Functions:where,sort_by,take, and other collection functions are Bengal template functions automatically available in all templates. They work with both|(Jinja2-style) and|>(Kida pipeline) operators.
Common Patterns
Collection Filtering
Jinja2:
{% set posts = site.pages | selectattr('type', 'eq', 'blog') | selectattr('draft', 'eq', false) | sort(attribute='date', reverse=true) %}
Kida (using Bengal template functions - default):
{% let posts = site.pages
|> where('type', 'blog')
|> where('draft', false)
|> sort_by('date', reverse=true) %}
Kida (using Jinja2-compatible filters):
{% let posts = site.pages
|> selectattr('type', 'eq', 'blog')
|> selectattr('draft', 'eq', false)
|> sort(attribute='date', reverse=true) %}
Conditional Rendering
Jinja2:
{% if page.type == "blog" %}
<article class="blog">{{ page.content | safe }}</article>
{% elif page.type == "doc" %}
<article class="doc">{{ page.content | safe }}</article>
{% else %}
<article>{{ page.content | safe }}</article>
{% endif %}
Kida:
{% match page.type %}
{% case "blog" %}
<article class="blog">{{ page.content | safe }}</article>
{% case "doc" %}
<article class="doc">{{ page.content | safe }}</article>
{% case _ %}
<article>{{ page.content | safe }}</article>
{% end %}
Template Variables
Jinja2:
{% set site_title = site.config.title %}
{% set nav_items = site.menus.main %}
Kida:
{% let site_title = site.config.title %}
{% let nav_items = site.menus.main %}
Migration Checklist
- Enable Kida in
bengal.yaml - Test templates work in compatibility mode
- Replace
{% endif %},{% endfor %}, etc. with{% end %} - Replace template-wide
{% set %}with{% let %} - Convert long
if/elifchains to pattern matching - Update filter chains to use
|>and Kida filter names - Add fragment caching where appropriate
- Test all templates
- Remove Jinja2 compatibility code
Troubleshooting
Validate Templates Before Building
Run template validation to catch syntax errors before a slow build:
# Validate all templates
bengal validate --templates
# Validate with migration hints
bengal validate --templates --fix
# Validate specific templates
bengal validate --templates --templates-pattern "autodoc/**/*.html"
Template Not Found
# Check template lookup order
bengal build --verbose
Filter Not Found
Kida uses different filter names. Check the Kida Syntax Reference for available filters.
Syntax Errors
Kida is stricter than Jinja2. Check:
- All blocks properly closed with
{% end %} - Variables defined before use
- Filter names match Kida syntax
Common Migration Gotchas
Macros Not Supported
Kida does not support Jinja2's{% macro %}syntax. Use{% def %}instead, which provides lexical scoping (functions can access outer variables).
Jinja2 (not supported in Kida):
{% macro hello(name) %}
Hello {{ name }}
{% endmacro %}
{{ hello('World') }}
Kida (use{% def %}instead):
{% def hello(name) %}
Hello {{ name }}
{% enddef %}
{{ hello('World') }}
Key Difference:{% def %}functions have access to outer template variables (likesiteandconfig), while Jinja2 macros are isolated. This means you don't need to pass common variables as parameters.
Note
Need full Jinja2 compatibility? If your templates rely heavily on{% macro %}, you can use the Jinja2 engine by settingtemplate_engine: jinja2in yourbengal.yamlconfig.
Include with Variables
Jinja2 allows passing variables directly in the include statement:
Jinja2 (not supported in Kida):
{% include 'partial.html' with param=value %}
Kida (set variables before include):
{% let param = value %}
{% include 'partial.html' %}
Dict Key Access (Method Name Conflicts)
Python dict methods (items,keys,values,get) conflict with key access. Using dotted notation returns the method, not the key value.
Problem:
{# This returns the items() method, not the 'items' key! #}
{{ schema.items }}
Solutions:
{# Solution 1: Bracket notation #}
{{ schema['items'] }}
{# Solution 2: get() filter (cleaner syntax) #}
{{ schema | get('items') }}
{{ schema | get('items', default_value) }}
Slice Filter Behavior
Kida'sslicefilter groups items (like Jinja2's slice), it doesn't do string slicing.
{# This groups items into 3 slices, not string slicing #}
{{ items | slice(3) }}
{# For string/list slicing, use Python slice syntax #}
{{ text[:5] }}
{{ items[1:4] }}
Undefined Variables with Nil-Coalescing
In strict mode, use??to handle undefined variables safely:
{# May error if 'schemas' is undefined in strict mode #}
{% let schema = schemas[name] if schemas else none %}
{# Safe - ?? handles undefined #}
{% let schemas_dict = schemas ?? {} %}
{% let schema = schemas_dict | get(name) %}
Complete Example
Before (Jinja2):
{% extends "baseof.html" %}
{% set site_title = site.config.title %}
{% set recent_posts = site.pages | selectattr('type', 'eq', 'blog') | selectattr('draft', 'eq', false) | sort(attribute='date', reverse=true) | slice(5) %}
{% block content %}
{% if page.type == "blog" %}
<article class="blog-post">
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
</article>
{% elif page.type == "doc" %}
<article class="doc">
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
</article>
{% else %}
<article>
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
</article>
{% endif %}
{% endblock %}
After (Kida):
{% extends "baseof.html" %}
{% let site_title = site.config.title %}
{% let recent_posts = site.pages
|> selectattr('type', 'eq', 'blog')
|> selectattr('draft', 'eq', false)
|> sort(attribute='date', reverse=true)
|> take(5) %}
{% block content %}
{% match page.type %}
{% case "blog" %}
<article class="blog-post">
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
</article>
{% case "doc" %}
<article class="doc">
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
</article>
{% case _ %}
<article>
<h1>{{ page.title }}</h1>
{{ page.content | safe }}
</article>
{% end %}
{% endblock %}
Next Steps
- Kida Syntax Reference — Complete syntax documentation
- Kida Tutorial — Learn Kida from scratch
- Create Custom Template — Build new templates with Kida
Seealso
- Template Functions — Available filters and functions
- Performance Guide — Performance benefits of Kida
- Kida Migration Guide — Standalone Kida migration documentation