# 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
Keine Internetverbindung. Sobald du wieder online bist, kannst du die App nutzen.