diff --git a/deploy/sync-static.sh b/deploy/sync-static.sh index d523672..3306fda 100755 --- a/deploy/sync-static.sh +++ b/deploy/sync-static.sh @@ -18,3 +18,10 @@ rsync -a --delete \ rsync -a "$src/index.html" "$site/index.html" rsync -a "$src/service-worker.js" "$site/service-worker.js" find "$site/_app/immutable" -type f -mtime +14 -delete 2>/dev/null || true + +# Warm the Cloudflare edge cache: fetch every immutable asset through the public +# domain so the FIRST real visitor after a deploy gets cache HITs instead of slow +# cold fetches from the (residential) origin — the post-deploy blank/slow-load cause. +echo " warming edge cache…" +find "$site/_app/immutable" -type f \( -name '*.js' -o -name '*.css' \) -printf '/_app/immutable/%P\n' \ + | xargs -P 8 -I{} curl -fsS -o /dev/null --max-time 20 "https://upbeatbytes.com{}" 2>/dev/null || true diff --git a/frontend/src/app.html b/frontend/src/app.html index df5580c..e45a89d 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -48,7 +48,7 @@ navigator.sendBeacon && navigator.sendBeacon('/api/events', b); } catch (e) { /* best-effort telemetry */ } } - var timer = setTimeout(showBoot, 7000); + var timer = setTimeout(showBoot, 10000); // Svelte calls this once it has mounted (see +layout.svelte). window.__ubBooted = function () { window.__ubMounted = true; diff --git a/frontend/src/service-worker.js b/frontend/src/service-worker.js index 9d5daed..626c0c8 100644 --- a/frontend/src/service-worker.js +++ b/frontend/src/service-worker.js @@ -1,29 +1,24 @@ /// -// 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'; +// Calm, lightweight service worker. It does NOT bulk-precache on install — the +// browser already caches the year-immutable assets on its own, and a cold +// precache storm right after a deploy hammers the (residential) origin and slows +// first loads. Instead: cache the shell for an offline fallback, and cache other +// assets lazily as they're actually used. Live data (API + server-rendered pages) +// is always fetched fresh. +import { 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. +// Paths the FastAPI server owns — the SW must NOT intercept or cache these. 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) => { + // Best-effort: grab the app shell as an offline fallback. No bulk precache. 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(); - }) + caches.open(CACHE).then((c) => c.add('/').catch(() => {})).then(() => self.skipWaiting()) ); }); @@ -43,8 +38,8 @@ self.addEventListener('fetch', (event) => { if (url.origin !== location.origin) return; if (isServerPath(url.pathname)) return; // let the network/server handle these - // Navigations: network-first. On success, keep the freshest *real HTML shell* - // as the offline fallback; on failure, serve that cached shell (never blank). + // Navigations: network-first; keep the freshest real HTML shell as the offline + // fallback; on a failed fetch, serve that cached shell (never blank). if (request.mode === 'navigate') { event.respondWith( fetch(request) @@ -60,6 +55,18 @@ self.addEventListener('fetch', (event) => { return; } - // Static assets (hashed JS/CSS, fonts, icons, word lists): cache-first. - event.respondWith(caches.match(request).then((cached) => cached || fetch(request))); + // Static assets: serve from cache if present, else fetch and cache for next time + // (cache-as-you-go — no install storm). Only cache successful same-origin GETs. + event.respondWith( + caches.match(request).then((cached) => { + if (cached) return cached; + return fetch(request).then((res) => { + if (res && res.ok && res.type === 'basic') { + const copy = res.clone(); + caches.open(CACHE).then((c) => c.put(request, copy)).catch(() => {}); + } + return res; + }); + }) + ); });