Repository navigation
Implement multi-version documentation support (follow-up to #320) #325
Description
Activity
- added 6 commits that reference this issue
on Jun 10, 2026 Progress update
The first two pieces of the must-have track are up:
Piece 1 — generator per-version pinning ✅ merged (#326)
- Reference generators are now parameterized by target collection (
COLLECTION=),
with URLs/canonicals derived from the collection rather than hardcoded/latest/. localize_linksrewrites a generated page's own intra-product links to its
version base, so frozen versions stay coherent (cross-product links still follow
latestper policy).- Fixed a latent OpenFact bug (pages stamped
/openvox/latestwhile writing into
_openfact_*) and keyed the Strings JSON cache per collection. - An explicit
VERSIONalready bypasses the prerelease filter, so a version can be
built from a prerelease tag (verified against9.0.0-alpha1). - CONTRIBUTING documents the
COLLECTIONflag.
Piece 1b + CI wiring —
references:all+_data/versions.yml🔍 in review (#328)_data/versions.ymlis the source-of-truth registry: per product/version
collection, base URL,latestalias, and (for generated products) the rake task- exact upstream tag to build from.
latestis an alias, never its own build row;
authored-only products carry no pin.
- exact upstream tag to build from.
rake references:allbuilds every pinned(tag → collection)pair in its own
subprocess;build.yamlnow calls it instead of the three hardcoded lines.
Heads-up — one intended, deploy-visible behavior change in #328: generated
reference pages now build into the real version dirs, so their inline body links
resolve to the concrete version (e.g./openvox/8.x/...) rather than/latest/.
This is what keeps a frozen version coherent once a new major ships. Scope is narrow
(the type-preamble links); nav chrome and canonicals are unaffected; a full local
build + htmlproofer found 0 broken links across 1776.Decisions settled along the way
- Reference pins use exact tags, not branches or "newest" — reproducible and
avoidsnewest/mainjumping majors. - Cross-product links follow
latest(already the documented policy); only
intra-product links get version-localized. - The version selector keys (
label,base,latest,single_version) live in
versions.ymlnow but aren't consumed until the selector lands.
Remaining on the must-have track
- Piece 2 — version selector UI (consumes the registry fields above).
- Cutover runbook + dry run against an OpenVox 9 prerelease.
The earlier hardening backlog (drift validation, link-hygiene lint, frozen-version
link tests, etc.) remains non-blocking.- Reference generators are now parameterized by target collection (
- added 4 commits that reference this issue
on Jun 11, 2026 Update
Progress since the last note:
Version selector — built and verified 🔍 (held to stack on #328)
A custom per-product selector at the top of the sidebar (driven by the pin
table). It lists the current product's versions, badges the onelatestaliases,
marks the version being viewed, and is hidden until a product actually has 2+
versions. Verified against a local two-version test bed (OpenVox 8.x + a
9.0.0-alpha1 9.x): correct badge/active on direct loads of 8.x, 9.x and the latest
alias, correct group-swap + active marking across Turbo navigation between
products, and Containers excluded as single-version.Notable: the VitePress theme ships a built-in version selector, but it's a single
global dropdown — it can't express per-product versions on a multi-product
site, so a custom selector was the right call. It also meant our data file was
squatting on the theme's reservedsite.data.versions, so the pin table was
renamed_data/versions.yml→_data/products.yml(folded into #328).Version links currently point at each version's root; keeping the reader on the
same page across a switch (path-preserving) needs per-navigation JS in the
persistent Turbo sidebar and is a noted follow-up.Cutover mechanics validated
Standing up the test bed doubled as a dry-run of the "add a major version" steps:
cp -rthe docs dir → register the collection in_config.yml(collections:+
defaults:) → add it to the product'snav_map.ymlentry → add a version block to
products.yml→ generate refs. All worked end-to-end, so the cutover runbook is
largely de-risked.State of the must-have track
- Generator per-version pinning — merged (Parameterize reference generation by target collection #326), live (no-op on output as expected).
references:all+ pin table + CI wiring — in review (Add references:all version pin table and wire it into CI #328), CI green.- Version selector — built + verified, committed locally, stacked on Add references:all version pin table and wire it into CI #328
(opens once Add references:all version pin table and wire it into CI #328 merges). - Cutover runbook — mechanics validated; remaining is writing it up + a real
dry-run as 9.0 nears.
- added a commit that references this issue
on Jun 11, 2026 Cutover-runbook note: version-string content sweep
A dry run of the cutover (standing up a local
_openvox_9xfrom_openvox_8x)
surfaced a step beyond the structural mechanics: after copying a product's docs
directory, the authored pages still carry the old version's strings — the index
# OpenVox 8title, the "OpenVox 8 Platform" nav heading, "8.x" references, and
version-specific prose. The version selector only routes between collections; it
doesn't rewrite content.So the cutover runbook needs an explicit content-sweep step for the copied
authored pages: update titles, nav headings, and intra-doc version references to the
new major. A blind find/replace is risky — some "OpenVox 8" / "8.x" mentions are
legitimately historical or compatibility notes — so it's a careful pass, not a
global substitution.Generated reference pages don't need this:
references:allrebuilds them from the
new version's pinned tag, so they self-update.Updated cutover steps (structural mechanics validated by the dry run):
cp -r docs/_<product>_<old> docs/_<product>_<new>- Register the new collection in
_config.yml(collections:+defaults:) - Add the new collection to the product's
nav_map.ymlentry - Add a version block to
_data/products.yml(and movelatest:if appropriate) - Content sweep the copied authored pages for version-specific strings ← new
- Generate refs for the new version (
references:all, or per-product with the pin)
Refinement on the content-sweep step (5)
Looked at whether the version strings could be templated to avoid the sweep
entirely — they can't, sensibly. In the OpenVox docs it's only ~40 hits (~23
"OpenVox 8" + ~17 "8.x") across ~10 files, and most are version-specific
assertions, not "current version" placeholders, e.g.:- "OpenVox 8.x still supports it for backward compatibility, but use v5 instead"
- "This style guide applies to OpenVox 8 and later"
- the
title: "Upgrading OpenVox 8"page is inherently about 8
A templated
OpenVox {{ version }}would auto-falsify these on cutover (the compat
note must not silently claim 9.x). Pagetitle:also lives in front matter, which
Jekyll doesn't run Liquid through. So the strings stay literal and the sweep stays a
human review — but a guided one.Refined step 5: instead of an ad-hoc "content sweep," run a grep over the freshly
copied collection and review each hit in context (decide bump vs. keep-as-historical):grep -rnE 'OpenVox [0-9]+|[0-9]+\.x' docs/_openvox_9x --include=*.md --include=*.markdownIt's a few-minutes, once-per-major task, and human review is required regardless
because the statements are version-specific (not mechanically substitutable).4 remaining items
- added 2 commits that reference this issue
on Jun 15, 2026 Status update — must-have track
Piece 2 (cutover + runbook) — done in #337.
MAINTAINING.mdcovers the copy → symlink repoint →_config.yml/nav registration → build-both-versions flow, plus a rollback section. The dry-run gate was exercised: a full two-phase OpenVox 9 cutover against9.0.0-alpha1, with 8.x confirmed frozen. (A final dry-run at the real 9.0 tag is still worth doing at cutover time.)Piece 3 (version selector) — shipped in #335, with one deferred refinement. The per-product picker reads
_data/products.yml, marks the current version, badges thelatest-aliased one, and stays hidden until a product has 2+ versions. It links to each version's root, not the same page path — path-preserving switching was deferred and is now tracked as a hardening-backlog item. Flagging so the checked box isn't read as "same-page switch works."Related: per-series generated data (prereq #332)
#339 (the new component-versions reference page) implements the multi-series data partitioning #332 proposed, and extends it beyond the agent table to server and OpenVoxDB:
- Generated data moved from shared global files to per-series files (
_data/<table>/<nav_key>.yml, e.g._data/agent_release_contents/openvox_8x.yml), exactly the nested-key approach in Make openvox-agent release-contents table multi-series before 9.x collection (prereq for #325) #332. - Pages render their own series via
site.data.<table>[page.nav], so the_latestsymlink flip needs no per-page edits. - Validated with a throwaway 9.x cutover (8.x and 9.x rendered their own data, no cross-contamination).
- The one remaining Make openvox-agent release-contents table multi-series before 9.x collection (prereq for #325) #332 item — CI generating each active series' file — is wired (
SERIES/MIN_RELEASE/NAV_KEY+ path overrides) and runs for 8.x today; the second-series invocation lands at cutover. That cutover step should be added toMAINTAINING.md(small follow-up).
Note #339 also relocated the agent release-contents table off
about_agent.md(now a pointer) onto the new page, so the original "about_agentshared-variable collision" in #332 no longer applies.- Generated data moved from shared global files to per-series files (
- added 11 commits that reference this issue
on Aug 3, 2026 Status update — 9.x preview shipped; must-have track complete
#430 merged today:
docs/_openvox_9xis live as a preview at/openvox/9.x/(pinned to
9.0.0-beta2),lateststill points at 8.x, the version selector now shows for OpenVox,
andreferences:allbuilds both versions in prod. That was the driver for this issue, so
the must-have track is done end to end. Server, OpenVoxDB, and OpenFact 6 get the same
Phase 1 treatment in separate PRs; the Phase 2 promotion happens at 9.0.0 GA per
MAINTAINING.md.Reconciling the hardening backlog against what has landed since:
Done (closing the boxes):
- No-redirect awareness — in the Phase 2 runbook (
MAINTAINING.md, step 5). - OpenVox Containers edge case —
single_version: trueinproducts.yml; the selector,
banner, andtest:products_dataall skip it. - Canonical on frozen builds — decided in Add pinned out-of-date banner and /latest/ canonicals for versioned docs #437: the numbered copy of the
latest
version canonicalizes to its/latest/twin; frozen older versions keep the theme's
self-canonical (_plugins/canonical_latest.rb). - Cross-write check — done during the Add maintainer runbook for the major-version cutover #337 dry run and again with the Add "Component versions in recent OpenVox releases" reference page #339 throwaway
9.x cutover (no cross-contamination); Add OpenVox 9.x docs collection as a preview (latest stays on 8.x) #430 in prod confirms. - Drift validation — partially:
rake test:products_dataruns in CI (latestnames a
real version, versions ordered newest-first). It does not cross-check_config.yml/
nav_map.yml/ symlinks againstproducts.yml; the runbook checklist still covers that.
Mostly done:
- Intra-product relative-link sweep — the ~148 OpenVox links are down to 3 (plus 4 in
OpenVox Server), swept as part of the Fix broken internal links in the openvox collection #262 link work. The 3 OpenVox ones are duplicated
into 9.x by the copy, so they currently point a 9.x reader at 8.x content
(openvox_strings.md,reporting_about.md,system_requirements.markdown). Small fix.
Still open, and more relevant now that a frozen/preview tree exists:
- Frozen-version link proofing —
rake test:linksstill proofs only<collection>/latest
(Rakefile), so the 9.x tree is unchecked in CI. A manual htmlproofer pass during Add OpenVox 9.x docs collection as a preview (latest stays on 8.x) #430
caught a real break (configuration.html#pluginsync, a setting removed in 9.x), so this is
the highest-value remaining item. - Lint gate for new hardcoded
/latest/body links — not built. - Path-preserving version switch — selector still links to each version's root.
- Cross-product link policy — v1 (follow
latest) is in effect; no affinity map. - Shared/version-invariant content — still out of scope.
Additions since the issue was written:
- Out-of-date banner on frozen-version pages — Add pinned out-of-date banner and /latest/ canonicals for versioned docs #437 (from the [Feature request]: Allow multiple versions #320 thread).
- 404-page search — 404 page should offer search for the missing page #438.
- Per-series component-version data at cutover (from the Add "Component versions in recent OpenVox releases" reference page #339 note): the second-series
invocation is still not inMAINTAINING.md. Add OpenVox 9.x docs collection as a preview (latest stays on 8.x) #430 added a build-time guard so the 9.x
Component Versions page renders a "no stable releases yet" note instead of empty tables
until 9.0.0 GA; the runbook needs a Phase 2 step to run the per-series tasks with
SERIES=9.and commit theopenvox_9x.ymldata files.
- No-redirect awareness — in the Phase 2 runbook (
Summary
Add multi-version documentation support, following the copy-on-major-release model
agreed in #320 (versioned directories on one branch, per-product independent
latest,pinned reference generation, a version selector in the UI).
Driver: OpenVox 9 is approaching and we can't yet stand up
_openvox_9xwhilefreezing
_openvox_8x. This issue tracks the work.Scope is split deliberately: a small must-have track to ship 9.0 docs, and a
hardening backlog of correctness/automation items that are good ideas but are
not release-blocking and should not pad the critical path. Follow-up to #320.
Must-have track (ship OpenVox 9 docs)
Three pieces. This is the whole critical path.
1. Reference generator: per-version pinning (the only real engineering)
Today output dirs and emitted URLs are fixed at load time (
OUTPUT_DIRlib/puppet_references.rb:15,puppet/type.rb:19; OpenBolt hardcodes_openbolt_5xin
openbolt/docs.rb:9), andrepo.newest_releaserejects prereleases (repo.rb:50).We need to build a chosen version into a chosen collection without cross-writing.
_openfact_*but stamp@latest = '/openvox/latest'(facter/core_facts.rb:12,facter/facter_cli.rb:10).Wrong product URL today, independent of versioning.
one process can target a specific collection (don't drive it by mutating
ENV['COLLECTION']in a loop — the constants freeze at require time).canonical:relative to its targetcollection, not hardcoded
/<product>/latest/.COLLECTION=<dir>+VERSION=; an explicitVERSIONbypassesthe prerelease filter (so we can build 9.x from an RC). No-arg behavior unchanged.
references:all-style task builds each pinned(tag → version dir)pair;build.yamlcalls it.latestis an alias, never its own build row — onlyreal version dirs are built; nothing writes through the
_latestsymlink.2. The cutover + runbook
_openvox_8x→_openvox_9x,repoint the
_latestsymlink, register the collection +defaultsin_config.yml, add nav files (_data/nav/,nav_map.yml,navigation.yml),build both versions.
entries, republish).
the gate the timeline is really about. Eyeball both trees; confirm 8.x didn't move.
(Done in Add maintainer runbook for the major-version cutover #337: full two-phase cutover dry-run against
9.0.0-alpha1, 8.x confirmed frozen. A final dry-run at the real 9.0 tag is still worth doing at cutover time.)3. Version selector
_includes/version-selector.htmlat the top ofsidebar.html; reuse the existingcurrent_nav_page_subpath/current_nav_baselogic for path mapping._data/products.yml);mark current; keep the reader on the same page path when switching, falling back
to the version root when the target page doesn't exist.
(Shipped in Add a per-product documentation version selector #335: per-product picker from
products.yml, marks current, badges thelatest-aliased version, hidden until a product has 2+ versions. It links to eachversion's root, not the same page path — path-preserving switching was
deferred as a follow-up and is tracked in the hardening backlog below.)
_data/products.ymlis introduced here as a lightweight list the selector reads (and ahandy home for the reference pins in piece 1) — not a heavyweight source-of-truth that
everything else is generated from.
Hardening backlog (not release-blocking)
Good ideas surfaced in review. Do them incrementally; none should gate 9.0. Most exist
because we're optimizing an event that happens ~once a year — a runbook checklist and a
human in the loop cover the risk for now.
/<product>/latest/authored bodylinks to relative so frozen docs stay internally coherent (~148 in OpenVox).
Cheaper before the copy, but non-blocking: if left as
latest, frozen 8.x linksdrift to 9.x — a real but fixable-later UX bug. Depth-aware (subdir pages need
../).cross-product links following
latest; later, optionally pin partner versions viaa
cross_product_affinitymap for lockstep products (e.g. openvoxdb 8x → openvox 8x).rake test:products_dataruns in CI, but onlyvalidates
products.ymlinternally.) Optional Rake task asserting_config.yml/navigation.yml/
nav_map.yml/_latestsymlinks agree withproducts.yml, run in CI beforejekyll build. A runbook checklist covers the once-a-year case until then. Note:_config.ymlcollections can't be generated within Jekyll (read before_data),so generation isn't an option for the highest-drift surface anyway.
/latest/body links(pattern-based, not existence-based). Pairs with the relative-link sweep.
build once to confirm no cross-write. Don't need a standing CI gate.
(Done: Add maintainer runbook for the major-version cutover #337 dry run and the Add "Component versions in recent OpenVox releases" reference page #339 throwaway 9.x cutover; Add OpenVox 9.x docs collection as a preview (latest stays on 8.x) #430 in prod confirms.)
rake test:links(today checks/latest/only,
Rakefile:76) to also proof frozen version trees.canonical: /<product>/latest/...(SEO → current) or get per-version canonicals.Default: keep pointing at
latest.(Decided in Add pinned out-of-date banner and /latest/ canonicals for versioned docs #437: the numbered copy of the
latestversion canonicalizes to its/latest/twin; frozen older versions keep the theme's self-canonical.)latestmoves 8→9, pages removed/renamed in 9.x404 at
/<product>/latest/<page>for old bookmarks (content still at/8.x/).Note in the runbook; we have no redirect mechanism. (In
MAINTAINING.md, Phase 2 step 5.)_openvox-containers_latestdir(not a symlink) and no numbered collection — selector/validator logic must not
assume the uniform model. (
single_version: trueinproducts.yml; selector, banner,and
test:products_dataall skip it.)root; keeping the reader on the same page path across a switch (falling back to
the root when the target page doesn't exist) needs per-navigation JS. Deferred
follow-up from piece 3.
Part of #320.