Plugins

View source

Extend Folio with typed build-time plugins.

Folio plugins expose a typed lifecycle API integrated with Folio's production build command and development server.

For a step-by-step implementation tutorial, see Create a plugin.

Register a plugin

Create configured plugins with a factory, then register the returned plugin in docs.config.ts:

typescript
import { createFolioPlugin } from "@nikala-ui/folio";

const reportPlugin = createFolioPlugin(
  (options: { file: string }) => ({
    name: "build-report",
    generate(context) {
      context.logger.info(`Would write ${options.file} for ${context.pages.length} pages`);
    },
  }),
  { file: "build-report.json" },
);

export default { plugins: [reportPlugin] };

defineFolioPlugin(plugin) is available when a plugin does not need options. Both helpers validate the plugin registration and require a non-empty unique name in a DocsConfig.plugins array. Hook fields, when present, must be functions. Invalid registrations fail configuration loading with an actionable error.

Minimal metadata plugin

The smallest useful plugin can transform page metadata while preserving the page route:

typescript
import { defineFolioPlugin } from "@nikala-ui/folio";

const metadataPlugin = defineFolioPlugin({
  name: "metadata-example",
  pageTransformed(page) {
    return {
      ...page,
      description: page.description || `Documentation for ${page.title}`,
    };
  },
});

export default { plugins: [metadataPlugin] };

Keep plugin names stable and lowercase; use a short capability-oriented name, not a consumer site name. Published plugins should use the @nikala-ui/folio-plugin-* package naming convention. A plugin package should declare Folio as a peer dependency and document the exact compatible Folio range. Before Folio 1.0, use a compatible minor range such as ~0.15.0; after 1.0, caret ranges may be used for non-breaking releases.

Public contract

FolioPlugin currently exposes these optional hooks:

  • configResolved(context)
  • buildStart(context)
  • pagesGenerated(pages, context)
  • pageCollected(page, context)
  • pageTransformed(page, context)
  • generate(context)
  • buildEnd(result, context)

The context and result types are exported from @nikala-ui/folio. The production build invokes these hooks through the lifecycle manager. The generate context exposes the absolute production outputDir and the final immutable pages catalog; outputDir is omitted for sessions that do not produce production artifacts, such as development sessions.

Lifecycle order

An integrating build tool must invoke hooks in this order:

text
load config
  -> configResolved
  -> buildStart
  -> content scan
  -> pagesGenerated (catalog expansion)
  -> pageCollected (for each page)
  -> pageTransformed (for each page and plugin)
  -> pageActions (for each page and plugin)
  -> route tree, sidebar, and pagination
  -> MDX compilation
  -> artifact generation
  -> generate
  -> buildEnd

Hooks run sequentially in registration order. A hook failure stops the current operation and is reported as FolioPluginHookError, preserving the original error in cause and adding the plugin name, hook, mode, and page location when available.

The hook responsibilities and contracts are:

If a hook fails, later plugins and later hooks in the current operation are not called. The integrating tool owns the final process-level failure handling, but the lifecycle manager always wraps an unhandled hook failure and retains its original cause. buildEnd receives success: false when the integrating tool reports a failed build; it does not convert a failed build into a successful one.

Development and watch behavior

The development server creates a development-mode session during server startup. It runs configResolved, then buildStart, and runs the page hooks when the virtual route modules first load. A content add, edit, or removal creates a fresh lifecycle manager and rescans the content directory, so the new catalog replaces the previous one rather than accumulating pages or plugin state. Every development context has mode: "development" and does not have an outputDir.

Development reloads intentionally do not run generate or buildEnd: those hooks describe production artifact generation and final build completion. A configuration reload creates a new session from the newly loaded config and runs its configResolved and buildStart hooks. If a development hook fails, Folio keeps the server alive, logs the actionable FolioPluginHookError, and sends the error to the Vite client overlay; the failed reload does not replace the last successful catalog.

pageTransformed may update a page's metadata, including its title, description, order, badge, icon, table of contents, and frontmatter-derived values. It must preserve the page route identity (slug and url). The first pageTransformed contract does not allow a plugin to add routes or replace MDX source content. Use pagesGenerated for route catalog expansion. A transformation is applied before route tree, sidebar, and pagination generation so those structures consume the transformed page metadata. Source-content transformation is intentionally excluded from this contract and requires a future, separately specified API.

Use pagesGenerated when a plugin owns route generation. It runs after the content scan and before pageCollected, so returned pages participate in the sidebar, pagination, virtual routes, and artifact generation:

typescript
const routePlugin = defineFolioPlugin({
  name: "route-generator",
  pagesGenerated: (pages) => [
    ...pages,
    { ...pages[0], slug: "generated", url: "/generated" },
  ],
});

Generated pages must have unique slug/url identity and a real filePath. The hook does not invent source content or change MDX compilation semantics.

Hook inputs are immutable snapshots, so plugins cannot mutate Folio's configuration or collected page data. This includes nested objects, arrays, maps, sets, dates, regular expressions, and supported provider instances. The original config and page objects remain unchanged.

Use pageActions to add links to the current page's heading action area:

typescript
const clickMePlugin = defineFolioPlugin({
  name: "click-me",
  pageActions: () => [{ label: "Click me", href: "/getting-started" }],
});

Actions are serializable and render in both SSR HTML and the hydrated browser. Set external: true for a new-tab link. Arbitrary event handlers are not part of the build-time contract.

Plugin packages should keep feature-specific behavior (such as i18n, API documentation, or workspace discovery) outside the core package.

Public API and compatibility

The package root exports the plugin authoring surface: FolioPlugin, FolioPluginContext, FolioBuildResult, FolioPluginFactory, FolioPluginLogger, FolioPluginConfig, FolioPluginHookError, createFolioPlugin, defineFolioPlugin, and the validation helpers. The FolioPluginLifecycleManager and its options are exported for integrating build tools; normal plugin authors should use the factory helpers instead.

All hooks are optional. Existing hook names, argument order, route identity rules, immutable snapshot guarantee, and error metadata are maintained within the compatible Folio range. New optional hooks and fields are additive. A breaking hook contract or removal requires a major release (or, before 1.0, the next incompatible minor release) and a migration note.

context.logger.debug, info, warn, and error are host-owned logging methods. Use them for diagnostic, progress, warning, and failure-related messages respectively; do not assume a particular console or file sink. FolioPluginHookError exposes pluginName, hook, mode, optional page location metadata, and the original exception in cause. Use that metadata for actionable reporting without replacing the original error.