Plugin System #

Extensions for a documentation tool usually begin with a small request.

Imagine adding local search. It first needs to read every Markdown file, divide content by heading, and generate an index. So far, all of the work happens during the build.

The website then needs a search button and result list. It must respond to the keyboard, follow the current language, and ask the Router to open the selected result. The feature now reaches into Vue and the browser as well.

To a site author, this is still one feature: search. In many static site generators, however, its implementation has already become several pieces that must be connected separately.

The KawaPress plugin system starts with this problem.

Pain Point: Configuration Scattered Across Multiple Places #

VitePress provides several useful extension points. You can adjust Markdown in configuration, add Vite Plugins, and use build Hooks. In the final website, a theme can use enhanceApp to install Vue plugins and components.

These entry points are useful and keep VitePress simple and flexible. But when search needs several of them, the same feature must be configured in several places, and the site author must remember how those pieces connect.

This is manageable when a feature only changes Markdown. Once it also generates data, registers components, imports styles, and handles browser interaction, the extension author must wire those pieces together. The site author may then need separate setup steps for configuration and the theme entry.

The missing piece is not another Hook. It is a way to tell the framework that all of this code belongs to one Plugin.

The KawaPress Plugin Contract #

Generating an index and displaying a search interface should happen in different places. KawaPress does not force them into one execution environment. It puts them in one plugin package:

Text
@kawapress/plugin-search
├─ Generator Plugin
│  └─ scans Markdown and generates the search index
└─ Runtime Plugin
   └─ registers search components, styles, and interaction

KawaPress calls the complete search feature a Plugin. That Plugin can contain two cooperating parts.

A Generator Plugin Participates in the Build #

A Generator Plugin works while KawaPress loads configuration, compiles Markdown, and builds the site. It can:

  • change site configuration;
  • extend Markdown syntax;
  • adjust data for each page;
  • install a Vite Plugin or change Vite configuration.

A custom container only changes generated HTML, so a Generator Plugin is enough. Local search generates its index here, and syntax highlighting turns source code into final markup here as well.

A Runtime Plugin Participates in the Page Runtime #

A Runtime Plugin works when KawaPress creates the Vue App and Vue Router. It can:

  • install regular Vue plugins;
  • register global components;
  • import runtime styles;
  • add navigation guards and other Router behavior.

Server rendering and the browser both load Runtime Plugins. Search interfaces, copy buttons, and theme layouts all need this part. Read SSR Compatibility before using browser APIs.

Publishing Conventions #

A Plugin published to npm exposes its Generator Plugin from the default entry. When it also has runtime behavior, it exposes a ./runtime-plugin entry. KawaPress reads the name of the Generator Plugin from the site configuration, then checks whether the package with that name exposes ./runtime-plugin. When it does, the runtime entry is loaded automatically.

A build-only Plugin does not expose ./runtime-plugin. A Plugin used only in the page runtime still needs a default entry whose Generator Plugin declares its stable identity; its setup() does not need to register any Hooks.

The site always imports the default entry only:

TypeScript
import searchPlugin from '@kawapress/plugin-search'

const plugins = [
  searchPlugin(),
]

Do not ask site authors to import @kawapress/plugin-search/runtime-plugin. Generation and runtime happen at different stages, but the feature still has one installation and one configuration.

KawaPress Plugins are best written in TypeScript and can publish .ts, .vue, and CSS source files directly. A separate bundling step is not required. The Vite used by KawaPress performs the final syntax transforms, dependency resolution, and browser build.

For the package.json layout, the code for each entry, and the publishing checklist, read Plugin Development.

Good Defaults Should Form a Complete Product #

Once search, syntax highlighting, custom containers, and themes can be installed independently, a site has more freedom. But if every site must select and connect each feature from scratch, a basic documentation site begins with a long configuration checklist.

A Preset solves this problem.

A Preset combines default configuration and an ordered set of Plugins into an experience that is ready to use:

Text
Preset
├─ default configuration
└─ an ordered set of Plugins
   ├─ theme
   ├─ Markdown features
   ├─ search
   └─ other defaults

nagi (凪) is the built-in KawaPress Preset. It prepares the documentation layout, syntax highlighting, custom containers, search, UnoCSS, and other features, so a site can begin with very little configuration:

TypeScript
import { nagi } from 'kawapress/nagi'

export default nagi({
  title: 'My Docs',
})

There is no privileged “official theme layer” here. A Runtime Plugin installs the nagi interface, and every feature it combines is a regular Plugin. Official code uses the same public extension points available to third parties.

Add Plugins to a Preset #

Using nagi does not prevent a site from adding independent features. This configuration keeps every nagi default and adds MathJax:

TypeScript
import mathjaxPlugin from '@kawapress/plugin-mathjax'
import { nagi } from 'kawapress/nagi'

export default nagi({
  plugins: [
    mathjaxPlugin(),
  ],
})

Preset Plugins are added first, followed by the entries in plugins. Array order is execution order. A later Plugin sees changes made earlier and can deliberately replace them.

KawaPress does not use numeric priorities or guess plugin dependencies behind the scenes. The order shown in configuration is the order in which features are combined.

A completely different interface can skip Presets and start with defineConfig():

TypeScript
import { defineConfig } from 'kawapress'
import themePlugin from 'my-kawapress-theme'
import markdownPlugin from 'my-markdown-plugin'

export default defineConfig({
  plugins: [
    themePlugin(),
    markdownPlugin(),
  ],
})

The custom theme Plugin must register the layout components KawaPress needs to render pages. Changing a Preset means changing the outer configuration function; there is no separate presets list to maintain.

Start Writing a Plugin #

This page explains why KawaPress organizes plugins this way. When you write one, read Plugin Development. It lists the six generator methods and two runtime methods by extension point, with code you can use as a reference.