Bengal's object model provides a rich, hierarchical representation of site content with clear relationships and responsibilities.
Core Objects
Central Data Container (bengal/core/site.py)
Holds all site content and delegates build coordination. It is a passive data container, not a "God object".
Key Attributes:
pages: List of all Page objectssections: List of all Section objectsassets: List of all Asset objectsconfig: Configuration dictionarymenu: Built navigation menus
Key Methods:
build(): Delegates toBuildOrchestratordiscover_content(): Delegates toContentOrchestrator
Content Unit (bengal/core/page/)
Represents a single content page with source, metadata, rendered HTML, and navigation.
Architecture:
- Composition Pattern:
Pagecontains aPageCoreinstance for cacheable metadata - Split into focused mixins:
page_core.py: Cacheable metadata (title, date, tags, etc.)metadata.py: Frontmatter parsingnavigation.py: Next/prev/parent linksrelationships.py: Section membershipcomputed.py: URL generation, TOCoperations.py: Rendering logic
PageCore Integration:
- Cacheable fields (title, date, tags, slug) stored in
page.core - Property delegates provide direct access:
page.title→page.core.title - Enables type-safe caching and lazy loading via
PageProxy
Structural Unit (bengal/core/section.py)
Represents folder-based grouping of pages with hierarchical organization.
Features:
- Hierarchy: Parent/child relationships (
subsections) - Navigation: Access to
regular_pagesandsections - Cascade: Inheritance of frontmatter metadata to descendants
- Path-based Registry: O(1) lookup via
Site._section_registryusing normalized paths - Stable References: Sections referenced by path strings (not object identity) for reliable incremental builds
Static Resource (bengal/core/asset/)
Handles static files (images, CSS, JS) with optimization.
Capabilities:
- Minification (CSS/JS)
- Image optimization
- Cache busting (fingerprinting)
- Output copying
Navigation Structure (bengal/core/menu.py)
Provides hierarchical navigation menus built from config + frontmatter.
Components:
MenuItem: Nested item with active stateMenuBuilder: Constructs hierarchy and marks active items
PageCore Architecture (Implemented in 0.1.4)
Bengal uses the PageCore composition pattern to enforce cache-proxy contract safety and enable fast incremental builds.
The Problem It Solves
To enable incremental builds, we cache page metadata and lazy-load full content only when needed. Previously, this required manually keeping three representations in sync:
Page(live object with full content)PageMetadata(cached metadata for navigation)PageProxy(lazy-loading wrapper)
Risk: Forgetting to update one representation caused cache bugs.
The Solution: PageCore
PageCoreis the single source of truth for all cacheable page metadata. Any field added toPageCoreautomatically becomes available in all three representations.
1 2 3 4 5 6 7 8 9 10 11 12 | |
Architecture
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Benefits
- Type Safety: Compiler enforces all three representations stay in sync
- Simplified Caching:
asdict(page.core)serializes all cacheable fields - Performance: Core fields accessible without lazy loading
- Maintainability: Adding new field requires only 3 changes (PageCore + 2 property delegates)
Adding New Cacheable Fields
When adding a new cacheable field, update three locations:
- Add to PageCore (
bengal/core/page/page_core.py):
1 2 3 4 | |
- Add property delegate to Page (
bengal/core/page/__init__.py):
1 2 3 | |
- Add property delegate to PageProxy (
bengal/core/page/proxy.py):
1 2 3 | |
That's it! The field is now available in Page, PageMetadata, and PageProxy. The compiler will catch any missing implementations.
What Goes in PageCore?
✅ DO Include If:
- Field comes from frontmatter (title, date, tags, slug, etc.)
- Field is computed without full content parsing (URL path components)
- Field needs to be accessible in templates without lazy loading
- Field is cascaded from section
_index.md(type, layout, etc.) - Field is used for navigation (section reference as path)
❌ DO NOT Include If:
- Field requires full content parsing (toc, excerpt, meta_description)
- Field is a build artifact (output_path, links, rendered_html)
- Field changes every build (timestamp, render_time)
- Field is computed from other non-cacheable fields
See:bengal/core/page/page_core.pyfor implementation details.
Stable Section References
Bengal uses path-based section references instead of object identity for reliable incremental builds.
Path-Based Registry
Sections are stored in a dictionary keyed by normalized paths:
1 2 3 4 5 6 | |
Benefits
- Stable Across Rebuilds: Path strings persist in cache, not object references
- O(1) Lookup: Dictionary lookup is constant time
- Reliable Incremental Builds: Sections can be renamed/moved without breaking references
- Feature Flag:
stable_section_referencesconfig flag enables path-based tracking
Implementation
- Sections stored as path strings in
PageCore.section(not Section objects) - Registry built during
Site.register_sections() - Dev server forces full rebuild on file create/delete/move to preserve relationships
- Performance regression tests validate no slowdown (
tests/integration/test_full_build_performance.py)
See:bengal/core/site.pyfor implementation details.
Object Model Relationships
Object Tree Access in Directives
As of v0.1.5, the object tree is directly accessible to MyST directives during markdown parsing. This enables powerful navigation directives like{child-cards},{breadcrumbs},{siblings}, and{prev-next}.
How It Works
During markdown rendering, theMistuneParsersetsrenderer._current_pageto the page being rendered:
1 2 | |
Directives can then access the full object tree:
1 2 3 4 5 6 | |
Performance Characteristics
| Access Pattern | Complexity | Notes |
|---|---|---|
page._section |
O(1) | Direct reference |
section.subsections |
O(1) | Pre-computed list |
section.pages |
O(1) | Pre-computed list |
page.ancestors |
O(depth) | Walks up tree |
page.related_posts |
O(n) | Tag matching |
Available on Page Object
| Property | Type | Description |
|---|---|---|
_section |
Section | Parent section |
metadata |
dict | Frontmatter values |
title |
str | Page title |
url |
str | Page URL |
ancestors |
list | Parent sections to root |
prev_in_section |
Page | Previous page |
next_in_section |
Page | Next page |
related_posts |
list | Pages with matching tags |
Available on Section Object
| Property | Type | Description |
|---|---|---|
name |
str | Section name |
index_page |
Page | Section's_index.md |
pages |
list | Direct child pages |
subsections |
list | Child sections |
sorted_pages |
list | Pages sorted by weight/date |
Writer Usage
Writers use navigation directives in markdown without knowing the implementation:
1 2 3 4 | |
The directive walkspage._section.subsectionsto generate cards automatically.
See Navigation Directives for full reference.