feat(pwa): add web manifest, SVG icon, and offline service worker
Service worker caches app shell + images for offline recipe access in the kitchen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#2b6a3d" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="apple-touch-icon" href="/icon.svg" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="Kochwas" />
|
||||
<title>Kochwas</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
89
src/service-worker.ts
Normal file
89
src/service-worker.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
/// <reference no-default-lib="true"/>
|
||||
/// <reference lib="esnext" />
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
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 });
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
});
|
||||
8
static/icon.svg
Normal file
8
static/icon.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="96" fill="#2b6a3d"/>
|
||||
<g fill="none" stroke="#ffffff" stroke-width="16" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M128 192 L256 320 L384 192" />
|
||||
<circle cx="256" cy="256" r="128" />
|
||||
</g>
|
||||
<text x="256" y="460" font-family="system-ui,sans-serif" font-size="80" font-weight="700" fill="#ffffff" text-anchor="middle">Koch</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 460 B |
19
static/manifest.webmanifest
Normal file
19
static/manifest.webmanifest
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user