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>
This commit is contained in:
8 files changed
+294
-9
No files matched your search
@@ -0,0 +1,43 @@
|
||||
.version-picker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
margin: 0 0.4em;
|
||||
font-size: 0.85em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.version-picker-label {
|
||||
color: var(--icons);
|
||||
text-transform: lowercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.version-picker-select {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--icons);
|
||||
border-radius: 3px;
|
||||
padding: 0.2em 0.45em;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.version-picker-select:hover,
|
||||
.version-picker-select:focus {
|
||||
border-color: var(--fg);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.version-picker-floating {
|
||||
position: fixed;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
z-index: 1000;
|
||||
background: var(--bg);
|
||||
padding: 0.4em 0.6em;
|
||||
border: 1px solid var(--icons);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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();
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user