Releases & versioning
A dockit site can keep every released version of its docs online, each at its own URL, with an in-site switcher in the topbar. This guide is the consumer-facing playbook: the pieces you need, the scripts that drive them, and the deploy mechanics — using this workspace as the worked example throughout.
The library ships a copy-in release kit with all of the pieces below — see Ship it with your own site.
Two independent version axes
Do not conflate these; they move on their own schedules:
- Your product/library semver — the thing the docs document
(here:
@dreamlake/dockit, published to npm). Its version lives in the package's ownpackage.jsonand shows in the topbar's version chips. A product can have its own dropdown and manifest. - The docs-deploy version — a version stamp for the docs site
itself, so readers can pin "the docs as of 0.2.0". It lives in two
files: the workspace root
package.jsonand the manifest atdocs/public/versions.json.
A docs release can ship without a library release (a big guide lands) and vice versa (a patch release with no doc changes worth freezing).
On this site, the Dockit v0.2.15 badge selects
/dockit-versions.json. That product manifest currently contains only
0.2.15, linked to the current site. /versions.json preserves the
separate historical docs-build sequence; it must not relabel the Dockit
package badge. The Python Autodoc tab displays main plus the source
revision with prefix: '' and no dropdown, because no package release
is implied by this documentation integration.
prefix changes the displayed prefix (default v). manifestUrl
selects a path or an absolute manifest URL; it defaults to /versions.json.
These settings are documented in the VersionChip reference.
versions.json — the manifest
The default docs-build manifest is served at /versions.json. A chip
with dropdown: true consumes this file unless manifestUrl selects
a product-specific manifest. Both manifests use the same schema:
current— the newest release; what the topbar chip labels itself when the hostname gives no better answer.stable— the version thestablealias points at (may trailcurrent).aliases— stable URLs that always point somewhere sensible;latestis your production domain.versions[]— newest first; each entry'surlis the documentation destination for that version. Only describe it as frozen when its deployment is actually retained unchanged.dateis optional but useful in the popover.
The switcher fetches this manifest at runtime, so a reader on an old
frozen deploy still sees the full, current version list — the manifest
on your production domain is the source of truth. That fetch is
cross-origin from a frozen v/* deploy, so production must serve
the selected manifest with a CORS header (/versions.json is already in the
netlify.example.toml template):
Use the same header for /dockit-versions.json when a product chip
selects that path. Changing one manifest does not change the other.
set-version.mjs — the bump
One script updates both files atomically. This is the actual script
this workspace runs (embedded from scripts/set-version.mjs, not a
copy):
#!/usr/bin/env node
/**
* Usage: node scripts/set-version.mjs <version>
*
* Updates the docs version in two places:
* - package.json at the workspace root (the canonical version used by
* version-branch.sh)
* - docs/public/versions.json (the version manifest served at /versions.json,
* consumed by the in-site version switcher)
*
* versions.json schema:
* {
* "current": "<latest>",
* "versions": [{ "version", "url", "date" }, ...] // newest first
* }
*
* Idempotent: if the version already exists, its url/date are refreshed.
*/
import { readFileSync, writeFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const version = process.argv[2];
if (!version || !/^\d+\.\d+\.\d+/.test(version)) {
console.error('Usage: node scripts/set-version.mjs <x.y.z>');
process.exit(1);
}
const pkgPath = resolve(__dirname, '..', 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
pkg.version = version;
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
const manifestPath = resolve(__dirname, '..', 'docs', 'public', 'versions.json');
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
// Canonical per-version URL: the Netlify branch-deploy address for branch
// v<x.y.z>. These always resolve once branch deploys are on — no DNS setup.
// (Pretty subdomains like v0-2-0.dockit.dreamlake.ai would need a manual
// Netlify domain-management step per release; we deliberately don't use them.)
const url = `https://v${version.replace(/\./g, '-')}--dockit-docs.netlify.app`;
const date = new Date().toISOString().slice(0, 10);
const existing = manifest.versions.find((v) => v.version === version);
if (existing) {
existing.url = url;
existing.date = date;
} else {
manifest.versions.unshift({ version, url, date });
}
manifest.current = version;
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
console.log(`Updated docs version to ${version}`);
console.log(` package.json → version: "${version}"`);
console.log(` versions.json → current: "${version}", entry: ${url}`);
Idempotent — re-running refreshes the existing entry's url/date instead of duplicating it. Note the URL it writes: the Netlify branch-deploy address, not a pretty subdomain — see the URL scheme below for why.
version-branch.sh — the frozen deploy
Each release is frozen by pushing HEAD to a v<x.y.z> branch. Again
the actual script, embedded from scripts/version-branch.sh:
#!/usr/bin/env bash
# Push current HEAD to a v<version> branch for Netlify branch deploy.
# Netlify serves branch "vx.y.z" at v<x-y-z>--<site>.netlify.app.
set -euo pipefail
VERSION=$(node -e "console.log(require('./package.json').version)")
BRANCH="v${VERSION}"
echo "Pushing to branch: ${BRANCH}"
git push origin "HEAD:refs/heads/${BRANCH}" --force
echo "Branch deploy will be available at: https://${BRANCH//./-}--dockit-docs.netlify.app"
With branch deploys enabled, Netlify builds that branch at its version URL. Keep the branch and deployed artifact unchanged to preserve that release snapshot.
Netlify branch deploys — the URL scheme
Enable branch deploys on your Netlify site (Site settings → Build &
deploy → Branches and deploy contexts → "Deploy all branches", plus
empty [context.branch-deploy] / [context.deploy-preview] blocks in
netlify.toml). Then every pushed branch serves at:
with non-alphanumeric characters in the branch name dashed — so branch
v0.2.1 on site dockit-docs serves at
v0-2-1--dockit-docs.netlify.app. These URLs use Netlify's branch-deploy domain and need no custom DNS once the corresponding deployment exists. They are what
set-version.mjs writes into the manifest.
A CLI netlify deploy --alias creates a named draft deploy, not a
branch deploy. Netlify advises against reusing a deployed branch name
as an alias. The existing prod script still includes that legacy
alias step; its predictable URL is not evidence of branch-deploy
retention. Create real branch deploys through a connected repository
with branch deployment enabled. See the
Netlify CLI reference.
Netlify exempts the latest successful deploy of each branch from automatic deploy deletion. Older deploys and draft aliases can be removed under the site's retention policy. Keep each release branch unchanged, verify its successful branch deploy, and retain a separate build archive with its source revision and checksum. A Git branch alone does not save the rendered output. See Netlify deploy retention.
You can have v0-2-1.your-domain.com instead, but only if your DNS
is on Netlify and you manually add each branch subdomain in the
Netlify domain-management UI, once per release — the special
NETLIFY-type DNS records this creates cannot be set up via the API,
so it cannot be scripted. Unless you have a strong cosmetic reason,
skip this: the v<x-y-z>--<site>.netlify.app form avoids that per-version DNS setup.
The promotion flow — prod and staging
Merging to main does not change production here. Production and
staging are promoted from the CLI, each ending in a force-push to
a record branch so the Netlify dashboard reflects what is actually
live:
Both deploy the prebuilt docs/dist/client with --no-build to a
pinned --site id. The typical release, end to end:
pnpm staging is the same promotion aimed at a second Netlify site,
with no version bookkeeping — use it to preview a release first.
In a pnpm workspace, a bare netlify deploy cannot tell which project
it belongs to and hangs on an interactive project picker. Pin it with
--filter <your-docs-package> (this workspace passes --filter docs)
— keep the flag if you invoke the CLI by hand.
The other axis: releasing the library itself
Publishing the documented package to npm is deliberately separate from
all of the above. This workspace wires it as the release pane in
mprocs.yaml: build the library, then pnpm publish with a fresh
OTP fetched at publish time. Docs releases never publish npm packages,
and npm releases never touch versions.json.
Ship it with your own site
@dreamlake/dockit ships all four pieces as templates:
Copy them in, then edit the marked EDIT ME block (your Netlify site
name) at the top of each script:
Wire the scripts into your root package.json:
The templates assume the same layout as this workspace — root
package.json holds the docs-deploy version, the manifest lives at
docs/public/versions.json — and resolve paths from the directory you
invoke them in.