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:
| Hook | When | Gets | Returns |
|---|---|---|---|
onResolveConfig | After the config loads + CLI overrides apply, before building | the resolved OvellumConfig | a config to replace it (chained), or nothing |
onBuildStart | Once, before any output | { config, cwd, mode } | — |
transformPage | Per rendered HTML page (manual mode), before write | { url, html, outputPath, frontmatter? } | { html } to replace the page, or nothing |
onBuildComplete | After 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:
rehypePluginsare 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.
Related: replacing the theme assets#
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 bundledovellum.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.