From 5d4909898f915b29821f93d146626c66c92d553f Mon Sep 17 00:00:00 2001 From: Philippe Cloutier Date: Sun, 4 Oct 2026 20:57:31 -0400 Subject: [PATCH] Serve the Pages project site from the /Wiki/ subpath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Pages serves project sites under //, so with base_url "/" every absolute link (/style.css, /zones/...) resolved to the domain root and 404'd on openperpetuum.github.io/Wiki/. The site keeps building against the domain root (local dev, the test job, and the served output all stay identical). The CI deploy build then re-points it at the Pages URL: - config.toml: base_url stays "/" (Zola 0.23 has no config env-var expansion, so the override lives in the pipeline, not a ${WIKI_BASE_URL} placeholder) - wiki.yml build job: rewrites base_url to the full Pages URL before building (sitemap/canonicals pick it up), then runs tools/rewrite_site_base.py — Zola 0.23 does NOT rewrite site-absolute links in templates/markdown, so this prefixes every href="/..." / src="/..." in the built HTML+SVG with /Wiki (idempotent, skips protocol/data/hash/relative values) - templates/base.html: carries config.base_url; the menu-selection, gamma-tier and footnav logic derive the base's path part ("" at the root, "/Wiki" in production) so both deploys work - map.js / zone-map.js: withRoot() prefixes JS-built zone links and the links/image-hrefs inside fetched zone-map SVGs (Zola never sees those) - search.js: the index fetch and result links use the base's PATH part on the current origin (a full-URL fetch would leave the origin) Verified in a browser served under a /Wiki prefix: styles, nav highlighting (incl. gamma tiers), world-map thumbnails, inlined zone maps, search and footnav all work; the local root build passes the full 93-test suite. --- .github/workflows/wiki.yml | 13 +++++++++ config.toml | 6 ++-- static/map.js | 23 ++++++++++++++- static/search.js | 32 +++++++++++++++----- static/zone-map.js | 22 +++++++++++++- templates/base.html | 41 ++++++++++++++++---------- tools/rewrite_site_base.py | 60 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 170 insertions(+), 27 deletions(-) create mode 100755 tools/rewrite_site_base.py diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml index 1718a0bc3..3a2e54d6b 100644 --- a/.github/workflows/wiki.yml +++ b/.github/workflows/wiki.yml @@ -173,11 +173,24 @@ jobs: echo "$GITHUB_WORKSPACE/tools" >> "$GITHUB_PATH" zola --version + - name: Point base_url at the Pages URL + # The Pages project site is served under the /Wiki/ subpath, so this + # build (and only this one — the test job keeps the local root base) + # compiles with the full Pages URL. Zola 0.23 has no config + # env-var expansion, so the value is rewritten in the pipeline. + run: sed -i 's|^base_url = .*|base_url = "https://openperpetuum.github.io/Wiki"|' config.toml + - name: Build # Zola 0.23 builds from the current directory (no -d flag); the # default working directory is the repo root where config.toml lives. run: zola build --force -o public + - name: Prefix links with the Pages path + # The source targets the domain root (base_url "/", so local and the + # test job share the output); Zola 0.23 does not rewrite + # site-absolute links, so prefix them with the /Wiki path here. + run: python3 tools/rewrite_site_base.py public https://openperpetuum.github.io/Wiki + - name: Upload artifact uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # ratchet:actions/upload-pages-artifact@v3 with: diff --git a/config.toml b/config.toml index 8b2f1104f..c952a2679 100644 --- a/config.toml +++ b/config.toml @@ -4,8 +4,10 @@ default_language = "en" title = "Open Perpetuum Wiki" description = "Player and developer reference for the Open Perpetuum server: what you can do in the game, entity stats, zone generation, and stat field semantics." -# GitHub Pages: set the repo Pages URL once the site is published, e.g. -# base_url = "https://.github.io//wiki" +# Local/test base: the site lives at the domain root. The CI DEPLOY build +# rewrites this to the GitHub Pages project URL (a subpath — +# openperpetuum.github.io/Wiki/), see .github/workflows/wiki.yml; Zola 0.23 +# has no config env-var expansion, so the override happens in the pipeline. base_url = "/" generate_feeds = false # Search is a lightweight client-side script (search.js + search_index.json diff --git a/static/map.js b/static/map.js index 4439a1a4b..6ecfd4b94 100644 --- a/static/map.js +++ b/static/map.js @@ -16,6 +16,19 @@ // opens pre-zoomed onto the cluster (initContentZoom, below). var MIN = 1, MAX = 8, INITIAL_MAX = 3, MIN_VISIBLE = 0.25; + // Deploy base (body[data-root] = config.base_url): "/" locally, the full + // GitHub Pages URL on the project site (subpath /Wiki/). The static + // builder (plus the CI rewrite pass) prefixes HTML links with the path + // part, but JS-built URLs and fetched-SVG links keep the site-absolute + // form — withRoot() prefixes them the same way. + var ROOT = (document.body && document.body.getAttribute('data-root')) || '/'; + var ROOT_PATH = ROOT.replace(/^https?:\/\/[^/]+/, '').replace(/\/+$/, ''); // "" | "/Wiki" + function withRoot(h) { + if (!h || h.charAt(0) !== '/') return h; // not site-absolute + if (ROOT_PATH && h.indexOf(ROOT_PATH + '/') === 0) return h; // already + return ROOT_PATH + h; + } + function limit(v, lo, hi) { return Math.min(hi, Math.max(lo, v)); } // Overlap length of [0, len] with the content span centered at offset t. @@ -249,6 +262,14 @@ wrap.setAttribute('data-zoom-init', '1'); wrap._zoomSvg = svg; + // The static builder rewrites HTML links but not SVG + // (the world-map node thumbnails) — prefix the deploy base for + // subpath deployments (no-op when the base is "/"). + Array.prototype.forEach.call(svg.querySelectorAll('image'), function (im) { + var h = im.getAttribute('href') || im.getAttribute('xlink:href'); + if (h) im.setAttribute('href', withRoot(h)); + }); + // Wheel = zoom (no key needed). The listener is non-passive so the // page scroll is suppressed while the pointer is over the map. wrap.addEventListener('wheel', function (e) { @@ -503,7 +524,7 @@ var cur = null, closeTimer = 0, inPop = false; // mirror of the generator's DisplayName (zone_asi_a_real -> "ASI A Real") - function hrefOf(n) { return '/zones/' + n.toLowerCase().replace(/_/g, '-') + '/'; } + function hrefOf(n) { return withRoot('/zones/' + n.toLowerCase().replace(/_/g, '-') + '/'); } // data-dests: ;-separated "display name|zone name" pairs — the // display name is the same string the column label shows (so the // selector entries read like the map), the zone name drives the line diff --git a/static/search.js b/static/search.js index 892e61e67..4654d5bc9 100644 --- a/static/search.js +++ b/static/search.js @@ -18,18 +18,30 @@ function wikiSearchInit() { }); } - // Relative path from the current page back to the site root (works under a - // subpath deployment too). The index lists site-absolute paths ('/features/x/'), - // so the root is derived from where the index itself was fetched from. - function relToRoot() { + // Site root, from the deploy base (body[data-root] = config.base_url): + // "/" locally, the full GitHub Pages URL on the project site. Only the + // PATH part matters — the site always lives on the CURRENT origin under + // that path — so both the index fetch and the result links are built + // origin-relative (a full-URL fetch would leave the origin). + var baseRoot = (document.body && document.body.getAttribute('data-root')) || ''; + function rootInfo() { + if (baseRoot) { + var p = baseRoot.replace(/^https?:\/\/[^/]+/, '').replace(/\/+$/, ''); // "" | "/Wiki" + return { fetch: p + '/search_index.json', path: p || '/' }; + } + // no data-root (older build): climb out of the current path var parts = location.pathname.replace(/\/+$/, '').split('/').filter(Boolean); - return parts.map(function () { return '..'; }).concat('search_index.json').join('/'); + return { + fetch: parts.map(function () { return '..'; }).concat('search_index.json').join('/'), + path: new URL(parts.map(function () { return '..'; }).join('/') + '/', location.href).pathname + }; } function loadIndex() { if (pages) return Promise.resolve(pages); - rootPath = new URL(relToRoot(), location.href).pathname.replace(/search_index\.json$/, ''); - return fetch(relToRoot(), { cache: 'force-cache' }) + var ri = rootInfo(); + rootPath = ri.path; + return fetch(ri.fetch, { cache: 'force-cache' }) .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); @@ -38,7 +50,11 @@ function wikiSearchInit() { pages = data.map(function (p) { var t = p.t || ''; var d = p.d || ''; - var u = rootPath + (p.u || '/').replace(/^\//, ''); + // p.u is site-absolute ("/content/x/"); rootPath is "" for + // "/" (no change) or the base path ("/Wiki") to prepend. + var u = p.u || '/'; + if (rootPath && rootPath !== '/' && u.indexOf(rootPath + '/') !== 0) + u = rootPath + (u.charAt(0) === '/' ? u : '/' + u); return { t: t, d: d, u: u, tl: t.toLowerCase(), dl: (d + ' ' + u).toLowerCase() diff --git a/static/zone-map.js b/static/zone-map.js index 6f128ff7b..859b23b5a 100644 --- a/static/zone-map.js +++ b/static/zone-map.js @@ -49,6 +49,18 @@ } } + // Deploy base (body[data-root]): the SVG files carry SITE-ABSOLUTE + // links and image hrefs (written by the generator; fetched as plain + // files, so nothing rewrites them) — prefix with the base's PATH part + // for subpath deploys (no-op when the base is "/"). + var ROOT = (document.body && document.body.getAttribute('data-root')) || '/'; + var ROOT_PATH = ROOT.replace(/^https?:\/\/[^/]+/, '').replace(/\/+$/, ''); // "" | "/Wiki" + function withRoot(h) { + if (!h || h.charAt(0) !== '/') return h; + if (ROOT_PATH && h.indexOf(ROOT_PATH + '/') === 0) return h; + return ROOT_PATH + h; + } + function upgrade(img) { var src = img.getAttribute('src'); fetch(src, { credentials: 'same-origin' }).then(function (r) { @@ -63,6 +75,14 @@ svg.classList.add('zonemap'); svg.removeAttribute('width'); svg.removeAttribute('height'); + // teleport links + terrain s: prefix the deploy base + Array.prototype.forEach.call(svg.querySelectorAll('a[href]'), function (a) { + a.setAttribute('href', withRoot(a.getAttribute('href'))); + }); + Array.prototype.forEach.call(svg.querySelectorAll('image'), function (im) { + var h = im.getAttribute('href') || im.getAttribute('xlink:href'); + if (h) im.setAttribute('href', withRoot(h)); + }); var wrap = document.createElement('div'); wrap.className = 'zonemap-wrap zonetp-wrap'; @@ -112,7 +132,7 @@ } function scan() { - var imgs = document.querySelectorAll('img[src^="/zonemaps/"]'); + var imgs = document.querySelectorAll('img[src*="/zonemaps/"]'); for (var i = 0; i < imgs.length; i++) { if (imgs[i].getAttribute('data-zonetp-init')) continue; imgs[i].setAttribute('data-zonetp-init', '1'); diff --git a/templates/base.html b/templates/base.html index 97d195344..255c0d4ae 100644 --- a/templates/base.html +++ b/templates/base.html @@ -30,7 +30,7 @@ - +