diff --git a/scripts/generate-pwa-icons.mjs b/scripts/generate-pwa-icons.mjs new file mode 100644 index 0000000..ac1e8dc --- /dev/null +++ b/scripts/generate-pwa-icons.mjs @@ -0,0 +1,34 @@ +// Erzeugt PWA-Icons aus dem echten App-Logo (logo_v3.svg) auf hellem Grund. +// Läuft manuell: `node scripts/generate-pwa-icons.mjs` +import sharp from "sharp"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const staticDir = join(root, "static"); +const BG = "#f0f5f1"; // wald-50 (wie Header-Hintergrund) + +const logoSvg = readFileSync( + join(root, "..", "cms_content", "windwiderstand", "assets", "logo_v3.svg"), + "utf8", +); + +async function makeIcon(size, logoRatio, out) { + const logoPx = Math.round(size * logoRatio); + const logo = await sharp(Buffer.from(logoSvg), { density: 384 }) + .resize(logoPx, logoPx, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .png() + .toBuffer(); + const pad = Math.round((size - logoPx) / 2); + await sharp({ create: { width: size, height: size, channels: 4, background: BG } }) + .composite([{ input: logo, top: pad, left: pad }]) + .png() + .toFile(join(staticDir, out)); + console.log("wrote", out, `${size}px`); +} + +await makeIcon(192, 0.78, "icon-192.png"); +await makeIcon(512, 0.78, "icon-512.png"); +await makeIcon(512, 0.6, "icon-maskable-512.png"); // mehr Rand für Maskable-Safe-Zone +await makeIcon(180, 0.78, "apple-touch-icon.png"); diff --git a/src/app.html b/src/app.html index ce39be1..c381126 100644 --- a/src/app.html +++ b/src/app.html @@ -3,6 +3,12 @@ + + + + + + diff --git a/src/service-worker.ts b/src/service-worker.ts new file mode 100644 index 0000000..f09840d --- /dev/null +++ b/src/service-worker.ts @@ -0,0 +1,69 @@ +/// +/// +/// +/// +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" }, + }) + ); + } + })(), + ); + } +}); diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png new file mode 100644 index 0000000..fbbccf7 Binary files /dev/null and b/static/apple-touch-icon.png differ diff --git a/static/icon-192.png b/static/icon-192.png new file mode 100644 index 0000000..a80d640 Binary files /dev/null and b/static/icon-192.png differ diff --git a/static/icon-512.png b/static/icon-512.png new file mode 100644 index 0000000..d237ba8 Binary files /dev/null and b/static/icon-512.png differ diff --git a/static/icon-maskable-512.png b/static/icon-maskable-512.png new file mode 100644 index 0000000..126625e Binary files /dev/null and b/static/icon-maskable-512.png differ diff --git a/static/manifest.webmanifest b/static/manifest.webmanifest new file mode 100644 index 0000000..6c43ee9 --- /dev/null +++ b/static/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "Windwiderstand Thüringen", + "short_name": "Windwiderstand", + "description": "Bürgerinitiativen gegen überdimensionierte Windkraftplanung in Südwestthüringen. Termine, Stellungnahmen, Hintergründe.", + "lang": "de", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait-primary", + "background_color": "#f0f5f1", + "theme_color": "#236335", + "icons": [ + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +}