# Extension Points URL: /bengal/docs/0.5.0/reference/architecture/meta/extension-points/ Section: meta Tags: meta, extension-points, plugins, customization, hooks, extensibility, strategies -------------------------------------------------------------------------------- Extension Points 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.md frontmatter): --- 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, str, str]: """Parse markdown and extract TOC, excerpt, and meta description. Returns ``(html, toc, excerpt, meta_description)``. Return empty strings for excerpt/meta if your parser does not derive them. """ 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; the plugin system does not currently expose a parser registration hook. Use the plugin system only for other kinds of extensions that do not require core modifications. 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: from bengal.plugins.protocol import Plugin from bengal.plugins.registry import PluginRegistry class MyFilterPlugin(Plugin): name = "my-filters" version = "1.0.0" def register(self, registry: PluginRegistry) -> None: registry.add_template_filter("custom", lambda value: value.upper()) Tip: The plugin system provides a stable public API for filter registration. See Plugin System for the full pattern including entry-point discovery. 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/" # Register via plugin phase hook # registry.on_phase("post_render", my_generator.generate) Tip: Use the plugin system's on_phase() to register post-processing hooks without modifying core code 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 Purpose: Define custom markdown syntax extensions Implementation (via plugin system): from bengal.plugins.protocol import Plugin from bengal.plugins.registry import PluginRegistry class AlertPlugin(Plugin): name = "alert-shortcode" version = "1.0.0" def register(self, registry: PluginRegistry) -> None: registry.add_shortcode( "alert", '<div class="alert alert-{{ type | default("info") }}">{{ content }}</div>', ) Usage in markdown: {{% alert type="warning" %}} This is a warning message! {{% /alert %}} 9. Plugin System Purpose: Unified extension framework with 6 wired extension points (directives, roles, template functions/filters/tests, build phase hooks) plus three more registered and planned (content sources, health validators, shortcodes), and thread-safe rendering Protocol: All plugins implement Plugin — a runtime-checkable protocol with name, version, and register(): 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: # Directives and roles registry.add_directive(MyDirectiveHandler) registry.add_role(MyRoleHandler) # Template extensions registry.add_template_function("my_func", my_func, phase=1) registry.add_template_filter("my_filter", my_filter) registry.add_template_test("my_test", my_test) # Content and validation registry.add_content_source("my-source", MySourceClass) registry.add_health_validator(MyValidator()) registry.add_shortcode("alert", '<div class="alert">{{ content }}</div>') # Build lifecycle registry.on_phase("pre_render", self.before_render) registry.on_phase("post_render", self.after_render) Discovery: Plugins are auto-discovered via the bengal.plugins entry point group. Declare your plugin in pyproject.toml: [project.entry-points."bengal.plugins"] my-plugin = "my_package:MyPlugin" Thread Safety: The registry uses a builder→immutable pattern. During startup, plugins register into a mutable PluginRegistry. Before rendering begins, freeze() produces a FrozenPluginRegistry dataclass that is safe to share across threads during parallel rendering. Extension Points: Method Purpose add_directive() Block-level content directives add_role() Inline markup roles add_template_function() Template globals (with phase ordering) add_template_filter() Value transformers ({{ x \| filter }}) add_template_test() Boolean predicates ({% if x is test %}) add_content_source() Content source types add_health_validator() Health check validators add_shortcode() Shortcode templates on_phase() Build phase lifecycle hooks Lifecycle hook names currently wired into builds: build_start, build_complete pre_discovery, post_discovery pre_content, post_content pre_parsing, post_parsing pre_snapshot, post_snapshot pre_assets, post_assets pre_render, post_render (pre_rendering and post_rendering are also emitted) pre_finalization, post_finalization pre_health, post_health Use bengal plugin list, bengal plugin info <name>, and bengal plugin validate to inspect installed plugins. The CLI reports both registered capabilities and whether each capability is currently wired. Six capabilities are wired today: directives, roles, template functions, template filters, template tests, and build phase hooks. The remaining three are registered but still reported as pending until their subsystem hooks are wired: content sources, health validators, and shortcodes. Future: Custom CLI Commands 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) Choosing an Extension Approach Most extensions should use the plugin system. For simpler cases: Plugin system: Recommended for directives, filters, content sources, validators, and build hooks Template-based: Many customizations possible via templates alone (no code needed) Theme-based: Package related template and asset customizations in a theme Wrapper scripts: Write scripts that call Bengal + custom logic for CI/deployment 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 at bengal-ssg.org/extensions/: Themes Plugins Template function packs Validators Content strategies CLI commands Migrating to the Plugin System If you have existing extensions using internal APIs or fork-based patterns: Create a class implementing the Plugin protocol (name, version, register()) Move registration logic into register() using the PluginRegistry methods Add a bengal.plugins entry point in your pyproject.toml Remove any code that accesses internal attributes (e.g., site._template_engine._env) Related Documentation Content Types - Custom content strategies Rendering - Template and parser customization Health Checks - Custom validators Design Principles - Extension design patterns -------------------------------------------------------------------------------- Metadata: - Author: lbliii - Word Count: 1402 - Reading Time: 7 minutes