---
title: "Versions plugin"
description: "htmx-ui-plugin-versions: keep every release's docs online as a frozen build, with a version switcher, an old-version banner and commands to name and archive versions."
url: "https://susonwaiba.github.io/htmx-ui/docs/plugins/versions"
section: "Plugins"
---

# Versions plugin

htmx-ui-plugin-versions: keep every release's docs online as a frozen build, with a version switcher, an old-version banner and commands to name and archive versions.

`htmx-ui-plugin-versions` keeps the docs of every release online. The version being written renders live at `/docs`, as usual. Each older one is a **frozen build** in `archive/v<id>/`, served at `/docs/v<id>/`: its pages, scripts and styles exactly as they were released, so later changes to components or layouts can never break them. This website's [versions](https://susonwaiba.github.io/htmx-ui/docs/versions) work this way.

## Install

```bash bun
bun add htmx-ui-plugin-versions
```

```bash npm
npm install htmx-ui-plugin-versions
```

```bash pnpm
pnpm add htmx-ui-plugin-versions
```

```bash yarn
yarn add htmx-ui-plugin-versions
```

```ts htmx-ui.config.ts
import { defineConfig } from "htmx-ui-engine";
import docs from "htmx-ui-plugin-docs";
import versions from "htmx-ui-plugin-versions";

export default defineConfig({
  plugins: [docs(), versions()],
});
```

List the versions. The one being written has no number yet; it is `next` until it is released:

```json data/versions.json
{
  "latest": "next",
  "versions": [{ "id": "next", "label": "next" }]
}
```

```css styles.css
@import "tailwindcss";
@import "htmx-ui/styles.css";
@import "htmx-ui-plugin-versions/styles.css";
```

```ts app.ts
import { initVersions } from "htmx-ui-plugin-versions/client";

document.addEventListener("DOMContentLoaded", () => initVersions());
```

It works without the docs plugin, but with it each archived version also keeps its Markdown and its own sitemap, the sitemaps list every version, and [search](https://susonwaiba.github.io/htmx-ui/docs/plugins/search) follows the version being read.

## The switcher and the banner

```jinja layouts/docs.html
{% from "versions/macros.html" import version_switcher, version_banner %}

<aside>
  {{ version_switcher(class="mb-6 w-full", links=[{ title: "Changelog", href: "/docs/changelog" }]) }}
</aside>
<article>
  {{ version_banner() }}
  <h1>{{ title }}</h1>
</article>
```

| Template | Description |
| --- | --- |
| `version_switcher(class, links)` | A Dropdown listing every version. links are extra entries under a separator: [{ title, href }]. At runtime the client points each version at the same page when it exists there, and adds versions released after the page was built. |
| `version_banner()` | Where an archived page's "you're viewing an old version" notice goes, linking to the same page in the latest version. Empty on the latest docs. |
| `docsVersions()` | The versions file as templates see it: latest, current, label, manifest and versions (id, label, released, path, latest). |

Archiving bakes the banner into every frozen page, so it shows without JavaScript. The client only refreshes it, so an old version learns about releases that came after it.

## Releasing a version

The plugin adds two CLI commands. Give them scripts, so they run on the runtime you build with:

```json package.json
{
  "scripts": {
    "docs:name": "htmx-ui versions:name",
    "docs:archive": "htmx-ui versions:archive"
  }
}
```

```bash bun
bun run docs:name 1.2.0
bun run docs:archive
```

```bash npm
npm run docs:name 1.2.0
npm run docs:archive
```

```bash pnpm
pnpm run docs:name 1.2.0
pnpm run docs:archive
```

```bash yarn
yarn run docs:name 1.2.0
yarn run docs:archive
```

1. **`versions:name <x.y.z>`** numbers `next` and dates it today. Docs are versioned per minor before 1.0 (`0.2.0` becomes `0.2`) and per major after (`1.2.0` becomes `1`); pass `0.2` or `1` to choose yourself. It refuses when the latest version already has a number.
2. **`versions:archive`** builds the site, freezes the latest version's built docs into `archive/v<id>/` and opens a new `next`. Links between docs pages are rewritten into the snapshot, assets move to `_assets/`, pages get `noindex` so old versions don't compete with the latest in search results, and every `<body>` gets `hx-boost="false"` so nothing on a frozen page boosts the live site's code into it. `--skip-build` archives the `dist/` you already have.

Commit `archive/` and `data/versions.json`.

> **Never edit the archive by hand** Snapshots are built output. Fix an old version by rebuilding it from its release and archiving it again. Patch releases update the current docs in place rather than getting a version of their own.

## URLs

| URL | Description |
| --- | --- |
| `/docs/...` | The latest version, rendered from pages/docs. |
| `/docs/v<id>/...` | An archived version, from archive/v<id>/. Also /docs/v<id>.md and each page's .md with the docs plugin. |
| `/docs/versions.json` | The manifest: every version with its path, release date, sitemap and the pages it has. |

The version in development never reaches a URL: it is always served at `/docs`, and only archived versions get a `/v<id>` prefix. Links that must point across versions carry `data-version-link`, which archiving leaves alone, and `hx-boost="false"`: every version ships its own theme and scripts, so switching versions must load the target page fresh instead of swapping the body under this version's stylesheet.

Archiving applies the same rule to the whole snapshot, by setting `hx-boost="false"` on its `<body>`. A frozen page ships its own theme, stylesheet and scripts, so no link inside it — its logo, footer, sidebar or a link in the prose — may be boosted into the current site, and no current page may be swapped into it: every navigation out of a snapshot is a full page load.

## Options

| Option | Description |
| --- | --- |
| `prefix` | Route prefix of the docs, the same as the docs plugin's. Default "/docs". |
| `file` | The versions file, relative to the project root. Default "data/versions.json". |
| `archive` | Where frozen versions are kept, relative to the project root. Default "archive". |

The functions behind the commands are exported for release scripts of your own: `nameVersion`, `startNext`, `archiveDocs`, `versionId`, `readVersionsFile` and `writeVersionsFile`. This repository's `bun run version:set` uses them to name the docs version and set its package versions in one step.
