diff --git a/frontend/src/service-worker.js b/frontend/src/service-worker.js index 0a5f17f..9d5daed 100644 --- a/frontend/src/service-worker.js +++ b/frontend/src/service-worker.js @@ -1,14 +1,30 @@ /// -// Minimal, calm service worker: precache the app shell + static assets so the -// site is installable (PWA) and the shell opens fast / works offline. Live data -// (the API and server-rendered /a/ pages) is always fetched fresh, never staled. +// Calm service worker: precache the app shell + static assets so the site is +// installable (PWA), fast, and resilient to transient network blips. Live data +// (the API and server-rendered pages) is always fetched fresh, never cached. import { build, files, version } from '$service-worker'; const CACHE = `upbeat-${version}`; const SHELL = [...build, ...files]; +// Paths the FastAPI server owns — the SW must NOT intercept or cache these +// (matches Caddy's @api matcher: docs, server-rendered article/brief pages, +// health, sitemap). Everything else is the static SvelteKit SPA. +function isServerPath(p) { + if (p.startsWith('/api/') || p.startsWith('/a/') || p.startsWith('/docs')) return true; + return p === '/openapi.json' || p === '/healthz' || p === '/today' || p === '/sitemap.xml'; +} + self.addEventListener('install', (event) => { - event.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)).then(() => self.skipWaiting())); + event.waitUntil( + caches.open(CACHE).then(async (c) => { + await c.addAll(SHELL); + // Cache the SPA shell so a failed navigation has a real page to fall back + // to (best-effort; it's also refreshed on every successful navigation). + try { await c.add('/'); } catch { /* refreshed on first online navigation */ } + await self.skipWaiting(); + }) + ); }); self.addEventListener('activate', (event) => { @@ -25,16 +41,25 @@ self.addEventListener('fetch', (event) => { if (request.method !== 'GET') return; const url = new URL(request.url); if (url.origin !== location.origin) return; + if (isServerPath(url.pathname)) return; // let the network/server handle these - // Always-fresh, never cached: the API and the server-rendered article pages. - if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/a/')) return; - - // Navigations: serve the cached app shell when offline (SPA fallback). + // Navigations: network-first. On success, keep the freshest *real HTML shell* + // as the offline fallback; on failure, serve that cached shell (never blank). if (request.mode === 'navigate') { - event.respondWith(fetch(request).catch(() => caches.match('/') || caches.match('/index.html'))); + event.respondWith( + fetch(request) + .then((res) => { + if (res && res.ok && (res.headers.get('content-type') || '').includes('text/html')) { + const copy = res.clone(); + caches.open(CACHE).then((c) => c.put('/', copy)).catch(() => {}); + } + return res; + }) + .catch(() => caches.match('/')) + ); return; } - // Static assets (JS/CSS/icons/fonts): cache-first, fall back to network. + // Static assets (hashed JS/CSS, fonts, icons, word lists): cache-first. event.respondWith(caches.match(request).then((cached) => cached || fetch(request))); });