Named extensions, templates, renderers, resolvers, assets, and site features.
Webifier extensions are installable Python packages that contribute rendering behavior to a site. An extension can provide renderers, templates, assets, themes, resolvers, content handlers, and build hooks.
The public config shape is a named mapping:
webifier:
extensions:
site:
uses: webifier.standard
search:
uses: webifier.search
content: true
links: true
theme:
uses: webifier.theme
default: system
switcher: true
resume:
uses: webifier.resume
Each key is a local instance name. uses points to the installed extension id.
The instance name is also the config namespace exported for that instance, so
resume: exports its settings to config.resume, publications: exports to
config.publications, and so on. This lets one extension be used more than
once:
webifier:
extensions:
publications:
uses: my.collection
source: papers/
projects:
uses: my.collection
source: projects/
YAML order is extension load order. A later extension can replace a registration
from an earlier one only when the instance says override: true.
| Extension | Provides |
|---|---|
webifier.standard |
Default page, section, content page, links, freeform renderers, templates, and base assets |
webifier.markdown |
Markdown renderer and .md / .markdown content pages |
webifier.notebook |
.ipynb content pages |
webifier.pdf |
Embedded .pdf content pages with page chrome and PDF controls |
webifier.search |
Search config and search.json generation |
webifier.theme |
Light/dark/system theme plumbing and theme assets |
webifier.analytics.google |
Google tag head injection |
webifier.comments |
Utterances comments renderer |
webifier.people |
People/profile renderer |
webifier.chapters |
Accordion/chapter renderer |
webifier.resume |
Resume experience renderer, inline detail toggles, and assets |
The template search path starts with your site root, then enabled extension template directories in config order.
To override a template, put a file with the same relative path in your site:
my-site/
page.html
templates/
publications.html
Template-only renderers still work:
my-site/
renderers/
gallery.html
photos:
kind: gallery
Expose an entry point:
[project.entry-points."webifier.extensions"]
"my.resume" = "my_resume.extension:ResumeExtension"
Expose an Extension subclass:
from webifier.core.extensions import AssetMount, Extension
class ResumeExtension(Extension):
id = "my.resume"
template_dirs = ["/absolute/path/to/templates"]
assets = [AssetMount("/absolute/path/to/assets", "assets/webifier/resume")]
renderers = {
"resume.experience": "my_resume.renderers.ExperienceRenderer",
}
content_renderers = {
".bib": "my_resume.content.render_bibliography_page",
}
resolvers = {
"resume_files": "my_resume.resolvers.resume_files",
}
hooks = {
"head": ["my_resume.hooks.render_head"],
"after_build": ["my_resume.hooks.after_build"],
}
| Field | Purpose |
|---|---|
id |
Stable installed extension id used by uses |
dependencies |
Extension ids that must already be enabled |
renderers |
kind aliases to RendererModule classes |
content_renderers |
File suffixes or aliases to content-page builders |
page_keys |
Page-level keys consumed before ordinary section rendering |
resolvers |
${resolver:...} functions |
formats |
Patch/load handlers for file extensions |
template_dirs |
Jinja2 template directories |
assets |
Asset directories copied into output |
hooks |
Optional build or HTML injection hooks |
default_config |
Defaults for that extension instance |
config_defaults |
Defaults merged into the site config |
| Hook | Purpose |
|---|---|
head |
Return HTML for the page <head> |
before_build |
Run before the root page is rendered |
after_build |
Run after all pages are rendered |
Hook functions receive the builder plus lifecycle keyword arguments. The head
hook is page-aware: it runs while that page's <head> is being rendered.
Useful head hook arguments:
| Argument | Purpose |
|---|---|
hook_context |
Stable object with area, builder, page, ctx, config, extension id, instance name, and instance config |
page |
The processed page data currently being rendered |
ctx |
The current NodeContext, including page URL |
config |
Global config merged with page-local config and Markdown page-preface config |
instance_config |
Config for this named extension instance |
baseurl |
Base URL for building asset links |
That means an extension can inject assets only on pages that actually use it:
from markupsafe import Markup
def render_head(builder, *, page=None, baseurl="", instance_config=None, **_):
if not page_uses_widget(page):
return ""
return Markup(f'<script defer src="{baseurl}/assets/widget/widget.js"></script>')
Or it can let individual pages opt in through page config:
---
title: Interactive notebook
config:
widget:
enabled: true
---
def render_head(builder, *, config=None, baseurl="", **_):
widget = (config or {}).get("widget", {})
if not widget.get("enabled"):
return ""
return f'<script defer src="{baseurl}/assets/widget/widget.js"></script>'
Head hooks usually return a string; build hooks usually return nothing.
By default, page keys outside reserved Webifier controls are visible content.
With webifier.standard, they render as sections in YAML order. Extensions can
claim additional page-level keys when a key should control behavior instead of
appearing as content.
Register a consumer imperatively:
from webifier.core.extensions import Extension
class WeatherExtension(Extension):
id = "example.weather"
def register(self, ctx):
ctx.consume_page_key("weather", self.consume_weather)
def consume_weather(self, builder, *, value, page, config, instance_name, **_):
page.setdefault("_weather", value)
Or declare a mapping:
class WeatherExtension(Extension):
id = "example.weather"
page_keys = {
"weather": "example_weather.consumers.consume_weather",
}
Then page data can include:
title: Field Notes
weather: cloudy
notes:
label: Notes
content: This section still renders normally.
weather is passed to the registered consumer and removed before renderer
dispatch sees normal sections. That means downstream renderers do not need to
know about keys owned by other extensions.
Consumer callbacks receive the builder plus lifecycle keyword arguments:
| Argument | Purpose |
|---|---|
key |
Consumed key name |
value |
Original value from the page |
page / data |
Remaining mutable page mapping after the key is removed |
ctx |
Current NodeContext |
config |
Merged page config |
instance_name |
Named extension instance that owns the key |
instance_config |
Config for that extension instance |
hook_context |
Stable context object with the same lifecycle data |
If the callback returns a value, Webifier stores it under internal
_extension_data.<instance-name>.<key>. A callback can also mutate page
directly when it wants to add hidden derived data for a renderer or hook.
Only one extension instance can consume a page key by default. A later instance
can replace the consumer with override: true:
config:
webifier:
extensions:
weather:
uses: example.weather
better_weather:
uses: other.weather
override: true
Content renderers turn linked files into generated pages. The markdown extension
registers .md, .markdown, md, and markdown. The notebook extension
registers .ipynb and notebook.
def render_report(builder, src, ctx):
html = convert_report_to_html(src)
return wrap_in_content_page(builder, html, ctx)
Then use a YAML link with src:
webifier:
extensions:
reports:
uses: my.reports
- text: Open report
src: reports/run-001.report
The first-party resume extension registers kind: resume.experience:
webifier:
extensions:
site:
uses: webifier.standard
markdown:
uses: webifier.markdown
resume:
uses: webifier.resume
experience:
label: Professional Experience
kind: resume.experience
inline: true
defaults:
compact: true
stability:
heading:
institution: Stability AI
role: Senior Machine Learning Scientist
date: Mar. 2025 - Present
location: Toronto, ON, Canada
expanded_heading:
institution: Stability AI
role: Senior Machine Learning Scientist
date: Mar. 2025 - Present
location: Toronto, ON, Canada
content: |
- Led model training work.
earlier_role:
compact: false
heading:
institution: Earlier Company
role: ML Engineer
date: 2021 - 2022
location: Toronto, ON, Canada
content: |
- This entry renders inline instead of using the detail toggle.
If content is missing, the compact toggle is not shown.
Set defaults.compact: false to render entries inline by default, or override
individual entries with compact: true / compact: false.
Compact entries render as short summaries first; the left-side toggle switches
that entry into the full non-compact layout and back again.
Set inline: true on the section for a dense divider-free stack where desktop
rows read as role - institution with location • date aligned to the right.