vitepress-plugin-single-page
A build-time Vite plugin: for every documentation version it generates single.md, all chapters of a version glued into a single pre-rendered page. Why:
- Ctrl+F across the whole document, instant, no N client requests.
- Pagefind (or another static search) indexes a version as one document.
- Opens as a plain static HTML page: no iframe, no JS waterfall.
Installation
bash
npm install -D @ampernic/vitepress-plugin-single-pageUsage
ts
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
import { SinglePagePlugin } from '@ampernic/vitepress-plugin-single-page'
export default defineConfig({
vite: {
plugins: [
SinglePagePlugin({ distroName: 'alt-server' }),
],
},
})Every version directory under docs/<locale>/ gets a single.md that VitePress compiles into the /<version>/single route.
Options
| Option | Type | Default | Description |
|---|---|---|---|
srcDir | string | 'docs' | VitePress source directory |
locale | string | 'ru' | Locale directory inside srcDir |
distroName | string | - | Slug; required to find .vitepress/sidebars/<distro>/<v>.ts |
versionPattern | RegExp | /^(\d+\.\d+(\.\d+)*|p\d+)$/ | Which directories to treat as versions |
pageName | string | 'single' | Name of the generated file |
titleTemplate | string | '{distro} {version} on a single page' | Title template ({distro} and {version} are substituted). When the book root has its own title, '{book} on a single page' is used |
frontmatter | object | - | Frontmatter added to every page (merged on top of the defaults outline/prev/next/breadcrumbs: false) |
dropInitialTOC | boolean | false | Cut the leading ## Contents (the heading and its body) from the first chapter: the single page IS the document, and a TOC at the top duplicates the content |
initialTOCHeadings | string[] | ['Table of Contents', 'Contents'] | Texts that dropInitialTOC uses to find the heading to cut (case-insensitive) |
injectTOC | boolean | false | Inserts the <ADBookTOC /> component at the start of the merged page. Lets a static TOC live on the multi-page route separate from the single-page one |
tocComponent | string | 'ADBookTOC' | Component name for injectTOC |
sidebarOrder | (srcDir, version, options) => string[] | built-in | Custom chapter ordering function |
debug | boolean | false | Verbose log: discovered versions, glued chapters, title source. When using the theme, toggled via createSharedConfig({ debug: true }) |
How it works
- On
configResolved(before VitePress scans the tree) the plugin walks the version directories. - For every version it reads the chapter order from
.vitepress/sidebars/.../<v>.ts(or fromsidebarOrder). - It glues
index.mdof the version and all chapters, rewriting relative asset paths so they resolve fromsingle.md(Vite deduplicates assets by content hash). - It writes
single.mdinto the tree before the VitePress scanner sees it, so the page shows up as a regular route.
Every chapter is assigned a stable anchor sp-<slug> (see singleAnchorId) that the client-side sidebar rebind in the theme points to.
Known limits / edge cases
- Unicode in paths: chapter slugs are normalized through
[^\p{L}\p{N}_-]+(not\w, which in JS only covers Latin). Without that a Cyrillic path such asinstall-packages-advanced/введение/would collapse into the same prefix as its parent pageinstall-packages-advanced/, and{#part-id}anchors would clash between pages. - Anchor dedup: explicit
{#id}and in-page](#id)links are prefixed with the chapter slug so VitePress does not fail on "duplicate user-defined id". dropInitialTOC: cuts the leading## Contents(the heading and everything under it to the end of the document) when the book root already contains a text TOC. It is independent ofinjectTOC, which places<ADBookTOC>at the top of the merged page.