Files
meshcore-spec/theme/version-picker.js
T
swaitsandClaude Opus 4.7 9ad31f7470 feat(mdbook): multi-version site, mise-pinned tools, justfile
Three intertwined changes — split conceptually but landed together
because they share the same workflow rewrite:

1. MULTI-VERSION SITE. The published site now keeps every prior spec
   version alongside the rolling latest build (docs.rs / readthedocs
   pattern):

   - .github/workflows/mdbook.yml builds latest from current HEAD and
     each v* tag in a git worktree, placing each into _site/<ver>/.
     Each per-version build gets MDBOOK_OUTPUT__HTML__SITE_URL set to
     /meshcore-spec/<ver>/ so the sitemap/canonical links resolve
     correctly per version.
   - Workflow emits _site/versions.json (newest first) and a root
     index.html that redirects to /latest/.
   - theme/version-picker.js is a small client-side dropdown that
     fetches /<base>/versions.json and lets the reader switch to the
     same chapter in any other version. Slots into mdbook's menu bar
     (.right-buttons); falls back to a fixed top-right widget if the
     menu isn't found.
   - theme/version-picker.css styles it to fit the navy theme.
   - book.toml wires the picker files via additional-js /
     additional-css; site-url defaults to /meshcore-spec/latest/ for
     the main build (CI overrides per tag).
   - Old tags built without these files render without a picker, which
     is fine — users won't typically need to navigate AWAY from an
     older version. v0.1.0 onward will all carry the picker.

   Spec versions use independent semver and do NOT mirror MeshCore
   firmware versions; each spec release records the upstream commit
   hash it was validated against in CHANGELOG.md.

2. MISE-PINNED TOOLS. mise.toml pins cargo:mdbook so local dev and
   CI use the same version. The workflow installs everything via
   jdx/mise-action instead of peaceiris/actions-mdbook, making
   mise.toml the single source of truth for tool versions.

3. JUSTFILE. justfile gives short names (build / serve / clean /
   validate / setup) for the common tasks. Recipes run under
   `mise exec -- sh -c …` so they always pick up the mise-pinned
   tools (sh is intentional, keeping recipes POSIX-compatible).

Documentation: README's spec link now mentions the versioning model;
src/00-overview.md gets a "Spec Versions" subsection explaining the
URL layout and version-picker.

Verified locally: `just build` produces a clean book/ with the
version-picker JS/CSS injected (hashed filenames, slotted into
theme/). JS parses clean under `node --check`.

Note for first-time deploy: GitHub Pages source must be set to
"GitHub Actions" in repo settings (one-time, manual). After that,
push to main builds + deploys latest, and pushing a v* tag adds
the snapshot to the site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 09:51:07 -06:00

102 lines
3.5 KiB
JavaScript

// version-picker.js
//
// Renders a version dropdown in the mdBook menu bar for multi-version
// deployments. Reads /<base>/versions.json at page load and lets the
// reader switch to the same page in a different version.
//
// The deployed URL layout is <base>/<version>/<page>, where <version>
// is "latest" or a semver-like tag (e.g., "v0.1.0"). When the reader
// chooses a different version, the picker navigates to the matching
// page under the new version subtree; if that page doesn't exist in
// the target version, the browser will show a 404, which is fine for
// a v0.1 deployment.
(function () {
'use strict';
// Match the version segment in a path. Accepts "latest" or "vX[.Y[.Z]][-pre]".
var VERSION_RE = /\/(latest|v\d+(?:\.\d+){0,2}(?:-[A-Za-z0-9.]+)?)(\/|$)/;
function parseLocation() {
var path = window.location.pathname;
var m = path.match(VERSION_RE);
if (!m) return null;
var version = m[1];
var versionStart = m.index;
var basePath = path.substring(0, versionStart + 1);
var pagePath = path.substring(versionStart + 1 + version.length);
return { version: version, basePath: basePath, pagePath: pagePath };
}
function buildPicker(versions, current, basePath, pagePath) {
var wrap = document.createElement('div');
wrap.className = 'version-picker';
wrap.setAttribute('role', 'group');
wrap.setAttribute('aria-label', 'Spec version');
var label = document.createElement('span');
label.className = 'version-picker-label';
label.textContent = 'version:';
wrap.appendChild(label);
var select = document.createElement('select');
select.className = 'version-picker-select';
select.setAttribute('aria-label', 'Choose spec version');
for (var i = 0; i < versions.length; i++) {
var v = versions[i];
var opt = document.createElement('option');
opt.value = v;
opt.textContent = (v === 'latest') ? 'latest (main)' : v;
if (v === current) opt.selected = true;
select.appendChild(opt);
}
select.addEventListener('change', function (e) {
var next = e.target.value;
if (!next || next === current) return;
window.location.pathname = basePath + next + pagePath;
});
wrap.appendChild(select);
return wrap;
}
function install() {
var loc = parseLocation();
if (!loc) return;
fetch(loc.basePath + 'versions.json', { cache: 'no-cache' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) {
if (!data) return;
// Accepted shapes:
// ["latest", "v0.1.0"]
// { "versions": ["latest", "v0.1.0"] }
var versions = Array.isArray(data) ? data : (data && data.versions);
if (!versions || !versions.length) return;
var picker = buildPicker(versions, loc.version, loc.basePath, loc.pagePath);
// Prefer slotting into mdBook's menu bar; fall back to floating.
var slot = document.querySelector('.right-buttons');
if (slot) {
slot.insertBefore(picker, slot.firstChild);
} else {
picker.classList.add('version-picker-floating');
document.body.appendChild(picker);
}
})
.catch(function (err) {
// Non-fatal: dev / file:// / pre-deploy will all land here.
if (window.console) console.warn('version-picker:', err);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', install);
} else {
install();
}
})();