1/** 2 * Offline navigation fallback. 3 * 4 * Scope: "/" (everywhere except "/offline/*", which is covered by the more 5 * specific sw.js registration). 6 * 7 * Only intercepts top-level page navigations. When the network fetch fails 8 * (offline / DNS failure), we serve a precached, self-contained fallback page 9 * (/offline.html) AS THE RESPONSE to the original request â we do NOT redirect. 10 * 11 * Why not redirect? A 302 redirect to /offline replaces the current history 12 * entry with /offline, which destroys the user's location: hitting Back no 13 * longer returns to the page they were on, and refreshing once back online 14 * reloads /offline instead of where they were. Answering the original 15 * navigation request with respondWith() keeps the URL and history intact, so 16 * Back works and a refresh after reconnecting restores the real page. 17 * 18 * Subresource requests (images, scripts, CSS, API calls) are not intercepted 19 * â they pass straight through to the network. 20 */ 21 22// ? This SW is hand-written and byte-stable across deploys, so the browser 23// ? only reinstalls it (and re-runs the precache below) when this file's bytes 24// ? change. If you edit offline.html, BUMP this version (v1 -> v2) so the SW 25// ? reinstalls and re-precaches the new page; the activate handler then deletes 26// ? the stale cache. Without a bump, clients keep serving the old offline.html. 27const OFFLINE_FALLBACK_CACHE = "offline-fallback-v2"; 28const OFFLINE_FALLBACK_URL = "/offline.html"; 29const EXCLUDED_NAVIGATION_PATHS = ["/auth/mcp"]; 30const EXCLUDED_NAVIGATION_PREFIXES = ["/api/auth/mcp/"]; 31 32self.addEventListener("install", (event) => { 33 event.waitUntil( 34 caches 35 .open(OFFLINE_FALLBACK_CACHE) 36 .then((cache) => cache.add(OFFLINE_FALLBACK_URL)) 37 .catch(() => { 38 // ? If precaching fails (e.g. offline at install), the fetch handler 39 // ? falls back to an inline response, so registration still succeeds. 40 }), 41 ); 42 self.skipWaiting(); 43}); 44 45self.addEventListener("activate", (event) => { 46 event.waitUntil( 47 caches 48 .keys() 49 .then((keys) => 50 Promise.all( 51 keys 52 .filter( 53 (key) => 54 key.startsWith("offline-fallback-") && 55 key !== OFFLINE_FALLBACK_CACHE, 56 ) 57 .map((key) => caches.delete(key)), 58 ), 59 ) 60 .then(() => self.clients.claim()), 61 ); 62}); 63 64self.addEventListener("fetch", (event) => { 65 if (event.request.mode !== "navigate") return; 66 67 const url = new URL(event.request.url); 68 if ( 69 EXCLUDED_NAVIGATION_PATHS.includes(url.pathname) || 70 EXCLUDED_NAVIGATION_PREFIXES.some((prefix) => 71 url.pathname.startsWith(prefix), 72 ) 73 ) { 74 return; 75 } 76 77 event.respondWith( 78 fetch(event.request).catch(async () => { 79 const cached = await caches.match(OFFLINE_FALLBACK_URL); 80 if (cached) return cached; 81 82 return new Response( 83 "<!doctype html><title>Offline</title><h1>You're offline</h1><p>Check your connection and try again.</p>", 84 { 85 status: 503, 86 headers: { "Content-Type": "text/html; charset=utf-8" }, 87 }, 88 ); 89 }), 90 ); 91});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.