Versioned Beta Docs Hosting Plan
This document captures the proposed implementation for hosting the VitePress documentation at docs.beta.sqlfluff.com using Netlify for serving and Cloudflare R2 as the persistent store for versioned builds.
The goal is to mirror the useful parts of the current Read the Docs model:
docs.beta.sqlfluff.com/en/latest/built frommaindocs.beta.sqlfluff.com/en/stable/built from the newest non-prerelease releasedocs.beta.sqlfluff.com/en/<version>/built from release tags- A version picker which works across all published versions
Summary
The recommended model is:
- GitHub Actions builds one docs target at a time.
- Cloudflare R2 stores the canonical assembled site tree.
- Netlify serves the assembled site snapshot.
- A shared manifest at
/en/versions.jsondrives the version picker. - VitePress handles
latest,stable, and all post-cutover releases. - Sphinx can later be used for pre-cutover releases.
This avoids rebuilding every historical release on every deployment. Each workflow run rebuilds only the version which changed, merges that output into the assembled site tree, updates the manifest, and republishes the assembled site.
Current Implementation Snapshot
The sections below still describe the target architecture, but the repository now contains a first working slice of that design.
Implemented in the repo so far:
- VitePress can now build against a configurable docs base using
SQLFLUFF_DOCS_BASE, withSQLFLUFF_DOCS_NOINDEXadding a beta-friendly robots tag. - Shared path handling for base-aware assets and redirects now lives in
docsv/.vitepress/path-utils.ts, keeping the VitePress config, main theme, and 404 redirect handling aligned. - The generated docs scripts now write UTF-8 explicitly so Windows builds do not fail on non-ASCII content.
docsv/package.jsonnow runs the Python prebuild step directly indocs:build, avoiding recursivepnpminvocation problems.docsv/scripts/assemble-site.pyassembles a minimal published site tree for Stage 1 with/en/latest/,/en/versions.json, Netlify_redirects, and Netlify_headers..github/workflows/publish-docs.yamlnow implements a latest-only beta publish workflow that builds VitePress, assembles the site, uploads an artifact, syncs the snapshot to R2, and deploys the same snapshot to Netlify.- The workflow now passes R2 S3-compatible credentials directly to the
awsCLI sync step rather than using the AWS-onlyaws-actions/configure-aws-credentialsaction. - The workflow currently triggers on pushes to
main, pushes toac/docsdeploy, andworkflow_dispatch.
Validated locally so far:
- A Windows build of
corepack pnpm run docs:buildsucceeded withSQLFLUFF_DOCS_BASE=/en/latest/andSQLFLUFF_DOCS_NOINDEX=1. - The assembled site helper produced the expected
site/en/latest/,site/en/versions.json,_redirects, and_headersoutputs.
Validated live so far:
- A successful GitHub Actions publish using the repository secrets deployed the Stage 1 latest-only snapshot.
https://docs.beta.sqlfluff.com/responds with an HTTP 302 redirect to/en/latest/.https://docs.beta.sqlfluff.com/en/latest/serves the assembled VitePress snapshot with anoindex,nofollowrobots tag.https://docs.beta.sqlfluff.com/en/versions.jsonserves the expected minimal manifest with a singlelatestentry.
Still not confirmed from this repository alone:
- Release-driven
/en/<version>/and/en/stable/publishing. - A live rollback or rebuild flow using the assembled snapshot in R2.
Architecture
Responsibilities
- GitHub Actions: Build docs, update the assembled site, and trigger deploys.
- Cloudflare R2: Durable store for the published multi-version site tree.
- Netlify: Serve the assembled snapshot at
docs.beta.sqlfluff.com. - VitePress: Render
latest,stable, and future release docs. - Sphinx: Later fallback for versions older than the VitePress cutover.
Why R2 Is The Source Of Truth
Netlify is good at serving static content, but each deploy effectively replaces the deployed site. That is not a good fit for long-lived version archives.
R2 is a better canonical store because it lets the workflows:
- keep old built versions indefinitely
- update only one version subtree at a time
- recover older versions without rebuilding everything else
- survive dependency drift in historical tags
Canonical Site Layout
The assembled site stored in R2 should look like this:
site/
en/
latest/
stable/
4.3.0/
4.2.1/
versions.json
shared/
version-picker.js
version-picker.cssNotes:
latestis built frommain.stableis built from the newest final release.- Release folders are built from git tags.
shared/holds assets reused by both VitePress and Sphinx outputs.
Snapshot Storage Strategy
R2 should retain both of the following:
- an exploded assembled site tree for the current published view
- immutable assembled snapshot archives for rollback, audit, and manual imports
This keeps day-to-day publishing simple while preserving a stronger rollback and recovery path.
Build And Deploy Model
Each deploy follows the same high-level pattern:
- Checkout the target ref.
- Download the current assembled site snapshot from R2.
- Build only the requested docs variant.
- Replace only the relevant subtree in the assembled snapshot.
- Regenerate
/en/versions.json. - Upload the updated snapshot back to R2.
- Deploy that assembled snapshot to Netlify.
This keeps the build incremental while still publishing a complete static site.
Deploy From main
Trigger: push to main
Expected result:
- Build VitePress once with base
/en/latest/ - Replace only
/en/latest/ - Regenerate
versions.json - Leave
/en/stable/and all release folders untouched - Sync the assembled tree to R2
- Deploy the assembled tree to Netlify
Deploy From Release Tag
Trigger: GitHub release published
Expected result:
- Checkout the release tag
- Build VitePress for
/en/<version>/ - Replace only
/en/<version>/ - If the release is not a prerelease, also build VitePress for
/en/stable/ - Regenerate
versions.json - Sync the assembled tree to R2
- Deploy the assembled tree to Netlify
Important detail:
stable should be built separately rather than implemented as a redirect or a copy of /en/<version>/. VitePress bakes the base path into links and asset paths, so /en/stable/ wants a dedicated build with its own base.
Manual Rebuild Of Older Release
Trigger: workflow_dispatch
Expected result:
- Accept a version tag as input
- Validate the tag exists
- Determine whether to use VitePress or Sphinx
- Build only that version
- Replace only
/en/<version>/ - Optionally refresh
/en/stable/when explicitly requested and appropriate - Regenerate
versions.json - Republish the assembled snapshot
Version Picker Design
Registry Model
Published versions should not need to register themselves with each other at build time. Instead, the picker should read a shared runtime manifest from /en/versions.json.
That manifest should be updated by the publishing workflow each time a version is added, rebuilt, promoted to stable, or hidden.
Initial Manifest Shape
The exact schema can evolve, but a practical initial contract is:
{
"default": "latest",
"latest": "latest",
"stable": "4.3.0",
"versions": [
{
"key": "latest",
"label": "latest",
"title": "Development",
"path": "/en/latest/",
"kind": "channel",
"builder": "vitepress",
"prerelease": false
},
{
"key": "stable",
"label": "stable",
"title": "Stable",
"path": "/en/stable/",
"kind": "channel",
"builder": "vitepress",
"prerelease": false
},
{
"key": "4.3.0",
"label": "4.3.0",
"title": "4.3.0",
"path": "/en/4.3.0/",
"kind": "release",
"builder": "vitepress",
"prerelease": false,
"published_at": "2026-05-19"
}
]
}Picker Behavior
The initial picker behavior should stay simple:
- First rollout: switch to the selected version root.
- Later enhancement: attempt to preserve the current relative page path, with fallback to the selected version root if the page does not exist.
Root-only switching is the default until redirect parity and mixed VitePress or Sphinx path compatibility are good enough to justify a smarter switch.
Shared Runtime Assets
The final picker should not be VitePress-only. Instead, publish shared assets under /en/shared/ so both VitePress and Sphinx versions can load the same runtime picker and manifest.
This avoids having to rebuild every historical version whenever a new version is published.
Workflow Layout
The workflows should be organized around one reusable deploy workflow plus thin trigger workflows.
Reusable Workflow
Purpose: perform the shared assembly and deploy steps.
Inputs should include:
- git ref or tag to build
- output target such as
latest,stable, or4.3.0 - builder type:
vitepressorsphinx - whether to refresh
stable - whether the target is prerelease
Trigger Workflows
Main docs deploy
- Trigger: push to
main - Builder: VitePress
- Target:
latest
- Trigger: push to
Release docs deploy
- Trigger: release published
- Builder: VitePress or Sphinx depending on cutover policy
- Target:
<version> - Also refresh
stablefor non-prereleases
Manual docs rebuild
- Trigger: workflow_dispatch
- Builder: auto-detected from version and cutover policy
- Target: explicit version tag
Compatibility And Operational Policy
Redirect Compatibility Scope
- Hard requirement: documentation URLs emitted by CLI and runtime surfaces in SQLFluff
2.0.0and later must continue to resolve. - Strong parity target: preserve all currently declared Sphinx permalinks and redirects.
- Best-effort target: preserve broader historical site structure where it helps with SEO or older inbound links.
This is a link-resolution guarantee, not a promise that every historical version is fully hosted on day one. When an older CLI or runtime link already targets a stable or permalink route, preserving that canonical route is sufficient.
Rebuild And Import Policy
- Historical rebuilds should default to rebuilding the tagged source with the currently approved builder image.
- A rebuild is considered acceptable when it preserves materially equivalent user-facing content, version identity, and important permalinks or redirects. It does not need to reproduce byte-identical HTML or search output.
- If a historical tag no longer builds cleanly with the maintained toolchain, maintainers may publish an archived static snapshot for that version instead.
- If neither a rebuild nor a snapshot import is practical, the version remains unpublished until a manual artifact is supplied.
- For the initial beta launch, it is enough to prove that historical versions can be hosted; broad backfill can happen later.
Release Channel Policy
- Final releases should publish
/en/<version>/and automatically refresh/en/stable/. - Prereleases may be published at their direct version URLs, but should be hidden from the version picker.
latestis the only channel where edit links are important. Other channels may either omit edit links or point tomain; version-accurate edit links are not required for the first rollout.- The beta site may be publicly reachable, but it should remain
noindexuntil cutover work begins.
Recommended Serving Defaults
- Treat HTML pages and
/en/versions.jsonas mutable content and serve them withno-cacheormust-revalidatesemantics. - Treat
latestandstableas mutable channels. - Cache fingerprinted static assets aggressively as immutable.
- If shared picker assets are not fingerprinted, keep them on a short cache.
Stable Promotion Safeguard
- Automatic promotion of
stableis the default behavior for final releases. - Maintainers should retain a manual override so
stablecan be repointed to an older version or snapshot if needed.
Staged Implementation Plan
Stage 0: Infrastructure Bootstrap
Status: complete for the current Stage 1 slice. The R2 bucket, Netlify site, custom domain, and repository secrets are now wired well enough to publish the live beta docs site.
Deliverables:
- Create the R2 bucket
- Create an R2 API token for GitHub Actions
- Create or reuse the Netlify site for beta docs
- Wire
docs.beta.sqlfluff.com - Add repository secrets
- Set the first VitePress-native release tag to
4.2.0 - Decide whether prereleases appear in the picker
Stopping point:
- No code deployed yet, but infrastructure and policies are ready
Stage 1: latest Only On Beta
Status: complete for the initial beta rollout. The repository workflow now publishes latest successfully and the beta site is live.
Deliverables:
- Make the VitePress base configurable at build time
- Replace the current GitHub Pages-only docs deploy path
- Add a workflow for pushes to
main - Build and publish only
/en/latest/ - Generate a minimal
versions.json
Stopping point:
docs.beta.sqlfluff.com/en/latest/is live- R2 is storing the canonical assembled tree
- Netlify is serving the assembled tree
Stage 2: Manifest And Initial Version Picker
Status: partially complete. The minimal shared manifest is now live, but the runtime version picker UI has not been added yet.
Deliverables:
- Define the
versions.jsonschema - Add a basic picker to the VitePress theme
- Read version data from the shared manifest at runtime
- Keep initial switching behavior simple by targeting version roots only
Stopping point:
latesthas a working picker framework- The registry model is validated before release builds depend on it
Stage 3: Tagged Releases And stable
Deliverables:
- Add release-triggered docs deployment
- Build and publish
/en/<version>/ - For final releases, also build and publish
/en/stable/ - Promote
stableautomatically using the existing non-prerelease semantics already used by the release process - Retain a manual override so maintainers can repoint
stableif required - Update the manifest so release versions appear in the picker
Stopping point:
/en/latest/,/en/stable/, and/en/<version>/all work for VitePress releases- The cutover point remains configurable so
4.2.0can be moved back to Sphinx later if unresolved VitePress issues are found
Stage 4: Manual Rebuild Workflow
Deliverables:
- Add
workflow_dispatchfor rebuilding a chosen version tag - Validate the tag exists before build
- Rebuild only the requested version subtree
- Support importing an archived static snapshot when rebuilding is not practical
- Keep immutable assembled snapshots so rollback and manual imports use the same artifact model
- Optionally refresh
stablein controlled cases
Stopping point:
- Historical versions can be republished without reissuing releases
Stage 5: Shared Picker Assets For Cross-Builder Support
Deliverables:
- Move picker runtime logic into shared JS and CSS under
/en/shared/ - Keep
versions.jsonas the single registry source of truth - Load the same picker assets in VitePress and future Sphinx outputs
Stopping point:
- Older published versions do not need rebuilding just to learn about new versions
Stage 6: Sphinx Backfill For Pre-Cutover Versions
Deliverables:
- Add a cutover version config with an initial value of
4.2.0as the first VitePress-native release - Build versions older than that cutoff using the existing Sphinx toolchain
- Inject shared picker assets into Sphinx output
- Add a controlled backfill workflow for selected historical releases
- Treat the initial proof as successful once
latest,stable, and one older Sphinx-hosted version are live under the beta domain
Stopping point:
- Beta can host a mixed set of VitePress and Sphinx versions under one domain
Stage 7: Redirect And Compatibility Hardening
Deliverables:
- Review parity with current
docs.sqlfluff.comURLs - Preserve documentation URLs emitted by CLI and runtime surfaces in supported SQLFluff versions from
2.0.0onward - Preserve or replace important
.html, permalink, and declared redirect routes - Add or refine Netlify redirects where needed
- Improve picker behavior to preserve current page paths where possible
- Review edit links, 404 behavior, and search behavior across versions
Stopping point:
- Beta is close enough to production parity to be considered for cutover
Stage 8: Final Cutover
Deliverables:
- Point
docs.sqlfluff.comat the Netlify-hosted assembled site - Preserve the
latest,stable, and versioned URL structure - Retire or reduce dependency on Read the Docs
Stopping point:
- The new docs hosting becomes the primary production site
Repository Changes Expected In Early Stages
These files are likely to be touched first:
docsv/.vitepress/config.tsdocsv/.vitepress/theme/index.tsdocsv/package.json.github/workflows/publish-docs.yaml- one or more new helper scripts for manifest generation and site assembly
These files are likely to be touched later:
pyproject.tomlutil.pydocs/source/conf.pydocs/generate-auto-docs.py
Secrets And Configuration
Expected repository secrets:
R2_ACCOUNT_IDR2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_BUCKETNETLIFY_AUTH_TOKENNETLIFY_SITE_ID
Expected repository or project-level configuration values:
- first VitePress-native release tag, initially
4.2.0 - redirect compatibility floor, starting at SQLFluff
2.0.0 - whether manual rebuilds may promote
stable - whether the beta environment should emit
noindex
Deferred Decisions
The following can be revisited after the initial beta proof:
- How much historical Sphinx backfill should be added beyond the first older hosted version?
Immediate Next Steps
When resuming this work on another machine, the next high-value checks are:
- Add the initial runtime version picker in VitePress, backed by the live
/en/versions.jsonmanifest. - Add the release-triggered publish path for
/en/<version>/and/en/stable/. - Teach the assembly flow to merge an existing R2 snapshot instead of always rebuilding a latest-only tree.
- Prove one historical rebuild path, either from a VitePress-native release or from a controlled Sphinx-hosted version.
Success Criteria
The plan should be considered successful when:
mainautomatically publishes to/en/latest/- each release automatically publishes to
/en/<version>/ - the newest final release is available at
/en/stable/ - the picker lists available versions from a shared manifest
- prereleases can be published directly without appearing in the picker
- rebuilding a historical version does not require rebuilding every other version
- documentation URLs emitted by SQLFluff
2.0.0and later continue to resolve - pre-cutover versions can still be hosted under the same beta domain
- the beta site can remain public while staying
noindexuntil cutover - the beta site is close enough to replace
docs.sqlfluff.comwhen desired