Boltdocs 3.2.0 — Warm Builds 5x Faster, WASM Syntax Highlighting, and Pipeline Timing

3.2.0 makes warm builds 5x faster by fixing the server build skip, adds WASM-powered syntax highlighting, parallelizes critical CSS processing, and shows you exactly where build time goes.
This one's about making cache actually work
3.1.0 shipped the foundation — turbo mode, Sätteri, zig-critters. 3.2.0 is about what happens when you build again. And again. And again.
Warm builds (no code changes) dropped from ~50s to ~10s — a 5x speedup. The server Vite build is now properly skipped when nothing changes, the client hash uses a lightweight pre-check, and critical CSS processing runs in parallel across CPU cores.
The cache problem
Before 3.2.0, every build ran the full server Vite build from scratch — even when nothing changed. The root cause was a single line at the end of every build:
await fs.remove(join(root, '.vite-react-ssg-temp'))
This deleted the entire SSR build output directory. The next build's serverBuildSkipped check required that directory to exist — but it was always gone. The result: the most expensive step in the pipeline (SSR Vite bundling) ran on every build, making the cache effectively useless.
The fix: Don't delete the SSR temp directory when the client build was bypassed. If the client code hash hasn't changed, the SSR bundle hasn't changed either — no need to rebuild it.
Benchmark results
| Build type | Before 3.2.0 | After 3.2.0 | Speedup |
|---|---|---|---|
| Cold (first build) | ~82s | ~78s | ~1.05x |
| Warm (no changes) | ~50s | ~10s | 5x faster |
| Incremental (1 file edit) | ~50s | ~15s | 3.3x faster |
The cold build barely changes because the server Vite build must run. But warm and incremental builds now skip it entirely — the server bundle is reused from the previous build.
Mtime cache: 5.9x faster stat calls
Every FileCache.get() called fs.statSync() to check if a file had changed — even when the same file was checked milliseconds apart. For 500 files across multiple cache layers, this meant thousands of blocking syscalls per build.
The new in-memory mtime cache stores { mtime, ts } per file with a 2-second TTL. Within the TTL window, the stat call is skipped entirely:
const MTIME_TTL_MS = 2000
const mtimeCache = new Map<string, { mtime: number; ts: number }>()
export function getFileMtime(filePath: string): number {
const now = Date.now()
const cached = mtimeCache.get(filePath)
if (cached && now - cached.ts < MTIME_TTL_MS) return cached.mtime
const mtime = fs.statSync(filePath).mtimeMs
mtimeCache.set(filePath, { mtime, ts: now })
return mtime
}
| Metric | Before | After | Speedup |
|---|---|---|---|
| 5 files × 500 rounds | 18.6ms | 3.2ms | 5.9x |
Client hash: single stat per file
computeClientCodeHash() was calling fs.statSync() up to 3 times per file — once for the pre-check Math.max(), once for the hash computation, and once more for the meta persistence. The new implementation does a single pass, collecting all stats upfront and reusing them for every purpose:
// Single pass: stat each file once
const fileStats = files.map(file => {
const stat = fs.statSync(file)
return { file, mtime: stat.mtimeMs, size: stat.size }
})
// Pre-check uses fileStats — no extra stat calls
const lastMtime = Math.max(...fileStats.map(s => s.mtime))
// Hash uses the same fileStats
for (const { file, mtime, size } of fileStats) {
hasher.update(relative(root, file)).update(mtime.toString()).update(size.toString())
}
For a project with 500 files, this reduces stat calls from ~1500 to 500 — a 66% reduction.
MDX cache: path+mtime for dev mode
The MDX cache key previously included a content hash (crypto.createHash('md5').update(code)), meaning the cache was invalidated on every keystroke. In dev mode, the cache now uses file path + mtime instead:
// Before: invalidated on every content change
const cacheKey = `${cleanId}:${contentHash}:${isProd}:${MDX_PLUGIN_VERSION}`
// After: survives across dev restarts when files haven't changed
const cacheKey = isDev
? `${cleanId}:${getFileMtime(cleanId)}:${isProd}:${MDX_PLUGIN_VERSION}`
: `${cleanId}:${contentHash}:${isProd}:${MDX_PLUGIN_VERSION}`
This means the MDX transform cache is reused when you restart the dev server without changing any files — no more recompiling 176 MDX files from scratch.
Bounded concurrency for route parsing
Route parsing used Promise.all(files.map(...)) with unbounded concurrency — launching all file parsers simultaneously. For projects with hundreds of files, this caused memory pressure and I/O contention.
Now capped at 32 concurrent workers:
parsed = await runWithConcurrency(files, 32, async (file) => {
const cached = docCache.get(file)
if (cached) return cached
const result = await parseDocFile(file, docsDir, finalBasePath, config)
docCache.set(file, result)
return result
})
The worker pool pattern pulls items from a shared queue, keeping exactly 32 parsers running at any time — enough to saturate I/O without overwhelming memory.
HMR: O(1) module graph lookup
When a file changed, the HMR handler searched Vite's fileToModulesMap with a brute-force O(N) scan — iterating every entry and comparing decoded lowercase keys. For large projects with thousands of modules, this added latency to every content edit.
The fix builds a pre-computed lowercase index on first use:
let lowerModuleIndex: Map<string, any> | null = null
function getLowerModuleIndex(): Map<string, any> {
if (lowerModuleIndex) return lowerModuleIndex
lowerModuleIndex = new Map()
for (const [key, value] of server.moduleGraph.fileToModulesMap.entries()) {
lowerModuleIndex.set(decodeURIComponent(key).toLowerCase(), value)
}
return lowerModuleIndex
}
// On file change:
mods = getLowerModuleIndex().get(normalized.toLowerCase()) || null
The index is invalidated when the module graph changes (onFileChange), keeping lookups O(1) while staying consistent.
docCache: no redundant disk reads
docCache.load() was called on every generateRoutes() invocation — reading the entire cache from disk (potentially gzipped JSON) even when it was already loaded in memory. Now a loaded flag prevents re-reading:
async load(): Promise<void> {
if (this.loaded) return // Skip if already in memory
// ... read from disk ...
this.loaded = true
}
invalidateAll(): void {
this.entries.clear()
this.loaded = false // Reset on invalidation
}
Prewarming with route priority
Previously, all routes were prewarmed in arbitrary order. Pages like /docs/ and /docs/getting-started are visited first by most users, but they weren't prioritized.
Now routes are sorted by priority before batching:
const PRIORITY_PATTERNS = [/\/index\./i, /\/getting-started/i, /\/intro/i, /\/readme/i]
const files = routes
.filter(r => r.filePath)
.map(r => r.filePath)
.sort((a, b) => getRoutePriority(a) - getRoutePriority(b))
The delay was also increased from 0ms to 150ms, giving the first page request a head start before prewarming consumes CPU.
Shiki: highlight at build time, zero client cost
Syntax highlighting never ships Shiki to the browser. The highlighter runs only during MDX transform (dev and production build). Tokens are baked into the page as real HTML elements — the client just paints spans.
Here's the architecture:
- At build time, the rehype plugin walks every
```langfence (except Mermaid), resolves light/dark themes fromtheme.codeTheme, and runs Shiki'scodeToHast. - The highlighted HAST tree is injected into the document tree — children and properties of the
<pre>are replaced in place. There is no giantdata-highlighted-htmlstring attribute and no client-side re-tokenization. - Dual themes (
light+dark) are encoded on each token via CSS variables, so theme switches are pure CSS — no re-highlight, no second copy of the highlighter. - The client
CodeBlockcomponent wraps that pre-rendered markup (copy button, expand, feedback). It never importsshiki, Oniguruma, or language grammars.
Shiki stays on the server. Grammars, themes, and the Oniguruma WASM engine load only in Node during transform. The browser receives static HTML with token colors already applied — same idea as Mermaid pre-render, for every code block.
Before vs. after (client payload)
| Concern | Client-side highlighter | Boltdocs (build-time Shiki) |
|---|---|---|
| Shiki + WASM + grammars in the bundle | Hundreds of KB–MB | 0 KB — never imported on the client |
| First paint of a code block | After JS loads and tokenizes | Instant — spans ship with the page |
| Light/dark switch | Re-highlight or ship two full trees | CSS variables on each token |
Runtime createHighlighter / codeToHtml | Per page or lazy chunk | 0 — only runs in the MDX pipeline |
Trade-off: the HTML grows (one styled span per token), but the JS bundle does not include Shiki. That is the intentional swap for docs sites where code blocks are static content.
WASM Oniguruma engine
Build-time highlighting also got faster: the engine is now WASM Oniguruma instead of the JavaScript regex engine:
// Before
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
engine: createJavaScriptRegexEngine()
// After
import { createOnigurumaEngine } from '@shikijs/engine-oniguruma'
engine: createOnigurumaEngine(import('shiki/wasm'))
| Metric | JS Regex | WASM Oniguruma | Difference |
|---|---|---|---|
| 3 langs × 50 rounds | 1101ms | 959ms | 13% faster |
| Per iteration | 22.0ms | 17.7ms | 4.3ms saved |
Critical CSS concurrency
The Beasties critical CSS processor was running at concurrency: 1 — processing 174 pages one at a time. Now it runs at concurrency: min(cpus, 4), processing up to 4 pages in parallel:
// Before
const crittersQueue = new PQueue({ concurrency: 1 })
// After
const crittersQueue = new PQueue({ concurrency: Math.min(os.cpus().length, 4) })
Pipeline parallelism
Two independent pipeline steps — SEO validation and type generation — now run in parallel:
// Before
.addStep(new SEOValidateStep())
.addStep(new TypeGenerateStep())
// After
.addParallelSteps([new SEOValidateStep(), new TypeGenerateStep()])
Pipeline timing logs
The build now reports per-step timing:
[pipeline] Build steps:
ConfigResolve 52ms
RouteGenerate 11ms
SEOValidate 7ms
TypeGenerate 7ms
SSGBuild 5.6s
SEOWrite 4ms
Total 6.7s
Dev mode: no gzip
TransformCache was gzipping every cache shard on write — even in dev mode. Now compression is skipped when NODE_ENV !== 'production'.
A Mermaid bundle that doesn't ship to the browser
If a page has diagrams, you expect to wait. Mermaid.js is ~800 KB of d3, DOMPurify, layout engines, and per-diagram parsers — all of it code-split on demand. On pages without a diagram, 2.8.0 already saved ~27 KB with a dynamic import. That helped, but it still left most of the cost on pages that actually used Mermaid.
3.2.0 eliminates it entirely for production builds.
Here's the architecture:
- At build time, the Remark plugin walks every
```mermaidblock and dispatches it to a persistent Node.js worker (render-worker.mjs) that loads the headless Mermaid engine (via Playwright + jsdom + DOMPurify) and renders both the light and dark theme variants to SVG. - Those two SVGs are attached to the
<Mermaid />component assvgLightandsvgDarkprops — they ride along in the SSG output as plain string props, no extra fetch at runtime. - The plugin then registers a Vite alias that swaps
@bdocs/plugin-mermaid/clientto@bdocs/plugin-mermaid/client/staticwhenevervite buildruns. The static component (mermaid-static.tsx, served via therender-workerpipeline) never referencesmermaid— it justdangerouslySetInnerHTMLs the pre-rendered SVG. - In dev, the alias is skipped so HMR keeps the live client renderer. Or opt out explicitly with
mermaidPlugin({ preRender: false }).
~800 KB of code-split Mermaid chunks — architectureDiagram, sequenceDiagram, d3, DOMPurify, layout engines — leave the client bundle entirely. Pages render the diagram as inline SVG: zero JS round-trip, no FOUC, no flash of fallback.
Before vs. after
| Page type | Before 3.2.0 | After 3.2.0 |
|---|---|---|
| Page without diagrams | ~0 KB mermaid runtime (dynamic import in useEffect) | ~0 KB — the static component never references mermaid |
| Page with diagrams | ~800 KB of mermaid chunks loaded on demand, then mermaid.initialize + render | ~0 KB — SVG is already in the initial HTML |
| First diagram paint | After client bundle + engine initialize (~depends on connection) | Instant — SVG ships with the page |
| Theme switch | Re-runs mermaid.initialize and re-renders on every change | Instant — swaps svgLight ↔ svgDark |
Client-side mermaid.initialize() calls per page | 1 (one per mount) | 0 — light/dark SVGs are serialized into HTML |
Trade-offs
This is opt-out, not opt-in via the preRender: false flag. Three things to know:
- Build time: every
```mermaidblock renders through the Playwright-driven worker once. For most projects it adds a few seconds; for projects with thousands of diagrams it can cost more (run your own benchmark). The worker is persistent and processes diagrams sequentially so the Playwright context stays warm. - Pre-render failures: if Mermaid throws on a diagram (e.g. syntax error), the static component falls back to displaying the raw chart source. You'll see a small
warnat build time — no client-side crash, no missing diagram. - Truly dynamic diagrams: this path is for static SVGs. If you need runtime diagrams (e.g. user-provided input), set
mermaidPlugin({ preRender: false })and the legacy client renderer stays in place. The alias backs off automatically.
Mermaid fullscreen animations
While we were in there, the fullscreen overlay picked up entrance/exit animations — a small detail that makes the feel match the rest of the site.
Other improvements
- Pipeline
addParallelSteps()— new API for running independent pipeline steps concurrently, with automatic rollback on failure - Server build skip preserved across builds — SSR output now lives under
.boltdocs/build/ssr/and is no longer deleted when client code hasn't changed - Hash meta persistence —
hash-meta.jsonfor fast cache validation without full directory scans - External page scroll fix — removed
overflow: hiddenfromhtml, body - Invalid Tailwind classes fixed —
from-bg-main→from-main,-z-1→-z-[1]
Package weight reduction
3.2.0 passes the heaviest tools in the framework through the same triage as the build pipeline: bundle what runs every page, defer what runs only sometimes, hoist to peers what the consumer already needs anyway. The result is a noticeably smaller install, a code-split client bundle, and a leaner node_modules for sites that don't use every feature.
Why this matters
The 3.1.x install carried react-aria-components as a hard dependency alongside the entire Mermaid pre-render toolchain, language icons, and the image optimizer native binaries — even for users who never rendered a Mermaid diagram, never had a code block, and never felt a need for sharp/svgo. 3.2.0 separates the what from the when.
Before vs. after
Two categories of "size" — client bundle (bytes shipped to every browser) and node_modules footprint (bytes unpacked on disk per consumer install) — measured separately so you can read the table top-to-bottom without confusion.
Client bundle — bytes shipped to browsers (per cold-load)
| Surface | Before 3.2.0 | After 3.2.0 | Notes |
|---|---|---|---|
client/index.js (front door) | 108 KB | 108 KB | Unchanged at the entry; savings are in deferred chunks. |
icons-dev.tsx language icon blob in the initial chunk | 44 KB on every page | 0 KB on all pages | The entire lang-icons.tsx module (twelve SVG icons + registry map) was removed from the core. Code block titles render the filename text only — no language icon is shipped to the client. Pages with code blocks save ~17 KB versus 3.1.x's eager bundle; pages without code blocks were already at zero bytes for these icons. |
| Social/nav icons in the navbar bundle | bundled with everything | ~1 KB isolated in icons-prod.tsx | Github, Discord, XSocial, Bluesky are namespaced separately so layouts that don't need them can override without dragging the language set along. |
node_modules footprint — bytes unpacked on disk (per pnpm install)
| Surface | Before 3.2.0 | After 3.2.0 | Notes |
|---|---|---|---|
react-aria-components (unpacked) | ~1 MB transitively in node_modules | 0 KB in core | Now a required peer — consumers install it once alongside React. No double-bundling. |
sharp + svgo native binaries | ~35 MB unpacked for every site install | 0 KB in core; install only if you use @bdocs/plugin-image-optimizer | Moved out of core into the image plugin's peerDependenciesMeta.optional. Alpine ARM, musl libc, and old glibc users no longer hit hard sharp postinstall failures during boltdocs install. |
shiki + @shikijs/engine-oniguruma + @mdx-js/rollup | Same as 3.1.x | Same as 3.1.x | Stayed in dependencies — the CLI unconditionally imports them when npx boltdocs build runs. They never reach the client bundle. |
optionalDependencies section | present (empty) | removed entirely | The "silent-install trap" of optionalDependencies is gone. |
| Question: "does my site install 35 MB of native binaries to render docs?" | Yes | No — only if you opt into the image optimizer | Most sites just say "no thanks" and skip the native cost. |
What changed (semver-wise: minor)
This release sits comfortably in the documented semver-minor bin because the public API contracts remain strict. Here's the audit:
-
react-aria-components— promoted fromdependenciesto a required peer. If you already use Boltdocs, install it once alongside React. If you don't, the nextpnpm installwill surface a peer-warning that you can ignore if your framework wrapper provides React Aria, or satisfy withpnpm add react-aria-components. The peer is not markedoptional, so a missing peer is a hard install-time warning — but the runtime was already importing it before, so the binary behavior is unchanged. -
sharpandsvgo— they moved out of core entirely. They used to be transitively installed through boltdocs; now they're a peer of@bdocs/plugin-image-optimizerwithpeerDependenciesMeta.optional: true. If your site doesn't use the image optimizer, you save ~35 MB of unpacked native binaries. If you do use it, you get the same binaries — pnpm hoists them via the plugin's own peer declarations. -
Lang icons in MDX code blocks — twelve language icons (TypeScript, JavaScript, React, JSON, CSS, HTML, Markdown, Shell, YAML, Rust, TOML, CSV) were eagerly bundled in 3.1.x, then lazy-loaded in
lang-icons.tsx. The whole module has now been removed from the core. Code block titles render the filename text with a genericFileicon — no per-language icon shipped to the client at all. Pages with code blocks save ~17 KB versus 3.1.x; pages without code blocks were already at zero bytes. -
Social/nav icons —
Github,Discord,XSocial,Blueskymoved into a newicons-prod.tsxfile that's eagerly bundled with the navbar. They're separately typed (IconProps) and exported from their own entry so end-user custom layouts can override them without touching the language icon set. -
Shape test — a new
packages/core/tests/package-shape.test.tspins the dependency contract: it assertsreact-aria-componentsis a hard peer,shiki/@shikijs/engine-oniguruma/@mdx-js/rollupremain independencies,sharp/svgoare not in core, andpeerDependenciesMetais absent entirely. This means any future PR that re-bloats the surface area fails CI before review. -
Client subpath exports preserved —
'boltdocs/client','boltdocs/primitives','boltdocs/mdx','boltdocs/server'remain split so apps can selectively import what they need without over-bundling.
Migration in one paste
// package.json
{
"dependencies": {
// ...existing...
"boltdocs": "^3.2.0",
// ADD this one line (or accept the peer warning):
"react-aria-components": "^1.16.0" // was transitive; now needed explicitly
}
}
No code changes. No config changes. Plugin/image-optimizer peer handling is unchanged if you already had it installed.
CI / lockfile-strict setups
Teams that hard-fail builds on peer warnings (Husky pre-commit, Renovate policies, monorepos with pnpm install --frozen-lockfile --strict-peer-dependencies) need a single line in .npmrc to silence only this peer warning without hiding real breakage:
Pick one, not both. Use either .npmrc OR .pnpmrc, not both. Picking both can double-hoist or hit a pnpm parsing quirk (public-hoist-pattern[] is npm-style array syntax; pnpm reads .pnpmrc as bracket-less per-line). Most teams only need one of the two.
# .npmrc
# Allow the documented boltdocs@3.2 → react-aria-components peer advisory only.
# Do NOT blanket-disable peer checks with legacy-peer-deps.
public-hoist-pattern[]=*react-aria-components*
Or, equivalently in .pnpmrc (pnpm-native syntax — no brackets):
# .pnpmrc
# Make react-aria-components an explicit, transparent peer in your lockfile.
# `.pnpmrc` does NOT use the `[]` array syntax — pnpm-native is comma-less per-line.
public-hoist-pattern=*react-aria-components*
peerDependencyRules.allowedVersions.react-aria-components=^1.16.0
Don't use legacy-peer-deps=true. That silences every peer warning across the tree and will mask future real breakages.
Read the full audit in Upgrading to 3.2.0 → Semi-breaking changes.
New @bdocs/unist-utils Package
Boltdocs 3.2.0 extracts the shared AST utilities into a standalone npm package:
@bdocs/unist-utils. Before this release the same helpers (visitNodes,
createMdxElement, setNodeProperty, parseMetaString, etc.) lived in two
places inside the monorepo with subtly different typings; now they have a single
source of truth.
pnpm add @bdocs/unist-utils
Why this matters for plugin authors:
- 100% typed surface. No
any, nounknownat the boundaries.Node,Parent,ElementNode,MdxJsxElementare all declared in the package so plugin authors can write fully typed code. - Decoupled from boltdocs internals. External plugins no longer need to reach into Boltdocs' internal barrel or write their own AST helpers with looser types.
- Re-exported by
boltdocsfor back-compat. Old code importing from'boltdocs'keeps working. New projects should preferimport ... from '@bdocs/unist-utils'.
The package includes: visitors (visitNodes, visitRehypeElements, visitMdxElements), builders (createMdxAttribute, createMdxElement, createRehypeElement), h-properties helpers, class-list mutation (addNodeClass, removeNodeClass, hasNodeClass), meta string parsing (parseMetaString), and type guards.
See the full @bdocs/unist-utils docs for API reference, migration guide, and examples.
Enriched Plugin API
Every plugin lifecycle hook now receives a richer PluginContext with seven
enriched namespaces. The slot subsystem (ctx.slots / BoltdocsPlugin.slots /
virtual:boltdocs-layout-slots / slots-prefixed diagnostics) has been
removed entirely. The remaining namespaces — caches, diagnostics, paths,
virtual modules, middleware, server, hmr — work exactly as before:
ctx.caches — PluginCachesAPI
Three cache types, each bound to a namespaced scope so plugins never collide:
// Transform cache (sharded, hash-keyed, async)
const cache = ctx.caches.transform('my-plugin')
await cache.get('key') // → string | null
cache.set('key', value)
// Routes cache (read/write/invalidate by file path)
const route = ctx.caches.routes.get('/abs/path/page.mdx')
ctx.caches.routes.invalidate('/abs/path/page.mdx')
// In-memory FIFO cache (Map-backed, no external deps)
const mem = ctx.caches.memory<MyType>('my-plugin', { max: 50, ttl: 60_000 })
mem.set('key', value)
mem.get('key')
ctx.diagnostics — PluginDiagnosticsAPI
Structured diagnostics channel instead of logger spam. Records accumulate in a FIFO-capped queue (256 entries) that downstream tools can drain:
ctx.diagnostics.report(
'warn',
'MY_PLUGIN_SLOW',
'Transformation took 3.2s',
{ filePath: '/abs/path/page.mdx' },
)
const allRecords = ctx.diagnostics.list()
ctx.diagnostics.clear()
ctx.paths — PluginPathsAPI
Safe path resolution with anti-traversal validation. Every result is guaranteed
to stay inside the workspace boundary, rejecting .. escapes and absolute path
injection:
const mdxFile = ctx.paths.resolveDocs('guides', 'start.mdx')
const asset = ctx.paths.resolveAsset('public', 'logo.webp')
const url = ctx.paths.safeFileURL('/abs/path/diagram.svg')
// Throws — path escapes the workspace
ctx.paths.resolveDocs('../../etc/passwd')
ctx.virtualModules — PluginVirtualModulesAPI
Declare virtual modules that Vite resolves and loads without touching the file
system. Registrations happen inside beforeBuild / beforeDev and are flushed
on config change:
ctx.virtualModules.add(
'virtual:my-plugin/theme.css',
() => `:root { --primary: #6366f1; }`,
)
Registration rules: duplicate ids throw, virtual:boltdocs-* prefix is
reserved, and the eager flag exists for future auto-injection.
See the Plugin API Reference for the complete reference with all method signatures and a worked example. The API is now split into dedicated pages — Caches, Diagnostics, Paths, Virtual Modules, Middleware, Server, and HMR.
Transform Middleware API
Plugins can now register standalone transform middleware — smaller, focused functions that transform source, MDX, or HTML content in a pipeline.
Unlike lifecycle hooks, middleware can be declared statically via
BoltdocsPlugin.middleware or registered programmatically from any hook
via ctx.middleware.add(). Each middleware supports enforce ordering
and signal-based chain control (__signal: 'skip' / __signal: 'break'):
const plugin = {
name: 'my-plugin',
middleware: [{
name: 'inject-copyright',
transformHtml: async (_ctx, { html, path }) => ({
html: html.replace('</body>', '<footer>© 2026</footer></body>'),
}),
}],
}
See the Middleware API docs for the full reference.
New <Timeline> MDX component
3.2.0 ships a vertical-timeline MDX component built for changelogs, release notes, status updates, and audit logs. Each entry has a colored dot strung onto the connector line, an optional date and badge, a title, and a Markdown body.
<Timeline>
<Timeline.Item
date="2026-07-20"
title="Boltdocs 3.2.0"
badge="Major"
icon={<Sparkles />}
>
Released **Plugin v3.2 API** — caches, diagnostics, paths, virtual modules,
middleware, server, and HMR hooks. See the [migration guide](/docs/blog/boltdocs-3.2.0).
</Timeline.Item>
<Timeline.Item
date="2026-06-01"
title="Added i18n support"
badge={{ text: 'Minor', variant: 'success' }}
>
New locale filesystem convention, version-aware routes, and SSR-safe URL prefixes.
</Timeline.Item>
</Timeline>
Renders as:
● Jul 20, 2026 [MAJOR]
│ Boltdocs 3.2.0
│ Released Plugin v3.2 API — caches, diagnostics, paths, virtual modules,
│ middleware, server, and HMR hooks. See the migration guide.
│
● Jun 1, 2026 [MINOR]
│ Added i18n support
│ Locale filesystem convention, version-aware routes, and SSR-safe URL prefixes.
What's in the box
- Auto-registered — write
<Timeline>in any.mdxfile. No import needed. - Two variant families — semantic (
primary,success,info,warning,danger) plus lifecycle aliases (major,minor,patch,new,deprecated,breaking) so changelogs can colour by release type without retyping hex codes. - Hydration-safe localized dates — the
dateprop renders viatoLocaleDateStringpinned to a stable locale ('en-US'by default) so server output and client output always agree. - Body accepts full Markdown — paragraphs, code, links, inline
<Callout>, lists. Whatever fits in an MDX paragraph fits inside an item. - Compact mode —
<Timeline compact>for dense lists of small tweaks. - Data-driven generation — every
Timeline.Itemis a normal React element, so you can.map()over a JSON array of releases to render an entire changelog without retyping each release block by hand.
See the full Timeline docs for the complete component API, accessibility notes, and the data-driven changelog pattern.
Upgrading
For most teams, this is a one-line install:
pnpm add boltdocs@latest
A small subset of installs will see a peerDependencies warning from npm/pnpm:
boltdocs@3.2.0 requires react-aria-components@^1.16.0 as a peer
Satisfy it with pnpm add react-aria-components (or pin a lower version if your React Aria vendor already provides it). Sites that use @bdocs/plugin-image-optimizer are unaffected — the optimizer's own peer declarations still work, and sharp/svgo install via the optimizer's peerDependenciesMeta.optional: true.
See Upgrading to 3.2.0 → Semi-breaking changes for the full audit, before/after package.json snippets, and platform-specific notes.
Check the full docs to explore everything new.