Versioned API Docs in Docusaurus

This guide is part of Docusaurus for API Portals within Developer Portal Frameworks & UI Setup. It covers running more than one API version in a Docusaurus portal without the repository, the build, or the version dropdown becoming unmanageable. The trigger is usually the first major API release: consumers on v1 still need their reference, v2 is live, and the naive answer — one snapshot per release — quietly becomes a portal with fourteen versions and a nine-minute build.

Current tree and versioned snapshots Generation always writes into the current docs tree; the versioning command copies it once into a frozen snapshot that is never regenerated. gen-api-docs every build docs/ — current mutable, regenerated served at /docs/ version-2.0 — frozen /docs/2.0/ version-1.0 — frozen /docs/1.0/ version-0.9 — pruned redirected to 1.0 docs:version

Problem & Context

Docusaurus versioning is a filesystem copy. docs:version 2.0 duplicates the entire docs/ tree into versioned_docs/version-2.0/, duplicates the sidebar into versioned_sidebars/, and adds an entry to versions.json. From then on that copy is frozen: nothing regenerates into it, and it is rebuilt in full on every deploy.

Two consequences follow, and both surprise teams.

The first is that generated API MDX and versioned snapshots interact in a way that looks broken but is not. Running gen-api-docs after cutting a version updates only the current tree; the snapshot keeps describing the API as it was. That is exactly right — a frozen version should describe a frozen API — but it reads as “my regeneration did nothing” the first time it happens.

The second is cost. Every snapshot is a complete copy that the build processes on every deploy, so a portal that cuts a version each quarter is building eight full references two years later. The build slows linearly, the repository grows, and the version dropdown becomes a list nobody can navigate.

Both problems are solved by the same discipline: version rarely, prune aggressively.

Step-by-Step Solution

1. Decide what a version means for your docs

Tie the docs version to whatever consumers actually choose between — almost always the API major, not the release:

API v1.x  →  docs version 1.0     (one snapshot for the whole v1 line)
API v2.x  →  docs version 2.0
API v2.4  →  NOT a docs version — it is the current tree

A version dropdown listing every patch release is unusable; one listing the majors a consumer might be on is exactly the tool they need.

2. Cut the version

Do this at the moment the successor becomes current, not before:

npm run docusaurus docs:version 2.0
[SUCCESS] Version 2.0 created!

Confirm what it produced:

cat versions.json
ls versioned_docs/
["2.0"]
version-2.0

3. Keep generation targeting the current tree only

The outputDir in the plugin config points at docs/, and it should stay there permanently:

// docusaurus.config.js — generation always targets the CURRENT tree
config: {
  acme: {
    specPath: 'openapi/api.yaml',
    outputDir: 'docs/api',            // never versioned_docs/*
    sidebarOptions: { groupPathsBy: 'tag', categoryLinkSource: 'tag' },
  },
},
# Safe to run on every build — it cannot touch a frozen version.
npm run docusaurus clean-api-docs acme
npm run docusaurus gen-api-docs acme

4. Redirect retired versions

When a version leaves support, delete it and redirect its paths rather than leaving a stale reference live:

// docusaurus.config.js
plugins: [
  [
    '@docusaurus/plugin-client-redirects',
    {
      createRedirects(existingPath) {
        // Anything under the retired 0.9 tree goes to the oldest supported version.
        if (existingPath.startsWith('/docs/1.0/')) {
          return [existingPath.replace('/docs/1.0/', '/docs/0.9/')];
        }
        return undefined;
      },
    },
  ],
],

5. Prune and measure the build

Deleting a version is three edits, and it is the step teams skip:

rm -rf versioned_docs/version-0.9 versioned_sidebars/version-0.9-sidebars.json
node -e "
const fs=require('fs');
const v=JSON.parse(fs.readFileSync('versions.json','utf8')).filter(x=>x!=='0.9');
fs.writeFileSync('versions.json', JSON.stringify(v,null,2)+'\n');
"

Then watch what it bought you:

time npm run build
real  1m54s     # was 3m12s with 0.9 still present
Cost of each retained version Every versioned snapshot is a full copy of the docs tree that is rebuilt on each deploy, so build time grows linearly with the number of versions kept. versions retained → build time 1 2 3 4 5 6 supported prune these

Complete Working Example

A script that cuts a version, prunes anything outside the support window, and reports the result:

#!/usr/bin/env bash
# cut-docs-version.sh — snapshot the current docs and prune unsupported versions.
set -euo pipefail

: "${NEW_VERSION:?set NEW_VERSION, e.g. 2.0}"
KEEP="${KEEP:-3}"          # how many versions to retain, newest first

# 1. Regenerate the current tree first so the snapshot is accurate.
npm run docusaurus clean-api-docs acme
npm run docusaurus gen-api-docs acme

# 2. Refuse to cut a version that already exists — snapshots are immutable.
if [ -d "versioned_docs/version-${NEW_VERSION}" ]; then
  echo "ERROR: version ${NEW_VERSION} already exists; snapshots are immutable" >&2
  exit 1
fi

npm run docusaurus docs:version "${NEW_VERSION}"

# 3. Prune anything beyond the retention window.
node -e '
const fs = require("fs");
const keep = Number(process.env.KEEP);
const all  = JSON.parse(fs.readFileSync("versions.json", "utf8"));  // newest first
const drop = all.slice(keep);
for (const v of drop) {
  fs.rmSync(`versioned_docs/version-${v}`, { recursive: true, force: true });
  fs.rmSync(`versioned_sidebars/version-${v}-sidebars.json`, { force: true });
  console.log(`pruned ${v}`);
}
fs.writeFileSync("versions.json", JSON.stringify(all.slice(0, keep), null, 2) + "\n");
' 

# 4. Report the cost.
echo "versions now: $(node -p 'require("./versions.json").join(", ")')"
du -sh versioned_docs 2>/dev/null || true

Expected output:

[SUCCESS] Version 2.0 created!
pruned 0.9
versions now: 2.0, 1.1, 1.0
4.2M	versioned_docs

The immutability guard in step 2 matters more than it looks. Re-cutting an existing version silently overwrites a published reference, which means a consumer who bookmarked a v1.0 page can find it describing something else entirely — the documentation equivalent of a force-pushed tag.

Gotchas & Edge Cases

Search indexes every version by default. Once three majors are live, a search for an operation returns the same page three times and the deprecated one often ranks first. Scope the crawl to supported versions and serve noindex on the rest — Scoping DocSearch crawls to versioned docs covers exactly this.

A frozen version keeps its broken links forever. Links inside a snapshot point at paths as they were when it was cut. If you later restructure the current tree, those links still resolve inside the version — but any link from a snapshot to the current tree can break silently. Run the link checker against every published version, not only the current one.

versions.json order is significant. It is newest-first, and the first entry is what the dropdown treats as current. Editing it by hand and reordering produces a portal whose “current” version is an old one, with no error anywhere.

Cutting a version before the successor is ready. Snapshotting early freezes an incomplete reference. Cut at the moment the new major becomes the default, so the snapshot captures the old API in its final, complete state.

Retiring a version is a content decision, not only a cleanup task. Before deleting a snapshot, check whether anything still links into it — internal guides, blog posts, support macros — and update those links rather than relying on the redirect to cover them forever. A redirect keeps external bookmarks working; it does not stop your own documentation pointing readers at a version you no longer support.

Version-specific banners need configuring, not writing. Docusaurus can display an “unmaintained version” banner automatically for old versions; setting banner: 'unmaintained' in the version config is far more reliable than adding a note to each page, which no one will maintain.

Decide the support window before you cut the first version. Versioning is easy to start and hard to stop, because every snapshot creates an expectation that it will remain available. Teams that never wrote down how long a version stays published end up keeping all of them forever, on the reasonable-sounding grounds that somebody might still be on one. Publishing a support window — twelve months after the successor ships, say — turns pruning from a judgement call into routine maintenance, and gives consumers a date to plan against rather than an open-ended promise you will eventually have to break.

Route readers to the right version rather than trusting the dropdown. A dropdown only helps somebody who has already noticed they are on the wrong version, and most readers arrive from a search engine directly onto a deep page with no idea which version it belongs to. Two cheap measures fix most of this: enable the unmaintained-version banner so every page in an old snapshot says so at the top, and make the version explicit in the page title so a search result is self-describing. Between them, a reader who lands on a two-year-old operation page knows within a second, instead of copying a request that has not worked since last March.

How readers reach a versioned page A reader navigating the site sees the version dropdown, but a reader arriving from search lands deep with no context unless the page states its own version. navigates the site sees the dropdown arrives from search lands deep, no context a versioned page /docs/1.0/api/… banner on the page works for both arrivals version in the title visible in the search result

FAQ

Should I cut a docs version for every API release?

No. Tie it to your API major version, or to whatever unit consumers actually choose between. A version dropdown with twenty entries is worse than none, because nobody can tell which one applies to them.

Why did my regenerated API docs not appear in the old version?

That is correct behaviour. A versioned snapshot is immutable — generation targets the current tree only. If a frozen version genuinely needs a correction, edit the file in versioned_docs/ directly and treat it as a deliberate exception.

How do I stop the build slowing down as versions accumulate?

Prune. Each snapshot is a full copy of the tree that gets rebuilt on every deploy, so build time grows linearly with the number of versions you keep. Retain only supported versions and redirect the rest.