Software Engineering · Documentation
Serving Several Documentation Versions From One URL Space
Versioned documentation is a URL design problem before it is a content problem. Path layout, redirect policy, relative links, and canonical hints decide whether a reader stays on the release they are running.
A library with three supported releases needs three documentation sets, and the moment the second one exists, every URL on the site has to answer a question it did not previously face: which release is this page describing. The answer cannot live in the page, because a reader arrives at a URL without having chosen anything. It has to live in the URL, and the layout chosen there determines what can be redirected, what can be indexed, and whether a link written inside an old page keeps the reader on the old release.
The forcing constraint is that a page describing version 1.4 cannot be updated to describe 2.0. If it is, a reader running 1.4 is told about an interface they do not have, and there is no way for them to recover the older text. Documentation that must stay true for several releases is therefore content addressed by version, and the site has to keep the old bytes around.
The version belongs in the path
Three layouts are in use, and two of them are workable.
A path segment, /docs/2.4/guide/retries/, is the common arrangement. It keeps every version on one host, so cookies, authentication, and the site’s own navigation apply uniformly; it makes the version visible in the URL, which is useful when a URL is pasted into a ticket; and it lets one server configuration route all versions. Its cost is that the version is part of every path, so every internal link has to be written in a form that survives being served under a different prefix.
A subdomain per version, 2.4.docs.example.org, isolates each version completely. Links inside a version’s pages are root-relative and cannot accidentally escape into another version, which removes an entire failure mode. The cost is fragmentation: certificates, cookies, and search indexing all have to be managed per host, and the “current version” has no single canonical home.
A query parameter, /docs/guide/retries/?version=2.4, is the layout to avoid. It produces one indexable URL for many documents, makes caching depend on the query string, and breaks the property that a documentation URL can be cited as a location. A fragment has the same problem, worse: the fragment is not sent to the server at all, so it cannot influence which version is served.
What “latest” means, and why citing it is fragile
Most sites add an alias, /docs/latest/, that points at the current stable release. It is useful for navigation: a bookmark that always opens current documentation, a link in a product UI that should never go stale, a “read the docs” button in a repository.
It is a poor citation target. A link to /docs/latest/guide/retries/ written today and read in two years resolves to a different document, and the sentence that cited it may no longer be true. Worse, the alias makes the version ambiguous in a way that is invisible to the reader, who cannot tell from the page whether they are reading current documentation or an archived copy, unless the page says so.
The arrangement that keeps both properties is to resolve latest to a specific version at the point of citation and to keep the alias only for navigation. Pages served under the alias can carry a banner naming the resolved version, and the version selector can treat latest as a synonym for the current stable release rather than as a version in its own right. When a prerelease exists, latest and stable diverge, and the site should name both explicitly rather than choosing one silently.
Relative links keep a reader inside a version
Here is the failure that versioned sites produce most often. A page in the 1.4 tree contains a link to /docs/latest/configuration/. The 1.4 page is correct for its own version, but the link points at a different one, and the reader who follows it is silently upgraded: the page they land on documents options their installation does not have, and the only clue is a version selector they may not look at.
The link was written as an absolute path because that is the form that works in a source document rendered on a code-hosting site, where relative paths resolve against the repository rather than the site. When the same source is built into a versioned site, the absolute path is preserved and escapes the version.
Two mechanisms prevent it. The first is to write internal links relative to the page, so the version prefix is inherited from the current URL. Relative links survive being served under a different prefix, which is exactly the property a version tree needs. The second is to rewrite links at build time: the source says “link to the configuration page”, and the generator resolves that reference to the current version’s path. The rewrite has the advantage of failing the build when the target does not exist in this version, which is the correct outcome for a link to a page that was only added in 2.0.
The second mechanism is the one to prefer, because it also handles the case where a page moved between versions. A hand-written relative path breaks when the target is renamed; a reference resolved against the version’s own link graph does not.
Redirects, tombstones, and the cost of a 404
Redirect policy differs by kind of move, and the status code is part of the contract.
A page that moved within a version is a permanent move, and 301 tells clients and caches to update their records. A 301 may be cached indefinitely, so it should only be used when the new location is genuinely permanent. A temporary move, such as a short-lived outage page or an A/B test of a new structure, uses 302, which clients must not treat as permanent. 307 and 308 preserve the request method, which matters for form submissions and not at all for documentation GETs, but 308 is the permanent variant and is the correct choice when the method must be preserved.
The latest alias is the interesting case. It is semantically temporary even though it is stable for months at a time: it points at whatever is current. Serving it with 302 and a Location header to the resolved version keeps caches from pinning an old target. Sites that serve latest by rewriting internally, without a redirect, avoid the question but lose the visible version in the URL.
# A moving alias: redirect, so the version becomes visible in the address.
rewrite ^/docs/latest/(.*)$ /docs/2.4/$1 redirect;
# A removed feature keeps a tombstone that points at the migration guide.
location = /docs/2.4/legacy-auth/ {
return 301 /docs/2.4/migration/#legacy-auth;
}
The redirect flag emits 302; permanent would emit 301 and invite caches to remember a target that changes with the next release. The second rule is per path rather than a pattern, because tombstones are written one at a time.
Removals need a third status. A page whose feature was removed in 2.0 has no content to show on the 2.0 tree, but the URL has almost certainly been cited. Returning 404 discards the citation and gives the reader nothing. Returning 410 is accurate about permanence and equally unhelpful. The useful response is a page that says the feature was removed, names the release, and links to the migration guide; whether it is served with 200 at the same URL or reached through a 301 to a migration section is a matter of taste. The page is a tombstone, and it exists because the changelog’s Removed entry and the reference page describe the same event from two directions.
A redirect map is required for both moves and removals, and it should be data rather than server configuration. One entry per source path, checked at build time for uniqueness and for a target that exists, is small enough to review and precise enough to audit.
Canonicalisation across versions
Several near-identical pages are a problem for search engines before they are a problem for readers: a query that matches all versions returns a list of duplicates, and a crawler spends its budget on archived copies. The rel="canonical" link relation, defined in RFC 6596, is the standard signal that one URL is the preferred representative of a set of duplicates. It is a hint, not a directive, and search engines may ignore it when other signals disagree.
The choice of canonical target is a policy decision with two defensible answers. Pointing every version at the current stable page consolidates ranking signal on the maintained documentation, at the cost of removing archived versions from search results, where they are sometimes the only correct answer for a reader on an old release. Pointing each version at itself keeps every version findable and leaves the duplication in place.
The policy that most sites converge on is a middle one: canonical to the current stable page for pages that are substantively the same, self-canonical for pages that describe behaviour unique to a release, and a version banner on every archived page so a reader who arrives from a search result knows what they are reading. Older major versions that are no longer supported can be excluded from indexing outright, which is a stronger statement than a canonical hint and should be timed with the end of support rather than with the release of a successor.
The same duplication affects the site’s own search, where the fix is a filter or a collapse rather than a canonical tag, as ranking across versions describes. The two mechanisms solve the same problem for different consumers, and they should agree about which versions are current.
Building an old version after support ends
A version tree has to be buildable long after the branch that produced it stopped receiving changes. Two arrangements make that possible.
The first is to keep the built output. The HTML for 1.4 is generated once, at release, and served as static files forever. This is insensitive to toolchain changes and requires no ability to rebuild, but it freezes the site chrome: the navigation, the search box, and the version selector are as they were on the day of the build, which is usually acceptable and occasionally embarrassing.
The second is to rebuild from the tag on demand. This keeps the chrome current and depends on the old source still building with the current toolchain — a dependency that degrades as the generator, the theme, and the language runtime move on. Pinning the toolchain per version restores the ability to rebuild, at the cost of maintaining several build environments.
The choice interacts with the redirect map. A rebuilt version must reproduce the same output paths as the original, or every internal link in the archived tree breaks, and the map has to cover the difference. Keeping the output is the safer default for versions that are read but not edited; rebuilding is right for the current and previous release, where the chrome and the search index need to match the rest of the site.
References
- RFC 9110: HTTP Semantics — IETF
- RFC 6596: The Canonical Link Relation — IETF
- How to specify a canonical URL with rel="canonical" and other methods — Google Search Central
- Read the Docs: Versions — Read the Docs