# 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](#ship-it-with-your-own-site).

## Two independent version axes

Do not conflate these; they move on their own schedules:

1. **Your product/library semver** — the thing the docs document
   (here: `@dreamlake/dockit`, published to npm). Its version lives in
   the package's own `package.json` and shows in the topbar's
   [version chips](/reference/components/topbar.md). A product can have
   its own dropdown and manifest.
2. **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.json` and the manifest at
   `docs/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.

```ts
versionChips: [{
  label: 'dockit', version: __DOCKIT_VERSION__, dropdown: true,
  manifestUrl: '/dockit-versions.json',
}]

// Python Autodoc tab override
versionChips: [{ label: 'autodoc-py', version: __AUTODOC_REVISION__, prefix: '' }]
```

`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](/reference/config.md#versionchip).

## 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:

```json file="docs/public/versions.json"
{
  "current": "0.2.1",
  "stable": "0.1.0",
  "aliases": {
    "latest": "https://dockit.dreamlake.ai",
    "stable": "https://stable.dockit.dreamlake.ai"
  },
  "versions": [
    { "version": "0.2.1", "url": "https://v0-2-1--dockit-docs.netlify.app", "date": "2026-07-20" },
    { "version": "0.2.0", "url": "https://v0-2-0--dockit-docs.netlify.app", "date": "2026-07-20" },
    { "version": "0.1.0", "url": "https://v0-1-0--dockit-docs.netlify.app" }
  ]
}
```

- **`current`** — the newest release; what the topbar chip labels
  itself when the hostname gives no better answer.
- **`stable`** — the version the `stable` alias points at (may trail
  `current`).
- **`aliases`** — stable URLs that always point somewhere sensible;
  `latest` is your production domain.
- **`versions[]`** — newest first; each entry's `url` is the documentation
  destination for that version. Only describe it as frozen when its
  deployment is actually retained unchanged. `date` is 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):

```toml file="netlify.toml"
[[headers]]
for = "/versions.json"

[headers.values]
Access-Control-Allow-Origin = "*"
```

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):

<pre
  style={{
    maxHeight: '32rem',
    overflow: 'auto',
    padding: '1rem 1.25rem',
    background: 'var(--color-doc-template-code)',
    border: '1px solid var(--color-doc-template-faint)',
    borderRadius: 'var(--radius-doc-template-sm)',
    color: 'var(--color-doc-template-ink)',
    fontFamily: 'var(--font-doc-template-mono)',
    fontSize: '0.8125rem',
    lineHeight: 1.6,
    whiteSpace: 'pre-wrap',
  }}
>
  {setVersionSrc}
</pre>

```bash
pnpm set-version 0.2.1
```

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](#netlify-branch-deploys--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`:

<pre
  style={{
    maxHeight: '32rem',
    overflow: 'auto',
    padding: '1rem 1.25rem',
    background: 'var(--color-doc-template-code)',
    border: '1px solid var(--color-doc-template-faint)',
    borderRadius: 'var(--radius-doc-template-sm)',
    color: 'var(--color-doc-template-ink)',
    fontFamily: 'var(--font-doc-template-mono)',
    fontSize: '0.8125rem',
    lineHeight: 1.6,
    whiteSpace: 'pre-wrap',
  }}
>
  {versionBranchSrc}
</pre>

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:

```text
https://<branch>--<site-name>.netlify.app
```

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](https://cli.netlify.com/commands/deploy/).

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](https://docs.netlify.com/deploy/manage-deploys/manage-deploys-overview/#automatic-deploy-deletion).

> **Note:** 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:

```bash
pnpm prod      # version-branch → build → netlify deploy --prod → force-push netlify-production
pnpm staging   # build → netlify deploy --prod (staging site) → force-push netlify-staging
```

Both deploy the prebuilt `docs/dist/client` with `--no-build` to a
pinned `--site` id. The typical release, end to end:

```bash
pnpm set-version 0.2.1
git commit -am "release: docs 0.2.1"
pnpm prod
```

`pnpm staging` is the same promotion aimed at a second Netlify site,
with no version bookkeeping — use it to preview a release first.

> **Warning:** ">
> 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:

```bash
ls node_modules/@dreamlake/dockit/templates/
# set-version.mjs  version-branch.sh  versions.example.json  netlify.example.toml
```

Copy them in, then edit the marked `EDIT ME` block (your Netlify site
name) at the top of each script:

```bash
cp node_modules/@dreamlake/dockit/templates/set-version.mjs scripts/
cp node_modules/@dreamlake/dockit/templates/version-branch.sh scripts/
cp node_modules/@dreamlake/dockit/templates/versions.example.json docs/public/versions.json
cp node_modules/@dreamlake/dockit/templates/netlify.example.toml netlify.toml
```

Wire the scripts into your root `package.json`:

```json
"scripts": {
  "set-version": "node scripts/set-version.mjs",
  "version-branch": "bash scripts/version-branch.sh"
}
```

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.
