How Webifier extensions are discovered, configured, ordered, and used during rendering.
Extensions are the way Webifier turns a small static-site core into a reusable publishing system. The core loads configuration, walks pages, resolves links, dispatches renderers, and writes files. Extensions register the parts that make those steps useful: renderers, content handlers, templates, assets, themes, hooks, resolvers, and defaults.
A build starts at one index page. That page can link to other YAML pages, Markdown files, notebooks, PDFs, HTML files, and static assets. Webifier renders the index, follows discovered links, renders reachable content pages, and copies assets into the output site.
Read this page first if you want the extension mental model. Then use the first-party pages as practical contracts for the syntax they add.
If you want to build your own extension, read this page, then use the Custom Extensions and Reusable Formats page for the practical install/configure/action workflow and the Extension and Customization Guide as the implementation reference.
Webifier is deliberately small. It knows how to load a page, read config,
run the configured extension pipeline, and hand content back to that
pipeline recursively when links discover more pages. Extensions define the
useful contracts: what YAML means, what a notebook means, what a PDF page
means, how head tags are injected, which assets are copied, and how custom
page formats behave.
With the standard extension enabled, the page model is deliberately boring:
a page is content from top to bottom. YAML keys become sections in the order
you write them, except reserved keys such as title, config, nav,
header, footer, meta, and style control page behavior instead of
becoming content.
Extensions can also claim additional page keys. If an extension registers a
consumer for weather, then a page-level weather: cloudy key is passed to
that extension and removed from the content stream before sections render.
Later renderers do not accidentally treat it as a visible section.
Extensions participate at three main moments:
kind, content-file extensions, templates, resolvers,
and head injections are used while pages are rendered.search.json.In rough pseudocode, the build looks like this:
root_config = preload_config(index_yml)
extension_manager.configure(root_config)
config = extension_manager.apply_config(root_config)
root_page = load_and_resolve(index_yml, config)
for page in reachable_pages(root_page):
page = extension_manager.consume_page_keys(page)
processed = process_sections(page)
html = render_page_template(processed)
write_output(page.url, html)
copy_extension_assets()
extension_manager.run_hooks("after_build")
Extension instance settings are the canonical place for site-wide defaults.
Page config can then override those settings without inventing a new
syntax for every content type. The contract is:
config is the site-wide configuration.config.webifier is reserved for Webifier itself: extension loading,
extension instance config, and other tool-level behavior.config is the page-local override.config.webifier.extensions can register or reconfigure
extension instances before that page renders.theme, comments, markdown, notebook, or
pdf.config render
as content sections.# index.yml
config:
webifier:
extensions:
site:
uses: webifier.standard
markdown:
uses: webifier.markdown
toc: true
notebook:
uses: webifier.notebook
toc: true
colab: true
pdf:
uses: webifier.pdf
toc: false
download: true
toolbar: true
Webifier exports those instance settings into the page config seen by renderers:
markdown:
toc: true
notebook:
toc: true
colab: true
pdf:
toc: false
download: true
toolbar: true
A Markdown page can then override only what it needs directly:
---
title: Short Note
config:
markdown:
toc: false
related:
kind: links
items:
- text: Full notes
src: notes/full.md
---
# Short Note
The config block affects rendering but does not show up on the page. The
related block is not reserved, so it renders after the Markdown body as a
normal Webifier section.
Or the page can reconfigure an extension instance before it renders:
---
title: Local Notebook Page
config:
webifier:
extensions:
notebook:
reset: true
colab: false
toc: false
---
reset: true means the page starts from that extension's own defaults
instead of inheriting the site-level instance settings for the exported
instance namespace.
Page-local config applies to that generated page only. Linked child pages use the root site config unless they declare their own page config. That keeps generated pages independent and avoids surprising config leakage through links.
When a page reconfigures a root extension instance, uses can be omitted.
Use uses when the page introduces a new extension instance that was not
declared in the root config.
Markdown page prefaces are read before Markdown links are processed, so a Markdown page can enable or reconfigure an extension before typed links in its body are resolved.
Visible page content and extension controls are intentionally separate.
config is for behavior settings, while page keys outside config normally
become content sections. Sometimes an extension needs a top-level page key
that looks like content but should actually drive logic.
For that case, an extension can consume a page key:
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, instance_name, **_):
page.setdefault("_weather", value)
Then a page can say:
title: Field Notes
config:
webifier:
extensions:
weather:
uses: example.weather
weather: cloudy
notes:
label: Notes
content: The weather key is not rendered as a section.
The weather key is seen by the weather extension and removed before
ordinary page sections render. If another extension also wants weather,
Webifier raises an error unless the later instance is explicitly marked
override: true.
Content-page prefaces use the same idea. Markdown front matter, notebook
first-cell page prefaces, and PDF page.yml files can contain extension
consumed keys, visible after-content sections, and config overrides in
one place.
Hooks let extensions affect places outside a single section renderer. Current first-party extensions use two hook moments:
head: returns HTML inserted into the page <head>. The callback receives
the page data, merged config, base URL, node context, instance name, and
instance config. This is how themes, analytics, and resume page assets are
injected.after_build: runs after pages and assets are written. This is how search
writes search.json.A hook can be page-aware. For example, webifier.resume inspects the current
page and injects its CSS/JS only when that page uses kind: resume.experience
or kind: resume.publications.
class ResumeExtension(Extension):
id = "webifier.resume"
renderers = {
"resume.experience": "...ResumeExperienceRenderer",
"resume.publications": "...ResumePublicationsRenderer",
}
def register(self, ctx):
super().register(ctx)
ctx.add_hook("head", self.render_head)
def render_head(self, builder, *, page=None, baseurl="", **kwargs):
if not page_uses_resume(page):
return ""
return f'<link rel="stylesheet" href="{baseurl}/assets/webifier/resume/css/resume.css">'
An extension must be importable in Python. In normal use that means you
install a package with pip, and the package exposes an entry point in the
webifier.extensions group.
Webifier only enables extensions listed under named entries in
config.webifier.extensions:
config:
webifier:
extensions:
site:
uses: webifier.standard
markdown:
uses: webifier.markdown
theme:
uses: webifier.theme
default: system
switcher: true
search:
uses: webifier.search
content: true
links: true
docs_people:
uses: webifier.people
The key such as site, markdown, or docs_people is a local instance
name and also the config namespace exported for that instance. It can be
whatever makes sense in your project. The uses value is the installed
extension id.
YAML order matters. Webifier enables extension instances from top to bottom. That order affects dependency checks, template search paths, hooks, and duplicate registrations.
webifier.chapters,
webifier.people, webifier.notebook, and webifier.resume depend on
webifier.standard.override: true.override: true is intentional friction. Use it when you really want a
later extension to replace an earlier behavior.uses, enabled, and override becomes instance config.theme exports to config.theme,
and an instance named comments exports to config.comments.config:
webifier:
extensions:
markdown:
uses: webifier.markdown
custom_markdown:
uses: my_lab.markdown
override: true
fenced_alerts: true
Extension packages expose subclasses of webifier.core.extensions.Extension.
Simple extensions can be almost entirely declarative.
from webifier.core.extensions import AssetMount, Extension
class LabReportExtension(Extension):
id = "my_lab.report"
dependencies = ("webifier.standard",)
template_dirs = ["/path/to/templates"]
assets = [AssetMount("/path/to/assets", "assets/my-lab")]
renderers = {
"lab.report": "my_lab.renderers.ReportRenderer",
}
config_defaults = {
"defaults": {"section": "section"},
}
More involved extensions override register() when they need callbacks,
conditional hooks, content renderers, resolvers, or build-time behavior.
class LabNotebookExtension(Extension):
id = "my_lab.notebook"
dependencies = ("webifier.standard",)
def register(self, ctx):
super().register(ctx)
ctx.register_content_renderer(".lab.ipynb", self.render_lab_notebook)
ctx.add_hook("head", self.render_head)
ctx.add_hook("after_build", self.write_manifest)
Installing webifier also installs webifier-extensions. These are the
bundled extension ids and their focused reference pages: