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 @@
+
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"
+ }
+ ]
+}