Bengal is designed with multiple extension points that allow customization without modifying core code.
1. Custom Content Strategies
Purpose: Define how different content types are sorted, paginated, and rendered
Implementation:
from bengal.content_types.base import ContentTypeStrategy
from bengal.content_types.registry import register_strategy
class NewsStrategy(ContentTypeStrategy):
"""Custom strategy for news articles."""
default_template = "news/list.html"
allows_pagination = True
def sort_pages(self, pages):
"""Sort by date, newest first."""
from datetime import datetime
return sorted(
pages,
key=lambda p: p.date or datetime.min,
reverse=True
)
def get_template(self, page=None, template_engine=None):
"""Custom template selection."""
if page and page.source_path.stem == "_index":
return "news/list.html"
return "news/single.html"
def detect_from_section(self, section):
"""Auto-detect news sections."""
return section.name.lower() in ("news", "announcements")
# Register your strategy (call early in build process)
register_strategy("news", NewsStrategy())
Configuration (in section _index.mdfrontmatter):
---
title: News
content_type: news
---
Documentation: See Content Types
2. Custom Markdown Parsers
Purpose: Add support for alternative markdown flavors or custom syntax
Implementation:
from typing import Any
from bengal.parsing.base import BaseMarkdownParser
class CustomMarkdownParser(BaseMarkdownParser):
def parse(self, content: str, metadata: dict[str, Any]) -> str:
"""Parse markdown to HTML."""
# Your parsing logic here
html = self._convert_to_html(content)
return html
def parse_with_toc(self, content: str, metadata: dict[str, Any]) -> tuple[str, str]:
"""Parse markdown and extract table of contents."""
html = self.parse(content, metadata)
toc_html = self._extract_toc(content)
return html, toc_html
# Register in parser factory
# (requires modification of bengal/parsing/__init__.py)
Using Patitas Low-Level API (with ContextVar configuration):
from bengal.parsing.backends.patitas import (
ParseConfig, RenderConfig,
parse_config_context, render_config_context,
)
from bengal.parsing.backends.patitas.renderers.html import HtmlRenderer
from patitas.parser import Parser
# Configure and parse
with parse_config_context(ParseConfig(tables_enabled=True, math_enabled=True)):
parser = Parser(source)
ast = parser.parse()
# Configure and render
with render_config_context(RenderConfig(highlight=True)):
renderer = HtmlRenderer(source)
html = renderer.render(ast)
Note: Custom parser registration currently requires modifying core code. A plugin-based registration system is planned for v0.4.0.
3. Custom Template Engines
Purpose: Bring your own template engine with full access to Bengal's 80+ template functions
Implementation:
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 satisfy TemplateEnvironment protocol
# (must have globals, filters, tests as dict-like attributes)
self._env = MyEnvironment()
# All 80+ template functions registered automatically!
register_all(self._env, site)
def render_template(self, name: str, context: dict) -> str:
...
register_engine("myengine", MyEngine)
Documentation: See Bring Your Own Template Engine
4. Custom Template Functions
Purpose: Add custom filters and functions to existing engines
Implementation:
# In your site's custom module
def my_custom_filter(value):
"""Custom filter implementation."""
return value.upper()
# Register via build hook (uses internal API - may change)
def register_filters(site):
# Note: _template_engine is internal; plugin API coming in v0.4.0
env = site._template_engine._env
env.filters['custom'] = my_custom_filter
Caution: This approach accesses internal attributes (
_template_engine._env). A stable public API for filter registration is planned for the plugin system (v0.4.0).
Usage in templates:
{{ page.title | custom }}
Documentation: See Add a Custom Filter
5. Custom Post-Processors
Purpose: Add custom build steps after page rendering
Implementation:
# In bengal/postprocess/ or custom module
class RobotsGenerator:
def __init__(self, site):
self.site = site
def generate(self) -> None:
"""Generate robots.txt"""
robots_content = self._build_robots_txt()
output_path = self.site.output_dir / 'robots.txt'
output_path.write_text(robots_content)
def _build_robots_txt(self) -> str:
# Build robots.txt content
return "User-agent: *\nDisallow: /admin/"
# Add to PostprocessOrchestrator
# (requires modification or plugin hook)
Future: Plugin system will provide hooks
6. Custom Validators
Purpose: Add custom health checks and validation rules
Implementation:
from bengal.health.base import BaseValidator
from bengal.health.report import CheckResult, CheckStatus
class CustomValidator(BaseValidator):
name = "custom"
description = "Custom validation checks"
def validate(self, site, build_context=None) -> list[CheckResult]:
results = []
# Your validation logic
for page in site.pages:
if not self._check_custom_requirement(page):
results.append(CheckResult(
status=CheckStatus.ERROR,
message=f"Page {page.title} fails custom check",
recommendation="Fix the issue"
))
return results
# Register with health system
# (requires modification of bengal/health/health_check.py)
Configuration:
[health_check.validators]
custom = true
Documentation: See Health Checks
7. Custom Themes
Purpose: Package templates, styles, and scripts for reuse
Structure:
my-theme/
├── templates/
│ ├── base.html
│ ├── page.html
│ └── blog/
│ ├── post.html
│ └── list.html
├── assets/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── main.js
│ └── fonts/
└── theme.toml # Theme metadata
Configuration:
theme = "my-theme"
Documentation: Themes automatically override default templates
8. Custom Shortcodes (Planned)
Purpose: Define custom markdown syntax extensions
Planned API:
# Planned API (not implemented yet):
# from bengal.rendering.plugins import ShortcodePlugin
class AlertShortcode(ShortcodePlugin):
name = "alert"
def render(self, content, **kwargs):
alert_type = kwargs.get('type', 'info')
return f'<div class="alert alert-{alert_type}">{content}</div>'
# Register shortcode
# (requires plugin system)
Usage in markdown:
{{% alert type="warning" %}}
This is a warning message!
{{% /alert %}}
9. Plugin System (Planned v0.4.0)
Purpose: Formal plugin architecture with lifecycle hooks
Planned API:
# Planned API (not implemented yet):
# from bengal.plugins import Plugin, hook
class MyPlugin(Plugin):
name = "my-plugin"
version = "1.0.0"
@hook('pre_build')
def before_build(self, site):
"""Called before build starts"""
print(f"Building {len(site.pages)} pages")
@hook('post_page_render')
def after_page_render(self, page, html):
"""Called after each page renders"""
return self._modify_html(html)
@hook('post_build')
def after_build(self, site, stats):
"""Called after build completes"""
print(f"Built in {stats.duration}s")
# Register plugin
# bengal.toml:
# [plugins]
# my-plugin = true
Planned Hooks:
pre_build- Before build startspost_content_discovery- After content discoveredpre_page_render- Before page renderspost_page_render- After page renderspost_build- After build completestemplate_context- Modify template contextpre_asset_process- Before asset processingpost_asset_process- After asset processing
10. Custom CLI Commands (Future)
Purpose: Add custom commands to Bengal CLI
Planned API:
import click
from bengal.cli import cli
@cli.command()
@click.option('--verbose', is_flag=True)
def custom(verbose):
"""My custom command."""
print("Executing custom command")
# Commands automatically discovered and registered
11. Custom Autodoc Extractors
Purpose: Generate documentation from other sources
Implementation:
from typing import Any
from pathlib import Path
from bengal.autodoc.base import Extractor, DocElement
class OpenAPIExtractor(Extractor):
"""Extract docs from OpenAPI specs."""
def extract(self, source: Any) -> list[DocElement]:
spec = self._load_openapi_spec(source)
elements = []
for path, methods in spec['paths'].items():
for method, details in methods.items():
element = DocElement(
name=f"{method.upper()} {path}",
qualified_name=f"api.{method}.{path}",
element_type='endpoint',
description=details.get('summary', ''),
metadata=details
)
elements.append(element)
return elements
def get_output_path(self, element: DocElement) -> Path | None:
"""Determine output path for the endpoint."""
return Path(f"api/{element.name.replace(' ', '-').lower()}.md")
# Register extractor
# (planned: autodoc registry system)
Current Workarounds
Until the plugin system is implemented, some extensions require:
- Fork and modify: Make changes in your own fork
- Wrapper scripts: Write scripts that call Bengal + custom logic
- Template-based: Many customizations possible via templates alone
- Theme-based: Package customizations in a theme
Extension Best Practices
- Prefer Templates: Many customizations possible without code
- Use Themes: Package related customizations together
- Follow Conventions: Match Bengal's patterns and style
- Document: Provide clear documentation for your extensions
- Test: Write tests for custom functionality
- Share: Consider contributing back to Bengal
Community Extensions (Future)
Planned extension registry atbengal-ssg.org/extensions/:
- Themes
- Plugins
- Template function packs
- Validators
- Content strategies
- CLI commands
Migration Path
When plugin system arrives:
- Existing extensions can be migrated incrementally
- Old patterns will continue working (backward compatibility)
- New APIs will be opt-in
- Migration guides will be provided
Related Documentation
- Content Types - Custom content strategies
- Rendering - Template and parser customization
- Health Checks - Custom validators
- Design Principles - Extension design patterns