Problem
The current @micro509/vitepress-versioned-docs design deliberately renders the complete documentation history in one VitePress build:
/ — newest published release
/next/ — checked-out tree
/vX.Y.Z/ — every superseded release
versionedDocs() fetches the npm packument, downloads/materializes every tagged source tree, regenerates the API reference for every release, and then exposes all of those pages to one VitePress build. The local tag-tree cache saves downloads, but clean CI builds still regenerate and render an increasingly large number of old sites. Build time and output size therefore grow with every release, and the existing 19,500-file guardrail will eventually become the limiting factor.
Today that cost is paid twice per push: the GitHub Pages workflow and Cloudflare's push-triggered build each render the complete history, and every PR preview repeats it on Cloudflare's always-clean builders.
Old documentation is immutable build output. It should be built once and preserved, not reconstructed from source on every deployment.
Proposed architecture
Keep the existing Cloudflare deployment and custom domain. Do not move to GitHub Pages and do not create a hostname/subdomain per version. Continue serving path-based versions from one origin:
/ latest stable release
/next/ current master
/v0.11.0/ immutable release snapshot
/v0.10.0/ immutable release snapshot
...
Use a generated orphan branch, e.g. docs-site, as a persistent deployment tree:
/
├── index.html
├── assets/ # latest stable root build
├── guide/
├── reference/
├── api/
├── next/ # replaced on each master deployment
├── v0.11.0/ # written once
├── v0.10.0/ # written once
├── versions.json # small mutable global catalog
└── ...
Cloudflare should deploy this already-built tree. GitHub Actions performs the incremental mutation; Cloudflare does not need to rebuild VitePress.
Build behaviour
Push to master
Build only the checked-out tree for /next/:
- Check out
docs-site into a worktree such as .publish.
- Build the current source with the
/next/ base/prefix.
- Replace only
.publish/next/ using rsync --delete within that directory.
- Refresh
versions.json metadata if necessary.
- Preserve
/, /v*/, and every other archived path untouched.
- Commit/push the resulting deployment tree and deploy it.
The cost remains one VitePress build regardless of the number of releases.
Release tag vX.Y.Z
Build only the new release:
- Check out the existing
docs-site tree.
- Build the tag with base
/vX.Y.Z/ and write it to .publish/vX.Y.Z/.
- Refuse to overwrite an existing release directory unless an explicit repair mode is used; release snapshots should be immutable.
- Build the same tag once more with base
/ and replace only the root/latest files, excluding next/ and v*/ from deletion.
- Update
versions.json.
- Commit/push and deploy.
A release therefore costs at most two builds: one immutable versioned snapshot and one root/latest build. No older release is read or rebuilt.
A later optimization could redirect the unversioned root to the latest /vX.Y.Z/ snapshot and eliminate the second build, but preserving the current root URLs and SEO behaviour is simpler initially.
Pull request preview
Cloudflare PR preview builds are the one retained benefit of push-triggered building. A preview renders only the checked-out tree as a single next-style target with release discovery offline: one VitePress build, no packument fetch, no tag trees. Previews never touch docs-site.
Required refactor
1. Split release discovery from release rendering
The plugin currently resolves the complete release catalog and materializes every release before VitePress starts. Introduce a single-target build mode instead, for example:
DOCS_CHANNEL=next
DOCS_CHANNEL=latest DOCS_VERSION=v0.11.0
DOCS_CHANNEL=archive DOCS_VERSION=v0.11.0
Exact names are not important, but one build must materialize and render only one source tree. The full npm release list should be metadata, not a list of source trees that must all be generated.
The existing all-versions mode can remain temporarily as a bootstrap/repair command, but it should no longer be used for normal deployments.
2. Make the version switcher use the global catalog at runtime
Currently themeConfig.versions is compiled into every generated site. An immutable old snapshot would therefore never learn about newer versions unless rebuilt.
Serve the authoritative catalog at /versions.json and have VersionSwitcher load it from the absolute root at runtime, with the embedded build-time list only as a fallback. This allows an old /vX.Y.Z/ snapshot to show every later release without modifying its files.
Version-specific sidebars, import maps, page rewrites, source links, and API docs remain static and baked into the snapshot.
3. Scope deletion carefully
Never run rsync --delete over the whole publish tree without exclusions.
For next:
rsync -a --delete site/.vitepress/dist/ .publish/next/
For root/latest, delete stale root files while preserving archives:
rsync -a --delete \
--exclude '/next/' \
--exclude '/v*/' \
site/.vitepress/dist/ \
.publish/
The exact staging paths may differ once single-target mode exists, but deletion must be constrained to the target being replaced.
4. Serialize deployments
Use one GitHub Actions concurrency group for the publish tree:
concurrency:
group: docs-site
cancel-in-progress: false
This prevents a master deployment and release deployment from checking out the same old docs-site tip and overwriting each other.
5. Keep generated-branch history bounded
docs-site should be an orphan branch containing generated output only. Avoid accumulating every historical replacement of large generated files forever.
Either:
- amend/squash the branch to one current snapshot and push with
--force-with-lease, or
- periodically recreate/squash the branch while keeping the deployed tree identical.
The immutable documentation directories remain present in the current tree; they do not need Git history to provide immutability.
6. Deploy the prebuilt tree to Cloudflare
Cloudflare is the only production origin. The GitHub Pages deployment serves no consumer and is retired as part of this work; its removal costs nothing because per-PR previews, the one feature in use, come from Cloudflare (preview_urls).
Two viable implementations:
- GitHub Actions directly runs
wrangler deploy against .publish, or
- Cloudflare watches the generated
docs-site branch and performs no VitePress build (exit 0, branch root as the static asset directory).
Direct deployment from the workflow is probably less magical and keeps the mutation and deployment atomic, while still retaining docs-site as an inspectable, host-independent artifact.
Migration
- Run the current complete-history build once to bootstrap
docs-site with all existing versions.
- Add single-target build support to
vitepress-versioned-docs.
- Change
VersionSwitcher to consume /versions.json dynamically.
- Add the serialized incremental deployment workflow.
- Point Cloudflare at the prebuilt output/direct Wrangler deployment.
- Retire the GitHub Pages deployment (
deploy-site.yml); Cloudflare serves production and PR previews.
- Stop using the complete-history build for ordinary master, release, and PR preview deployments.
- Keep an explicit manual repair workflow capable of rebuilding one selected version or, if genuinely necessary, bootstrapping the complete site again.
Acceptance criteria
Non-goals
- Moving the site to GitHub Pages.
- Creating a Cloudflare Worker/project or custom domain per release.
- Rebuilding old releases merely to update global navigation metadata.
- Switching existing patch-version URLs to minor-version URLs as part of this change; that can be considered separately without coupling it to the incremental deployment work.
Problem
The current
@micro509/vitepress-versioned-docsdesign deliberately renders the complete documentation history in one VitePress build:/— newest published release/next/— checked-out tree/vX.Y.Z/— every superseded releaseversionedDocs()fetches the npm packument, downloads/materializes every tagged source tree, regenerates the API reference for every release, and then exposes all of those pages to one VitePress build. The local tag-tree cache saves downloads, but clean CI builds still regenerate and render an increasingly large number of old sites. Build time and output size therefore grow with every release, and the existing 19,500-file guardrail will eventually become the limiting factor.Today that cost is paid twice per push: the GitHub Pages workflow and Cloudflare's push-triggered build each render the complete history, and every PR preview repeats it on Cloudflare's always-clean builders.
Old documentation is immutable build output. It should be built once and preserved, not reconstructed from source on every deployment.
Proposed architecture
Keep the existing Cloudflare deployment and custom domain. Do not move to GitHub Pages and do not create a hostname/subdomain per version. Continue serving path-based versions from one origin:
Use a generated orphan branch, e.g.
docs-site, as a persistent deployment tree:Cloudflare should deploy this already-built tree. GitHub Actions performs the incremental mutation; Cloudflare does not need to rebuild VitePress.
Build behaviour
Push to
masterBuild only the checked-out tree for
/next/:docs-siteinto a worktree such as.publish./next/base/prefix..publish/next/usingrsync --deletewithin that directory.versions.jsonmetadata if necessary./,/v*/, and every other archived path untouched.The cost remains one VitePress build regardless of the number of releases.
Release tag
vX.Y.ZBuild only the new release:
docs-sitetree./vX.Y.Z/and write it to.publish/vX.Y.Z/./and replace only the root/latest files, excludingnext/andv*/from deletion.versions.json.A release therefore costs at most two builds: one immutable versioned snapshot and one root/latest build. No older release is read or rebuilt.
A later optimization could redirect the unversioned root to the latest
/vX.Y.Z/snapshot and eliminate the second build, but preserving the current root URLs and SEO behaviour is simpler initially.Pull request preview
Cloudflare PR preview builds are the one retained benefit of push-triggered building. A preview renders only the checked-out tree as a single
next-style target with release discovery offline: one VitePress build, no packument fetch, no tag trees. Previews never touchdocs-site.Required refactor
1. Split release discovery from release rendering
The plugin currently resolves the complete release catalog and materializes every release before VitePress starts. Introduce a single-target build mode instead, for example:
Exact names are not important, but one build must materialize and render only one source tree. The full npm release list should be metadata, not a list of source trees that must all be generated.
The existing all-versions mode can remain temporarily as a bootstrap/repair command, but it should no longer be used for normal deployments.
2. Make the version switcher use the global catalog at runtime
Currently
themeConfig.versionsis compiled into every generated site. An immutable old snapshot would therefore never learn about newer versions unless rebuilt.Serve the authoritative catalog at
/versions.jsonand haveVersionSwitcherload it from the absolute root at runtime, with the embedded build-time list only as a fallback. This allows an old/vX.Y.Z/snapshot to show every later release without modifying its files.Version-specific sidebars, import maps, page rewrites, source links, and API docs remain static and baked into the snapshot.
3. Scope deletion carefully
Never run
rsync --deleteover the whole publish tree without exclusions.For
next:For root/latest, delete stale root files while preserving archives:
The exact staging paths may differ once single-target mode exists, but deletion must be constrained to the target being replaced.
4. Serialize deployments
Use one GitHub Actions concurrency group for the publish tree:
This prevents a master deployment and release deployment from checking out the same old
docs-sitetip and overwriting each other.5. Keep generated-branch history bounded
docs-siteshould be an orphan branch containing generated output only. Avoid accumulating every historical replacement of large generated files forever.Either:
--force-with-lease, orThe immutable documentation directories remain present in the current tree; they do not need Git history to provide immutability.
6. Deploy the prebuilt tree to Cloudflare
Cloudflare is the only production origin. The GitHub Pages deployment serves no consumer and is retired as part of this work; its removal costs nothing because per-PR previews, the one feature in use, come from Cloudflare (
preview_urls).Two viable implementations:
wrangler deployagainst.publish, ordocs-sitebranch and performs no VitePress build (exit 0, branch root as the static asset directory).Direct deployment from the workflow is probably less magical and keeps the mutation and deployment atomic, while still retaining
docs-siteas an inspectable, host-independent artifact.Migration
docs-sitewith all existing versions.vitepress-versioned-docs.VersionSwitcherto consume/versions.jsondynamically.deploy-site.yml); Cloudflare serves production and PR previews.Acceptance criteria
masterdeployment invokes VitePress exactly once and changes only/next/plus global metadata./vX.Y.Z/snapshot plus root/latest./vX.Y.Z/directories are byte-for-byte untouched by later deployments./,/next/, and/vX.Y.Z/public URL scheme remains valid.Non-goals