Theme Developer Journey
From CSS tweaks to building complete custom themes.
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
Themer Quickstart
Customize themes and create your own designs
Themer Quickstart
Learn to customize Bengal themes and create your own designs.
Prerequisites
- 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:
- Your project —
templates/(overrides everything) - Your theme —
themes/your-theme/templates/ - Installed themes — Via pip or uv
- 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 URLsurl_for('path')— Generate page URLsget_menu('name')— Get a navigation menutime_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
- Theme Customization — Deep dive into overrides
- Template Functions — All available filters and functions
- Variables Reference — Complete template variables
- Assets — CSS, JS, and image handling
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:
- Project themes -
themes/your-theme/(highest priority) - Installed themes - Installed via pip/uv
- 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>© {{ 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_nameelement.descriptionelement.childrenelement.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
Theme Not Found
Issue:Theme 'my-theme' not found
Solutions:
- Verify theme directory exists:
themes/my-theme/ - Check
theme.tomlhas correctnamefield - Run
bengal utils theme listto see available themes
Template Inheritance Not Working
Issue: Changes to parent theme not reflected
Solutions:
- Verify
extendspath is correct:"default/base.html" - Check theme chain:
bengal utils theme debug - Clear cache:
bengal clean --cache
CSS Not Loading
Issue: Custom CSS not applying
Solutions:
- Use
asset_url()filter:{{ asset_url('css/style.css') }} - Check file location:
themes/your-theme/static/css/ - Hard refresh:
Cmd+Shift+R
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
- Templating — Template basics
- Assets — Asset pipeline
- Icon Reference — SVG icons and customization
- Variables Reference — Available template variables
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:
- Understand what swizzling is and why it's useful
- Discover available templates in the default theme
- Swizzle specific templates to your project
- Customize swizzled templates to match your needs
- Track and update swizzled templates safely
- Build a working customized site
Prerequisites
- Python 3.14+ installed
- Bengal installed (
pip install bengaloruv add bengal) - A Bengal site initialized (run
bengal 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:
- Your project →
{site.root_path}/templates/(highest priority) - Installed themes → Theme packages
- Bundled themes → Built-in themes like
default
If you swizzle a template, your version intemplates/takes precedence. Everything else continues to use the theme's original templates.
- 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-siteYou 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) └── .gitignoreTip
Why
templates/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
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 discoverThis 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
What should I swizzle?
Start small: Swizzle only what you need to customize. Common starting points:
partials/navigation-components.html— Navigation menuspartials/search.html— Search functionalitybase.html— Site-wide layoutpage.html— Individual page layout
- Root templates:
- 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.htmlYou should see:
✓ Swizzled to /path/to/my-custom-site/templates/partials/navigation-components.htmlVerify the Swizzle
Check that the file was created:
ls -la templates/partials/You should see
navigation-components.htmlin your project'stemplates/partials/directory.Check Swizzle Registry
Bengal tracks swizzled templates in
.bengal/themes/sources.json. Let's see what's tracked:bengal utils theme swizzle-listOutput:
- partials/navigation-components.html (from default)This confirms Bengal knows where the template came from.
- 4
Customize Your Swizzled Template
Make targeted changes to the copied template.
Now let's customize the navigation. Open
templates/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 like
breadcrumbsor 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
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.htmlCustomize the Search Modal
Open
templates/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-listYou should see both templates:
- partials/navigation-components.html (from default) - partials/search-modal.html (from default) - 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.htmlUse Inheritance Instead
Instead of modifying the entire
base.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
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.
The
swizzle-updatecommand only updates templates where your local file matches the original swizzled checksum (meaning you haven't modified it):bengal utils theme swizzle-updateOutput:
Updated: 0, Skipped (changed): 2, Missing upstream: 0This 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:
- The local file matches the original swizzled checksum (you haven't modified it)
- The upstream template in the theme has changed since you swizzled it
- The source template still exists in the theme
This prevents overwriting your customizations. Modified templates are skipped automatically.
- 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 buildThis generates static files in
public/using your swizzled templates.Verify Customizations
- Check navigation: Breadcrumbs should use
→instead of/ - Check search: Search placeholder should say "Find anything..."
- Check structure: Site should render correctly
Review Build Output
public/ ├── index.html ├── static/ │ └── css/ │ └── ... └── ...Your customizations are baked into the HTML files.
- Check navigation: Breadcrumbs should use
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
Template Not Found
Issue:FileNotFoundError: Template not found in theme chain
Solution: Verify the template path:
# List available templates
bengal utils theme discover
# Use the exact path shown
bengal utils theme swizzle partials/navigation-components.html
Changes Not Appearing
Issue: Customizations don't show up after swizzling
Solutions:
- Clear cache:
bengal clean --cache - Restart dev server: Stop and restart
bengal serve - Check file location: Ensure template is in
templates/(notthemes/)
Swizzle Update Overwrites Changes
Issue:swizzle-updateoverwrote your customizations
Solution: This shouldn't happen if you've modified the file. If it does:
- Check
.bengal/themes/sources.jsonfor the checksum - Restore from git if you use version control
- Re-apply your customizations
Template Inheritance Not Working
Issue:{% extends "default/base.html" %}doesn't work
Solutions:
- Verify theme name: Use
bengal utils theme info defaultto confirm - Check syntax: Use
"default/base.html"format (theme name, forward slash, template path) - Verify template exists: Use
bengal utils theme discoverto list available templates - Clear cache:
bengal clean --cache
What You've Learned
In this tutorial, you:
- ✅ Discovered available templates with
bengal utils theme discover - ✅ Swizzled templates with
bengal utils theme swizzle - ✅ Customized swizzled templates to match your needs
- ✅ Tracked swizzled templates with
bengal utils theme swizzle-list - ✅ Updated templates safely with
bengal utils theme swizzle-update - ✅ Built a customized site using swizzled templates
Next Steps
Now that you can swizzle templates, explore further:
- Theme Customization Guide — Deep dive into advanced customization techniques
- Template Reference — Learn about Kida templates and available functions
- Variables Reference — Discover all template variables available
- Assets Guide — Customize CSS and JavaScript
Summary
Swizzling lets you customize Bengal's default theme safely:
- Discover templates with
bengal utils theme discover - Swizzle templates with
bengal utils theme swizzle <path> - Customize swizzled templates in
templates/ - Track swizzled templates with
bengal utils theme swizzle-list - Update safely with
bengal 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.
Assets
CSS, JavaScript, images, and fonts
Asset Pipeline
Bengal processes your CSS, JavaScript, images, and fonts with optional minification and fingerprinting.
How Assets Flow
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.
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.
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
Use the page property (recommended)
<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
- Template Functions — All filters
- List Recent Posts — Include reading time in post lists
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
- Template Functions — All filter options
- Group by Category — Organize posts by category
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
Bengal searches: Your project → Theme → Bengal 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
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.
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
- Template Functions Reference — Complete documentation with all parameters and examples
- Theme Variables Reference — Available template variables (
page,site,section) - Kida Syntax — Kida template engine syntax
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 }} -->