Add themed board game library at /mygames/
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e0e08ba67d
commit
f6611257b3
@@ -0,0 +1,129 @@
|
||||
"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"));
|
||||
})();
|
||||
Reference in New Issue
Block a user