Extensions

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.

flowchart TD A["Read root index YAML"] --> B["Preload config.webifier.extensions"] B --> C["Discover installed extension entry points"] C --> D["Enable named extension instances in YAML order"] D --> E["Register renderers, templates, assets, hooks, defaults"] E --> F["Load and resolve page data"] F --> G["Process sections and links"] G --> H["Render page template"] H --> I["Run page/head hooks and copy extension assets"] I --> J["Follow discovered pages and content files"] J --> K["Run after_build hooks"]

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:

  • Runup: Webifier reads extension config before full page interpolation, discovers installed extension ids, and lets each enabled instance register capabilities.
  • Render: section kind, content-file extensions, templates, resolvers, and head injections are used while pages are rendered.
  • Finish: side-effect hooks can write build artifacts such as 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:

  • Root config is the site-wide configuration.
  • config.webifier is reserved for Webifier itself: extension loading, extension instance config, and other tool-level behavior.
  • Page-preface config is the page-local override.
  • Page-local config.webifier.extensions can register or reconfigure extension instances before that page renders.
  • Each extension instance owns a namespace inside config with the same name as the instance, such as theme, comments, markdown, notebook, or pdf.
  • Extensions receive the full merged page config and decide which parts of their namespace mean what.
  • Reserved keys configure the page; free-form keys outside 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.

  • Dependencies must be enabled first. For example, webifier.chapters, webifier.people, webifier.notebook, and webifier.resume depend on webifier.standard.
  • If two instances register the same content renderer, the later one must set override: true.
  • override: true is intentional friction. Use it when you really want a later extension to replace an earlier behavior.
  • Any key besides uses, enabled, and override becomes instance config.
  • Each instance's config is merged into the page config under that instance name. For example, an instance named 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:

  • Foundation and content
    • Standard: page shell, sections, links, templates, shared CSS/JS, and base assets.
    • Markdown: Markdown sections and .md content pages.
    • Notebook: .ipynb content pages with YAML page prefaces.
    • PDF: embedded .pdf content pages with site chrome, page data, comments, and navigation.
  • Section renderers
    • Chapters: accordion-style nested chapter sections.
    • People: profile and team cards.
    • Resume: experience, education, activity, and publication renderers.
    • Comments: Utterances-backed discussion sections.
  • Site features
    • Theme: light/dark/system theme plumbing and custom theme CSS.
    • Search: search index generation from rendered content and links.
    • Google Analytics: Google tag injection through a page head hook.