Ovellum v0.21.0
English

Edited

Plugins#

A plugin is a named unit of build lifecycle hooks. Plugins are where deploy logic lives (onBuildComplete), where you tweak config from the environment (onResolveConfig), and where you post-process rendered pages (transformPage). They compose: list several, and each hook runs in order.

Plugins are functions, so they live in a TS or JS config (ovellum.config.ts / .js) — a JSON config can't carry them. They also flow through the programmatic API (build({ plugins: [...] })).

Declaring plugins#

// ovellum.config.ts
import { defineConfig } from 'ovellum';

export default defineConfig({
  plugins: [
    {
      name: 'deploy-to-cdn',
      onBuildComplete: async ({ outDir, manifest }) => {
        await syncToCdn(outDir, manifest.files); // your deploy
      },
    },
  ],
});

A plugin is { name, ...hooks }. name identifies it in logs and error messages; every hook is optional and may be async. A third-party plugin is just a function that returns such an object — plugins: [myPlugin()].

Lifecycle hooks#

Hooks fire in this order, each across all plugins in array order:

HookWhenGetsReturns
onResolveConfigAfter the config loads + CLI overrides apply, before buildingthe resolved OvellumConfiga config to replace it (chained), or nothing
onBuildStartOnce, before any output{ config, cwd, mode }
transformPagePer rendered HTML page (manual mode), before write{ url, html, outputPath, frontmatter? }{ html } to replace the page, or nothing
onBuildCompleteAfter the build finishes{ outDir, manifest, cwd, mode }

A hook that throws fails the build with a message attributed to the plugin — [plugin: <name>] <hook> failed: … — so a broken plugin is never silent.

onResolveConfig — config from the environment#

{
  name: 'preview-url',
  onResolveConfig: (config) =>
    process.env.DEPLOY_URL
      ? { ...config, site: { ...config.site, baseUrl: process.env.DEPLOY_URL } }
      : undefined, // unchanged
}

Return a config to replace it (later plugins see your version); return nothing to leave it untouched. CLI overrides (--out / --base) are applied after hooks, so the most explicit source still wins. The returned config is used as-is — you own its validity.

transformPage — post-process each page#

Fires for every rendered HTML page of a manual-mode site (the landing, each doc page, the 404), just before it's written. Return { html } to replace it; plugins chain, so each sees the previous one's HTML.

{
  name: 'inject-banner',
  transformPage: ({ url, html }) => ({
    html: html.replace('<body>', '<body><div class="preview-banner">Preview</div>'),
  }),
}

(Auto/hybrid output is Markdown, not pages, so transformPage doesn't fire there.)

onBuildComplete — the deploy hook#

Fires once after the build. outDir is the absolute output path, and manifest is the deploy inventory (every file with its size + sha256) — always computed when a plugin defines this hook, even without --manifest, so a deploy plugin always has the file list.

{
  name: 'deploy',
  onBuildComplete: async ({ outDir, manifest }) => {
    // upload only what changed, verify completeness, etc.
    for (const file of manifest.files) await upload(outDir, file.path, file.sha256);
  },
}

This is the "Ovellum builds; the host deploys" contract made concrete: Ovellum hands you a finished folder + an inventory, and your hook takes it from there.

Markdown plugins#

A plugin can extend the Markdown pipeline with remark and rehype plugins — each a unified Pluggable (a plugin function, or a [plugin, options] tuple):

import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';

export default defineConfig({
  plugins: [
    { name: 'math', remarkPlugins: [remarkMath], rehypePlugins: [rehypeKatex] },
  ],
});

remarkPlugins run after Ovellum's built-in remark plugins and before the HTML conversion; rehypePlugins run on the HTML tree. They apply to manual-mode page rendering (doc pages + landing prose).

Security: rehypePlugins are injected before sanitization — Ovellum's sanitize step is the guard over everything they produce, so a plugin can't inject <script> or other unsafe HTML. (A <script> a rehype plugin adds is stripped, same as raw HTML in a page.) If you need an element/attribute the sanitizer drops, that's a deliberate boundary, not a bug.

Plugins cover config, per-page HTML, Markdown extensions, and deploy. The other half of customization is the CSS/JS layer:

  • site.css — layer extra stylesheets (override design tokens, add rules).
  • site.templateDir — replace the bundled ovellum.css / ovellum.js / fonts wholesale with your own, no fork required.

The page HTML structure is generated in code; a full layout/partial system (beyond CSS/JS) is intentionally out of scope for now.

Edit this page