Skip to content
Blume
Esc
↑↓navigate↵open⌘Jpreview
Guides

Versioning

Version your docs for a breaking release

Snapshot your docs before a breaking release, so readers on the old and new versions each find the right instructions.

By 8 min read

By the end of this guide, your docs describe two major versions of an SDK at once. The new version lives at your usual URLs, the old one is a frozen copy under /v1/, and readers move between them with a version switcher. Search stays inside the version being read, and old pages tell search engines which page is current.

The example is a fictional Acme SDK, a client for sending email and SMS, whose setup changes in v2. Swap in your own pages as you go.

Decide whether you need a snapshot

A snapshot is a full copy of your docs. Every page, every search index entry, and the navigation data exist once per version, so it's worth cutting one only when readers will stay on the old version for a while and the instructions really differ.

Acme's v2 is that case. In v1 you construct a client with new Acme() and pass callbacks. In v2 you call createClient() and every method returns a promise. Someone still on v1 needs the v1 quickstart, word for word, for months.

For a smaller change, such as one renamed option or a new default, skip the snapshot. A callout on the affected page or an entry in your changelog serves those readers without doubling the site. Cut one snapshot per major version, not one per release.

The starting point

Before v2, the Acme docs have four pages:

docs/
  index.mdx         ->  /
  quickstart.mdx    ->  /quickstart
  callbacks.mdx     ->  /callbacks
  errors.mdx        ->  /errors

The v1 quickstart sets up the client like this:

import { Acme } from "@acme/sdk";

const acme = new Acme({ apiKey: process.env.ACME_API_KEY });

acme.messages.send({ to: "+15550100", body: "Hi" }, (error, message) => {
  if (error) throw error;
  console.log(message.id);
});

In v2, quickstart.mdx will change, callbacks.mdx will go away because v2 has no callbacks, and errors.mdx stays the same. Those three cases are the ones to watch through the rest of this guide.

Cut the snapshot

Cut the snapshot while the docs still describe v1, before any v2 edits land. If your v2 changes are already on a branch, run this on the main branch first. From the project root:

npx blume version v1

The id, v1 here, is both the folder name and the URL segment, so pick what you want in your URLs: v1 for one snapshot per major version, or v1.4 if you want the exact release. It has to start with a letter, and can only contain letters, digits, dots, hyphens, and underscores.

The command does three things, then reports what it did:

  1. Copies everything in your content folder into docs/v1/, including images and meta.ts files. It leaves out hidden files and any earlier snapshot folders.
  2. Rewrites root-absolute links inside the copy so they stay in the snapshot: a link to /errors in docs/v1/quickstart.mdx becomes /v1/errors. Relative links need no change, and links inside code are left alone.
  3. Turns on versioning in blume.config.ts. On the first cut it adds this block, labeling the live docs "Latest":
versions: {
  archived: [{ id: "v1" }],
  current: { label: "Latest" },
},

If your config is built in a way the command won't edit, it prints the entry to paste instead. Paste it before you go on: until the id is registered, the snapshot builds as ordinary pages and every page ships twice.

Inspect the copy

Restart npx blume dev so it picks up the new folder, then look at what changed:

docs/
  index.mdx            ->  /
  quickstart.mdx       ->  /quickstart
  callbacks.mdx        ->  /callbacks
  errors.mdx           ->  /errors
  v1/
    index.mdx          ->  /v1
    quickstart.mdx     ->  /v1/quickstart
    callbacks.mdx      ->  /v1/callbacks
    errors.mdx         ->  /v1/errors

Open /v1/quickstart. It has a notice above the page saying you're viewing documentation for v1, with a "Go to latest" link to /quickstart. The header has a version dropdown listing "Latest" and "v1".

Search the copied pages for links that still point at the live docs. The command only rewrites links to pages the snapshot has a copy of, so links to a generated API reference or a changelog from a remote source keep pointing at the current pages. That's usually what you want, since the snapshot has no copy of them. Commit the folder once it looks right:

git add docs/v1 blume.config.ts
git commit -m "Snapshot the v1 docs"

What the snapshot doesn't freeze

Versioning covers your docs content folder. Your blog, your changelog, API references generated from OpenAPI specs, and custom pages always show the current version. blume version doesn't copy an OpenAPI spec either. If your API changed too and v1 readers need the old reference, publish the old spec as a second source with its own route:

reference: [
  openapi({
    sources: [
      { label: "API", spec: "./openapi.yaml" },
      { label: "API v1", route: "/api-v1", spec: "./openapi-v1.yaml" },
    ],
  }),
],

That second reference sits beside the docs rather than inside the version switcher.

Update the current docs for v2

Now edit the live pages at the root of docs/. Those are the v2 docs from here on. Rewrite the quickstart for the new API:

import { createClient } from "@acme/sdk";

const acme = createClient({ apiKey: process.env.ACME_API_KEY });

const message = await acme.messages.send({ to: "+15550100", body: "Hi" });
console.log(message.id);

Delete docs/callbacks.mdx, since v2 has no callbacks, and add an upgrade page, docs/upgrade.mdx, that walks v1 users through the change. Leave errors.mdx alone.

Deleting a page breaks every link to its old URL, so send /callbacks to the archived copy, where the content still exists:

redirects: [{ from: "/callbacks", to: "/v1/callbacks" }],

Then give the versions their real names:

versions: {
  current: { label: "v2", badge: "Latest" },
  archived: [
    { id: "v1", banner: "These docs cover the 1.x SDK. Version 2 changed how you create a client." },
  ],
},

current.label names the live docs in the switcher, and badge adds a small tag beside it. An archived version is labeled by its id unless you set label. The banner string replaces the built-in notice text ("You're viewing documentation for v1. It may be out of date.") and the "Go to latest" link stays beside it. Set banner: false to hide the notice for a version.

When you add v3 later, list archived versions newest first. That order is the switcher's order.

With the dev server running, open /v1/quickstart and switch to v2 in the dropdown. You land on /quickstart, the same page in the other version. Now open /v1/callbacks and switch. There's no /callbacks in v2, so you land on the v2 root instead. If you'd rather every switch go to the root, set switcher: { redirect: "root" } in versions.

Open search on a v1 page and search for "callbacks". Results come from v1 only. The "All versions" toggle in the dialog's footer widens the search, and results from another version name it on the row. The reader's choice is remembered.

Check canonical URLs and the sitemap

Two copies of a page compete in search results unless one says which is authoritative. By default, each archived page names its equivalent in the current docs as its canonical URL, matched by path. A page that only exists in the archived version keeps its own. The sitemap follows the same rule:

PageCanonical URLIn the sitemap
/quickstartItselfYes
/v1/quickstart/quickstartNo
/v1/errors/errorsNo
/v1/callbacksItselfYes

Archived pages stay indexable either way. The canonical tag tells search engines which copy you consider current. It doesn't decide what they rank. To check, view the source of /v1/quickstart and find the <link rel="canonical"> tag, then run a build and read the sitemap.xml it writes. Both the canonical tag and the sitemap need your site URL, set in deployment.site or detected on your host.

/v1/quickstart is the case to think about. Its content differs from the v2 page, but it still points at it. If a lot of your readers stay on v1 and search for v1 instructions, give that version its own canonical URLs. If you'd rather an old version leave search results altogether, turn indexing off for it:

archived: [
  { id: "v1", canonical: "self" }, // every v1 page is its own canonical
  { id: "v0", noindex: true },     // no v0 page is indexed or in the sitemap
],

A page's own seo.canonical frontmatter always wins over the version setting.

What agents see

Coding agents get the same split. If you run the MCP server, its search and page-list tools default to the current docs and take a version argument for v1. llms.txt lists archived versions after the current docs, marked as archived, while llms-full.txt stays current-only. The assistant answers from the version the reader is viewing.

Maintain the old version

Archived means frozen: new work goes in the live tree. But the snapshot is ordinary files, so when a v1 page is wrong, such as a broken command or a security note v1 users need, edit the file in docs/v1/ directly and commit it.

Don't try to refresh a snapshot by cutting it again. blume version refuses an id that's already in versions.archived, so a stray re-run can't overwrite your v1 docs with v2 content. If you translate your docs, snapshots keep the translations they were cut with, and blume translate never retranslates them.

When v1 reaches end of life, retire it in two steps. First set noindex: true on it, so it leaves search results while links to it still work. Later, delete docs/v1/ and its entry, and redirect its URLs:

redirects: [
  { from: "/v1/callbacks", to: "/upgrade" },
  { from: "/v1/:slug*", to: "/:slug*" },
],

The pattern sends each v1 page to the same path in the current docs, and /v1 itself to the root. An exact redirect wins over a pattern, so pages with no v2 equivalent, like /v1/callbacks, go wherever you point them. Update the earlier /callbacks redirect too, so it doesn't lead to a page that's gone. Pattern redirects need a host that reads Blume's redirect files or a server build: see Pattern redirects. The URL migration guide shows how to test redirects before and after you deploy.

Troubleshooting

blume version refuses to run

It exits without changing anything when the id doesn't start with a letter (v1, not 1), when the id is already registered, when the snapshot folder exists (pass --force to overwrite a folder that isn't registered), or when the project has errors. A snapshot of a broken site would freeze the breakage, so fix those first: npx blume doctor lists them.

The snapshot doesn't show up

blume dev only picks up a new snapshot folder when it starts. Stop it and run it again.

Every page appears twice

The snapshot folder exists but its id isn't in versions, so its pages build as a regular section at /v1/…. This happens when the command couldn't edit your config and printed the entry instead. Paste it in. npx blume doctor flags the folder with BLUME_VERSIONS_UNCONFIGURED_VERSION.

Search shows every version with no toggle

Your search provider doesn't scope by version. Pagefind, Orama Cloud, and Mixedbread search every version at once. Switch to Orama, FlexSearch, Algolia, or Typesense if scoping matters.

The sidebar looks different on archived pages

Header tabs are defined against the current docs, so inside a snapshot the sidebar isn't scoped by tab. An explicit navigation.sidebar also applies only to the current docs: a snapshot's sidebar always comes from its own files.

Next step

Snapshot your docs

Snapshot your docs as they are today, before the breaking change lands.

npx blume version v1
Read the versioning docs

A step here not working for you? Report a broken step.

Keep going.More guides.

Upgrade your docs with Blume.

Install today and ship a production-grade docs site in minutes. Free and open source, forever.

npx blume init