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
Track Contents
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 (or willingness to learn)
- Basic Jinja2 template syntax
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/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/
├── theme.yaml
├── templates/
│ ├── base.html
│ ├── page.html
│ └── partials/
│ ├── header.html
│ └── footer.html
└── static/
├── css/
│ └── style.css
└── js/
└── main.js
Configure Your Theme
Editconfig/_default/theme.yaml:
1 2 | |
Override Templates Selectively
You don't need to copy all templates. Extend the default:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Everything not overridden inherits from the default theme (or parent theme).
Add Custom CSS
Createthemes/my-theme/static/css/custom.css:
1 2 3 4 5 6 7 8 9 | |
Include in your template:
1 2 3 | |
Template Variables
Key variables available in templates:
| Variable | Description |
|---|---|
site.title |
Site title |
site.pages |
All pages |
page.title |
Current page title |
page.content |
Raw content |
page.rendered_html |
Rendered HTML |
page.url |
Page URL |
menu.main |
Main navigation menu |
Debug Theme Issues
1 2 3 4 5 6 7 8 | |
Next Steps
- Theme Customization — Deep dive into overrides
- Template Functions — Available filters
- Variables Reference — All 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
1 2 3 4 5 6 7 8 | |
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:
1 2 3 | |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Partial Overrides
Override specific partials:
themes/my-custom-theme/templates/partials/footer.html:
1 2 3 4 | |
Bengal will use your partial instead of the theme's default.
Customize CSS
Method 1: Override Theme CSS
Create your own CSS file that overrides theme styles:
themes/my-custom-theme/static/css/custom.css:
1 2 3 4 5 6 7 8 9 10 11 | |
Include in your base template:
themes/my-custom-theme/templates/base.html:
1 2 3 4 5 | |
Method 2: Use CSS Variables
Many themes support CSS variables. Override them:
themes/my-custom-theme/static/css/overrides.css:
1 2 3 4 5 6 | |
Theme Configuration Options
Themes can expose configuration options:
themes/my-custom-theme/theme.yaml:
1 2 3 4 5 6 7 8 9 | |
Access in templates:
1 2 3 | |
Configure inbengal.toml:
1 2 3 4 5 6 7 | |
Best Practices
Don't Modify Installed Themes
❌ Bad:
1 2 | |
✅ Good:
1 2 3 | |
Use Theme Inheritance
✅ Good:
1 2 3 4 | |
❌ Bad:
1 2 3 4 5 | |
Troubleshooting
Theme Not Found
Issue:Theme 'my-theme' not found
Solutions:
- Verify theme directory exists:
themes/my-theme/ - Check
theme.yamlhas 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
Seealso
- Templating — Template basics
- Assets — Asset pipeline
- Icon Reference — SVG icons and customization
- Variables Reference — Available template variables
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 |
Page Features
Add features to individual pages.
| Example | What You'll Learn |
|---|---|
| Add Table of Contents | page.toc, scroll highlighting |
| Show Reading Time | reading_timefilter |
Quick Reference
The Essentials
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
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 |
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_timefilter.
The Pattern
1 2 3 | |
That's it. Bengal'sreading_timefilter calculates based on word count (200 wpm default).
What's Happening
| Component | Purpose |
|---|---|
page.content |
Raw content of the page |
reading_time |
Bengal filter: counts words, divides by 200 |
Variations
With Word Count
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.
Custom Calculation
1 2 3 4 5 | |
Handle Short Content
1 2 3 4 5 6 7 8 9 10 11 | |
Frontmatter Override
Allow manual override for complex content:
1 2 3 4 5 | |
Then in frontmatter:
1 2 3 4 | |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
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 |
Variations
From Any Section
1 2 3 4 5 | |
With Featured Post
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Exclude Current Page
1 2 3 4 5 6 7 8 9 | |
Seealso
- Template Functions — All filter options
- Group by Category — Organize posts by category
Templating
Jinja2 layouts, inheritance, and partials
Jinja2 Templating
Bengal uses Jinja2 for all templates. If you know Python, you'll feel at home.
Template Lookup Order
Bengal searches: Your project → Theme → Bengal defaults
Quick Start
1 2 3 4 5 6 7 8 9 | |
1 2 3 4 5 6 7 8 9 10 11 12 | |
1 2 3 4 5 6 7 8 | |
Key Concepts
| Concept | Syntax | Purpose |
|---|---|---|
| Extends | {% extends "base.html" %} |
Inherit from parent template |
| Block | {% block name %}...{% endblock %} |
Replaceable section |
| Include | {% include "partial.html" %} |
Insert another template |
| Variable | {{ page.title }} |
Output a value |
| Filter | {{ text \| truncate(100) }} |
Transform a value |
Template Inheritance
Tip
Override sparingly: You only need to create templates you want to customize. Start by copying one template from your theme, modify it, and let the rest fall through to defaults.
Template Functions Reference
Complete reference for Bengal's template filters and functions
Template Functions Reference
Bengal provides powerful template filters for querying, filtering, and transforming content collections.
Collection Filters
These filters work with lists of pages, dictionaries, or any iterable.
where
Filter items where a key matches a value. Supports comparison operators.
Basic Usage:
1 2 3 4 5 | |
With Comparison Operators:
| Operator | Description | Example |
|---|---|---|
eq |
Equal (default) | where('status', 'published', 'eq') |
ne |
Not equal | where('status', 'draft', 'ne') |
gt |
Greater than | where('date', one_year_ago, 'gt') |
gte |
Greater than or equal | where('priority', 5, 'gte') |
lt |
Less than | where('weight', 100, 'lt') |
lte |
Less than or equal | where('order', 10, 'lte') |
in |
Value in list | where('tags', 'python', 'in') |
not_in |
Value not in list | where('status', ['archived'], 'not_in') |
Operator Examples:
1 2 3 4 5 6 7 8 | |
where_not
Filter items where a key does NOT equal a value.
1 2 | |
sort_by
Sort items by a key, with optional reverse order.
1 2 3 4 5 | |
group_by
Group items by a key value, returning a dictionary.
1 2 3 4 5 6 7 8 9 10 | |
limit
Take the first N items from a list.
1 2 | |
offset
Skip the first N items from a list.
1 2 | |
first / last
Get the first or last item from a list.
1 2 | |
reverse
Reverse a list (returns a new list).
{% set newest_first = chronological | reverse %}
uniq
Remove duplicate items while preserving order.
{% set unique_tags = all_tags | uniq %}
flatten
Flatten nested lists into a single list.
{% set all_tags = nested_tags | flatten | uniq %}
Set Operations
Perform set operations on lists.
union
Combine two lists, removing duplicates.
{% set combined = featured | union(recent) %}
intersect
Get items that appear in both lists.
{% set featured_python = featured | intersect(python) %}
complement
Get items in the first list that are NOT in the second list.
{% set regular = all_posts | complement(featured) %}
Chaining Filters
Filters can be chained for powerful queries:
1 2 3 4 5 6 | |
Quick Reference
| Filter | Purpose | Example |
|---|---|---|
where(key, val) |
Filter by value | pages \| where('type', 'post') |
where(key, val, 'gt') |
Greater than | pages \| where('date', cutoff, 'gt') |
where(key, val, 'in') |
Value in list | pages \| where('tags', 'python', 'in') |
where_not(key, val) |
Exclude value | pages \| where_not('draft', true) |
sort_by(key) |
Sort ascending | pages \| sort_by('title') |
sort_by(key, reverse=true) |
Sort descending | pages \| sort_by('date', reverse=true) |
group_by(key) |
Group by value | pages \| group_by('category') |
limit(n) |
Take first N | pages \| limit(5) |
offset(n) |
Skip first N | pages \| offset(10) |
first |
First item | pages \| first |
last |
Last item | pages \| last |
reverse |
Reverse order | pages \| reverse |
uniq |
Remove duplicates | tags \| uniq |
flatten |
Flatten nested lists | nested \| flatten |
union(list2) |
Combine lists | list1 \| union(list2) |
intersect(list2) |
Common items | list1 \| intersect(list2) |
complement(list2) |
Difference | list1 \| complement(list2) |
Seealso
- Variables Reference — Available template variables
- Templating — Template basics
Theme Variables Reference
Complete reference of all variables available in Jinja2 templates
Theme Variables Reference
Complete reference for 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 |
page
The current page being rendered.
| Attribute | Type | Description |
|---|---|---|
page.title |
str |
Page title |
page.content |
str |
Raw content |
page.rendered_html |
str |
Rendered HTML content |
page.date |
datetime |
Publication date |
page.url |
str |
URL with baseurl applied (for display in templates) |
page.relative_url |
str |
Relative URL without baseurl (for comparisons) |
page.permalink |
str |
Alias forurl(backward compatibility) |
page.metadata |
dict |
All frontmatter keys |
page.toc |
str |
Auto-generated Table of Contents |
page.is_home |
bool |
True if homepage |
page.is_section |
bool |
True if section index |
URL Properties
Bengal provides three URL properties with clear purposes:
page.url - 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
page.relative_url - For comparisons and logic
- Relative URL without baseurl (e.g.,
/docs/page/) - Use for comparisons:
{% if page.relative_url == '/docs/' %} - Use for menu activation, filtering, and conditional logic
page.permalink - Backward compatibility
- Alias for
url(same value) - Maintained for compatibility with existing themes
Example: Usage
1 2 3 4 5 6 7 | |
Global Functions
Functions available in all templates.
asset_url(path)
Generates a fingerprint-aware URL for an asset.
1 2 | |
url_for(page_or_slug)
Generates a URL for a page object or slug.
<a href="{{ url_for(page) }}">Link</a>
dateformat(date, format)
Formats a date object.
{{ dateformat(page.date, "%B %d, %Y") }}
get_menu(menu_name)
Retrieves a navigation menu.
1 2 3 | |
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 }} -->
Seealso
- Template Functions — Filter and function reference
- Templating — Template basics