What Bengal Computes vs What Themes Display
Last Updated: 2025-10-26
Philosophy
Bengal computes. Themes display.
Thecontentconfig section controls what Bengal computes during build time. These computed properties are available to ALL themes consistently. Themes decide whether to display them and how to style them.
Current Computed Properties
Page Properties (page.*)
| Property | Config Control | Template Access | Purpose |
|---|---|---|---|
page.excerpt |
excerpt_length(200) |
{{ page.excerpt }} |
Content preview for cards/listings |
page.meta_description |
summary_length(160) |
{{ page.meta_description }} |
SEO meta description |
page.reading_time |
reading_speed(200 WPM) |
{{ page.reading_time }} |
Estimated reading time in minutes |
page.toc |
toc_depth(3),toc_min_headings(2),toc_style("nested") |
{{ page.toc \| safe }} |
Generated table of contents HTML |
page.toc_items |
(derived fromtoc) |
{% for item in page.toc_items %} |
Structured TOC data for custom rendering |
page.related_posts |
related_count(3),related_threshold(0.3) |
{% for post in page.related_posts %} |
Related pages based on tag similarity |
page.url |
(automatic) | {{ page.url }} |
URL with baseurl applied (for display in templates) |
page.relative_url |
(automatic) | {{ page.relative_url }} |
Relative URL without baseurl (for comparisons and logic) |
page.permalink |
(automatic) | {{ page.permalink }} |
Alias forurl(backward compatibility) |
page.title |
(from metadata or generated) | {{ page.title }} |
Page title (humanized from filename if missing) |
URL Pattern Strategy:
page.url: Primary property for display - automatically includes baseurl from site configpage.relative_url: Use for comparisons, menu activation, filtering (without baseurl)page.permalink: Alias forurl(maintained for backward compatibility)
This pattern is ergonomic: templates use{{ page.url }}for display and it "just works" for all deployment scenarios (GitHub Pages, Netlify, S3, file://, etc.). Use{{ page.relative_url }}when you need to compare URLs without baseurl.
|page.date|date_format("long") |{{ page.date }}| Parsed date object |
|page.slug| (from metadata or generated) |{{ page.slug }}| URL-safe slug |
|page.keywords| (from metadata) |{{ page.keywords }}| SEO keywords list |
|page.is_home| (automatic) |{% if page.is_home %}| Boolean: is this the home page? |
|page.is_section| (automatic) |{% if page.is_section %}| Boolean: is this a section? |
|page.is_page| (automatic) |{% if page.is_page %}| Boolean: is this a regular page? |
|page.kind| (automatic) |{{ page.kind }}| Page type: "home", "section", or "page" |
|page.draft| (from metadata) |{% if page.draft %}| Boolean: is this a draft? |
|page.next| (automatic) |{% if page.next %}| Next page in site-wide collection |
|page.prev| (automatic) |{% if page.prev %}| Previous page in site-wide collection |
|page.next_in_section| (respectssort_pages_by) |{% if page.next_in_section %}| Next page in same section |
|page.prev_in_section| (respectssort_pages_by) |{% if page.prev_in_section %}| Previous page in same section |
|page.parent| (automatic) |{{ page.parent.title }}| Parent section |
|page.ancestors| (automatic) |{% for ancestor in page.ancestors %}| List of ancestor sections |
Section Properties (section.*)
| Property | Config Control | Template Access | Purpose |
|---|---|---|---|
section.sorted_pages |
sort_pages_by("weight"),sort_order("asc") |
{% for page in section.sorted_pages %} |
Pages sorted by weight/date/title |
section.sorted_subsections |
(respectsweightin index metadata) |
{% for sub in section.sorted_subsections %} |
Sorted child sections |
section.subsection_index_urls |
(automatic) | {% if page.relative_url not in section.subsection_index_urls %} |
Set of subsection index URLs (for nav de-duplication) |
section.regular_pages |
(automatic) | {% for page in section.regular_pages %} |
Non-index pages in this section |
section.regular_pages_recursive |
(automatic) | {% for page in section.regular_pages_recursive %} |
All descendant pages |
section.url |
(automatic) | {{ section.url }} |
Section URL with baseurl applied (for display) |
section.relative_url |
(automatic) | {{ section.relative_url }} |
Relative URL without baseurl (for comparisons) |
section.permalink |
(automatic) | {{ section.permalink }} |
Alias forurl(backward compatibility) |
section.hierarchy |
(automatic) | {{ section.hierarchy }} |
List of section names from root |
section.depth |
(automatic) | {{ section.depth }} |
Nesting depth |
section.root |
(automatic) | {{ section.root.title }} |
Top-most ancestor section |
section.title |
(from index metadata or generated) | {{ section.title }} |
Section title |
section.aggregate_content() |
(automatic) | {{ section.aggregate_content().page_count }} |
Aggregated stats (page counts, tags) |
Config Examples
Current Computations
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
Theme Display Preferences (Separate File)
1 2 3 4 5 6 7 8 9 | |
Key Insight: Changingcontent.excerpt_lengthchanges whatpage.excerptreturns. Changingtheme.show_excerptschanges whether the theme displays it.
Future Enhancements (Not Yet Implemented)
These are potential computed properties Bengal could provide in future versions:
Word Count
1 2 | |
Template Access:{{ page.word_count }} words
Rationale: Currently computed internally forreading_timebut not exposed. Useful for writers, analytics, and content dashboards.
Author Formatting
1 2 | |
Template Access:{{ page.author }}
Rationale: Currently just raw metadata. Bengal could format consistently: "Jane Doe", "Jane Doe jane@example.com", "Jane Doe (Software Engineer)".
Custom Excerpt Truncation
1 2 | |
Template Access:{{ page.excerpt }}
Rationale: Currently hard-coded to "...". Custom endings improve UX: "... [read more]", "...(continued)", etc.
Social Media Excerpts
1 2 | |
Template Access:{{ page.social_excerpt }}
Rationale: Different platforms have different length limits (Twitter 280, LinkedIn 600, Facebook 500). Separate property optimizes for each.
Git-Based Last Modified
1 2 | |
Template Access:{{ page.last_modified }}
Rationale: More accurate than file system timestamps. Shows "Last updated: 2025-10-25" based on git history.
Reading Level Analysis
1 2 | |
Template Access:{{ page.reading_level }}
Rationale: Help content authors gauge readability. "Grade 8" means readable by 8th graders. Useful for docs targeting specific audiences.
Section Statistics
1 2 | |
Template Access:
1 2 3 | |
Rationale: Help site authors understand content health. "This section has 12,500 words across 8 articles, averaging 6 min read. Last updated 3 days ago."
Non-Computational Properties (Raw Data)
These are NOT computed by Bengal but are available directly from frontmatter metadata:
page.metadata- Raw frontmatter dictpage.tags- Tags listpage.content- Raw markdown contentpage.rendered_html- Rendered HTMLpage.source_path- Source file pathpage.output_path- Output file pathpage.links- Extracted linkspage.lang- Language code (i18n)page.translation_key- Translation identifier (i18n)page.version- Version string (for versioned docs)
These can be accessed directly without configuration:
1 2 | |
Architectural Boundaries
✅ Content Config SHOULD Control:
- Computations: How to calculate derived values
- Algorithms: Related posts similarity thresholds, TOC depth
- Organization: Sorting, filtering, grouping
- Content Analysis: Reading time, excerpts, word counts
❌ Content Config SHOULD NOT Control:
- Display: show/hide features (use
theme.yaml) - Styling: colors, fonts, layouts (use theme CSS)
- Presentation: card layouts, grid vs list (use theme templates)
- Interactions: animations, transitions (use theme JS)
Examples
✅ Good (Content Config):
1 2 | |
❌ Bad (Content Config):
1 2 | |
✅ Good (Theme Config):
1 2 | |
Testing Computational API
To verify config-controlled computations work correctly:
1 2 3 4 5 | |
Seetests/unit/test_page_computed.pyfor full test suite.
Migration Guide: Hugo → Bengal
| Hugo Config | Bengal Equivalent | Reason |
|---|---|---|
params.show_reading_time |
theme.show_reading_time |
Display control (theme) |
params.excerpt_length |
content.excerpt_length |
Computation control (Bengal) |
params.default_content_type |
content.default_type |
Content model control (Bengal) |
params.custom_var |
params.custom_var |
User variable (unchanged) |
Best Practices
Config Organization:
content.yaml: Controls what Bengal computestheme.yaml: Controls what themes displayparams.yaml: Custom variables for templates
Theme Development:
- Always check
if page.tocbefore rendering TOC - Use
page.excerptnotpage.content[:200] - Respect
theme.show_*preferences
- Always check
Content Authoring:
- Override computed values via frontmatter:
excerpt: "Custom excerpt" - Control page order via
weightmetadata - Use
draft: trueto hide pages
- Override computed values via frontmatter:
Site Configuration:
- Start with defaults
- Tune for your audience (faster readers? increase
reading_speed) - Validate changes (lower
related_thresholdshows more related posts)
Summary
Bengal provides a clean separation:
- Content Config → What Bengal computes (theme-independent)
- Theme Config → What themes display (presentation layer)
- Params Config → Custom user data (arbitrary variables)
This architecture ensures:
- ✅ Themes are interchangeable (computation is consistent)
- ✅ Config is composable (change one without affecting others)
- ✅ Upgrades are safe (new themes work with existing config)
- ✅ Behavior is predictable (same config = same computation)
Core Principle: Bengal's content API is a contract between the build system and themes. Config controls the contract. Themes consume the contract.