docs-overlay

Versioning documentation in a monorepo

Several packages, several release schedules, one documentation site — where the content lives, who cuts the versions, and what CI has to filter.

Unchanged since 0.2.1

A monorepo publishing several packages has a problem one repository does not: 2.0.0 of ui has nothing to do with 2.0.0 of react, so a single version list is wrong for all but one of them.

Several documentations is the mechanism that solves it — a scope per product, each with its own version folders and its own latest. This page is the part around it: where the content lives, who cuts a version, and what has to change in CI.

One site, or one site per package

Both work. The question is whether your packages are read together.

One site, one scope per packageOne site per package
URLs/docs/ui/2.0.0/…ui.example.com/docs/2.0.0/…
Searchone index, tagged by product and versionone index each, naturally separate
Relative linksresolve inside a product, never acrossinside that site only
Deploymentsone build, one deployone per package, each independently
Autonomyone repository decides the theme and the release cadence of the siteeach team owns its own

Choose one site when a reader crosses between packages — a design system and its React bindings, an SDK and its CLI. Choose one per package when they are separate products that happen to share a repository, because then a single build couples release cadences that have no reason to be coupled.

Everything below applies either way. Only the scope option does not.

Where the content lives

Two arrangements are possible, and one of them is a trap.

✔  docs/content/docs/<product>/<version>/…      one collection, at the site
✘  packages/ui/docs/<version>/…                 one collection per package

Co-locating documentation with each package reads well and breaks the two things that make a documentation site worth having. A page tree and a search index belong to a loader, and a relative link such as ./b.md is resolved against the file system that loader owns — so splitting the content across packages means splitting the loader, and a link from one package's page to another's stops resolving. Keep one collection at the site and let the product be a folder inside it.

If the documentation genuinely has to live next to its package — because that is where its authors are — mirror it into the site's content directory as a build step, and read the last section of this page first.

Independent version lists

One overlaySource() per product, all feeding one loader(). That is the whole configuration, and it is written out in Several documentations. A working site is examples/fumadocs-multi, where alpha is at 2.0.0 while beta is still at 1.0.0 — which is the point of the example.

Cutting versions from the release process

A version folder should be created by whatever already knows a release happened, not by someone remembering afterwards. With Changesets, that is the version pull request:

{
  "scripts": {
    "changeset:version": "changeset version && npm install --package-lock-only && node scripts/cut-docs.mjs"
  }
}

This repository does exactly that, so the behaviour below is inspectable rather than advisory:

  • the cut is committed in the same pull request as the bump, and reviewed with it;
  • it is idempotent, because that pull request is rebuilt on every push to the base branch;
  • it refuses a name the engine would not read as a version, rather than creating a folder that would vanish from the site with only a warning.

docs-overlay cut <version> is the same operation from the command line, with --dry-run — see The command line.

Which package's version the folder takes

The question every monorepo hits, and it has no universal answer — only a decision you should make once and write down.

  • A site documenting one library takes that library's version. This site takes the engine's, which is why a release of an adapter alone cuts nothing: the unreleased pages stay in the channel until the engine ships.
  • A site documenting several products takes each product's own version, in that product's scope. Nothing is shared, so nothing has to agree.
  • A site documenting the repository — a platform whose packages are released together — takes the repository's release tag. If your packages are versioned independently, this is the arrangement that will hurt: a folder named after a tag no package carries answers nobody's question.

Only rebuild what can have changed

A monorepo's CI runs on every change, and a documentation build is not free. Filter on the paths that can affect it:

on:
  push:
    branches: [main]
    paths:
      - "docs/**"
      - "packages/*/src/**"

Include the package sources only if the site reads from them — a generated API reference does, a hand-written guide does not. Over-filtering is worse than under-filtering here: a site that silently stopped rebuilding looks exactly like a site with nothing new in it.

Content diagnostics are cheap enough to run unfiltered:

docs-overlay check --fail-on warning

Seconds, no framework build, and it catches the mistakes that are otherwise invisible until someone follows a dead link.

When the documentation is mirrored in from elsewhere

Any script that copies documentation into a version folder wholesale — a mirror out of a library repository, a backport onto a maintenance branch — writes an override for every file it touches, including the ones identical to what they inherit. Nothing breaks, so nothing reports it, and the repository quietly re-inflates until the overlay stops meaning anything.

docs-overlay prune

Run it after the copy, as part of the same job. It removes only files whose bytes already resolve through inheritance, keeps any file carrying an overlay: directive, and leaves the resolved site identical.

On this page