Skip to content

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-page

Usage

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

OptionTypeDefaultDescription
srcDirstring'docs'VitePress source directory
localestring'ru'Locale directory inside srcDir
distroNamestring-Slug; required to find .vitepress/sidebars/<distro>/<v>.ts
versionPatternRegExp/^(\d+\.\d+(\.\d+)*|p\d+)$/Which directories to treat as versions
pageNamestring'single'Name of the generated file
titleTemplatestring'{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
frontmatterobject-Frontmatter added to every page (merged on top of the defaults outline/prev/next/breadcrumbs: false)
dropInitialTOCbooleanfalseCut 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
initialTOCHeadingsstring[]['Table of Contents', 'Contents']Texts that dropInitialTOC uses to find the heading to cut (case-insensitive)
injectTOCbooleanfalseInserts 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
tocComponentstring'ADBookTOC'Component name for injectTOC
sidebarOrder(srcDir, version, options) => string[]built-inCustom chapter ordering function
debugbooleanfalseVerbose log: discovered versions, glued chapters, title source. When using the theme, toggled via createSharedConfig({ debug: true })

How it works

  1. On configResolved (before VitePress scans the tree) the plugin walks the version directories.
  2. For every version it reads the chapter order from .vitepress/sidebars/.../<v>.ts (or from sidebarOrder).
  3. It glues index.md of the version and all chapters, rewriting relative asset paths so they resolve from single.md (Vite deduplicates assets by content hash).
  4. It writes single.md into 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 as install-packages-advanced/введение/ would collapse into the same prefix as its parent page install-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 of injectTOC, which places <ADBookTOC> at the top of the merged page.