1// Off The Press service worker (2026-08-31). 2// 3// PURPOSE IS NARROW ON PURPOSE. This worker exists for two reasons only: 4// 1. Chromium's install-prompt algorithm still wants a fetch handler present 5// (the *menu* install requirement was dropped in Chrome 108 mobile / 112 6// desktop, but the prompt heuristic was not), so `beforeinstallprompt` 7// fires reliably for the home-screen banner in index.html. 8// 2. Give an installed reader a branded page instead of the browser's dinosaur 9// when they open the icon with no signal. 10// 11// IT CACHES NO NEWS CONTENT, EVER. This is a news site with two known staleness 12// windows already (the ~20min post-deploy origin lag, and unversioned main.js on 13// max-age=300). A caching service worker would be a third and by far the worst, 14// because it outlives a deploy and a hard refresh. So: 15// 16// * The fetch handler returns early for anything that is not a top-level 17// navigation. It never calls respondWith() for subresources, which means 18// feed.json, main.js, style.css, px.js, Ezoic, Connatix and Hotjar are not 19// intercepted at all -- not cached, not proxied, not delayed. 20// * Navigations are network-FIRST with no cache write. A reader who is online 21// always gets the live page. The cached copy is only ever reached when 22// fetch() itself rejects, i.e. the network is genuinely gone. 23// 24// The only two things in the cache are /offline and the 192px icon it 25// draws, both of which are static and change only on an explicit deploy. 26// 27// KILL SWITCH. Service workers outlive the page that registered them, so 28// removing the <script> that registers this does NOT uninstall it from readers 29// who already have it. To retire it, REPLACE this file's body with: 30// 31// self.addEventListener('install', () => self.skipWaiting()); 32// self.addEventListener('activate', (e) => e.waitUntil( 33// self.registration.unregister().then(() => self.clients.claim()))); 34// 35// and deploy that. Every existing reader unregisters on their next visit. 36// Do not simply delete the file -- /sw.js would then 404 and the old worker 37// stays installed and in charge. _worker.js serves this path `no-cache` so the 38// replacement is picked up on the next navigation rather than up to a day later. 39 40const CACHE = 'otp-offline-v1'; 41// '/offline', NOT '/offline.html'. Cloudflare Pages serves offline.html at the 42// extensionless path and 308-redirects the .html form; precaching the redirect 43// would leave a redirected response in the cache that caches.match() cannot 44// reliably return, so the fallback would silently never appear. 45const OFFLINE_URL = '/offline'; 46const PRECACHE = [OFFLINE_URL, '/icons/otp-192.png']; 47 48self.addEventListener('install', (event) => { 49 event.waitUntil( 50 caches 51 .open(CACHE) 52 .then((cache) => cache.addAll(PRECACHE)) 53 // Never let a precache failure wedge the install: a worker with an empty 54 // cache still satisfies the install criteria, and the fetch handler below 55 // degrades to a plain network error exactly as if it were not here. 56 .catch(() => {}) 57 .then(() => self.skipWaiting()) 58 ); 59}); 60 61self.addEventListener('activate', (event) => { 62 event.waitUntil( 63 caches 64 .keys() 65 .then((keys) => 66 Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))) 67 ) 68 .catch(() => {}) 69 .then(() => self.clients.claim()) 70 ); 71}); 72 73self.addEventListener('fetch', (event) => { 74 const req = event.request; 75 76 // Everything that is not a top-level navigation is left completely alone. 77 // Returning without calling respondWith() hands the request back to the 78 // browser untouched -- this is the line that keeps ads, analytics and 79 // feed.json out of the worker's path entirely. 80 if (req.mode !== 'navigate') return; 81 82 // Range requests are navigations only in exotic cases, but a partial response 83 // must never be answered from the offline page. 84 if (req.headers.has('range')) return; 85 86 event.respondWith( 87 // Network first, always. No cache write on success: nothing about a news 88 // page is worth persisting past this request. 89 fetch(req).catch(async () => { 90 const cached = await caches.match(OFFLINE_URL); 91 if (cached) return cached; 92 // Cache evicted (iOS clears PWA storage after ~7 days unused) and still 93 // offline: fall through to the browser's own error rather than an 94 // undefined response, which would surface as a confusing failure. 95 return Response.error(); 96 }) 97 ); 98});
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.