Skip to content
duxt

Several versions

Serve the same documentation at several refs, with a switcher and a banner.

A ref becomes a version. Point a source at tags and each one is published, with a version segment in the URL and an entry in the switcher.

Refs require an explicit repo, including when you publish another ref of this repository. Without repo, a source reads the current local checkout and rejects refs (including latest) and local locale-level ref overrides. origin.repo and origin.ref only supply provenance links; they do not select content. A locale with its own explicit repo may select its own ref. For versions kept locally, use a source's version with distinct folders or a generated section's file-based versions.

Steps

  1. List the refs. A bare string is a branch. A tag has to say so — git keeps the two in separate namespaces, and asking for a tag under refs/heads fails the build.
    sources: [
      {
        repo: 'acme/api',
        path: 'docs',
        refs: [
          { branch: 'main', status: 'upcoming' },
          { tag: 'v2.0.0' },
          { tag: 'v1.4.0' },
          { tag: 'v0.9.0', status: 'eol' }
        ]
      }
    ],
    sourceOptions: { defaultRef: 'v2.0.0' }
    
  2. Name the default. defaultRef is the ref served without a version prefix and the one search engines are pointed at. Left unset it is the first in the list — which is rarely what you want when the list starts at a branch.
  3. State the lifecycle where it cannot be derived. Semver orders tags, so "older than the default" needs no help. It cannot place a branch against a tag: main is upcoming, and without saying so its reader is told to upgrade to a version older than what they are reading.
  4. Discover releases if you do not want to edit this list every release. Say how much history should remain addressable: every release, the newest patch in every minor line, or the newest release in every major line.
    releases: { select: 'minor' },
    refs: [{ branch: 'main', status: 'upcoming' }]
    

    all, minor and major are URL-retention policies, not ranking modes. Pre-releases stay out unless prereleases: true; an explicit ref for a discovered tag overrides its label, status, locales or default in place.
    For a site that wants only one moving release, the reserved 'latest' ref still resolves to the newest semver tag at build time.
  5. Mark a page's version requirement in the prose, where a reader looking at one version cannot see the others:
    ### Retries :since{version="v2.0.0"}
    

How coarse a version is

The name is the site's choice, not the mechanism's, and it is the decision that matters most here — it is what every link to your documentation will carry.

A line per version, as a branch. What Laravel, Symfony and Django do: 11.x, 10.x, 9.x are branches that keep moving, and a patch release changes nothing about the URL.

sources: [
  {
    repo: 'acme/api',
    path: 'docs',
    refs: [
      { branch: '12.x', status: 'upcoming' },
      '11.x',
      { branch: '10.x', status: 'deprecated' },
      { branch: '9.x', status: 'eol' }
    ]
  }
],
sourceOptions: { defaultRef: '11.x' }

A bare string is a branch; only a tag has to say so.

A line per version, from tags. Where the release you want to publish is a tag but the URL should not move with every patch, label renames it:

refs: [
  { tag: 'v2.4.1', label: 'v2' },
  { tag: 'v1.9.3', label: 'v1', status: 'deprecated' }
]

Every release its own version. { tag: 'v2.4.1' } with no label. Each is citable forever, and the switcher grows by one entry per release — which is the right answer for a specification and the wrong one for a manual.

The same policies can be maintained from tags instead of repeated in config:

// Every citable release.
releases: { select: 'all' }

// Or the newest patch from each long-lived minor line.
releases: { select: 'minor' }

// Or one current release for each major line.
releases: { select: 'major' }

The newest selected release is the default unless an explicit default: true ref or sourceOptions.defaultRef says otherwise. A selection that matches no tag fails the build; an unreachable repository is reported as a git failure instead of being mistaken for an empty selection.

The same choice applies to a section that names its own versions: version: 'v2' or version: '2.4.1', with exactly the same trade-off. One model, one decision, whether the content comes from a ref or from a file.

When the versions are not refs

A version is normally a checkout. Two shapes are not, and both are ordinary:

Documentation in folders. docs/v1 beside docs/v2 in one repository, no tags. Each source names the version it is:

sources: [
  { path: 'docs/v2', version: 'v2' },
  { path: 'docs/v1', version: 'v1', status: 'deprecated' }
]

An API in files. openapi/v1.yaml beside openapi/v2.yaml — the usual way an API is kept, and the versions belong to the section, not to the source:

generated: [
  {
    type: 'openapi',
    label: 'API',
    path: 'openapi/v2.yaml',
    versions: [
      { version: 'v2', path: 'openapi/v2.yaml' },
      { version: 'v1', path: 'openapi/v1.yaml', status: 'deprecated' }
    ]
  }
]

Both produce exactly what refs produce: a segment in the URL, an entry in the switcher, the banner and the canonical. The switcher stays scoped to the artefact you are reading — on /api it offers the API's versions, on a prose page the documentation's.

What happens to the old versions

A non-default version carries noindex and a canonical pointing at the same page in the default one, and an eol version leaves the sitemap entirely. That is what stops a search engine offering v0.9.0 where the reader wanted today's documentation — see URLs and versions.

Checklist

  • Every tag is written as { tag: … }, not as a bare string
  • The version names are as coarse as the project's own support lines
  • defaultRef names the version readers should land on
  • A branch in the list carries an explicit status
  • The switcher shows every version, with a badge on the dead ones
  • A dead version's page is absent from /sitemap.xml
  • Where the versions are folders or files rather than refs, exactly one of refs, a source's version and a section's versions is in play
Was this page helpful?