Skip to content

Commit 0cf870d

Browse files
kwagyemanclaude
andcommitted
docs: version-channel awareness (conf.py + switcher JS)
Foundation for multi-version hosting (docs.openmv.io/<channel>/...): - conf.py: OPENMV_DOCS_CHANNEL env var (default 'dev'); language switcher, hreflang, and canonical URLs are now channel-prefixed; html_baseurl is per-channel; expose current_version + a bootstrap versions list for Shibuya's nav-versions switcher. - nav-versions.js: populate the version switcher from a site-root /versions.json with per-page links that keep the current language, so even frozen old snapshots show the full current version list. - lang-redirect.js: channel-aware -- auto-route language within the current version channel instead of at the site root. No CI/deploy changes yet; live site unaffected (branch only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 96c2941 commit 0cf870d

3 files changed

Lines changed: 177 additions & 66 deletions

File tree

docs/conf.py

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@
4242
_build_date_value = _dt.date.today()
4343
build_date = _build_date_value.strftime("%d %b %Y")
4444

45+
# Documentation version channel for this build -- the top-level URL segment the
46+
# site is hosted under (docs.openmv.io/<channel>/...). The CI sets it: "dev" for
47+
# the rolling master build, "v5.0.0" etc. for a tagged release snapshot. Old
48+
# release snapshots are frozen HTML and never rebuilt; "/latest/" and "/" are
49+
# redirect shims to the newest release. Defaults to "dev" for local builds.
50+
doc_channel = os.environ.get("OPENMV_DOCS_CHANNEL", "dev")
51+
4552
# Values exposed to topindex.html and footer templates.
4653
# Resolve this submodule's HEAD SHA at build time so "Edit this
4754
# page" and the AI dropdown's raw-source link both pin to the exact
@@ -75,8 +82,9 @@
7582

7683
# Site root — used by sphinx-llms-txt to emit absolute URLs in
7784
# /llms.txt and by Shibuya's "Copy page" / "Open in ChatGPT" dropdown
78-
# to fetch raw page source from /_sources/.
79-
html_baseurl = "https://docs.openmv.io/"
85+
# to fetch raw page source from /_sources/. Version-channel aware: every build
86+
# is hosted under docs.openmv.io/<channel>/.
87+
html_baseurl = "https://docs.openmv.io/{}/".format(doc_channel)
8088

8189
# -- Landing page code examples (rendered via Pygments to match site code style) --
8290
from pygments import highlight as _pygments_highlight
@@ -356,38 +364,57 @@ def _render_landing_code(src):
356364
# sphinx-build -b html -D language=zh_CN . _build/html/zh_CN
357365
# sphinx-build -b html -D language=zh_TW . _build/html/zh_TW
358366
#
359-
# The zh_CN / zh_TW entries below go live once those builds are deployed.
360-
# English first (default), then A–Z by English language name.
367+
# (display name, language subdirectory, hreflang). English is the bare root of
368+
# the version channel; each translation lives under <subdir>/. English first,
369+
# then A–Z by English language name.
370+
_languages = [
371+
("English", "", "en"),
372+
("العربية", "ar", "ar"), # Arabic
373+
("简体中文", "zh_CN", "zh-Hans"), # Chinese (Simplified)
374+
("繁體中文", "zh_TW", "zh-Hant"), # Chinese (Traditional)
375+
("Hrvatski", "hr", "hr"), # Croatian
376+
("Čeština", "cs", "cs"), # Czech
377+
("Nederlands", "nl", "nl"), # Dutch
378+
("Suomi", "fi", "fi"), # Finnish
379+
("Français", "fr", "fr"), # French
380+
("Deutsch", "de", "de"), # German
381+
("עברית", "he", "he"), # Hebrew
382+
("Magyar", "hu", "hu"), # Hungarian
383+
("Bahasa Indonesia", "id", "id"), # Indonesian
384+
("Italiano", "it", "it"), # Italian
385+
("日本語", "ja", "ja"), # Japanese
386+
("한국어", "ko", "ko"), # Korean
387+
("Polski", "pl", "pl"), # Polish
388+
("Português (Brasil)", "pt_BR", "pt-BR"), # Portuguese (Brazil)
389+
("Português (Portugal)", "pt_PT", "pt-PT"), # Portuguese (Portugal)
390+
("Română", "ro", "ro"), # Romanian
391+
("Русский", "ru", "ru"), # Russian
392+
("Español", "es", "es"), # Spanish
393+
("Svenska", "sv", "sv"), # Swedish
394+
("ไทย", "th", "th"), # Thai
395+
("Türkçe", "tr", "tr"), # Turkish
396+
("Українська", "uk", "uk"), # Ukrainian
397+
("Tiếng Việt", "vi", "vi"), # Vietnamese
398+
]
399+
# Channel-prefixed URL patterns; %s is the page name (filled by Shibuya's
400+
# i18n_link). Switching language keeps the current version + page, e.g.
401+
# "/dev/de/%s.html". English is "/dev/%s.html".
361402
html_context["languages"] = [
362-
("English", "/%s.html", "en"),
363-
("العربية", "/ar/%s.html", "ar"), # Arabic
364-
("简体中文", "/zh_CN/%s.html", "zh-Hans"), # Chinese (Simplified)
365-
("繁體中文", "/zh_TW/%s.html", "zh-Hant"), # Chinese (Traditional)
366-
("Hrvatski", "/hr/%s.html", "hr"), # Croatian
367-
("Čeština", "/cs/%s.html", "cs"), # Czech
368-
("Nederlands", "/nl/%s.html", "nl"), # Dutch
369-
("Suomi", "/fi/%s.html", "fi"), # Finnish
370-
("Français", "/fr/%s.html", "fr"), # French
371-
("Deutsch", "/de/%s.html", "de"), # German
372-
("עברית", "/he/%s.html", "he"), # Hebrew
373-
("Magyar", "/hu/%s.html", "hu"), # Hungarian
374-
("Bahasa Indonesia", "/id/%s.html", "id"), # Indonesian
375-
("Italiano", "/it/%s.html", "it"), # Italian
376-
("日本語", "/ja/%s.html", "ja"), # Japanese
377-
("한국어", "/ko/%s.html", "ko"), # Korean
378-
("Polski", "/pl/%s.html", "pl"), # Polish
379-
("Português (Brasil)", "/pt_BR/%s.html", "pt-BR"), # Portuguese (Brazil)
380-
("Português (Portugal)", "/pt_PT/%s.html", "pt-PT"), # Portuguese (Portugal)
381-
("Română", "/ro/%s.html", "ro"), # Romanian
382-
("Русский", "/ru/%s.html", "ru"), # Russian
383-
("Español", "/es/%s.html", "es"), # Spanish
384-
("Svenska", "/sv/%s.html", "sv"), # Swedish
385-
("ไทย", "/th/%s.html", "th"), # Thai
386-
("Türkçe", "/tr/%s.html", "tr"), # Turkish
387-
("Українська", "/uk/%s.html", "uk"), # Ukrainian
388-
("Tiếng Việt", "/vi/%s.html", "vi"), # Vietnamese
403+
(label,
404+
"/{c}/{p}%s.html".format(c=doc_channel, p=(sub + "/" if sub else "")),
405+
hreflang)
406+
for (label, sub, hreflang) in _languages
389407
]
390408

409+
# Documentation-version switcher (Shibuya's nav-versions component, rendered
410+
# left of the language switcher). ``current_version`` is the button label.
411+
# ``versions`` is only a bootstrap so the element renders; the real,
412+
# always-current list -- with per-page links that keep the current language --
413+
# is populated client-side from /versions.json by static/nav-versions.js, so
414+
# even frozen old snapshots show the full, current version list.
415+
html_context["current_version"] = doc_channel
416+
html_context["versions"] = [(doc_channel, "")]
417+
391418
# There are two options for replacing |today|: either, you set today to some
392419
# non-false value, then it is used:
393420
# today = ''
@@ -577,6 +604,10 @@ def _render_landing_code(src):
577604
# Re-publish --sy-s-offset-top from the real header height so sticky
578605
# elements stay aligned when the navbar wraps (long translated labels).
579606
"navbar-height.js",
607+
# Populate the version switcher from /versions.json (per-page links that
608+
# keep the current language), so even frozen old snapshots list every
609+
# version. See static/nav-versions.js.
610+
"nav-versions.js",
580611
]
581612
# Add any extra paths that contain custom files (such as robots.txt or
582613
# .htaccess) here, relative to this directory. These files are copied

docs/static/lang-redirect.js

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
/*
22
* Automatic language selection for the OpenMV docs.
33
*
4-
* Each translation is deployed under its own top-level subdirectory
5-
* (/zh_CN/, /de/, ...) with English at the site root. On first visit this
6-
* script sends the reader to the build that best matches their browser
7-
* locale, falling back to English when nothing matches.
4+
* The site is versioned: every page lives under a top-level version channel,
5+
* docs.openmv.io/<channel>/... ("dev" = rolling master build, "vX.Y.Z" = frozen
6+
* release snapshots). Within a channel, English is at the channel root and each
7+
* translation lives under <channel>/<lang>/. On first visit this script sends
8+
* the reader to the build matching their browser locale -- keeping the current
9+
* version channel -- falling back to English when nothing matches.
810
*
911
* A reader's explicit choice from the navbar "Translations" switcher is
10-
* remembered (localStorage) and always wins over browser detection, so the
11-
* switcher is never fought by the auto-redirect. Runs synchronously from
12-
* <head> so the swap happens before paint, and uses location.replace so it
13-
* never pollutes history (no back-button trap / redirect loop).
12+
* remembered (localStorage) and always wins over browser detection. Runs
13+
* synchronously from <head> so the swap happens before paint, and uses
14+
* location.replace so it never pollutes history (no back-button trap / loop).
1415
*/
1516
(function () {
1617
"use strict";
1718

1819
// Language subdirectories that actually exist (mirror conf.py languages).
19-
// he/ar are translated and selectable in the switcher; their layout ships
20-
// left-to-right for now (the theme has no RTL chrome) but the text reads
21-
// correctly, so they are auto-routed like every other locale.
2220
var SUBDIRS = ["zh_CN", "zh_TW", "de", "ja", "es", "ru", "fr", "ko", "it", "pt_BR", "nl",
2321
"ro", "hr", "cs", "pl", "fi", "sv", "hu", "tr", "ar", "he",
2422
"vi", "id", "th", "uk", "pt_PT"];
@@ -39,13 +37,30 @@
3937

4038
var STORE_KEY = "omvLangPref"; // "en" or a subdir; set on manual switch.
4139

40+
// Path layout: ["", <channel>, <lang?>, ...page]. The language, if any, is the
41+
// segment right after the channel.
4242
function langOfPath(pathname) {
43-
var parts = pathname.split("/");
44-
// parts[0] is "" (leading slash); first real segment is parts[1].
45-
if (parts.length > 1 && SUBDIRS.indexOf(parts[1]) !== -1) return parts[1];
43+
var p = pathname.split("/");
44+
if (p.length > 2 && SUBDIRS.indexOf(p[2]) !== -1) return p[2];
4645
return "en";
4746
}
4847

48+
function channelOf(pathname) {
49+
var p = pathname.split("/");
50+
return p.length > 1 ? p[1] : "";
51+
}
52+
53+
// Rebuild the path for `target` language, keeping the version channel + page.
54+
function pathForLang(pathname, target) {
55+
var p = pathname.split("/");
56+
var channel = p[1] || "";
57+
var inner = p.slice(2); // [<lang?>, ...page]
58+
var curLang = (inner.length && SUBDIRS.indexOf(inner[0]) !== -1) ? inner[0] : "en";
59+
var page = (curLang === "en") ? inner : inner.slice(1); // page segments only
60+
var langSeg = (target === "en") ? [] : [target];
61+
return "/" + [channel].concat(langSeg, page).join("/");
62+
}
63+
4964
function detectFromBrowser() {
5065
var langs = (navigator.languages && navigator.languages.length)
5166
? navigator.languages
@@ -62,17 +77,6 @@
6277
return "en"; // fallback
6378
}
6479

65-
function stripLang(pathname, cur) {
66-
if (cur === "en") return pathname;
67-
var rest = pathname.replace(new RegExp("^/" + cur + "(?=/|$)"), "");
68-
return rest === "" ? "/" : rest;
69-
}
70-
71-
function withLang(pathname, target) {
72-
if (target === "en") return pathname;
73-
return "/" + target + pathname;
74-
}
75-
7680
function safeGet() {
7781
try { return localStorage.getItem(STORE_KEY); } catch (e) { return null; }
7882
}
@@ -84,7 +88,8 @@
8488

8589
// Remember the reader's explicit choice when they use the switcher, so it
8690
// overrides browser detection on later visits. The Shibuya switcher renders
87-
// plain <a> links; capture clicks on anything pointing at a known build.
91+
// plain <a> links; capture clicks on anything pointing at another language
92+
// build of the current page.
8893
document.addEventListener("click", function (ev) {
8994
var a = ev.target && ev.target.closest ? ev.target.closest("a[href]") : null;
9095
if (!a) return;
@@ -93,19 +98,20 @@
9398
try { path = new URL(href, window.location.href).pathname; }
9499
catch (e) { return; }
95100
var chosen = langOfPath(path);
96-
// Only treat it as a language choice if it targets the current page in
97-
// another build (the switcher links do exactly this).
98101
if (chosen !== current) safeSet(chosen);
99102
}, true);
100103

101-
var pref = safeGet();
102-
var target = pref ? pref : detectFromBrowser();
103-
if (SUBDIRS.indexOf(target) === -1 && target !== "en") target = "en";
104+
// Only auto-route when we are actually under a version channel.
105+
if (channelOf(window.location.pathname)) {
106+
var pref = safeGet();
107+
var target = pref ? pref : detectFromBrowser();
108+
if (SUBDIRS.indexOf(target) === -1 && target !== "en") target = "en";
104109

105-
if (target !== current) {
106-
var newPath = withLang(stripLang(window.location.pathname, current), target);
107-
if (newPath !== window.location.pathname) {
108-
window.location.replace(newPath + window.location.search + window.location.hash);
110+
if (target !== current) {
111+
var newPath = pathForLang(window.location.pathname, target);
112+
if (newPath !== window.location.pathname) {
113+
window.location.replace(newPath + window.location.search + window.location.hash);
114+
}
109115
}
110116
}
111117
})();

docs/static/nav-versions.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Populate the documentation version switcher (Shibuya's .nav-versions) from a
3+
* site-root /versions.json, with per-page links that keep the current language.
4+
*
5+
* Why client-side: every version is hosted under its own top-level path
6+
* (docs.openmv.io/<channel>/...) -- "dev" is the rolling master build, each
7+
* "vX.Y.Z" is a frozen HTML snapshot that is never rebuilt. A frozen snapshot's
8+
* server-rendered switcher could only list versions that existed when it was
9+
* built; fetching the always-current /versions.json at load time means even old
10+
* snapshots show the full, current list.
11+
*
12+
* versions.json format (kept stable so old frozen snapshots can read new files):
13+
* { "versions": [ {"id": "dev", "label": "dev"},
14+
* {"id": "v5.0.0", "label": "v5.0.0 (latest)"} ],
15+
* "latest": "v5.0.0" }
16+
*
17+
* Switching version keeps the rest of the path (language subdir + page), so you
18+
* stay on the same page in the same language. If that page does not exist in the
19+
* target version, that version's 404 shim handles the redirect.
20+
*/
21+
(function () {
22+
"use strict";
23+
24+
function channelOf(pathname) {
25+
var p = pathname.split("/"); // ["", "<channel>", ...rest]
26+
return p.length > 1 ? p[1] : "";
27+
}
28+
29+
// Everything after the leading /<channel> segment, e.g. "/de/library/index.html".
30+
function tailOf(pathname) {
31+
var p = pathname.split("/");
32+
return "/" + p.slice(2).join("/");
33+
}
34+
35+
function populate(data) {
36+
var box = document.querySelector(".nav-versions");
37+
if (!box || !data || !Array.isArray(data.versions)) return;
38+
var choices = box.querySelector(".nav-versions-choices");
39+
if (!choices) return;
40+
41+
var cur = channelOf(window.location.pathname);
42+
var tail = tailOf(window.location.pathname);
43+
44+
var ul = document.createElement("ul");
45+
data.versions.forEach(function (v) {
46+
var id = typeof v === "string" ? v : v.id;
47+
var label = (typeof v === "object" && v.label) ? v.label : id;
48+
var li = document.createElement("li");
49+
var a = document.createElement("a");
50+
a.href = "/" + id + tail;
51+
a.textContent = label;
52+
if (id === cur) a.setAttribute("aria-current", "true");
53+
li.appendChild(a);
54+
ul.appendChild(li);
55+
});
56+
57+
var existing = choices.querySelector("ul");
58+
if (existing) existing.replaceWith(ul);
59+
else choices.appendChild(ul);
60+
}
61+
62+
function load() {
63+
fetch("/versions.json", { cache: "no-cache" })
64+
.then(function (r) { return r.ok ? r.json() : null; })
65+
.then(function (data) { if (data) populate(data); })
66+
.catch(function () { /* offline / local build: keep the server bootstrap */ });
67+
}
68+
69+
if (document.readyState === "loading") {
70+
document.addEventListener("DOMContentLoaded", load);
71+
} else {
72+
load();
73+
}
74+
})();

0 commit comments

Comments
 (0)