Take control of how your site looks. Start with CSS variable overrides, progress to template recipes, and end with a complete custom theme.

Tip

Duration: ~90 min | Prerequisite: HTML, CSS, basic Jinja2

1

Themer Quickstart

Customize themes and create your own designs

Themer Quickstart

Learn to customize Bengal themes and create your own designs.

Prerequisites

0/4 complete
  • Bengal installed
  • Basic knowledge of HTML
  • Familiarity with CSS
  • Basic template syntax (Bengal uses Kida, similar to Jinja2 but with{% end %}closers)

Understand Theme Resolution

Bengal looks for templates in this order:

  1. Your projecttemplates/(overrides everything)
  2. Your themethemes/your-theme/templates/
  3. Installed themes — Via pip or uv
  4. Default theme — Built into Bengal

You only need to override what you want to change.

Create a Custom Theme

bengal new theme my-theme

This creates:

themes/my-theme/
├── templates/
│   ├── base.html
│   ├── home.html
│   ├── page.html
│   └── partials/
│       ├── header.html
│       └── footer.html
└── assets/
    ├── css/
    │   └── style.css
    ├── js/
    │   └── main.js
    └── images/

Configure Your Theme

Updatebengal.toml:

[theme]
name = "my-theme"

Or use config/_default/theme.yamlfor split configuration:

theme:
  name: "my-theme"

Override Templates Selectively

You do not need to copy all templates. Extend the default:

{# themes/my-theme/templates/base.html #}
{% extends "base.html" %}

{% block header %}
<header class="custom-header">
    <h1>{{ site.title }}</h1>
    {% for item in menus.main %}
    <a href="{{ item.href }}">{{ item.name }}</a>
    {% end %}
</header>
{% end %}

Everything not overridden inherits from the default theme or parent theme.

Add Custom CSS

Createthemes/my-theme/assets/css/custom.css:

:root {
    --color-primary: #3498db;
    --color-text: #2c3e50;
}

.custom-header {
    background: var(--color-primary);
    padding: 2rem;
}

Include in your template:

{% block extra_head %}
<link rel="stylesheet" href="{{ asset_url('css/custom.css') }}">
{% end %}

Template Variables

Key variables available in templates (all support safe dot-notation access):

Variable Description
site.title Site title from configuration
site.description Site description
site.baseurl Site base URL
site.pages All pages in the site
menus.main Main navigation menu (safe access)
page.title Current page title
page.content Rendered HTML content (use with| safe)
page.href Page URL with baseurl applied
page.date Publication date
page.tags List of tags
page.description Page description
params Page frontmatter (cascades page → section → site)
theme Theme configuration (safe dot-notation access)
bengal Engine metadata

Bengal provides 80+ template functions. Common ones:

  • asset_url('path')— Generate asset URLs
  • url_for('path')— Generate page URLs
  • get_menu('name')— Get a navigation menu
  • time_ago / date_iso — Format dates ({{ page.date | time_ago }})
  • truncate_chars(text, length)— Truncate text

Debug Theme Issues

# List available themes
bengal utils theme list

# Get theme info
bengal utils theme info default

# Debug theme resolution
bengal utils theme debug

# Discover installed themes
bengal utils theme discover

# Swizzle a template for customization
bengal utils theme swizzle partials/header.html

Next Steps

2

Customize Themes

Customize existing themes without breaking updates

Customize Themes

Customize Bengal themes without breaking theme updates. Use theme inheritance, template overrides, and CSS customization techniques.

Understand Theme Resolution

Bengal resolves themes in this order:

  1. Project themes -themes/your-theme/(highest priority)
  2. Installed themes - Installed via pip/uv
  3. Bundled themes - Built into Bengal (e.g.,default)

Check Active Theme

# List available themes
bengal utils theme list

# Get theme info
bengal utils theme info default

# Debug theme resolution
bengal utils theme debug

Create a Project Theme

Option 1: Start from Scratch

bengal new theme my-custom-theme

This creates:

themes/my-custom-theme/
├── theme.yaml
├── templates/
│   ├── base.html
│   ├── page.html
│   └── partials/
│       ├── header.html
│       └── footer.html
└── static/
    ├── css/
    │   └── style.css
    └── js/
        └── main.js

Configure Your Theme

Editbengal.toml:

[theme]
name = "my-custom-theme"
default_appearance = "light"

Override Templates Selectively

Template Inheritance

You don't need to copy all templates. Override only what you need:

themes/my-custom-theme/templates/base.html:

{# Extend default theme's base template #}
{% extends "default/base.html" %}

{# Override only the header block #}
{% block header %}
<header class="custom-header">
    <h1>{{ site.title }}</h1>
    <nav>
        {% for item in menu.main %}
        <a href="{{ item.href }}">{{ item.name }}</a>
        {% end %}
    </nav>
</header>
{% end %}

{# Everything else inherits from default theme #}

Partial Overrides

Override specific partials:

themes/my-custom-theme/templates/partials/footer.html:

<footer class="custom-footer">
    <p>&copy; {{ site.author }} {{ "now" | date("%Y") }}</p>
    <p>Custom footer content</p>
</footer>

Bengal will use your partial instead of the theme's default.

Page Hero Templates

The page hero templates display the header section of content pages including title, breadcrumbs, description, and stats.

Template Structure

Thepage-hero/directory contains separated hero templates:

templates/partials/
├── page-hero.html           # Dispatcher (routes by hero_style)
└── page-hero/               # Separated hero templates
    ├── _macros.html         # Shared macros and helpers
    ├── index.html           # Index page hero template
    ├── element.html         # DocElement pages (modules, classes, commands)
    └── section.html         # Section-index pages (packages, CLI groups)

The dispatcher (page-hero.html) routes to the appropriate template based on page type and hero_styleconfiguration.

Override Page Hero for API Pages

To customize the hero for API documentation pages:

For element pages (modules, classes, functions, commands):

Overridepartials/page-hero/element.htmlto customize the hero for API documentation elements:

{# themes/my-theme/templates/partials/page-hero/element.html #}
{% from 'partials/page-hero/_macros.html' import hero_element %}

{# Use the macro with custom parameters #}
{{ hero_element(element, params, theme) }}

{# Or create a completely custom hero #}
<div class="page-hero">
  <div class="page-hero__badges">
    {% include 'autodoc/python/partials/badges.html' %}
  </div>
  <h1 class="page-hero__title page-hero__title--code">
    <code>{{ element.qualified_name }}</code>
  </h1>
  {% if element.description %}
  <div class="page-hero__description">
    {{ element.description | markdownify | safe }}
  </div>
  {% end %}
</div>

For section-index pages:

Overridepartials/page-hero/section.htmlto customize section index heroes:

{# themes/my-theme/templates/partials/page-hero/section.html #}
{% from 'partials/page-hero/_macros.html' import hero_section %}

{# Use the macro #}
{{ hero_section(section, params, theme) }}

{# Or create a custom section hero #}
<div class="page-hero">
  <h1 class="page-hero__title">{{ section.title }}</h1>
  {% let desc = section.metadata.description %}
  {% if desc %}
  <div class="page-hero__description">
    {{ desc | markdownify | safe }}
  </div>
  {% end %}
</div>

Using hero_context

For CLI reference sections, pass explicit context to avoid URL sniffing:

{# In autodoc/cli/section-index.html #}
{% let hero_context = {'is_cli': true} %}
{% include 'partials/page-hero/section.html' %}

The hero_context.is_cliflag controls whether stats display:

  • true: "X Groups, Y Commands"
  • false: "X Packages, Y Modules"

Template Data Access Patterns

Element templates receive aDocElementdataclass—use attribute access:

  • element.qualified_name
  • element.description
  • element.children
  • element.source_file

Section templates receive aSectionobject—use attribute access:

  • section.title(section title)
  • section.metadata.description(safe access, returns empty string if missing)
  • section.sorted_pages(sorted child pages)
  • section.sorted_subsections(sorted child sections)

Customize CSS

Method 1: Override Theme CSS

Create your own CSS file that overrides theme styles:

themes/my-custom-theme/static/css/custom.css:

/* Override theme colors */
:root {
    --color-primary: #3498db;
    --color-text: #2c3e50;
}

/* Custom styles */
.custom-header {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    padding: 2rem;
}

Include in your base template:

themes/my-custom-theme/templates/base.html:

{% extends "default/base.html" %}

{% block extra_head %}
<link rel="stylesheet" href="{{ asset_url('css/custom.css') }}">
{% end %}

Method 2: Use CSS Variables

Many themes support CSS variables. Override them:

themes/my-custom-theme/static/css/overrides.css:

:root {
    /* Override default theme variables */
    --theme-primary: #3498db;
    --theme-secondary: #2ecc71;
    --theme-font-sans: 'Inter', sans-serif;
}

Theme Configuration Options

Themes can expose configuration options in two ways:

Theme-Level Configuration (theme.yaml)

Define default configuration in your theme'stheme.yamlfile:

themes/my-custom-theme/theme.yaml:

name: my-custom-theme
version: 1.0.0

# Custom configuration options
show_author: true
show_date: true
sidebar_position: left
color_scheme: light

Site-Level Overrides (bengal.toml)

Override theme defaults in your site'sbengal.toml:

[theme]
name = "my-custom-theme"

# Override theme config values
show_author = true
sidebar_position = "right"
color_scheme = "dark"

Accessing Configuration in Templates

Access theme configuration usingtheme.get()or direct property access:

{# Using theme.get() method (recommended) #}
{% if theme.get('show_author', false) %}
<p>By {{ page.author or site.author }}</p>
{% end %}

{# Direct config key access #}
{% if theme.config.show_author %}
<p>By {{ page.author or site.author }}</p>
{% end %}

Note: All keys in the [theme] section are accessible via theme.get('key') (with default) or theme.config.key(direct access).

Best Practices

Don't Modify Installed Themes

Bad:

# Don't edit installed theme directly
vim $(python -m site --user-site)/bengal/themes/default/templates/base.html

Good:

# Create project theme that extends default
bengal new theme my-theme
# Override only what you need

Use Theme Inheritance

Good:

{% extends "default/base.html" %}
{% block header %}
  {# Only override header #}
{% end %}

Bad:

{# Copying entire base.html #}
<!DOCTYPE html>
<html>
  {# ... hundreds of lines ... #}
</html>

Troubleshooting

Navigation with NavTree

Bengal provides a pre-computed navigation tree for efficient template rendering. Useget_nav_tree(page)to access the navigation structure.

Basic Usage

<nav class="sidebar">
  {% for item in get_nav_tree(page) %}
    <a href="{{ item.href }}"
       {% if item.is_current %}class="active"{% end %}
       {% if item.is_in_trail %}class="in-trail"{% end %}>
      {{ item.title }}
    </a>

    {% if item.has_children %}
      <ul>
        {% for child in item.children %}
          <li>
            <a href="{{ child.href }}">{{ child.title }}</a>
          </li>
        {% end %}
      </ul>
    {% end %}
  {% end %}
</nav>

NavNode Properties

Each navigation node provides:

Property Type Description
item.title str Display title
item.href str Page URL with baseurl applied
item._path str Site-relative URL without baseurl
item.icon str | None Icon identifier
item.weight int Sort weight
item.children list[NavNode] Child navigation items
item.is_current bool True if this is the current page
item.is_in_trail bool True if in active trail to current page
item.is_expanded bool True if node should be expanded
item.has_children bool True if node has children
item.depth int Nesting level (0 = top level)

Scoped Navigation

For section-specific navigation (e.g., docs-only sidebar):

{% let root = page._section.root if page._section else none %}
{% for item in get_nav_tree(page, root_section=root) %}
  <a href="{{ item.href }}">{{ item.title }}</a>
{% end %}

Benefits

  • Performance: O(1) lookup via cached structure (<1ms render overhead)
  • Simplicity: Single function call replaces version-filtering boilerplate
  • Consistency: Pre-computed structure ensures consistent navigation across pages
  • Version-aware: Automatic version filtering and shared content injection

Seealso

3

Swizzle and Customize the Default Theme

Learn to copy and customize theme templates without breaking updates

Swizzle and Customize the Default Theme

In this tutorial, you'll learn how to swizzle (copy) templates from Bengal's default theme into your project, customize them, and keep them updated. By the end, you'll have a personalized site that inherits from the default theme while allowing you to make targeted customizations.

Note

Who is this for? This tutorial is for developers who want to customize Bengal's default theme without forking it entirely. You should have basic familiarity with HTML and Kida templates. No prior experience with swizzling is required.

Goal

By the end of this tutorial, you will:

  1. Understand what swizzling is and why it's useful
  2. Discover available templates in the default theme
  3. Swizzle specific templates to your project
  4. Customize swizzled templates to match your needs
  5. Track and update swizzled templates safely
  6. Build a working customized site

Prerequisites

  • Python 3.14+ installed
  • Bengal installed (pip install bengal or uv add bengal)
  • A Bengal site initialized (runbengal new site mysiteif you haven't already)
  • Basic knowledge of HTML and Kida templates

What is Swizzling?

Swizzling is the process of copying a template from a theme into your project'stemplates/directory. Once swizzled, you can customize the template without modifying the original theme files.

Why Swizzle?

  • Safe customization: Modify templates without touching installed theme files
  • Update-friendly: Track which templates you've customized
  • Selective changes: Only copy what you need to change
  • Provenance tracking: Bengal remembers where templates came from

How It Works

When Bengal renders a page, it looks for templates in this order:

  1. Your project{site.root_path}/templates/(highest priority)
  2. Installed themes → Theme packages
  3. Bundled themes → Built-in themes likedefault

If you swizzle a template, your version intemplates/takes precedence. Everything else continues to use the theme's original templates.

  1. 1

    Set Up Your Project

    Create or use an existing Bengal site as your starting point.

    Let's start with a fresh Bengal site. If you already have one, you can use it.

    # Create a new Bengal site
    bengal new site my-custom-site
    cd my-custom-site
    

    You should see this structure:

    my-custom-site/
    ├── config/           # Configuration directory
    │   └── _default/     # Default environment settings
    ├── content/          # Your markdown files
    ├── assets/           # CSS, JS, images
    ├── templates/        # Template overrides (empty initially)
    └── .gitignore
    

    Tip

    Whytemplates/is empty Thetemplates/directory starts empty because Bengal uses templates from the default theme. Once you swizzle templates here, they'll override the theme versions.

  2. 2

    Discover Swizzlable Templates

    Explore the default theme's template structure to plan your customizations.

    Before swizzling, let's see what templates are available in the default theme.

    bengal utils theme discover
    

    This lists all templates you can swizzle. You'll see output like:

    404.html
    base.html
    page.html
    partials/action-bar.html
    partials/navigation-components.html
    partials/search-modal.html
    partials/search.html
    ...
    

    Understanding Template Structure

    The default theme organizes templates into:

    • Root templates:base.html, page.html, 404.html— Main page templates
    • Partials:partials/*.html— Reusable components (navigation, search, etc.)
    • Content types:blog/, doc/, autodoc/python/— Type-specific templates
  3. 3

    Swizzle Your First Template

    Copy a theme template to your project for customization.

    Let's swizzle the navigation components template. This is a good starting point because navigation is often customized.

    bengal utils theme swizzle partials/navigation-components.html
    

    You should see:

    ✓ Swizzled to /path/to/my-custom-site/templates/partials/navigation-components.html
    

    Verify the Swizzle

    Check that the file was created:

    ls -la templates/partials/
    

    You should see navigation-components.html in your project's templates/partials/directory.

    Check Swizzle Registry

    Bengal tracks swizzled templates in.bengal/themes/sources.json. Let's see what's tracked:

    bengal utils theme swizzle-list
    

    Output:

    - partials/navigation-components.html (from default)
    

    This confirms Bengal knows where the template came from.

  4. 4

    Customize Your Swizzled Template

    Make targeted changes to the copied template.

    Now let's customize the navigation. Opentemplates/partials/navigation-components.htmlin your editor.

    Understand the Template Structure

    The file contains template functions (macros) for navigation components. The exact structure depends on your theme version, but you'll typically find functions for breadcrumbs, menus, and navigation items.

    To customize breadcrumbs, search for a function likebreadcrumbsor look for separator elements. The structure might look like:

    {% def breadcrumbs(page) %}
      <nav class="breadcrumbs">
        {% for item in page.get_breadcrumbs() %}
          {% if not loop.last %}
            <a href="{{ item.url | absolute_url }}">{{ item.title }}</a>
            <span class="separator">/</span>
          {% else %}
            <span class="current">{{ item.title }}</span>
          {% end %}
        {% end %}
      </nav>
    {% end %}
    

    Note

    Kida vs Jinja2 syntax Bengal uses Kida templates by default, but Kida can parse Jinja2 syntax. The examples here use Kida's unified{% end %} syntax, but if your theme uses Jinja2 syntax ({% endif %}, {% endfor %}), that works too. See Kida syntax reference for details.

    Make a Simple Change

    Find the separator element and change it from/ to :

    <span class="separator">→</span>
    

    Save the file and preview your site:

    Navigate to a page with breadcrumbs and verify the separator changed. The dev server automatically reloads when you save template files.

    Tip

    Live reload The dev server watches for file changes. Save your template and refresh the browser to see changes immediately.

  5. 5

    Swizzle and Customize Multiple Templates

    Apply the same pattern to additional components.

    Let's swizzle the search modal and customize it.

    Swizzle the Search Modal

    bengal utils theme swizzle partials/search-modal.html
    

    Customize the Search Modal

    Opentemplates/partials/search-modal.htmlin your editor. Common customizations include:

    • Change the placeholder text
    • Modify CSS classes for styling
    • Add custom search behavior or attributes

    Example: Change Placeholder Text

    Find the search input field (look for<input type="search">):

    <!-- Find the input field -->
    <input
      type="search"
      placeholder="Search the site..."
      class="search-input"
    >
    

    Change the placeholder attribute:

    <input
      type="search"
      placeholder="Find anything..."
      class="search-input"
    >
    

    Verify Your Changes

    Check your swizzled templates:

    bengal utils theme swizzle-list
    

    You should see both templates:

    - partials/navigation-components.html (from default)
    - partials/search-modal.html (from default)
    
  6. 6

    Understand Template Inheritance

    Learn a lighter-weight alternative to full swizzling.

    Swizzling copies the entire template. But you can also use template inheritance to override only specific parts.

    Swizzle the Base Template

    bengal utils theme swizzle base.html
    

    Use Inheritance Instead

    Instead of modifying the entirebase.html, you can create a minimal override that extends the original theme template:

    <!-- templates/base.html -->
    {% extends "default/base.html" %}
    
    {# Override only the header block #}
    {% block header %}
    <header class="custom-header">
      <h1>{{ site.title }}</h1>
      <nav>
        {% for item in menu.main %}
        <a href="{{ item.url }}">{{ item.name }}</a>
        {% end %}
      </nav>
    </header>
    {% end %}
    
    {# Everything else inherits from default/base.html #}
    

    This approach:

    • Keeps your template minimal (only what you override)
    • Makes updates easier (most of the template stays in the theme)
    • Reduces maintenance burden

    Warning

    Full swizzle vs. inheritance

    Choose the right approach based on your needs:

    • Full swizzle: Copy entire template

      • ✅ Full control over all template code
      • ❌ Harder to update when theme changes
      • Use when: You need extensive customizations or structural changes
    • Inheritance: Override specific blocks only

      • ✅ Easier to maintain and update
      • ✅ Minimal code to manage
      • ❌ Limited to block-level overrides
      • Use when: You only need to customize specific sections

      Start with inheritance if possible, then swizzle if you need more control.

  7. 7

    Track and Update Swizzled Templates

    Keep your customizations maintainable as the theme evolves.

    Bengal tracks which templates you've swizzled and whether you've modified them. This helps you update templates safely.

    How Modification Detection Works

    When you swizzle a template, Bengal records a checksum of the copied file. If you later modify the template locally, Bengal detects the change by comparing checksums.

    Theswizzle-updatecommand only updates templates where your local file matches the original swizzled checksum (meaning you haven't modified it):

    bengal utils theme swizzle-update
    

    Output:

    Updated: 0, Skipped (changed): 2, Missing upstream: 0
    

    This means:

    • Updated: 0 — No templates were updated (you've modified them)
    • Skipped (changed): 2 — Two templates were skipped because you changed them
    • Missing upstream: 0 — All source templates still exist

    Update Conditions

    Templates are updated only when all of these conditions are met:

    1. The local file matches the original swizzled checksum (you haven't modified it)
    2. The upstream template in the theme has changed since you swizzled it
    3. The source template still exists in the theme

    This prevents overwriting your customizations. Modified templates are skipped automatically.

  8. 8

    Build and Test

    Generate production files and verify your customizations work.

    Let's build your customized site and verify everything works.

    Build for Production

    bengal build
    

    This generates static files in public/using your swizzled templates.

    Verify Customizations

    1. Check navigation: Breadcrumbs should use instead of /
    2. Check search: Search placeholder should say "Find anything..."
    3. Check structure: Site should render correctly

    Review Build Output

    public/
    ├── index.html
    ├── static/
    │   └── css/
    │       └── ...
    └── ...
    

    Your customizations are baked into the HTML files.

Best Practices

1. Swizzle Only What You Need

Don't swizzle everything at once. Start with one template, customize it, then move to the next.

# ✅ Good: Swizzle one at a time
bengal utils theme swizzle partials/navigation-components.html
# Customize it
# Then swizzle the next one

# ❌ Avoid: Swizzling everything
# bengal utils theme swizzle base.html
# bengal utils theme swizzle page.html
# bengal utils theme swizzle partials/*.html
# (Too many files to maintain)

2. Document Your Changes

Add comments in swizzled templates explaining why you changed something. This helps you remember the reason later and helps others understand your customizations:

{# Custom: Changed separator from '/' to '→' for better visual hierarchy #}
<span class="separator">→</span>

Comments also make it easier to re-apply changes if you need to re-swizzle a template.

3. Use Template Inheritance When Possible

If you only need to override a block, use inheritance instead of full swizzle. This keeps your templates minimal and easier to maintain:

{# ✅ Good: Override only what's needed #}
{% extends "default/base.html" %}
{% block header %}
  {# Your custom header code #}
{% end %}

{# ❌ Avoid: Copying entire template when you only need one block #}
{# This creates unnecessary maintenance burden #}

Inheritance is especially useful for:

  • Overriding header/footer sections
  • Customizing specific page blocks
  • Adding site-wide elements without copying entire templates

4. Keep Swizzled Templates Updated

Periodically runswizzle-updateto get bug fixes and improvements:

# Check what would be updated
bengal utils theme swizzle-update

# Review changes, then rebuild
bengal build

5. Test After Updates

After updating templates, test your site:

bengal serve
# Navigate through your site
# Verify customizations still work

Troubleshooting

What You've Learned

In this tutorial, you:

  1. ✅ Discovered available templates withbengal utils theme discover
  2. ✅ Swizzled templates withbengal utils theme swizzle
  3. ✅ Customized swizzled templates to match your needs
  4. ✅ Tracked swizzled templates withbengal utils theme swizzle-list
  5. ✅ Updated templates safely withbengal utils theme swizzle-update
  6. ✅ Built a customized site using swizzled templates

Next Steps

Now that you can swizzle templates, explore further:

Summary

Swizzling lets you customize Bengal's default theme safely:

  • Discover templates withbengal utils theme discover
  • Swizzle templates withbengal utils theme swizzle <path>
  • Customize swizzled templates intemplates/
  • Track swizzled templates withbengal utils theme swizzle-list
  • Update safely withbengal utils theme swizzle-update

Your customizations are preserved while you can still benefit from theme updates. Use template inheritance when possible to minimize maintenance overhead.

4

Assets

CSS, JavaScript, images, and fonts

Asset Pipeline

Bengal processes your CSS, JavaScript, images, and fonts with optional minification and fingerprinting.

How Assets Flow

flowchart LR subgraph Sources A[static/] B[assets/] C[Theme assets] D[Page bundles] end subgraph Processing E[Collect] F[Minify] G[Fingerprint] end subgraph Output H[public/] end A --> E B --> E C --> E D --> E E --> F --> G --> H

Asset Locations

Location Copied To Processing Use For
static/ public/ None Files that don't need processing
assets/ public/ Full pipeline CSS/JS needing minification
Theme'sstatic/ public/ None Theme's static files
Page bundles public/ Scope-limited Page-specific images/data

Quick Reference

# bengal.toml
[assets]
minify = true
optimize = true
fingerprint = true   # main.css → main.a1b2c3.css
{# Basic asset URL (function syntax) #}
<link rel="stylesheet" href="{{ asset_url('css/main.css') }}">

{# Fingerprinting is automatic when enabled in config #}
{# Outputs: /assets/css/main.a1b2c3.css #}

{# Images #}
<img src="{{ asset_url('images/logo.png') }}" alt="Logo">
{# Access assets co-located with current page #}
{% for image in page.resources.match("*.jpg") %}
  <img src="{{ image.rel_permalink }}" alt="{{ image.title }}">
{% end %}

Tip

Fingerprinting adds a hash to filenames (main.a1b2c3.css) for cache-busting. Enable it in production for optimal caching.

Icons

Bengal includes a comprehensive Icon Reference powered by Phosphor Icons. Use inline icons in content with{icon}name`` syntax:

{icon}`check:24:icon-success` Task complete!

For custom icons, place SVG files in themes/your-theme/assets/icons/. See the Icon Reference for the full gallery and customization options.

5

Template Cookbook

Common templating patterns and Bengal-specific features

Template Cookbook

Practical examples showing how to accomplish common tasks with Bengal's templating system.

Content Queries

Work with pages, sections, and taxonomies.

Example What You'll Learn
List Recent Posts where, sort_by, limitfilters
Group by Category group_byfilter, nested loops
Filter by Multiple Tags Chaining filters,inoperator
Archive Page group_by_year, group_by_monthfilters
Featured Posts section.featured_posts, highlighting content
Template Views EndpointView, SchemaView, TagViewnormalized objects

Page Features

Add features to individual pages.

Example What You'll Learn
Add Table of Contents page.toc, scroll highlighting
Show Reading Time page.reading_timeproperty and filter
Content Freshness age_days, age_months, "new" badges
Author Byline page.author, avatars, social links
Series Navigation prev_in_series, next_in_series
Social Sharing Buttons share_url(), platform share links
Section Statistics post_count, word_count, totals
Blog Comments params.comments, Giscus, Disqus, Utterances

Quick Reference

The Essentials

{# Get pages from a section #}
{% let posts = site.pages |> where('section', 'blog') %}

{# Sort by date, newest first #}
{% let recent = posts |> sort_by('date', reverse=true) %}

{# Limit to 5 #}
{% let latest = recent |> limit(5) %}

{# Or chain it all #}
{% let latest = site.pages
  |> where('section', 'blog')
  |> sort_by('date', reverse=true)
  |> limit(5) %}

Common Filters

Filter Purpose Example
where Filter by field pages |> where('draft', false)
sort_by Sort results pages |> sort_by('title')
limit Take first N pages |> limit(10)
group_by Group by field pages |> group_by('category')
first Get first item pages |> first

Both\|> (Kida pipeline) and \| (Jinja-style) work. Examples use \|>for consistency.

See Template Functions for the complete reference.

6

Show Reading Time

Display estimated reading time using Bengal's reading_time filter

Show Reading Time

Display estimated reading time using Bengal'sreading_timeproperty or filter.

The Pattern

<span class="reading-time">
  {{ page.reading_time }} min read
</span>

Use the filter on content

<span class="reading-time">
  {{ page.content | reading_time }} min read
</span>

Both approaches calculate reading time at 200 words per minute by default.

What's Happening

Component Purpose
page.reading_time Property: returns reading time in minutes (cached)
reading_time Filter: counts words, divides by 200

Variations

Bengal provides aword_countfilter that strips HTML and counts words:

<span>{{ page.content | word_count }} words · {{ page.content | reading_time }} min read</span>

Both filters work together seamlessly since they use the same word counting logic.

{# 250 words per minute instead of 200 #}
{% let words = page.content | word_count %}
{% let minutes = (words / 250) | round(0, 'ceil') | int %}

<span>{{ minutes }} min read</span>
{% let minutes = page.content | reading_time %}

<span class="reading-time">
{% match minutes %}
  {% case m if m < 1 %}
    Quick read
  {% case 1 %}
    1 min read
  {% case m %}
    {{ m }} min read
{% end %}
</span>

Allow manual override for complex content:

{% let minutes = page.metadata.reading_time ?? page.content | reading_time %}

Then in frontmatter:

---
title: Complex Technical Guide
reading_time: 25  # Override calculated time
---

Seealso

7

List Recent Posts

Query and display recent content using Bengal's filters

List Recent Posts

Display the most recent posts from a section using Bengal's query filters.

The Pattern

{% let posts = site.pages
  |> where('section', 'blog')
  |> where('draft', false)
  |> sort_by('date', reverse=true)
  |> limit(5) %}

<ul class="recent-posts">
{% for post in posts %}
  <li>
    <a href="{{ post.href }}">{{ post.title }}</a>
    <time>{{ post.date | date('%B %d, %Y') }}</time>
  </li>
{% end %}
</ul>

What's Happening

Filter Purpose
where('section', 'blog') Only pages in theblog/directory
where('draft', false) Exclude drafts
sort_by('date', reverse=true) Newest first
limit(5) Take only 5
date('%B %d, %Y') Format date (alias fordateformat)

Variations

{# All non-draft pages, any section #}
{% let recent = site.pages
  |> where('draft', false)
  |> sort_by('date', reverse=true)
  |> limit(10) %}
{% let posts = site.pages |> where('section', 'blog') |> sort_by('date', reverse=true) %}
{% let featured = posts |> first %}
{% let rest = posts |> offset(1) |> limit(4) %}

<div class="featured">
  <h2>{{ featured.title }}</h2>
  <p>{{ featured.description }}</p>
</div>

<ul class="more-posts">
{% for post in rest %}
  <li><a href="{{ post.href }}">{{ post.title }}</a></li>
{% end %}
</ul>
{# In a sidebar, show recent posts excluding the current one #}
{% let others = site.pages
  |> where('section', 'blog')
  |> sort_by('date', reverse=true)
  |> limit(6) %}

{% for post in others if post._path != page._path %}
  <a href="{{ post.href }}">{{ post.title }}</a>
{% end %}

Seealso

8

Templating

Kida template engine, layouts, inheritance, and partials

Templating

Bengal's template system uses Kida as the default engine, with support for Jinja2 and custom engines.

Template Engines

  • Kida (local reference) — Bengal's native template engine (default). Unified{% end %}blocks, pattern matching, pipeline operators
  • Jinja2 — Industry-standard engine with excellent documentation and tooling
  • Custom engines — Bring your own via the plugin API

Tip

Kida is Jinja2-compatible: Your existing Jinja2 templates work without changes. Use Kida-specific features incrementally.

Template Lookup Order

flowchart LR A[Page Request] --> B{templates/ ?} B -->|Found| C[Use Your Template] B -->|Not Found| D{Theme templates/ ?} D -->|Found| E[Use Theme Template] D -->|Not Found| F[Use Bengal Default]

Bengal searches: Your projectThemeBengal defaults

Quick Start

{# templates/layouts/single.html #}
{% extends "baseof.html" %}

{% block content %}
<article>
  <h1>{{ page.title }}</h1>
  {{ page.content | safe }}
</article>
{% end %}
{# templates/baseof.html #}
<!DOCTYPE html>
<html>
<head>
  <title>{% block title %}{{ page.title }}{% end %}</title>
</head>
<body>
  {% block header %}{% include "partials/header.html" %}{% end %}
  {% block content %}{% end %}
  {% block footer %}{% include "partials/footer.html" %}{% end %}
</body>
</html>
{# templates/partials/header.html #}
<header>
  <nav>
    {% for item in site.menus.main %}
      <a href="{{ item.href }}">{{ item.title }}</a>
    {% end %}
  </nav>
</header>

Key Concepts

Concept Syntax Purpose
Extends {% extends "base.html" %} Inherit from parent template
Block {% block name %}...{% end %} Replaceable section
Include {% include "partial.html" %} Insert another template
Variable Templating Output a value
Filter {{ text \| truncate(100) }} Transform a value

Template Inheritance

flowchart TB A["baseof.html<br/>(blocks: head, content, footer)"] B["single.html<br/>(extends baseof)"] C["list.html<br/>(extends baseof)"] D["doc.html<br/>(extends single)"] A --> B A --> C B --> D

Kida Features

Kida is Bengal's default template engine:

  • Unified syntax:{% end %} closes all blocks (no more {% end %}, {% end %})
  • Pattern matching:{% match %}...{% case %}for cleaner conditionals
  • Pipeline operator:|>for readable filter chains
  • Fragment caching: Built-in{% cache %}directive
  • Jinja2 compatible: Existing Jinja2 templates work without changes

Choose Your Engine

Kida is the default. To use a different engine, configurebengal.toml (or config/_default/site.yaml):

site:
  template_engine: jinja2  # Options: kida (default), jinja2, mako

Custom Engines (BYOR)

Bring your own template engine via the protocol API. Your engine automatically gets all 80+ template functions if it satisfies theTemplateEnvironmentprotocol:

from bengal.rendering.engines import register_engine
from bengal.rendering.template_functions import register_all

class MyEngine:
    def __init__(self, site):
        self.site = site
        self.template_dirs = [site.root_path / "templates"]

        # Environment must have globals, filters, tests dicts
        self._env = MyEnvironment()

        # Automatically get all 80+ template functions!
        register_all(self._env, site)

    def render_template(self, name: str, context: dict) -> str:
        # Your implementation
        ...

register_engine("myengine", MyEngine)

Then configure:

site:
  template_engine: myengine

See Bring Your Own Template Engine for the complete guide.

Tip

Override sparingly: You only need to create templates you want to customize. Usebengal utils theme swizzle <template>to copy a template for customization. Let the rest fall through to theme defaults.

9

Template Functions

Quick overview of filters and functions available in templates

Template Functions

Bengal provides 80+ template functions and filters organized by category.

Functions vs Filters

Bengal provides filters (transform values) and functions (standalone operations).

Filters use the pipe operator:

{{ text | upper }}
{{ pages |> where('draft', false) |> sort_by('date') }}

Functions are called directly:

{{ get_page('docs/about') }}
{{ get_data('data/config.json') }}
{{ ref('docs/getting-started') }}

Tip

Quick rule: Transform a value? Use a filter (|). Perform an operation? Use a function (direct call).

See Template Functions Reference for complete explanation.

Function Categories

Category Examples Use For
Collection where, sort_by, group_by, limit Querying and filtering pages
Navigation get_section, get_breadcrumbs, get_nav_tree Building navigation
Linking ref, doc, anchor, relref Cross-references
Text truncatewords, slugify, markdownify Text transformations
Date time_ago, days_ago, date_iso Date formatting
i18n t, current_lang, locale_date Internationalization

Quick Examples

Filter Pages

{% let posts = site.pages
  |> where('type', 'blog')
  |> where('draft', false)
  |> sort_by('date', reverse=true)
  |> limit(10) %}

Build Navigation

{% let docs = get_section('docs') %}
{% let crumbs = get_breadcrumbs(page) %}

Cross-Reference

{{ ref('docs/getting-started') }}
{{ ref('docs/api', 'API Reference') }}

Seealso

10

Theme Variables

Comprehensive reference of all variables and functions available in Kida templates.

Theme Variables

This reference lists all variables, objects, and functions available in Bengal templates.

Global Objects

These variables are available in all templates.

site

The global site object.

Attribute Type Description
site.title str Site title from config
site.baseurl str Base URL (e.g.,https://example.com)
site.author str Site author name
site.language str Language code (e.g.,en)
site.pages list[Page] All pages in the site
site.regular_pages list[Page] Content pages only (no list pages)
site.sections list[Section] Top-level sections
site.taxonomies dict Map of taxonomies (tags, categories)
site.data dict Data loaded fromdata/directory
site.config dict Full configuration object

section

Available on index pages (_index.md) and doc-type pages.

Attribute Type Description
section.name str Directory name
section.title str Section title (from_index.md)
section.index_page Page The section's_index.mdpage
section.pages list[Page] Direct child pages
section.sorted_pages list[Page] Pages sorted by weight/date
section.subsections list[Section] Child sections
section.metadata dict Frontmatter from_index.md

Example: Auto-Generated Child Cards

<div class="child-cards">
  {% for subsection in section.subsections %}
    <a href="{{ subsection.index_page.href }}" class="card">
      <h3>{{ subsection.title }}</h3>
      <p>{{ subsection.metadata.description }}</p>
    </a>
  {% end %}
</div>

Example: Section Navigation

<nav class="section-nav">
  <h4>In this section:</h4>
  <ul>
    {% for page in section.sorted_pages %}
      <li><a href="{{ page.href }}">{{ page.title }}</a></li>
    {% end %}
  </ul>
</nav>

page

The current page being rendered.

Attribute Type Description
page.title str Page title
page.nav_title str Short title for navigation (falls back to title)
page.content str Raw content
page.rendered_html str Rendered HTML content
page.date datetime Publication date
page.href str URL with baseurl applied (for display in templates)
page._path str Site-relative URL without baseurl (for comparisons)
page.slug str URL slug for the page
page.metadata dict All frontmatter keys
page.toc str Auto-generated Table of Contents HTML
page.toc_items list[dict] Structured TOC data (id, title, level)
page.is_home bool True if homepage
page.is_section bool True if section index
page.is_page bool True if regular page (not section)
page.kind str Returns 'home', 'section', or 'page'
page.type str Page type from frontmatter
page.draft bool True if page is a draft
page.description str Page description
Navigation Properties
Attribute Type Description
page._section Section Parent section object (direct access to object tree)
page.ancestors list[Section] Parent sections from root to current (for breadcrumbs)
page.prev_in_section Page | None Previous page in section (by weight/date)
page.next_in_section Page | None Next page in section (by weight/date)
page.related_posts list[Page] Pages with matching tags

Example: Custom Breadcrumbs

<nav class="breadcrumbs">
  <a href="/">Home</a>
  {% for ancestor in page.ancestors %}
    > <a href="{{ ancestor.href }}">{{ ancestor.title }}</a>
  {% end %}
  > <span>{{ page.title }}</span>
</nav>

Example: Prev/Next Navigation

<nav class="prev-next">
  {% if page.prev_in_section %}
    <a href="{{ page.prev_in_section.href }}">← {{ page.prev_in_section.title }}</a>
  {% end %}
  {% if page.next_in_section %}
    <a href="{{ page.next_in_section.href }}">{{ page.next_in_section.title }} →</a>
  {% end %}
</nav>

Example: Related Posts

{% if page.related_posts %}
<aside class="related">
  <h3>Related Articles</h3>
  <ul>
    {% for post in page.related_posts[:5] %}
      <li><a href="{{ post.href }}">{{ post.title }}</a></li>
    {% end %}
  </ul>
</aside>
{% end %}
URL Properties

Bengal provides two URL properties with clear purposes:

page.href - Primary property for display

  • Automatically includes baseurl (e.g.,/bengal/docs/page/)
  • Use in<a href>, <link>, <img src>attributes
  • Works correctly for all deployment scenarios (GitHub Pages, Netlify, S3, etc.)

page._path - For comparisons and logic

  • Site-relative URL without baseurl (e.g.,/docs/page/)
  • Use for comparisons:{% if page._path == '/docs/' %}
  • Use for menu activation, filtering, and conditional logic
  • NEVER use in template href attributes

Example: Usage

{# Display URL (includes baseurl) #}
<a href="{{ page.href }}">{{ page.title }}</a>

{# Comparison (without baseurl) #}
{% if page._path == '/docs/' %}
  <span class="active">Current Section</span>
{% end %}

{# url_for() also works for URL generation #}
<a href="{{ url_for(page) }}">{{ page.title }}</a>

Why This Pattern?

  • Ergonomic: Templates use/bengal/docs/reference/theme-variables/for display - it "just works"
  • Clear:_pathmakes comparisons explicit (without baseurl)
  • No wrappers: Page objects handle baseurl via their_sitereference
  • Works everywhere: Supports file://, S3, GitHub Pages, Netlify, Vercel, etc.

Global Functions

Functions available in all templates.

asset_url(path)

Generates a fingerprint-aware URL for an asset.

<link rel="stylesheet" href="{{ asset_url('css/style.css') }}">
<!-- Outputs: /assets/css/style.a1b2c3d4.css -->

url_for(page_or_slug)

Generates a URL for a page object or slug. Applies baseurl automatically.

<a href="{{ url_for(page) }}">Link</a>
<a href="{{ url_for('/docs/getting-started/') }}">Getting Started</a>

dateformat(date, format)

Formats a date object.

{{ dateformat(page.date, "%B %d, %Y") }}

get_menu(menu_name)

Retrieves a navigation menu.

{% for item in get_menu('main') %}
  <a href="{{ item.url }}">{{ item.name }}</a>
{% end %}

Template Helpers

Bengal includes categorized helper modules:

  • Strings:truncate, slugify, markdownify
  • Collections:where, group_by, sort_by
  • Dates:time_ago, date_iso
  • Images:image_processed, image_url
  • Taxonomies:related_posts, popular_tags

Tip

Debugging You can inspect available variables by printing them in a comment: <!-- {{ page.metadata }} -->

✓ Track Complete