diff --git a/src/app.html b/src/app.html index c265455..e310fd7 100644 --- a/src/app.html +++ b/src/app.html @@ -4,6 +4,12 @@ + + + + + + Kochwas %sveltekit.head% diff --git a/src/service-worker.ts b/src/service-worker.ts new file mode 100644 index 0000000..76b14da --- /dev/null +++ b/src/service-worker.ts @@ -0,0 +1,89 @@ +/// +/// +/// +/// + +import { build, files, version } from '$service-worker'; + +const sw = self as unknown as ServiceWorkerGlobalScope; + +const APP_CACHE = `kochwas-app-${version}`; +const IMAGE_CACHE = `kochwas-images-v1`; +const APP_ASSETS = [...build, ...files]; + +sw.addEventListener('install', (event) => { + event.waitUntil( + caches.open(APP_CACHE).then((cache) => cache.addAll(APP_ASSETS)) + ); + // Activate new worker without waiting for old clients to close. + void sw.skipWaiting(); +}); + +sw.addEventListener('activate', (event) => { + event.waitUntil( + (async () => { + const keys = await caches.keys(); + await Promise.all( + keys + .filter((k) => k.startsWith('kochwas-app-') && k !== APP_CACHE) + .map((k) => caches.delete(k)) + ); + await sw.clients.claim(); + })() + ); +}); + +sw.addEventListener('fetch', (event) => { + const req = event.request; + if (req.method !== 'GET') return; + + const url = new URL(req.url); + if (url.origin !== location.origin) return; + + // Images served from /images/* — cache-first with background update + if (url.pathname.startsWith('/images/')) { + event.respondWith( + (async () => { + const cache = await caches.open(IMAGE_CACHE); + const cached = await cache.match(req); + const network = fetch(req) + .then((res) => { + if (res.ok) void cache.put(req, res.clone()); + return res; + }) + .catch(() => undefined); + return cached ?? (await network) ?? new Response('Offline', { status: 503 }); + })() + ); + return; + } + + // App shell assets (build/* and static files) — cache-first + if (APP_ASSETS.includes(url.pathname)) { + event.respondWith( + (async () => { + const cache = await caches.open(APP_CACHE); + const cached = await cache.match(req); + return cached ?? fetch(req); + })() + ); + return; + } + + // API and HTML pages — network-first, fall back to cache for HTML + if (req.destination === 'document') { + event.respondWith( + (async () => { + try { + const res = await fetch(req); + const cache = await caches.open(APP_CACHE); + if (res.ok) void cache.put(req, res.clone()); + return res; + } catch { + const cached = await caches.match(req); + return cached ?? new Response('Offline', { status: 503 }); + } + })() + ); + } +}); diff --git a/static/icon.svg b/static/icon.svg new file mode 100644 index 0000000..31652d7 --- /dev/null +++ b/static/icon.svg @@ -0,0 +1,8 @@ + + + + + + + Koch + diff --git a/static/manifest.webmanifest b/static/manifest.webmanifest new file mode 100644 index 0000000..563d18f --- /dev/null +++ b/static/manifest.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Kochwas", + "short_name": "Kochwas", + "description": "Persönliches Rezeptbuch — lokal, einheitlich, küchentauglich", + "start_url": "/", + "display": "standalone", + "background_color": "#f8faf8", + "theme_color": "#2b6a3d", + "lang": "de", + "orientation": "portrait", + "icons": [ + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + } + ] +}