feat(pwa): installierbar + Offline-Grundgeruest
- 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:
@@ -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");
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<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://cms.pm86.de">
|
||||||
<link rel="preconnect" href="https://api.iconify.design">
|
<link rel="preconnect" href="https://api.iconify.design">
|
||||||
<link rel="preconnect" href="https://analytics.pm86.de">
|
<link rel="preconnect" href="https://analytics.pm86.de">
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user