Plugins are the primary extension mechanism for docmd. They allow you to inject HTML, modify Markdown parsing, inject build-time data, and automate post-build tasks. This guide outlines the plugin API.
Plugin Descriptor
Every plugin must export a plugin descriptor declaring its identity and capabilities. This enables the engine to validate and isolate boundaries at load time.
"plugin": {
"name": "my-analytics",
"version": "1.0.0",
"capabilities": ["head", "body", "post-build"]
},
"generateScripts": (config, opts) => { ... },
"onPostBuild": async (ctx) => { ... }
Note: The descriptor is strictly required. Plugins without it will fail to load.
Core Capabilities
The capabilities array dictates which hooks your plugin is allowed to use.
| Capability | Allowed Hooks | Phase |
|---|---|---|
init |
onConfigResolved |
Init |
markdown |
markdownSetup |
Setup |
head |
generateMetaTags, generateScripts (head) |
Render |
body |
generateScripts (body) |
Render |
build |
onBeforeParse, onAfterParse, onBeforeBuild, onBeforeRender, onPageReady |
Build |
post-build |
onPostBuild |
Post-Build |
dev |
onDevServerReady |
Dev Server |
assets |
getAssets |
Output |
actions |
actions |
Interactive |
events |
events |
Interactive |
translations |
translations |
i18n |
Plugin API Reference
A docmd plugin is a standard JavaScript object that implements one or more of the following hooks.
| Hook | Description |
|---|---|
markdownSetup(md, opts) |
Extend the markdown-it instance. Synchronous. |
generateMetaTags(config, page, root) |
Inject <meta> or <link> tags into the <head>. |
generateScripts(config, opts) |
Return an object containing headScriptsHtml and bodyScriptsHtml. |
getAssets(opts) |
Define external files or CDN scripts to be injected. |
onPostBuild(ctx) |
Run logic after the generation of all HTML files. |
translations(localeId) |
Return an object of translated strings for the given locale. |
actions |
An object of named action handlers for WebSocket RPC calls. |
events |
An object of named event handlers for browser messages. |
Creating a Local Plugin
Creating a plugin is as simple as defining a JavaScript file. For example, my-plugin.js:
import path from "path";
export default {
plugin: {
"name": "my-plugin",
"version": "1.0.0",
"capabilities": ["head", "post-build"]
},
markdownSetup: (md, options) => {
// Add custom parser rules
},
generateMetaTags: async (config, page, relativePathToRoot) => {
return `<meta name="x-build-id" content="${config._buildHash}">`;
},
onPostBuild: async ({ config, pages, outputDir, log, options }) => {
log(`Custom Plugin: Verified ${pages.length} pages.`);
}
};
To enable your plugin, reference its full package name in your docmd.config.json:
"plugins": {
"my-awesome-plugin": {}
}
Note: Shorthand names (e.g.
math,search) are reserved for official@docmd/plugin-*packages. Third-party plugins must always use their full npm package name.
Plugin Resolution
The docmd engine resolves plugin names as follows:
- Official shorthands (
math,search) expand to@docmd/plugin-<name>. Only official packages can exist under the@docmdscope. - Third-party plugins must use their full package name (e.g.
my-awesome-plugin,@myorg/docmd-extras). There is no alias system for external plugins. This eliminates supply-chain attack vectors.
Plugin Isolation
Every hook invocation is wrapped in a try/catch block. A broken plugin cannot crash the build or interfere with other plugins. Errors are logged and collected into a summary.
Scoping Plugins (noStyle)
Plugins inject their CSS/JS universally by default. Developers can explicitly prevent their plugin from rendering on noStyle pages by exporting a noStyle boolean:
export default {
noStyle: false,
generateScripts: () => { ... }
}
Users can override this via configuration (plugins: { math: { noStyle: false } }) or dynamically via Markdown frontmatter (plugins: { math: true }).
Lifecycle Hooks
Docmd provides deep integration hooks. They allow plugins to manipulate configuration, raw sources, and page data.
| Hook | Description | Expected Return |
|---|---|---|
onConfigResolved(config) |
Reads or modifies the active config right after initialisation. | void or Promise<void> |
onDevServerReady(server, wss) |
Exposes the raw Node.js server during npx @docmd/core dev. |
void or Promise<void> |
onBeforeParse(src, frontmatter, filePath?) |
Pre-processes raw markdown string data immediately before parsing. | string or Promise<string> |
onAfterParse(html, frontmatter, filePath?) |
Post-processes generated HTML representing the markdown body. | string or Promise<string> |
onBeforeBuild(ctx) |
Called after all markdown is parsed but before HTML generation. Used for heavy pre-computation. | void or Promise<void> |
onBeforeRender(page) |
Called before template rendering. Mutations to frontmatter and html are reflected in output. |
void or Promise<void> |
onPageReady(page) |
Accesses fully assembled page metadata just before it is written to the destination file. | void or Promise<void> |
Engine Acceleration & Background Tasks (runWorkerTask)
docmd executes intensive operations via a Pluggable Engine Architecture. Plugins can easily offload custom heavy I/O or CPU-bound subroutines through the configured build engine (e.g., JavaScript or native Rust workers).
The runWorkerTask method is injected transparently into PageContext, PostBuildContext, and ActionContext.
{
"plugin": { "name": "my-plugin", "version": "1.0.0", "capabilities": ["post-build"] },
"onPostBuild": async (ctx) => {
// Pass a registered engine action name or absolute script path
const result = await ctx.runWorkerTask('/path/to/worker.js', 'parseData', [ctx.outputDir]);
}
}
Data Fetching and Indexing (onBeforeBuild)
The onBeforeBuild hook runs after markdown parsing but before the HTML rendering loop begins. It is optimal for heavy data indexing or API calls.
It receives the BeforeBuildContext, containing all pages and the tui instance. This allows plugins to show isolated progress bars.
export async function onBeforeBuild({ pages, tui }) {
tui.step('Fetching remote plugin data', 'WAIT');
let processed = 0;
for (const page of pages) {
if (page.sourcePath) {
page.frontmatter.remoteData = await fetchHeavyData(page.sourcePath);
}
processed++;
if (processed % 10 === 0 || processed === pages.length) {
tui.progress('Fetching remote plugin data', processed, pages.length);
}
}
tui.step('Fetching remote plugin data', 'DONE');
}
onBeforeRender and PageContext
Use onBeforeRender to inject build-time data derived from the source file.
interface PageContext {
sourcePath: string;
frontmatter: Record<string, any>;
html: string;
localeId?: string;
versionId?: string;
relativePathToRoot?: string;
runWorkerTask<T = any>(modulePath: string, functionName: string, args: any[]): Promise<T>;
}
export default {
plugin: {
name: "my-metadata-plugin",
version: "1.0.0",
capabilities: ["build"]
},
onBeforeRender: async (page) => {
const stats = fs.statSync(page.sourcePath);
page.frontmatter.wordCount = page.html.split(/\s+/).length;
page.frontmatter.fileSize = stats.size;
}
}
Deep Dive: Asset Injection
The getAssets() hook allows your plugin to bundle client-side logic securely.
export default {
getAssets: (options) => {
return [
{
url: "https://example.com/script.js",
type: "js",
location: "head"
},
{
src: path.join(__dirname, "plugin-init.js"),
dest: "assets/js/plugin-init.js",
type: "js",
location: "body"
}
];
}
}
Translating Plugins (i18n)
Plugins rendering client-side UI should expose strings via the translations(localeId) hook. The engine merges these with core strings automatically.
The standard pattern stores a JSON file for each language in an i18n/ directory:
import fs from "fs";
import path from "path";
export default {
plugin: {
name: "my-plugin",
version: "1.0.0",
capabilities: ["translations", "body"]
},
translations: (localeId) => {
try {
const p = path.join(__dirname, "i18n", `${localeId}.json`);
return JSON.parse(fs.readFileSync(p, "utf8"));
} catch { }
return {};
}
}
WebSocket RPC Actions
Plugins can register action handlers and event handlers that run on the dev server. They are callable from the browser via the window.docmd API.
export default {
plugin: {
name: "my-live-plugin",
version: "1.0.0",
capabilities: ["actions", "events"]
},
actions: {
"my-plugin:save-note": async (payload, ctx) => {
const content = await ctx.readFile(payload.file);
const updated = content + "\n\n> " + payload.note;
await ctx.writeFile(payload.file, updated);
return { "saved": true };
}
},
events: {
"my-plugin:page-viewed": (data, ctx) => {
console.log(`Page viewed: ${data.path}`);
}
}
};
The ctx (ActionContext) provides:
| Method | Description |
|---|---|
ctx.readFile(path) |
Read a file relative to the project root. |
ctx.writeFile(path, content) |
Write a file (triggers rebuild + reload). |
ctx.readFileLines(path) |
Read a file as an array of lines. |
ctx.broadcast(event, data) |
Push an event to all connected browsers. |
ctx.runWorkerTask(module, fn, args) |
Offload heavy CPU tasks to the worker pool. |
ctx.source |
Source editing tools for block-level markdown manipulation. |
ctx.projectRoot |
Absolute path to the project root. |
ctx.config |
Current docmd site configuration. |
All file operations are sandboxed to the project root.
The WebSocket RPC system is only active during npx @docmd/core dev. Production builds do not include the API client or server-side action handling.
Best Practices
- Declare Capabilities: Always export a
plugindescriptor with declared capabilities. - Use
onBeforeRenderfor data injection: If your plugin computes frontmatter fields, useonBeforeRender. - Async/Await: Always use
asyncfunctions foronPostBuild,onBeforeRender, and action handlers. - Statelessness: Avoid maintaining state within the plugin object. The engine may re-initialise plugins dynamically.
- Naming Convention: Prefix community package names with
docmd-plugin-. - Action Namespacing: Prefix action names with your plugin name (e.g.,
my-plugin:save-note). - Action Validation: Define and require an explicit payload schema in your actions.
- Logging: Use the provided
log()helper inonPostBuildto respect user verbosity settings.
The docmd plugin API is LLM-Optimal. Because the hooks use standard JavaScript objects, AI agents can generate bug-free plugins with minimal instruction.