summaryrefslogtreecommitdiff
path: root/docs/_static
diff options
context:
space:
mode:
authorYuriy Andamasov <yuriy@vyos.io>2026-07-10 17:14:13 +0300
committerGitHub <noreply@github.com>2026-07-10 16:14:13 +0200
commit21689ef59b0eb34b1a29eda739dc10d33d25b44a (patch)
tree95883c2ee70d4c09562ae3e0289e18f34999240c /docs/_static
parent6d2d28d4411ff52ad0f9314cf3696336748ab89d (diff)
downloadvyos-documentation-21689ef59b0eb34b1a29eda739dc10d33d25b44a.tar.gz
vyos-documentation-21689ef59b0eb34b1a29eda739dc10d33d25b44a.zip
docs: Cloudflare Workers hosting pipeline (apex, content workers, CI, previews) (#2140)
* docs-infra: scaffold Cloudflare workers workspace (versions.json v2, matrix, toolchain) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: record full Phase-0 plan decision in workers/PLAN.md ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: shared content worker โ€” asset serving, cache classes, X-Docs-Build, canary no-store ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: run worker script before assets; test fetch entrypoint ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex manifest loader + dispatch map + runtime binding guard (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex redirects (aliases, PDF, trailing-slash) + special paths (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: PDF redirect honors pdf:null and preserves query ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex UA gate โ€” allowlist-wins, log-only AI crawlers, empty block list at launch ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex router (pipeline ยง3.2), themed 404/503, /kb seam, env configs + congruence test ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: add missing-User-Agent regression test for apex UA gate ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: R2-streaming preview worker โ€” MIME map, noindex, no-store (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: preview 404 no-store + fetch handler tests ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: bootstrap script โ€” binding-target workers must exist before apex deploys ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex run_worker_first, lockfile for npm ci, PDF Location from manifest ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: derive html_baseurl from DOCS_VERSION_SLUG with RTD fallback (canonical gate prereq) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: version picker + status banner + language scaffold (vanilla JS, TDD pure core) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: picker preserves query+hash across switch; valid breadcrumb markup ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: Pagefind search wrapper with runtime base-path + preview prefix handling (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: pagefind wrapper โ€” asset-failure notice + UI stylesheet load ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: gate Pagefind searchbox to CF builds (RTD keeps stock search until cutover) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: deploy sanity gates โ€” limits, critical pages, count-delta, canonical (TDD) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: hermetic gate tests via fixture versions.json ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: docs-build workflow โ€” candidate/smoke/promote two-stage deploy + registry + rollback Two-stage CF Workers pipeline: build in pinned container, assemble artifact, sanity gates, deploy candidate, scoped pre-traffic smoke via canary apex, promote (rollback-id capture, hostname purge, registry upload), post-promote probe + auto-rollback. DOCS_CF_LIVE repo variable gates every docs.vyos.io production interaction pre-cutover. scripts/docs_gates/smoke.py adds one authorized check beyond the spec: the version's index.html probe asserts the #vyos-search mount div is present in the response body, guarding CI silently forgetting DOCS_VERSION_SLUG (which would otherwise ship stock RTD search without the Pagefind gate noticing). ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: build docs image in-workflow with buildx cache (v4.1 โ€” digest pin dropped) Plan v4.1 amendment: the ghcr.io digest-pinned image does not exist (workflow would hard-fail at the first docker step on every push). Replace the BUILD_IMAGE env placeholder with an in-workflow docker build from docker/Dockerfile via docker/setup-buildx-action@v3 + docker/build-push-action@v6 (context: docker/, load: true, tags: docs-build:local, GHA cache from/to). The checked-out commit is the pin; buildx GHA cache keeps repeat builds cheap. Sphinx-build step swaps to docs-build:local; inner script unchanged. ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: apex/preview deploy workflow โ€” canary auto, production behind environment approval * docs-infra: apex-deploy concurrency guard (per-ref, cancel-in-progress) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: fork-safe PR preview pipeline โ€” approval record, R2 prefixes, label consumption, cleanup * docs-infra: nightly preview sweep โ€” pipefail + per-prefix failure isolation ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: nightly canary QA โ€” per-entry sweep + URL-parity corpus vs RTD ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: parity sweep scoped to CF-built versions; transport-error resilience ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: one-off bootstrap workflow (binding targets โ€” runs once on this push) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: remove one-off bootstrap workflow (bootstrap complete) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: one-off canary apex + preview deploy (route targets for Task 3.6 step 2c) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: remove one-off canary deploy workflow (targets live) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: address Phase-0 CodeRabbit findings (canonical gate, error caching, registry pointer, validation) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: strengthen manifest tests (full dispatch iteration, mutation-free validate) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: address GitHub CodeRabbit review (pointer-after-probe, fail-closed sweeps, block-precedence UA gate, preview hardening) ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: adversarial review fixes โ€” error no-store, probe retry, PR-list membership, preview dotted-segment ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: serve oversized legacy PDF from R2 via apex (spec ยง5 fallback) The 1.3 PDF (29.2 MiB) exceeds the 25 MiB static-asset cap and is absent from the legacy content Worker's build, so /_/downloads/en/1.3/pdf/ (and the picker's PDF link) 301'd into a dead-end 404 post-cutover. Add the R2 object fallback spec ยง5 already documented but never implemented: a DOCS_PDFS R2 bucket binding on the apex Worker, a manifest pdf_r2_key field (1.3 only), and a router step ahead of version dispatch that streams the object with its own cache class (canary/error still force no-store). ๐Ÿค– Generated by [robots](https://vyos.io) * docs-infra: PDF R2 fallback honors Range + If-None-Match, preserves ETag ๐Ÿค– Generated by [robots](https://vyos.io)
Diffstat (limited to 'docs/_static')
-rw-r--r--docs/_static/css/version-picker.css11
-rw-r--r--docs/_static/js/pagefind-wrapper.js69
-rw-r--r--docs/_static/js/version-picker.js153
3 files changed, 233 insertions, 0 deletions
diff --git a/docs/_static/css/version-picker.css b/docs/_static/css/version-picker.css
new file mode 100644
index 00000000..16ce16c3
--- /dev/null
+++ b/docs/_static/css/version-picker.css
@@ -0,0 +1,11 @@
+.vyos-version-picker-item { list-style: none; }
+#vyos-version-picker { display: flex; align-items: center; gap: .5em; padding: .4em 0; font-size: .9em; }
+#vyos-version-picker select { max-width: 14em; padding: .15em .3em; }
+#vyos-version-picker select:focus { outline: 2px solid #ffae12; outline-offset: 1px; }
+.vyos-pdf-link { font-weight: 600; }
+.vyos-version-banner { position: relative; padding: .6em 2.2em .6em 1em; font-size: .95em; line-height: 1.4; }
+.vyos-banner-dev { background: #e7f2fa; color: #2a6496; }
+.vyos-banner-newer-lts { background: #fff6e5; color: #6b5900; }
+.vyos-banner-eol { background: #fdecea; color: #8a1f11; }
+.vyos-version-banner a { text-decoration: underline; color: inherit; font-weight: 600; }
+.vyos-banner-dismiss { position: absolute; right: .5em; top: .35em; background: none; border: 0; font-size: 1.2em; cursor: pointer; color: inherit; }
diff --git a/docs/_static/js/pagefind-wrapper.js b/docs/_static/js/pagefind-wrapper.js
new file mode 100644
index 00000000..a4acadf8
--- /dev/null
+++ b/docs/_static/js/pagefind-wrapper.js
@@ -0,0 +1,69 @@
+/* Version-scoped Pagefind loader. Derives its base from the current URL so the
+ * same file works on production, canary, and /pr-<n>/ previews (ยง9). */
+(function (window) {
+ 'use strict';
+
+ function basePathFor(pathname) {
+ var m = pathname.match(/^((\/pr-\d+)?\/[a-z]{2}(?:_[A-Z]{2})?\/[^/]+\/)/);
+ if (!m) return null;
+ return { base: m[1], prefix: m[2] || '' };
+ }
+
+ function prefixResultUrl(url, prefix) {
+ return prefix ? prefix + url : url;
+ }
+
+ function assetUrlsFor(base) {
+ return {
+ css: base + 'pagefind/pagefind-ui.css',
+ js: base + 'pagefind/pagefind-ui.js',
+ };
+ }
+
+ function init() {
+ var mount = document.getElementById('vyos-search');
+ if (!mount) return;
+ var ctx = basePathFor(window.location.pathname);
+ if (!ctx) return;
+ var assets = assetUrlsFor(ctx.base);
+ /* Stylesheet first so the UI never renders unstyled. CSS load failure is
+ * cosmetic only โ€” no error handler on the link. */
+ var l = document.createElement('link');
+ l.rel = 'stylesheet';
+ l.href = assets.css;
+ document.head.appendChild(l);
+ var s = document.createElement('script');
+ s.src = assets.js;
+ s.onerror = function () {
+ /* Per-version Pagefind assets missing (index not built yet, preview
+ * namespace, transient failure) โ€” show a visible notice instead of
+ * leaving #vyos-search a silent empty div. */
+ var p = document.createElement('p');
+ p.className = 'vyos-search-unavailable';
+ p.textContent = 'Search is temporarily unavailable for this version.';
+ mount.appendChild(p);
+ };
+ s.onload = function () {
+ /* global PagefindUI */
+ new window.PagefindUI({
+ element: '#vyos-search',
+ baseUrl: ctx.base,
+ bundlePath: ctx.base + 'pagefind/',
+ processResult: function (result) {
+ result.url = prefixResultUrl(result.url, ctx.prefix);
+ return result;
+ },
+ });
+ };
+ document.head.appendChild(s);
+ }
+
+ window.VyOSSearch = {
+ basePathFor: basePathFor,
+ prefixResultUrl: prefixResultUrl,
+ assetUrlsFor: assetUrlsFor,
+ init: init,
+ };
+ if (typeof document !== 'undefined' && document.addEventListener)
+ document.addEventListener('DOMContentLoaded', init);
+})(window);
diff --git a/docs/_static/js/version-picker.js b/docs/_static/js/version-picker.js
new file mode 100644
index 00000000..f13ad60f
--- /dev/null
+++ b/docs/_static/js/version-picker.js
@@ -0,0 +1,153 @@
+/* VyOS docs version picker + status banner + language scaffold.
+ * Vanilla JS, no dependencies, no build step. Degrades silently when
+ * /versions.json is unreachable (docs stay fully readable). */
+(function (window) {
+ 'use strict';
+
+ // Deliberately does not match /pr-<n>/ preview prefixes โ€” previews are single-version,
+ // so the picker has nothing to switch between and stays hidden there by design.
+ function parseLocation(pathname) {
+ var m = pathname.match(/^\/([a-z]{2}(?:_[A-Z]{2})?)\/([^/]+)\/(.*)$/);
+ if (!m) return null;
+ return { lang: m[1], slug: m[2], rest: m[3] };
+ }
+
+ // Contract: versions.json lists versions newest-first, so the first 'lts' entry found
+ // here is the newest LTS โ€” callers rely on that ordering rather than comparing versions.
+ function newestLts(manifest) {
+ for (var i = 0; i < manifest.versions.length; i++)
+ if (manifest.versions[i].status === 'lts') return manifest.versions[i].slug;
+ return null;
+ }
+
+ function bannerFor(slug, manifest) {
+ var entry = null, i;
+ for (i = 0; i < manifest.versions.length; i++)
+ if (manifest.versions[i].slug === slug) entry = manifest.versions[i];
+ if (!entry) return null;
+ var newest = newestLts(manifest);
+ if (entry.status === 'dev') return { kind: 'dev', newest: newest };
+ if (entry.status === 'eol') return { kind: 'eol', newest: newest };
+ if (entry.status === 'lts' && newest && newest !== slug)
+ return { kind: 'newer-lts', newest: newest };
+ return null;
+ }
+
+ function targetUrlFor(loc, targetSlug) {
+ return '/' + loc.lang + '/' + targetSlug + '/' + loc.rest;
+ }
+
+ /* Full navigation URL for a version switch: same path on the target version
+ * with the current query string + fragment re-attached, so deep links
+ * (?highlight=โ€ฆ, #section) survive the switch (ยง4 URL-stability contract). */
+ function navUrlFor(loc, targetSlug, search, hash) {
+ return targetUrlFor(loc, targetSlug) + (search || '') + (hash || '');
+ }
+
+ /* ---- DOM layer (no execution at import time) ---- */
+ function bannerText(b, manifest) {
+ if (b.kind === 'dev') return 'You are reading the development (rolling) docs.';
+ if (b.kind === 'eol') return 'This VyOS version is end-of-life; these docs are frozen. See the ' + b.newest + ' (LTS) docs.';
+ return 'A newer LTS (' + b.newest + ') is available.';
+ }
+
+ function init() {
+ var anchor = document.getElementById('vyos-version-picker');
+ if (!anchor) return;
+ var loc = parseLocation(window.location.pathname);
+ if (!loc) return;
+
+ fetch('/versions.json', { headers: { accept: 'application/json' } })
+ .then(function (r) { if (!r.ok) throw new Error('versions.json ' + r.status); return r.json(); })
+ .then(function (manifest) {
+ renderPicker(anchor, loc, manifest);
+ renderLang(anchor, loc, manifest);
+ renderBanner(loc, manifest);
+ })
+ .catch(function () { /* silent degradation (ยง4) */ });
+ }
+
+ function renderPicker(anchor, loc, manifest) {
+ var label = document.createElement('label');
+ label.setAttribute('for', 'vyos-version-select');
+ label.textContent = 'Version: ';
+ var sel = document.createElement('select');
+ sel.id = 'vyos-version-select';
+ manifest.versions.forEach(function (v) {
+ var o = document.createElement('option');
+ o.value = v.slug; o.textContent = v.label; o.selected = v.slug === loc.slug;
+ sel.appendChild(o);
+ });
+ sel.addEventListener('change', function () {
+ var search = window.location.search, hash = window.location.hash;
+ var target = navUrlFor(loc, sel.value, search, hash);
+ var fallback = '/' + loc.lang + '/' + sel.value + '/' + (search || '') + (hash || '');
+ fetch(targetUrlFor(loc, sel.value), { method: 'HEAD' })
+ .then(function (r) {
+ window.location.href = (r.status === 404) ? fallback : target;
+ })
+ .catch(function () { window.location.href = fallback; });
+ });
+ anchor.appendChild(label);
+ anchor.appendChild(sel);
+
+ var entry = manifest.versions.filter(function (v) { return v.slug === loc.slug; })[0];
+ if (entry && entry.pdf) {
+ var a = document.createElement('a');
+ a.href = entry.pdf; a.className = 'vyos-pdf-link'; a.textContent = 'PDF';
+ anchor.appendChild(a);
+ }
+ }
+
+ function renderLang(anchor, loc, manifest) {
+ if (!manifest.languages || manifest.languages.length <= 1) return; // scaffold: hidden while en-only (ยง4)
+ var sel = document.createElement('select');
+ sel.id = 'vyos-lang-select';
+ sel.setAttribute('aria-label', 'Language');
+ manifest.languages.forEach(function (l) {
+ var o = document.createElement('option');
+ o.value = l.code; o.textContent = l.label; o.selected = l.code === loc.lang;
+ sel.appendChild(o);
+ });
+ sel.addEventListener('change', function () {
+ window.location.href = '/' + sel.value + '/' + loc.slug + '/' + loc.rest +
+ window.location.search + window.location.hash;
+ });
+ anchor.appendChild(sel);
+ }
+
+ function renderBanner(loc, manifest) {
+ var b = bannerFor(loc.slug, manifest);
+ if (!b) return;
+ var key = 'vyos-banner-dismissed-' + loc.slug;
+ try { if (window.localStorage.getItem(key)) return; } catch (e) { /* private mode */ }
+ var div = document.createElement('div');
+ div.className = 'vyos-version-banner vyos-banner-' + b.kind;
+ div.setAttribute('role', b.kind === 'eol' ? 'alert' : 'note');
+ var span = document.createElement('span');
+ span.textContent = bannerText(b, manifest);
+ div.appendChild(span);
+ if (b.newest && b.kind !== 'dev') {
+ var link = document.createElement('a');
+ link.href = '/' + loc.lang + '/' + b.newest + '/';
+ link.textContent = ' Switch to ' + b.newest + '.';
+ div.appendChild(link);
+ }
+ var x = document.createElement('button');
+ x.textContent = 'ร—'; x.className = 'vyos-banner-dismiss';
+ x.setAttribute('aria-label', 'Dismiss');
+ x.addEventListener('click', function () {
+ try { window.localStorage.setItem(key, '1'); } catch (e) { /* ignore */ }
+ div.remove();
+ });
+ div.appendChild(x);
+ document.body.insertBefore(div, document.body.firstChild);
+ }
+
+ window.VyOSVersionPicker = {
+ parseLocation: parseLocation, bannerFor: bannerFor,
+ targetUrlFor: targetUrlFor, navUrlFor: navUrlFor, init: init,
+ };
+ if (typeof document !== 'undefined' && document.addEventListener)
+ document.addEventListener('DOMContentLoaded', init);
+})(window);