const CACHE = 'hamham-public-v1'; async function savePage(address) { const url = new URL(address, self.location.origin); if (url.origin !== self.location.origin) return; const match = /^\/rezepte\/([a-f0-9-]{36})(?:\/kochen)?$/.exec(url.pathname); if (match) { const recipe = await fetch(`/api/recipes/${match[1]}`).then((r) => (r.ok ? r.json() : null)); if (!recipe?.published) return; } else if (!['/', '/favoriten', '/einkaufsliste'].includes(url.pathname)) return; const response = await fetch(url.href, { headers: { Accept: 'text/html' } }); if (!response.ok || !response.headers.get('content-type')?.includes('text/html')) return; const cache = await caches.open(CACHE), html = await response.clone().text(); await cache.put(url.href, response); const paths = [ ...new Set( Array.from( html.matchAll(/(?:src|href)="(\/(?:_next\/static\/|images\/|icons\/|media\/)[^"<>]+)"/g), (m) => m[1].replaceAll('&', '&'), ), ), ]; await Promise.all( paths.map(async (path) => { try { const r = await fetch(path); if (r.ok) await cache.put(path, r); } catch {} }), ); } self.addEventListener('message', (event) => { if (event.data?.type === 'CACHE_PAGE' && typeof event.data.url === 'string') event.waitUntil(savePage(event.data.url).catch(() => {})); }); self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE).then((cache) => cache.addAll(['/offline.html', '/icons/icon-192.png'])), ); self.skipWaiting(); }); self.addEventListener('activate', (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))), ); self.clients.claim(); }); self.addEventListener('fetch', (event) => { const req = event.request, url = new URL(req.url); if ( req.method !== 'GET' || url.origin !== self.location.origin || url.pathname.startsWith('/admin') || url.pathname.startsWith('/api') || url.searchParams.has('_rsc') || req.headers.get('rsc') ) return; const asset = /^\/(?:_next\/static\/|images\/|icons\/|media\/)/.test(url.pathname); const recipe = /^\/rezepte\/([a-f0-9-]{36})(?:\/kochen)?$/.exec(url.pathname); if (!asset && req.mode !== 'navigate') return; event.respondWith( (async () => { const cache = await caches.open(CACHE); if (asset) { const cached = await cache.match(req); if (cached) return cached; } try { const response = await fetch(req); if (response.ok && asset) await cache.put(req, response.clone()); if (response.ok && recipe) { // Never persist a draft or an administrator page. Only published recipes. const data = await fetch(`/api/recipes/${recipe[1]}`) .then((r) => (r.ok ? r.json() : null)) .catch(() => null); if (data?.published) await cache.put(req, response.clone()); else await cache.delete(req); } return response; } catch { const saved = (await cache.match(req)) ?? (req.mode === 'navigate' ? await cache.match(req, { ignoreSearch: true }) : null); if (saved) return saved; if (req.mode === 'navigate') return await cache.match('/offline.html'); return new Response('', { status: 503 }); } })(), ); });