An override that finds nothing to override is silent, by design — that is what makes speculative overrides usable. It also means a typo does nothing at all, so check the spelling if an override seems to have no effect.
Plugins
Everything Docspire does beyond rendering Markdown is a plugin: callouts, the sidebar, the page outline, code blocks. Yours work exactly the same way, with the same API.
A plugin is a plain object, exported from a module:
// my-plugin/index.js
export default {
id: "my-plugin",
url: import.meta.url,
};
id names your plugin, and url is how Docspire finds its directory, so always pass import.meta.url if your plugin has files of its own.
Register it in your config:
// docspire.js
import myPlugin from "./my-plugin/index.js";
export default {
title: "My site",
plugins: [myPlugin],
};
What a plugin is made of
Everything is optional.
A plugin that only ships a stylesheet needs nothing but url and styles.
| Key | What it does |
|---|---|
id |
Names the plugin, and derives its options key (page-outline → pageOutline) |
url |
import.meta.url, so Docspire can find the plugin’s files |
defaultOptions |
Defaults users override in their config |
styles, scripts |
File names from the plugin’s assets/styles/ and assets/scripts/, included on every page |
icons |
Icons your templates and components can use by name |
slotted |
Templates put into the site’s layout slots |
data |
Values added to the data cascade — functions become computed data |
plugin |
(config, options), for everything else |
Files live next to index.js, in directories Docspire knows by name:
my-plugin/
index.js
assets/
styles/my-plugin.css
scripts/my-plugin.js
templates/
my-slot-template.njk
components/
my-thing/
components/ is the interesting one — see Components.
Overriding a file with +
Docspire’s own assets/, templates/ and components/ are overlaid with every plugin’s, file by file, in that order.
So a plugin changes one layout, or one component’s template, by shipping a file of the same name in the same place, and everything it says nothing about stays as it was.
A site joins that stack in two ways.
Its components/ — next to docspire.js, plus anything in the components option — is overlaid last, after every plugin’s.
For assets/ and templates/, the site registers itself as a plugin, which is all a plugin is:
// docspire.js
export default {
plugins: [{ url: import.meta.url, styles: "extras.css" }, myPlugin],
};
Now assets/ and templates/ next to your docspire.js are in the overlay too, and templates/layouts/+page.njk overrides the page layout.
A name that starts with + is an override, and it differs from a same-named file in two ways:
- it is applied after every plain name, whichever source it came from
- it is skipped entirely when there is nothing there to override
my-plugin/
templates/layouts/+page.njk replaces Docspire's page layout
components/+callout/style.css restyles callouts — if the site has callouts
Both of those matter for a plugin. The first means a plugin can override something declared by a plugin loaded after it, so overriding does not depend on plugin order. The second means a plugin can ship an override speculatively — “if you have callouts, here is the bit that makes them match” — and it costs nothing on a site that doesn’t.
Stripping one + gives what a name overrides, so ++page.njk overrides +page.njk, and is itself skipped if there is no +page.njk to override.
On a file the rule means “replace it”; on a directory it means “merge into it”, and the names inside follow the same rule. Those are the same thing: a directory is its file tree.
Options
Declare defaultOptions, and users configure your plugin under a key derived from its id:
export default {
id: "my-plugin",
url: import.meta.url,
defaultOptions: {
greeting: "Hello",
},
plugin (config, options) {
// options.greeting is "Howdy" for the config below, "Hello" for everyone else
},
};
// docspire.js
export default {
plugins: [myPlugin],
myPlugin: {
greeting: "Howdy",
},
};
Resolved options are also available to your templates and components, so you rarely need to pass them around yourself.
Slots
slotted puts a template into one of the layout’s slots:
export default {
id: "my-plugin",
url: import.meta.url,
slotted: {
sidebarSecondary: "my-slot-template",
},
};
The name resolves to templates/my-slot-template.njk inside your plugin.
Slot templates are rendered with the page’s data, and any components in their output are expanded, so a slot template can be as simple as one tag.
Transforming content
Docspire gives you two phases, and the difference matters.
config.addContentTransform(fn) runs on Markdown content only, before the layout is applied.
This is where data extraction belongs: things the layout then consumes.
It is also the only phase that can rewrite Markdown output without touching the page’s chrome — heading anchors, for instance, must not reach the sidebar’s own headings.
plugin (config) {
config.addContentTransform(function (tree, data) {
tree.match("h2", node => {
// …collect headings for the outline the layout renders
return node;
});
return tree;
});
}
config.addPageTransform(fn) runs once over the whole document, after the layout.
This is where rendering belongs.
Every page transform shares a single parse and serialization per page.
plugin (config) {
config.addPageTransform(function (tree, context) {
tree.match("my-tag", node => ({ tag: "div", content: node.content }));
});
}
Both use PostHTML trees and the same tree.match(selector, fn) API.
If you are transforming a tag into markup, you probably want a component rather than a page transform. Components give you templates, Markdown syntax, styles and custom elements for the same tag, and the runner takes care of the phase, the expansion and the recursion.
Styles
config.addStyleTransform(fn, { filter }) runs PostCSS over the CSS in the output.
Each file is parsed once and serialized once, no matter how many transforms are registered, and filter restricts a transform to files whose output-relative path passes it.
config.addStyleAliases({ ".my-class": "h4" }) says “style .my-class like h4”.
Every rule that matches h4 is rewritten to match .my-class too, with h4’s own specificity, wherever those rules live — including CSS you did not write.
It is the tidiest way to make your markup look like something the theme already styles, instead of copying declarations.