Plugin API
Extend Fibel with services, lifecycle hooks, routes, validation, and project-specific integrations.
Plugins extend Fibel at defined points: before content is loaded, before pages are rendered, after pages are rendered, and when additional routes are registered. This lets projects add rules, endpoints, and alternative implementations without changing the core.
Plugin shape
import type { FibelPlugin } from "@k2b/fibel";
export function projectPlugin(): FibelPlugin {
return {
name: "project-plugin",
setup(context) {},
transformContent(context) {},
afterContent(context) {},
routes(context) {
return [];
},
};
}setup runs before Markdown files are loaded. Use it to replace or wrap services.
transformContent runs after all Markdown and custom pages have been loaded but before their Markdown and navigation are rendered. Use it for small metadata transformations that rendering, search, and later plugins must observe.
afterContent runs after pages have been loaded and rendered. Use it for validation, indexes, or derived metadata.
routes adds Fetch routes. Use it for generated files, internal endpoints, or project-specific APIs.
Wrap a service
This example adds a notice before every Markdown document and then calls the existing Markdown renderer.
import type { FibelPlugin } from "@k2b/fibel";
export function markdownNoticePlugin(): FibelPlugin {
return {
name: "markdown-notice",
setup(context) {
const renderMarkdown = context.services.renderMarkdown;
context.services.renderMarkdown = (markdown, page, ctx) => {
const notice = `> This page belongs to ${ctx.config.title}.\n\n`;
return renderMarkdown(`${notice}${markdown}`, page, ctx);
};
},
};
}Use this pattern for shared notices, project banners, or small content adjustments before rendering.
Validate content
Validation plugins fail startup when content does not match project rules.
import type { FibelPlugin } from "@k2b/fibel";
export function requireTagsPlugin(): FibelPlugin {
return {
name: "require-tags",
afterContent(context) {
for (const page of context.pages) {
if (!page.meta.hidden && page.meta.tags.length === 0) {
throw new Error(`${page.sourcePath} has no tags.`);
}
}
},
};
}Useful checks include missing descriptions, invalid tags, incomplete translations, or pages that should not be published.
Add a route
Routes receive the current request and the Fibel context. This example serves a small JSON manifest of all pages.
import type { FibelPlugin } from "@k2b/fibel";
export function docsManifestPlugin(): FibelPlugin {
return {
name: "docs-manifest",
routes(context) {
return [
{
path: "/manifest.json",
handler() {
return Response.json({
title: context.config.title,
pages: context.pages.map((page) => ({
title: page.meta.title,
href: page.href,
locale: page.locale.code,
})),
});
},
},
];
},
};
}A path ending in /* matches by prefix instead of exactly. The assets plugin uses this to serve a whole directory. Set scope: "internal" to expose a route only below routing.internalPath, or scope: "public" to expose it only at the public path. scope: "origin" matches the request pathname before routing.basePath is removed and is reserved for domain-level protocols such as Agent Skills discovery. Routes without a scope keep the existing behavior and match public and internal paths, but never origin paths.
An origin-scoped route works automatically when Fibel owns the complete Fetch
handler. A host that mounts Fibel below a subrouter must separately forward the
origin path to fibel.fetch.
Choose custom route paths so they do not collide with internal routes or page paths.
Add head tags
context.headTags collects functions that render markup into the <head> of every page. Each function receives the current page and the context, so tags can differ per page and per locale.
import type { FibelPlugin } from "@k2b/fibel";
export function analyticsPlugin(siteId: string): FibelPlugin {
return {
name: "analytics",
setup(context) {
context.headTags.push(() => `<script defer data-site="${siteId}" src="https://example.com/a.js"></script>`);
},
};
}Return an empty string to skip a page. The built-in SEO plugin uses this hook for language alternates, social cards, and structured data.
Add body items
context.bodyItems works like headTags, but renders markup after the page shell and before Fibel's client script. Use it for opt-in overlays or launchers that need the current page and locale. Return an empty string to skip a page.
Compose with defaults
Most projects append their plugins to the default set.
import { defineFibel, defaultPlugins } from "@k2b/fibel";
import { docsManifestPlugin } from "./plugins/docs-manifest";
import { requireTagsPlugin } from "./plugins/require-tags";
export default defineFibel({
title: "Product Docs",
plugins: [
...defaultPlugins(),
requireTagsPlugin(),
docsManifestPlugin(),
],
});Order matters. Plugins that replace services should run before plugins that use those services. Validation and additional routes can usually run after the defaults.
Context reference
type FibelContext = {
config: ResolvedFibelConfig;
pages: FibelPage[];
nav: Map<string, NavSection[]>;
footerItems: string[];
headTags: HeadTag[];
bodyItems: BodyItem[];
searchIndex: SearchEntry[];
routes: FibelRoute[];
services: FibelServices;
};The context is the shared workspace for plugins. It contains configuration, loaded pages, navigation, footer and body items, head tags, search data, registered routes, and replaceable services.
Every type shown on this page is exported from @k2b/fibel as a type import.
Service reference
Four services can be replaced or wrapped in setup. Replace one to change behavior; wrap one to extend it.
type FibelServices = {
renderMarkdown: (markdown: string, page: FibelPage, context: FibelContext) => string;
renderPage: (page: FibelPage, request: Request, context: FibelContext) => string;
getTheme: (request: Request, context: FibelContext) => ThemeMode;
search: (query: string, locale: string, context: FibelContext) => SearchEntry[];
};renderMarkdown turns page bodies into HTML and runs once per page at startup. renderPage produces the full HTML document for a request. getTheme resolves the theme before rendering. search answers queries from the search endpoint.
The default implementations live in the Markdown, layout, theme, and search plugins. A plugin list without one of them leaves that service at its no-op default.