forge site deploy sets FORGE_SITE_BASE and serves dist/ from a file server with no extensionless routes, so with it set the pages, manifest shortcuts and service worker name pages by file (./fuji.html). The service worker reads paths relative to its own directory, which changes nothing at the root of gud.pics. .forge/sites declares the app so it deploys to https://pages.src.rest/sites/cat/gudpics/app/.
- commit
- 55012d4d1e21bc5446ea8d0440f5fce9981db042
- parent
- 970270f355f14975280b365df6773a4fc7cddd61
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 10s, signed by cat
ci attestation
forge-attestation 1 type ci commit 55012d4d1e21bc5446ea8d0440f5fce9981db042 tree 725d2719eab744b2f69cc9098e30642ad75a5bb9 script .forge/ci 819a0cb8964222e9eb5fb2d832f164d8bac9903a result pass exit 0 started 2026-10-17T23:02:57Z duration 10s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:0851155a5c7c7c6037ba8a510ffa19d9fe37e347a8c435d53d008035069a9819 36 lines |Extracted detector, classification, RANSAC, fit and homography. |shared:INFO: (Emscripten: Running sanity checks) |cache:INFO: generating system headers: sysroot_install.stamp... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot_install.stamp" for subsequent builds) |cache:INFO: - ok |cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libGL-getprocaddr.a... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot/lib/wasm32-emscripten/libGL-getprocaddr.a" for subsequent builds) |system_libs:INFO: compiled 4 inputs in 0.13s |cache:INFO: - ok |cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libal.a... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot/lib/wasm32-emscripten/libal.a" for subsequent builds) |system_libs:INFO: compiled 1 inputs in 0.12s |cache:INFO: - ok |cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libhtml5.a... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot/lib/wasm32-emscripten/libhtml5.a" for subsequent builds) |system_libs:INFO: compiled 5 inputs in 0.13s |cache:INFO: - ok |cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libstubs.a... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot/lib/wasm32-emscripten/libstubs.a" for subsequent builds) |system_libs:INFO: compiled 2 inputs in 0.12s |cache:INFO: - ok |cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libnoexit.a... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot/lib/wasm32-emscripten/libnoexit.a" for subsequent builds) |system_libs:INFO: compiled 1 inputs in 0.12s |cache:INFO: - ok |cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libc.a... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot/lib/wasm32-emscripten/libc.a" for subsequent builds) |system_libs:INFO: compiled 1074 inputs in 2.43s |cache:INFO: - ok |cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libdlmalloc.a... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot/lib/wasm32-emscripten/libdlmalloc.a" for subsequent builds) |system_libs:INFO: compiled 2 inputs in 0.20s |cache:INFO: - ok |cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libclang_rt.builtins.a... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot/lib/wasm32-emscripten/libclang_rt.builtins.a" for subsequent builds) |system_libs:INFO: compiled 184 inputs in 0.74s |cache:INFO: - ok |cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libsockets.a... (this will be cached in "/Users/dami/.cache/gudpics/emscripten/sysroot/lib/wasm32-emscripten/libsockets.a" for subsequent builds) |system_libs:INFO: compiled 27 inputs in 0.29s |cache:INFO: - ok |PASS mode 1: 3.614° → 0.446° RMS; crop contained |PASS mode 2: 1.601° → 0.251° RMS; crop contained |PASS mode 3: 2.943° → 0.378° RMS; crop contained |PASS blank image, invalid dimensions, folded homography |Packaged 55 files; core.b602927904.wasm, core.25d6942921.mjs, raw.48e3d270ea.wasm, raw.674930b5af.mjs, acros-green.dc33224d47.png, acros-red.c9ba86b7d8.png, acros-yellow.e198e03d72.png, acros.631a0b2eae.png, astia.33c3293155.png, classic_chrome.04b7606f95.png, eterna.2d3d2a4cb7.png, mono-green.f04ced1ef1.png, mono-red.1070aed30c.png, mono-yellow.738ac66b08.png, mono.143d0831a0.png, neutral-dr100.72f6dd2400.png, neutral-dr200.b7e34cc826.png, neutral-dr400.d636d840a5.png, pro_neg_high.591920eab7.png, pro_neg_std.e33d080388.png, provia.b05e63f38e.png, sepia.f4ce5b13b3.png, velvia.64039fbf9b.png, geometry.6660c0e901.mjs, worker.92841bc082.mjs, app.fe46c6e082.mjs, raf.c00c769200.mjs, lens.2a04982f14.mjs, rawworker.a8337972bc.mjs, fuji.8b2451a2a4.mjs, social.4a6cced539.mjs, elaworker.6979153b3e.mjs, ela.fb26e870b2.mjs, home.b02e4fda83.mjs, viewport.a47af6790b.js, style.dd8bbfbf98.css
Signature: good, cat
| A | .forge/ | 2 | ++ |
| M | scripts/ | 15 | +++++++++++++-- |
| M | sw.js | 7 | +++++-- |
@@ -6,7 +6,7 @@ reference to them is rewritten, so browsers and the service worker can cache
66 them forever while HTML pages are always fetched fresh.77 """88 from pathlib import Path9-import shutil, hashlib, re9+import shutil, hashlib, re, os1010 root = Path(__file__).resolve().parent.parent1111 dist = root / 'dist'1212 if dist.exists(): shutil.rmtree(dist)@@ -22,6 +22,16 @@ DIRS = ['LICENSES']
2222 ICONS = ['assets/icon.svg', 'assets/icon-192.png', 'assets/icon-512.png']2323 2424 def digest(data): return hashlib.sha256(data).hexdigest()[:10]25+26+# `forge site deploy` sets FORGE_SITE_BASE and serves dist/ from a plain file server under that27+# path. It has no extensionless routes like Cloudflare's, so there pages are reached by file name.28+ROUTES = ['perspective', 'fuji', 'social', 'ela'] if os.environ.get('FORGE_SITE_BASE') else []29+def routed(text):30+ for r in ROUTES:31+ for q in '"\'':32+ text = text.replace(f'{q}./{r}{q}', f'{q}./{r}.html{q}').replace(f'{q}/{r}{q}', f'{q}/{r}.html{q}')33+ return text34+2535 mapping = {} # source path -> dist path (relative to dist)2636 2737 def rewrite(text, at_dir):@@ -47,6 +57,7 @@ for src in PAGES + FIXED:
4757 (dist / src).parent.mkdir(parents=True, exist_ok=True)4858 text = (root / src).read_text()4959 if src.endswith('.html'): text = rewrite(text, '.')60+ if src.endswith(('.html', '.webmanifest')): text = routed(text)5061 (dist / src).write_text(text)5162 for d in DIRS: shutil.copytree(root / d, dist / d)5263 (dist / 'assets').mkdir(exist_ok=True)@@ -58,5 +69,5 @@ h = hashlib.sha256()
5869 for p in sorted(dist.rglob('*')):5970 if p.is_file(): h.update(p.read_bytes())6071 sw = (root / 'sw.js').read_text().replace('__CACHE__', 'gudpics-' + h.hexdigest()[:12]).replace('__FILES__', ','.join(f"'{f}'" for f in precache))61-(dist / 'sw.js').write_text(sw)72+(dist / 'sw.js').write_text(routed(sw))6273 print(f'Packaged {sum(1 for p in dist.rglob("*") if p.is_file())} files; ' + ', '.join(Path(v).name for v in mapping.values()))@@ -3,12 +3,15 @@
33 const CACHE='__CACHE__';44 const FILES=[__FILES__];55 const PAGES={'/':'./','/perspective':'./perspective','/fuji':'./fuji','/social':'./social','/ela':'./ela'};6+// Paths are taken relative to the worker's directory, so the app also works under a path.7+const BASE=new URL('./',self.location).pathname;68 self.addEventListener('install',e=>{self.skipWaiting();e.waitUntil(caches.open(CACHE).then(c=>c.addAll(FILES)));});79 self.addEventListener('activate',e=>e.waitUntil(caches.keys().then(keys=>Promise.all(keys.filter(k=>k!==CACHE).map(k=>caches.delete(k)))).then(()=>self.clients.claim())));810 self.addEventListener('fetch',e=>{911 const url=new URL(e.request.url);1012 if(e.request.method!=='GET'||url.origin!==self.location.origin)return;11- const path=url.pathname.replace(/\/+$/,'')||'/';13+ const rel=url.pathname.startsWith(BASE)?'/'+url.pathname.slice(BASE.length):url.pathname;14+ const path=rel.replace(/\/+$/,'')||'/';1215 if(e.request.mode==='navigate'||path in PAGES){1316 // Pages: network first so a new deploy is picked up on the next open; cache only when offline.1417 e.respondWith(fetch(e.request).then(r=>{if(r.ok)caches.open(CACHE).then(c=>c.put(PAGES[path]||e.request,r.clone()));return r;})@@ -18,6 +21,6 @@ self.addEventListener('fetch',e=>{
1821 // Hashed assets never change under the same name: cache first.1922 e.respondWith(caches.open(CACHE).then(async c=>{2023 const cached=await c.match(e.request,{ignoreSearch:true});if(cached)return cached;21- const r=await fetch(e.request);if(r.ok&&url.pathname.startsWith('/static/'))c.put(e.request,r.clone());return r;24+ const r=await fetch(e.request);if(r.ok&&rel.startsWith('/static/'))c.put(e.request,r.clone());return r;2225 }));2326 });