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

Section 3: Page Not Found

Could not find page:docs/tutorials/swizzle-default-theme

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

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

Section 10: Page Not Found

Could not find page:docs/theming/variables