feat(pwa): installierbar + Offline-Grundgeruest
Deploy / verify (push) Successful in 1m7s
Deploy / deploy (push) Successful in 1m45s

- Manifest (name/standalone/theme-color wald), Icons aus echtem Logo
  (192/512/maskable/apple-touch) via scripts/generate-pwa-icons.mjs
- app.html: manifest + theme-color + apple-touch-icon + iOS-Meta
- service-worker.ts: precache Build-Assets, Seiten network-first mit
  Offline-Cache-Fallback (SvelteKit registriert autom. in Prod)
- Push (VAPID) folgt separat

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-07-01 15:58:35 +02:00
parent e5566ff797
commit c12d626507
8 changed files with 126 additions and 0 deletions
+6
View File
@@ -3,6 +3,12 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#236335" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<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="Windwiderstand" />
<link rel="preconnect" href="https://cms.pm86.de">
<link rel="preconnect" href="https://api.iconify.design">
<link rel="preconnect" href="https://analytics.pm86.de">
+69
View File
@@ -0,0 +1,69 @@
/// <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 CACHE = `ww-cache-${version}`;
// Vom Build erzeugte Assets + statische Dateien (Icons, Manifest, Fonts…).
const PRECACHE = [...build, ...files];
sw.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(CACHE)
.then((cache) => cache.addAll(PRECACHE))
.then(() => sw.skipWaiting()),
);
});
sw.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
for (const key of await caches.keys()) {
if (key !== CACHE) await caches.delete(key);
}
await sw.clients.claim();
})(),
);
});
sw.addEventListener("fetch", (event) => {
const req = event.request;
if (req.method !== "GET") return;
const url = new URL(req.url);
// Nur same-origin — CMS-/Cross-Origin-Requests (dynamischer Content) nicht cachen.
if (url.origin !== sw.location.origin) return;
// Precache-Assets → cache-first (unveränderlich pro Version).
if (PRECACHE.includes(url.pathname)) {
event.respondWith(caches.match(req).then((r) => r ?? fetch(req)));
return;
}
// Seiten → network-first, bei Offline aus Cache; sonst Hinweis.
if (req.mode === "navigate") {
event.respondWith(
(async () => {
try {
const res = await fetch(req);
const cache = await caches.open(CACHE);
cache.put(req, res.clone());
return res;
} catch {
const cached = await caches.match(req);
return (
cached ??
new Response("Du bist offline. Diese Seite wurde noch nicht geladen.", {
status: 503,
headers: { "Content-Type": "text/plain; charset=utf-8" },
})
);
}
})(),
);
}
});