Functions

Reusable template components that automatically access outer variables

5 min read 922 words

Kida's{% def %}creates reusable template components that can automatically access variables from their surrounding context—no need to pass everything as parameters.

Defining Functions

{% def card(item) %}
  <div class="card">
    <h3>{{ item.title }}</h3>
    <p>{{ item.description }}</p>
  </div>
{% end %}

{# Call the function #}
{{ card(page) }}

Automatic Access to Outer Variables

Kida functions can "see" variables defined outside them. This is called lexical scoping—functions inherit the context where they're defined.

Why this matters: You don't need to pass site configuration, theme settings, or other shared values as parameters. Define them once at the top of your template, and all your functions can use them.

{% let site_name = site.config.title %}
{% let theme_color = config.theme.primary_color %}

{% def card(item) %}
  <div class="card" style="border-color: {{ theme_color }}">
    <h3>{{ item.title }}</h3>
    <p>{{ item.description }}</p>
    <small>From: {{ site_name }}</small>  {# ✅ Works! Accesses outer scope #}
  </div>
{% end %}

{# Just pass the item—site_name and theme_color are available automatically #}
{{ card(page) }}

Compare to Jinja2, where macros are isolated and require passing every variable explicitly:

{# Jinja2: Must pass ALL needed variables as parameters #}
{% def card(item, site_name, theme_color) %}
  <div class="card" style="border-color: {{ theme_color }}">
    <small>From: {{ site_name }}</small>
  </div>
{% end %}

{# Caller must remember to pass everything #}
{{ card(page, site.config.title, config.theme.primary_color) }}

Benefits of Kida's approach:

  • Less boilerplate: No need to thread variables through every function call
  • Easier refactoring: Add a new shared variable once, use it everywhere
  • Cleaner function signatures: Parameters reflect what's unique to each call, not shared context

Default Parameters

{% def button(text, href, variant="primary", size="md") %}
  <a href="{{ href }}" class="btn btn-{{ variant }} btn-{{ size }}">
    {{ text }}
  </a>
{% end %}

{{ button("Get Started", "/docs", variant="success", size="lg") }}
{{ button("Learn More", "/about") }}

Slots (Block Content)

Functions can accept block content using{% slot %}:

{% def modal(title, size="md") %}
  <div class="modal modal-{{ size }}">
    <div class="modal-header">
      <h2>{{ title }}</h2>
    </div>
    <div class="modal-body">
      {% slot %}
        {# Default content if no call block #}
        <p>Modal content goes here</p>
      {% endslot %}
    </div>
  </div>
{% end %}

Use with{% call %}to pass content:

{% call modal("Confirm Action", size="sm") %}
  <p>Are you sure you want to delete this item?</p>
  <div class="modal-actions">
    <button class="btn btn-danger">Delete</button>
    <button class="btn btn-secondary">Cancel</button>
  </div>
{% endcall %}

Common Patterns

Reusable UI Components

{% def tag_badge(tag) %}
  <a href="/tags/{{ tag | slugify }}" class="tag">{{ tag }}</a>
{% end %}

{% def author_card(author) %}
  <div class="author-card">
    {% if author.avatar %}
      <img src="{{ author.avatar }}" alt="{{ author.name }}">
    {% end %}
    <span>{{ author.name }}</span>
  </div>
{% end %}

Conditional Rendering

{% def status_badge(status) %}
  {% match status %}
    {% case "published" %}
      <span class="badge badge-success">Published</span>
    {% case "draft" %}
      <span class="badge badge-warning">Draft</span>
    {% case "archived" %}
      <span class="badge badge-secondary">Archived</span>
    {% case _ %}
      <span class="badge badge-muted">Unknown</span>
  {% end %}
{% end %}

{{ status_badge(page.status) }}
{% def nav_item(item, current_path) %}
  {% let is_active = current_path == item.url or current_path | startswith(item.url ~ '/') %}
  <li class="nav-item{% if is_active %} active{% end %}">
    <a href="{{ item.url }}"
       {% if is_active %}aria-current="page"{% end %}>
      {{ item.title }}
    </a>
  </li>
{% end %}

Custom Filters

Add custom filters using a build hook:

# python/build_hooks.py
from bengal.core import Site

def reading_time(content: str, wpm: int = 200) -> int:
    """Calculate reading time in minutes."""
    words = len(content.split())
    return max(1, round(words / wpm))

def format_number(value: int) -> str:
    """Format number with commas."""
    return f"{value:,}"

def register_filters(site: Site) -> None:
    """Register custom Kida filters."""
    # Get the Kida environment from the template engine
    if hasattr(site, '_template_engine') and site._template_engine:
        env = site._template_engine._env
        env.add_filter("reading_time", reading_time)
        env.add_filter("format_number", format_number)

Add the build hook tobengal.yaml:

build_hooks:
  - python.build_hooks.register_filters

Use in templates:

<span>{{ page.content | reading_time }} min read</span>
<span>{{ view_count | format_number }} views</span>

Note:page.reading_timeis already available as a built-in Page property, so you may not need a custom filter for reading time. See Add a Custom Filter for more details.

Functions vs Partials

Feature Functions Partials
Definition In template Separate file
Parameters Explicit Via{% let %}before include
Scope access Sees outer variables automatically Only sees passed context
Cacheability Part of template (cached with template) Separate file (can be cached independently)
Use case Reusable within template Shared across templates

Use functions for components reused within a single template.

Use partials for components shared across multiple templates.

Complete Example

{% extends "baseof.html" %}

{# Template-level configuration #}
{% let theme_accent = config.theme.accent_color %}

{# Reusable function with access to outer scope #}
{% def post_card(post) %}
  <article class="post-card" style="border-color: {{ theme_accent }}">
    <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
    <div class="meta">
      <time>{{ post.date | dateformat('%B %d, %Y') }}</time>
      <span>{{ post.content | reading_time }} min</span>
    </div>
    {% if post.tags %}
      <div class="tags">
        {% for tag in post.tags |> take(3) %}
          {{ tag_badge(tag) }}
        {% end %}
      </div>
    {% end %}
  </article>
{% end %}

{% def tag_badge(tag) %}
  <a href="/tags/{{ tag | slugify }}" class="tag">{{ tag }}</a>
{% end %}

{% block content %}
  <section class="posts">
    {% for post in site.pages |> where('type', 'blog') |> take(10) %}
      {{ post_card(post) }}
    {% end %}
  </section>
{% endblock %}