# PWA Guide - Progressive Web Apps im Monorepo Diese Anleitung beschreibt, wie eine bestehende Web-App in eine installierbare PWA (Progressive Web App) umgewandelt wird. ## Übersicht Eine PWA benötigt folgende Komponenten: | Komponente | Datei | Zweck | |------------|-------|-------| | Web App Manifest | `static/manifest.json` | App-Metadaten, Icons, Shortcuts | | Service Worker | `static/sw.js` | Offline-Support, Caching | | Offline-Seite | `static/offline.html` | Fallback wenn offline | | App-Icons | `static/icons/` | Icons für verschiedene Plattformen | | Meta-Tags | `src/app.html` | PWA-Konfiguration im HTML | | SW Registration | `+layout.svelte` | Service Worker starten | --- ## Schritt 1: App-Icons erstellen ### Verzeichnis anlegen ``` static/icons/ ├── icon.svg # Basis-Icon (SVG, skalierbar) ├── icon-72x72.png # Optional: PNG für ältere Browser ├── icon-96x96.png ├── icon-128x128.png ├── icon-144x144.png ├── icon-152x152.png ├── icon-192x192.png ├── icon-384x384.png ├── icon-512x512.png └── apple-touch-icon.png # 180x180 für iOS ``` ### SVG-Icon Vorlage SVG ist ideal, da es skalierbar ist und nur eine Datei benötigt: ```svg ``` ### Farbschema pro App | App | Primary Color | Gradient | |-----|---------------|----------| | Todo | `#8b5cf6` | `#8b5cf6` → `#7c3aed` | | Chat | `#3b82f6` | `#3b82f6` → `#2563eb` | | Picture | `#ec4899` | `#ec4899` → `#db2777` | | Quotes | `#f59e0b` | `#f59e0b` → `#d97706` | | Calendar | `#10b981` | `#10b981` → `#059669` | | Contacts | `#6366f1` | `#6366f1` → `#4f46e5` | | Mana Games | `#00ff88` | `#00ff88` → `#00cc6a` | --- ## Schritt 2: Web App Manifest ### Datei: `static/manifest.json` ```json { "name": "App Name - Beschreibung", "short_name": "App Name", "description": "Kurze Beschreibung der App", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#8b5cf6", "orientation": "any", "categories": ["productivity"], "lang": "de", "icons": [ { "src": "/icons/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }, { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" } ], "shortcuts": [ { "name": "Shortcut Name", "short_name": "Kurz", "description": "Beschreibung", "url": "/path?action=shortcut", "icons": [ { "src": "/icons/icon-96x96.png", "sizes": "96x96" } ] } ] } ``` ### Manifest-Felder erklärt | Feld | Beschreibung | Beispiel | |------|--------------|----------| | `name` | Vollständiger App-Name | "Todo - Aufgabenverwaltung" | | `short_name` | Kurzname (max 12 Zeichen) | "Todo" | | `description` | App-Beschreibung | "Aufgaben verwalten" | | `start_url` | Start-URL beim Öffnen | "/" | | `display` | Anzeigemodus | "standalone", "fullscreen", "minimal-ui" | | `background_color` | Hintergrund beim Laden | "#ffffff" | | `theme_color` | Statusleisten-Farbe | "#8b5cf6" | | `orientation` | Bildschirmorientierung | "any", "portrait", "landscape" | | `categories` | App Store Kategorien | ["productivity", "utilities"] | | `lang` | Sprache | "de" | | `icons` | App-Icons Array | Siehe oben | | `shortcuts` | Quick Actions | Siehe oben | ### Display-Modi | Modus | Beschreibung | |-------|--------------| | `fullscreen` | Komplett bildschirmfüllend, keine Browser-UI | | `standalone` | Wie native App, mit Statusleiste | | `minimal-ui` | Mit minimaler Browser-Navigation | | `browser` | Normaler Browser-Tab | --- ## Schritt 3: Service Worker ### Datei: `static/sw.js` ```javascript const CACHE_NAME = 'app-name-v1'; const OFFLINE_URL = '/offline.html'; // Statische Assets die immer gecacht werden const STATIC_CACHE_URLS = [ '/', '/offline.html', '/icons/icon.svg', '/manifest.json' ]; // Cache-Strategien für verschiedene Ressourcen const CACHE_STRATEGIES = { // Network First: Immer aktuell, Cache als Fallback networkFirst: [ /\/$/, // Root /\.html$/, // HTML-Dateien /^\/app/, // App-Routen ], // Cache First: Schnell laden, im Hintergrund aktualisieren cacheFirst: [ /\.css$/, /\.js$/, /\.woff2?$/, /\.ttf$/, /\.svg$/, /\.png$/, /\.jpg$/, /\.jpeg$/, /\.webp$/, /\/_app\//, // SvelteKit Assets ], // Network Only: Nie cachen (API-Calls) networkOnly: [ /\/api\//, /localhost:\d+/, ] }; // Installation self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME) .then((cache) => { console.log('SW: Caching static assets'); return cache.addAll(STATIC_CACHE_URLS); }) .then(() => self.skipWaiting()) ); }); // Aktivierung (alte Caches löschen) self.addEventListener('activate', (event) => { event.waitUntil( caches.keys() .then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name.startsWith('app-name-') && name !== CACHE_NAME) .map((name) => caches.delete(name)) ); }) .then(() => self.clients.claim()) ); }); // Fetch-Handler self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Chrome Extensions ignorieren if (url.protocol === 'chrome-extension:') return; // Cross-Origin ignorieren if (url.origin !== self.location.origin) return; const strategy = getStrategy(url.pathname); if (strategy === 'networkFirst') { event.respondWith(networkFirst(request)); } else if (strategy === 'cacheFirst') { event.respondWith(cacheFirst(request)); } else if (strategy === 'networkOnly') { event.respondWith(fetch(request)); } else { event.respondWith(networkFirst(request)); } }); // Network First Strategy async function networkFirst(request) { try { const response = await fetch(request); if (response.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, response.clone()); } return response; } catch (error) { const cached = await caches.match(request); if (cached) return cached; // Offline-Seite für Navigation if (request.mode === 'navigate') { const offline = await caches.match(OFFLINE_URL); if (offline) return offline; } throw error; } } // Cache First Strategy async function cacheFirst(request) { const cached = await caches.match(request); if (cached) return cached; try { const response = await fetch(request); if (response.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, response.clone()); } return response; } catch (error) { console.error('SW: Fetch failed:', error); throw error; } } // Strategie ermitteln function getStrategy(pathname) { for (const [strategy, patterns] of Object.entries(CACHE_STRATEGIES)) { if (patterns.some((pattern) => pattern.test(pathname))) { return strategy; } } return 'networkFirst'; } // Update-Nachricht empfangen self.addEventListener('message', (event) => { if (event.data?.type === 'SKIP_WAITING') { self.skipWaiting(); } }); ``` ### Caching-Strategien erklärt | Strategie | Wann verwenden | Verhalten | |-----------|----------------|-----------| | **Network First** | HTML, dynamische Inhalte | Versucht Netzwerk, fällt auf Cache zurück | | **Cache First** | Statische Assets (CSS, JS, Bilder) | Lädt aus Cache, aktualisiert im Hintergrund | | **Network Only** | API-Calls, Echtzeit-Daten | Kein Caching, immer Netzwerk | | **Stale While Revalidate** | Häufig aktualisierte Inhalte | Cache sofort, Netzwerk im Hintergrund | ### Cache-Versionierung Bei Updates den Cache-Namen erhöhen: ```javascript // Version erhöhen bei Breaking Changes const CACHE_NAME = 'app-name-v2'; // War v1 ``` --- ## Schritt 4: Offline-Seite ### Datei: `static/offline.html` ```html Offline - App Name

Du bist offline

Keine Internetverbindung. Sobald du wieder online bist, kannst du die App nutzen.

Verbindung wird gesucht...
``` --- ## Schritt 5: PWA Meta-Tags ### Datei: `src/app.html` Im `` hinzufügen: ```html %sveltekit.head%
%sveltekit.body%
``` ### Meta-Tags erklärt | Tag | Zweck | |-----|-------| | `` | Verlinkt das Web App Manifest | | `` | Farbe der Browser-Statusleiste | | `apple-mobile-web-app-capable` | Ermöglicht "Add to Home Screen" auf iOS | | `apple-mobile-web-app-status-bar-style` | iOS Statusleisten-Stil | | `apple-mobile-web-app-title` | App-Name auf iOS Homescreen | | `apple-touch-icon` | Icon für iOS Homescreen | | `msapplication-TileColor` | Windows Tile Farbe | --- ## Schritt 6: Service Worker Registration ### Datei: `src/routes/(app)/+layout.svelte` Im `onMount` hinzufügen: ```typescript import { onMount } from 'svelte'; onMount(async () => { // ... andere Initialisierungen ... // Service Worker registrieren if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.register('/sw.js', { scope: '/', }); console.log('PWA: Service Worker registered', registration.scope); // Update-Erkennung registration.addEventListener('updatefound', () => { const newWorker = registration.installing; if (newWorker) { newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // Neue Version verfügbar console.log('PWA: New version available'); // Optional: Benutzer informieren showUpdateNotification(); } }); } }); } catch (error) { console.error('PWA: Service Worker registration failed', error); } } }); function showUpdateNotification() { // Optional: Toast/Banner anzeigen // "Neue Version verfügbar - Seite neu laden?" } ``` --- ## Schritt 7: Optionale Features ### Install-Prompt anzeigen ```typescript let deferredPrompt: BeforeInstallPromptEvent | null = null; let showInstallButton = $state(false); onMount(() => { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e as BeforeInstallPromptEvent; showInstallButton = true; }); window.addEventListener('appinstalled', () => { showInstallButton = false; deferredPrompt = null; }); }); async function installApp() { if (!deferredPrompt) return; deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; if (outcome === 'accepted') { console.log('PWA installed'); } deferredPrompt = null; showInstallButton = false; } ``` ```svelte {#if showInstallButton} {/if} ``` ### Update-Banner ```svelte {#if showUpdateBanner}
Neue Version verfügbar
{/if} ``` ### Offline-Status anzeigen ```svelte {#if !isOnline}
Offline
{/if} ``` --- ## Checkliste ### Vor dem Deployment - [ ] `manifest.json` erstellt mit korrekten Metadaten - [ ] App-Icons in allen benötigten Größen vorhanden - [ ] `sw.js` erstellt mit passenden Cache-Strategien - [ ] `offline.html` erstellt mit App-Branding - [ ] PWA Meta-Tags in `app.html` eingefügt - [ ] Service Worker Registration im Layout - [ ] Theme-Color passt zur App - [ ] `start_url` ist korrekt - [ ] Shortcuts sind sinnvoll definiert ### Testen ```bash # Chrome DevTools 1. F12 öffnen 2. Application Tab 3. "Manifest" prüfen 4. "Service Workers" prüfen 5. "Cache Storage" prüfen # Lighthouse Audit 1. F12 öffnen 2. Lighthouse Tab 3. "Progressive Web App" aktivieren 4. "Analyze page load" klicken ``` ### PWA-Kriterien (Lighthouse) | Kriterium | Anforderung | |-----------|-------------| | Installierbar | Manifest + Service Worker | | Offline-fähig | Service Worker mit Caching | | HTTPS | Erforderlich (außer localhost) | | Responsive | Viewport Meta-Tag | | Schnell | First Contentful Paint < 3s | --- ## Referenz-Implementierungen ### Im Monorepo | App | Verzeichnis | |-----|-------------| | Mana Games | `games/mana-games/apps/web/public/` | | Todo | `apps/todo/apps/web/static/` | ### Externe Ressourcen - [Web.dev PWA Guide](https://web.dev/progressive-web-apps/) - [MDN Web App Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) - [Workbox (Google's SW Library)](https://developer.chrome.com/docs/workbox/) --- ## Troubleshooting ### Service Worker wird nicht registriert ``` Fehler: "Service Worker registration failed" ``` **Lösungen:** 1. HTTPS verwenden (oder localhost) 2. Pfad zu `sw.js` prüfen (muss im root sein) 3. Browser-Cache leeren 4. DevTools → Application → Service Workers → "Update on reload" aktivieren ### Manifest wird nicht erkannt ``` Fehler: "No matching service worker detected" ``` **Lösungen:** 1. `` im `` prüfen 2. JSON-Syntax in `manifest.json` validieren 3. Icons-Pfade prüfen ### Offline-Seite wird nicht angezeigt **Lösungen:** 1. `offline.html` ist in `STATIC_CACHE_URLS` enthalten 2. Service Worker ist aktiviert 3. Cache-Name stimmt überein ### App lässt sich nicht installieren **Voraussetzungen:** 1. HTTPS (oder localhost) 2. Gültiges Manifest mit `name`, `icons`, `start_url`, `display` 3. Service Worker registriert 4. Icon mindestens 192x192px