feat: add GitHub Actions CI and support hosting under a subpath

Mirrors the GitLab pipeline — typecheck, tests, feed sanity — and adds a
Pages deploy.

Pages serves from /<repo>/, and the app used absolute asset paths, so that
job would have shipped a site that 404s on everything. Assets now resolve
against a <base href> the build substitutes, the feed URL resolves against
document.baseURI so deep links work too, and the service worker derives its
paths from its own registration scope. Root-hosted builds are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 01:22:22 +02:00
co-authored by Claude Opus 5
parent 517066dc65
commit b86794a62d
8 changed files with 173 additions and 17 deletions
+4 -1
View File
@@ -13,7 +13,10 @@ export type FeedState =
* worse than one that asks you to reload.
*/
export async function fetchFeed(signal?: AbortSignal): Promise<EventFeed> {
const res = await fetch("/data/events.v1.json", { signal: signal ?? null });
// Resolved against <base>, so this is correct at a domain root, under a
// GitHub Pages subpath, and on a deep link alike.
const feedUrl = new URL("data/events.v1.json", document.baseURI);
const res = await fetch(feedUrl, { signal: signal ?? null });
if (!res.ok) {
throw new Error(`Feed request failed (${res.status}).`);
}
+2 -1
View File
@@ -15,7 +15,8 @@ createRoot(root).render(
// guarded because file:// and older browsers have no service worker at all.
if ("serviceWorker" in navigator && location.protocol.startsWith("http")) {
window.addEventListener("load", () => {
void navigator.serviceWorker.register("/sw.js").catch(() => {
const swUrl = new URL("sw.js", document.baseURI);
void navigator.serviceWorker.register(swUrl).catch(() => {
// An unavailable worker costs offline support, nothing else. The app
// works exactly as before.
});
+3 -3
View File
@@ -2,15 +2,15 @@
"name": "Event Clock",
"short_name": "Event Clock",
"description": "Live and upcoming gacha events, sorted by what expires next.",
"start_url": "/",
"scope": "/",
"start_url": "./",
"scope": "./",
"display": "standalone",
"background_color": "#12141c",
"theme_color": "#12141c",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icon.svg",
"src": "icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
+6 -2
View File
@@ -15,8 +15,12 @@
*/
const CACHE_VERSION = "event-clock-v1";
const SHELL = ["/", "/index.html", "/styles.css", "/main.js"];
const FEED = "/data/events.v1.json";
// Paths are derived from the registration scope, so the same worker is
// correct at a domain root and under a subpath (GitHub Pages) alike.
const BASE = new URL("./", self.registration.scope);
const at = (path) => new URL(path, BASE).toString();
const SHELL = ["", "index.html", "styles.css", "main.js"].map(at);
const FEED = new URL("data/events.v1.json", BASE).pathname;
const FONT_HOSTS = new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
self.addEventListener("install", (event) => {