Plugins extend Bengal with custom directives, template functions, content sources, validators, and build lifecycle hooks — all through a singleregister()method. The plugin system uses Python entry points for automatic discovery and a builder→immutable pattern for thread safety.
Quick Start
Create a plugin in three steps:
1. Implement the Plugin Protocol
# my_bengal_plugin/__init__.py
from bengal.plugins.protocol import Plugin
from bengal.plugins.registry import PluginRegistry
class MyPlugin(Plugin):
name = "my-plugin"
version = "1.0.0"
def register(self, registry: PluginRegistry) -> None:
registry.add_template_filter("shout", lambda v: v.upper() + "!")
2. Declare the Entry Point
# pyproject.toml
[project.entry-points."bengal.plugins"]
my-plugin = "my_bengal_plugin:MyPlugin"
3. Install and Build
uv pip install -e ./my-bengal-plugin
bengal build # Plugin auto-discovered
Your filter is now available in templates:
{{ page.title | shout }}
Plugin Protocol
Every plugin must havename, version, and a register()method:
from bengal.plugins.protocol import Plugin
from bengal.plugins.registry import PluginRegistry
class MyPlugin(Plugin):
name: str = "my-plugin"
version: str = "1.0.0"
def register(self, registry: PluginRegistry) -> None:
# Register extensions here
...
Plugin is a runtime_checkableProtocol — no base class inheritance required. Any object with the right attributes works.
Extension Points
ThePluginRegistryprovides 9 registration methods:
Template Extensions
def register(self, registry: PluginRegistry) -> None:
# Global function: {{ my_func(page) }}
registry.add_template_function("my_func", my_func, phase=1)
# Filter: {{ value | my_filter }}
registry.add_template_filter("my_filter", my_filter)
# Test: {% if value is my_test %}
registry.add_template_test("my_test", my_test)
The phase parameter on add_template_functioncontrols registration order (1–9), matching Bengal's internal template function phases.
Directives and Roles
def register(self, registry: PluginRegistry) -> None:
# Block directive: :::{my_directive}
registry.add_directive(MyDirectiveHandler)
# Inline role: {my_role}`text`
registry.add_role(MyRoleHandler)
Content Sources
def register(self, registry: PluginRegistry) -> None:
registry.add_content_source("my-source", MyContentSource)
Health Validators
def register(self, registry: PluginRegistry) -> None:
registry.add_health_validator(MyValidator())
Shortcodes
def register(self, registry: PluginRegistry) -> None:
registry.add_shortcode(
"alert",
'<div class="alert alert-{{ type | default("info") }}">{{ content }}</div>',
)
Build Phase Hooks
def register(self, registry: PluginRegistry) -> None:
registry.on_phase("pre_render", self.before_render)
registry.on_phase("post_render", self.after_render)
def before_render(self, site, build_context):
print(f"Rendering {len(site.pages)} pages")
def after_render(self, site, build_context):
print("Render complete")
How Discovery Works
Bengal discovers plugins via thebengal.pluginsentry point group:
- On startup,
discover_plugins()callsimportlib.metadata.entry_points(group="bengal.plugins") - Each entry point is loaded and validated against the
Pluginprotocol - Valid plugins call
register()on a mutablePluginRegistry - The registry is frozen into an immutable
FrozenPluginRegistrybefore rendering begins
The frozen registry is a dataclass with tuple fields — safe to share across threads during parallel rendering.
Combining Multiple Extensions
A single plugin can register any number of extensions:
class KitchenSinkPlugin(Plugin):
name = "kitchen-sink"
version = "1.0.0"
def register(self, registry: PluginRegistry) -> None:
registry.add_template_filter("slugify", self._slugify)
registry.add_template_function("badge", self._badge, phase=1)
registry.add_directive(MyDirective)
registry.add_health_validator(MyValidator())
registry.on_phase("post_render", self._post_render)
Programmatic Registration
If you don't want entry-point discovery, pass plugins directly:
from bengal.plugins.loader import load_plugins
frozen = load_plugins(extra_plugins=[MyPlugin()])
See Also
- Extension Points — Full reference for all extension points
- Custom Directives — Directive authoring guide
- Template Shortcodes — Template-only shortcodes (no Python required)