From 4478c46a437d1ab048cbd1e210b9971eb8638513 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Tue, 1 Sep 2026 21:26:45 +0200 Subject: [PATCH] fix(pwa): make manifest/icon hrefs absolute for multi-segment SPA routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relative hrefs in the built index.html ("manifest.webmanifest", "icon.svg") resolve against the *document* URL, not the site root. Since this is an SPA where every route serves the same shell, a route with more path segments than the base path (e.g. /budget2.0/month/2026-09, which is where the app's own catch-all route redirects on first load) resolved these to a wrong, 404ing path — breaking the manifest and favicon, and with them installability, on the app's default landing URL. build.ts now rewrites both hrefs to be absolute (prefixed with basePath) as part of the existing dist/index.html -> dist/404.html step, keeping the two files byte-identical. src/index.html itself keeps the plain relative hrefs unchanged, since the substitution only happens in the built dist/ output. Also fixes a pre-existing, previously-uncaught bug surfaced while verifying the dev server: bun --hot src/index.ts 500'd on every request because Bun's dev-server HTML bundler (unlike Bun.build) has no equivalent of the `external` build option and always tries to resolve hrefs as module imports — manifest.webmanifest never existed as a real file under src/, only as build.ts's generated dist/ output. Added src/manifest.webmanifest as a small static placeholder so the dev server can resolve it; the production build still ignores this file entirely (external: ["*.webmanifest"] in build.ts) and writes its own generated manifest, unaffected by this addition. Co-Authored-By: Claude Opus 5 (1M context) --- build.ts | 16 +++++++++++++++- src/manifest.webmanifest | 10 ++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/manifest.webmanifest diff --git a/build.ts b/build.ts index 10a2dc8..f0bacf0 100644 --- a/build.ts +++ b/build.ts @@ -95,9 +95,23 @@ export function buildManifest(base: string): string { await Bun.write(path.join(outdir, "manifest.webmanifest"), buildManifest(basePath)); await Bun.write(path.join(outdir, "icon.svg"), Bun.file("src/icon.svg")); +// This is an SPA: every route serves the same shell, so the *document* URL +// can be several segments deep (e.g. /budget2.0/month/2026-09). Relative +// hrefs in the built HTML resolve against that document URL, not the site +// root, so "manifest.webmanifest" and "icon.svg" would 404 on any route +// with more path segments than the base path itself. Rewrite them to be +// absolute (prefixed with basePath, which always ends in "/") so they +// resolve correctly no matter how deep the current route is. Done here +// rather than hardcoded in src/index.html so the dev server (which always +// serves from "/") keeps working unchanged. +let indexHtml = await Bun.file(path.join(outdir, "index.html")).text(); +indexHtml = indexHtml + .replace('href="manifest.webmanifest"', `href="${basePath}manifest.webmanifest"`) + .replace('href="icon.svg"', `href="${basePath}icon.svg"`); +await Bun.write(path.join(outdir, "index.html"), indexHtml); + // GitHub Pages has no rewrite rules, so a deep link on a cold load lands on // 404.html. Serving the shell from there makes client-side routing work. -const indexHtml = await Bun.file(path.join(outdir, "index.html")).text(); await Bun.write(path.join(outdir, "404.html"), indexHtml); console.log(`Built for base path ${basePath}`); diff --git a/src/manifest.webmanifest b/src/manifest.webmanifest new file mode 100644 index 0000000..7076822 --- /dev/null +++ b/src/manifest.webmanifest @@ -0,0 +1,10 @@ +{ + "name": "Budget 2.0", + "short_name": "Budget", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#0f172a", + "theme_color": "#0f172a", + "icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml" }] +}