Embed the bggpipe "Game Shelves" export (static/games/) into a themed Hugo page. A small loader (embed.js) fetches each export page, lifts out its <main>, rewrites relative URLs, and rewires game links to ?g=<slug> so the blog chrome stays on every page; embed.css scopes the export's component styling to the embed. The export itself is untouched and stays re-exportable. Adds the menu entry and excludes badges/ from deploy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
130 lines
5.0 KiB
JavaScript
130 lines
5.0 KiB
JavaScript
"use strict";
|
|
/*
|
|
* Themed embed for the bggpipe "Game Shelves" export.
|
|
*
|
|
* The export under /games/ is a self-contained static site regenerated by
|
|
* bggpipe; nothing here edits it. This loader fetches an export page, lifts
|
|
* out its <main>, rewrites the export's relative URLs to absolute /games/…
|
|
* paths, and drops the result into the themed /mygames/ page. Individual game
|
|
* links are rewired to ?g=<slug> so the blog chrome stays on every page.
|
|
*/
|
|
(function () {
|
|
const BASE = "/games/"; // where the export is served
|
|
const mount = document.getElementById("games-embed");
|
|
if (!mount) return;
|
|
|
|
// Which export page to show: ?g=<slug> for a game, otherwise the index.
|
|
function targetFor(slug) {
|
|
return slug ? `${BASE}${slug}/` : BASE;
|
|
}
|
|
|
|
// Resolve a relative href/src from the export page against its real URL,
|
|
// returning an absolute path so it works from /mygames/.
|
|
function absolutize(value, pageUrl) {
|
|
if (!value) return value;
|
|
// Leave absolute URLs, anchors, and protocol-relative links alone.
|
|
if (/^([a-z]+:|\/\/|#|\/)/i.test(value)) return value;
|
|
return new URL(value, new URL(pageUrl, location.origin)).pathname +
|
|
(new URL(value, new URL(pageUrl, location.origin)).search || "");
|
|
}
|
|
|
|
// Turn an export game link (e.g. "agricola/" or "../agricola/") into the
|
|
// themed equivalent (?g=agricola). Returns null if it isn't a game link.
|
|
function slugFromGameHref(href, pageUrl) {
|
|
const abs = new URL(href, new URL(pageUrl, location.origin)).pathname;
|
|
if (!abs.startsWith(BASE)) return null;
|
|
const rest = abs.slice(BASE.length).replace(/\/$/, "");
|
|
// A game link is a single path segment that isn't an asset folder.
|
|
if (!rest || rest.includes("/") || rest === "art") return null;
|
|
return rest;
|
|
}
|
|
|
|
function render(slug) {
|
|
// no-cache: revalidate against the server so a re-export shows up on
|
|
// the next visit instead of whenever the heuristic cache expires
|
|
fetch(targetFor(slug), { cache: "no-cache" })
|
|
.then((r) => {
|
|
if (!r.ok) throw new Error(`${r.status} for ${targetFor(slug)}`);
|
|
return r.text();
|
|
})
|
|
.then((html) => {
|
|
const pageUrl = targetFor(slug);
|
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
const main = doc.querySelector("main");
|
|
if (!main) throw new Error("no <main> in export page");
|
|
|
|
// Rewrite asset URLs (images, etc.) to absolute /games/ paths.
|
|
main.querySelectorAll("[src]").forEach((el) => {
|
|
el.setAttribute("src", absolutize(el.getAttribute("src"), pageUrl));
|
|
});
|
|
|
|
// Rewrite links: game links become ?g=<slug>; everything else
|
|
// (BGG, back-to-library, etc.) gets absolutized.
|
|
main.querySelectorAll("a[href]").forEach((a) => {
|
|
const href = a.getAttribute("href");
|
|
const gameSlug = slugFromGameHref(href, pageUrl);
|
|
const backToLibrary =
|
|
new URL(href, new URL(pageUrl, location.origin)).pathname
|
|
.replace(/\/$/, "") === BASE.replace(/\/$/, "");
|
|
if (backToLibrary) {
|
|
a.setAttribute("href", "?");
|
|
} else if (gameSlug) {
|
|
a.setAttribute("href", `?g=${encodeURIComponent(gameSlug)}`);
|
|
} else {
|
|
a.setAttribute("href", absolutize(href, pageUrl));
|
|
}
|
|
});
|
|
|
|
mount.innerHTML = "";
|
|
mount.append(...main.childNodes);
|
|
wireLinks();
|
|
wireSearch();
|
|
document.title = doc.title.replace(/ · The Game Shelves$/, "") +
|
|
" · Eric's Game Shelves";
|
|
window.scrollTo(0, 0);
|
|
})
|
|
.catch((err) => {
|
|
mount.innerHTML =
|
|
`<p>Sorry, the game library couldn't load. ` +
|
|
`<a href="${BASE}">Open it directly</a>.</p>`;
|
|
console.error("games embed:", err);
|
|
});
|
|
}
|
|
|
|
// Intercept in-page (?g=…) links so navigation stays client-side and themed.
|
|
function wireLinks() {
|
|
mount.querySelectorAll('a[href^="?"]').forEach((a) => {
|
|
a.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const url = new URL(a.getAttribute("href"), location.href);
|
|
const slug = url.searchParams.get("g");
|
|
history.pushState({ slug }, "", url);
|
|
render(slug);
|
|
});
|
|
});
|
|
}
|
|
|
|
// The export's index carries a search box; its inline <script> doesn't run
|
|
// when injected via innerHTML, so re-bind the same behavior here.
|
|
function wireSearch() {
|
|
const q = mount.querySelector("#q");
|
|
if (!q) return;
|
|
const cards = Array.from(mount.querySelectorAll("a.game")).map((c) => ({
|
|
el: c,
|
|
text: ((c.dataset.name || "") + " " + c.textContent).toLowerCase(),
|
|
}));
|
|
q.addEventListener("input", () => {
|
|
const needle = q.value.trim().toLowerCase();
|
|
cards.forEach((r) => {
|
|
r.el.style.display = !needle || r.text.includes(needle) ? "" : "none";
|
|
});
|
|
});
|
|
}
|
|
|
|
window.addEventListener("popstate", (e) => {
|
|
render((e.state && e.state.slug) || new URLSearchParams(location.search).get("g"));
|
|
});
|
|
|
|
render(new URLSearchParams(location.search).get("g"));
|
|
})();
|