Versioning
Keep the docs for older releases live, with a version switcher and version-scoped navigation.
Experimental
Versioning sits outside the 1.0 stability promise. DocsVersions, currentOnly, the archive marker, and archive-version may change in a minor
release until authors have used them for real. An archive freezes prose, code
samples and URLs — not what a page imports. Live examples on an archived page
keep resolving to whatever the app currently has installed.
Why version your docs
Ship a breaking major and two readers need different things: the one still on the old release needs its docs, and the one on the new release should not trip over stale pages. Versioning keeps the old release live at its own URL, scopes the sidebar and search to the version being read, and steers visitors who land on an archived page to the current one.
It is opt-in, and it is designed so that turning it on changes nothing. Your current docs stay exactly where they are.
The model
The current version is the docs for your latest release. It lives at your
docs root and is served unprefixed, so /docs/theming is /docs/theming forever. It is also the only version you edit.
An archived version is a frozen copy of the docs for a superseded release,
served under its own prefix at /docs/v1/theming. You never edit an archive.
You create one at the moment you ship a breaking release, and then carry on
editing the docs root.
That means adopting versioning never moves a URL, and shipping a new release never moves one either. Only the archive is new.
Declare your versions
Versions live in the docsmith() Vite plugin, because it scans them at build
time to tag each page.
Each archived version’s id is both its folder name under your docs directory
and its URL prefix. The current version has no folder of its own: its pages are
the ones that are not inside an archive.
Declaring only current is perfectly valid, and is how you start. Nothing
renders differently until the first archive exists.
What makes a valid id
An id has to start with a letter or a digit, and can then hold letters, digits, ., - and _. So v1, v1.0, 2.x and next are all fine, while ../evil, .hidden and api/v1 are rejected at build time.
The rule applies to current as well as to archives, even though the current
version’s id never appears in a URL. Archiving turns today’s current id into an
archive folder, so an id that was legal only as current would fail one release
later.
When the config and your folders disagree
Versions are declared by hand, so your config and your docs folder are two separate claims about which versions exist. The build checks that they agree and fails if they do not, because neither mismatch shows up in the output: an archive the config does not know about gets merged into your current docs, and a section folder wrongly declared as a version disappears from your current sidebar.
Telling the two apart takes a marker. archive-version writes a .docsmith-archive file into every archive it creates, and the build reads it.
A folder under your docs root is an archive when it has that file and is
declared in versions. Any other combination is an error:
| On disk | In versions | Result |
|---|---|---|
| Has the marker | Declared | Served as an archived version |
| Has the marker | Not declared | Build fails: undeclared archive |
| No marker | Declared | Build fails: not an archive |
| No marker | Not declared | An ordinary section of your docs |
Copying an archive by hand
If you create an archive folder yourself rather than with archive-version, add
an empty .docsmith-archive file inside it. Without one the build treats it as
part of your current docs and refuses to accept it as a version.
Wire it into the shell
The plugin emits a versions manifest next to your content. Pass both to DocsShell.
That is the whole wiring. No redirect, no route changes. DocsShell reads the
active version from the URL and scopes the sidebar, search, prev/next, and
breadcrumbs to it.
If you use ErrorPage, pass it versions too.
A 404 under an archived prefix then keeps that version’s search scope and its noindex, instead of falling back to the current version.
What readers get
Once at least one archive exists, a version switcher appears in the header. It keeps readers on the same page when they switch, falling back to that version’s home when the page has no equivalent there.
On an archived version, a banner tells readers what they are looking at and links to the current equivalent. Archived pages also drop their “Edit this page” link, since the archive is frozen.
Search engines see one version
Archived docs should not compete with the current release in search results, but
they are real content people still look for. So archives stay indexable with a
self-canonical, while sitemap.xml and llms.txt list the current version
only. Scope those endpoints with currentOnly:
To keep a version out of search engines entirely, set noindex: true on it.
Archive a release
When you ship a breaking release, freeze the docs that described the old one:
This copies your current docs into v1/, and does three things a plain copy
would get wrong. It rewrites in-content links so they stay inside the archive,
because an absolute link like ](/docs/theming) would otherwise keep pointing
at your newest docs. It writes each page’s real last-updated date into its
frontmatter, so the archive does not claim every page changed on the day you
created it. And it marks the folder as an archive.
Review the diff, then update your config to serve the archive and rename the current version:
Until you paste that in, the build fails and reports the new archive as undeclared. That is deliberate: it is the reminder, and it arrives while you are still looking at the terminal. The command prints the block to paste.
What archives cost
Worth knowing before you keep many of them around:
- Archived pages are compiled by your current setup, so a breaking change to a docs component changes how every archive renders.
- An archive freezes your content, not what it imports. A page importing a
component from
$libor a package keeps resolving to whatever your app has installed, so a live example on an archived page demonstrates your current library rather than the version the page documents.archive-versionlists the pages this applies to as it copies them. - Each archive adds its pages to the content index and its text to the search index, both of which grow linearly with the number of versions you keep.
- Archive folders are real files in your repo, permanently.
Keeping one or two archives is cheap. Keeping ten is a decision worth making deliberately.