# PWA Implementation — WorkRoot IT Solutions ## Overview Progressive Web App (PWA) features have been added to the WorkRoot website. The implementation follows a **progressive enhancement** approach — the site works fully without JavaScript; PWA features layer on top. --- ## Files Added / Modified | File | Type | Purpose | |------|------|---------| | `public/manifest.json` | New | Web App Manifest (installability) | | `public/sw.js` | New | Service Worker (offline + background sync) | | `src/pages/offline.astro` | New | Offline fallback page | | `src/components/PWAInstallPrompt.astro` | New | Install prompt banner + background sync helper | | `src/layouts/BaseLayout.astro` | Modified | Links manifest, registers SW, includes install prompt | | `src/middleware.ts` | Modified | CSP updated: `worker-src 'self'`, `manifest-src 'self'` | --- ## Features Implemented ### 1. Web App Manifest (`public/manifest.json`) - **Display mode:** `standalone` (hides browser chrome when installed) - **Theme color:** `#0891b2` (matches brand primary) - **Background color:** `#0f172a` (dark splash screen) - **Shortcuts:** Quick links to `/contact` and `/services` from the home screen - **Icons:** Uses existing `apple-touch-icon.png` and `favicon.svg` ### 2. Service Worker (`public/sw.js`) **Caching Strategies:** | Request Type | Strategy | Details | |---|---|---| | Static assets (JS, CSS, images, fonts) | Cache-first | Served from cache; fetched and cached on miss | | HTML pages | Stale-while-revalidate | Serve cache instantly, update in background | | API routes (`/api/*`) | Network-only | Never cached — always fresh | **Pre-cached on install:** - `/` (home) - `/offline` (fallback page) - `/manifest.json` - `/favicon.svg` - `/apple-touch-icon.png` **Cache names (versioned for easy invalidation):** - `workroot-static-v1` - `workroot-pages-v1` To invalidate caches on next deploy, increment `CACHE_VERSION` in `public/sw.js`. ### 3. Offline Fallback (`src/pages/offline.astro`) - Served at `/offline` - Clean branded page with "Try Again" and "Go to Home" actions - Pre-cached by the service worker on install - Shown automatically when a navigation request fails offline ### 4. Background Sync (`public/sw.js` + `src/components/PWAInstallPrompt.astro`) When a form submission fails due to no network: 1. Form handler calls `window.queueFormSync(url, method, body, formType)` 2. The payload is stored in **IndexedDB** (`workroot-pwa` DB, `workroot-sync-queue` store) 3. A background sync tag `form-sync` is registered with the browser 4. When connectivity is restored, the service worker automatically retries all queued submissions 5. On success, clients receive a `pwa:sync-success` custom event they can listen to **Usage in form pages:** ```js // In contact or newsletter form submit handler: try { const res = await fetch('/api/contact', { method: 'POST', body: JSON.stringify(data) }); if (!res.ok) throw new Error('Server error'); // handle success } catch { if (!navigator.onLine) { await window.queueFormSync('/api/contact', 'POST', data, 'contact'); showMessage('You are offline. Your message will be sent when you reconnect.'); } } // Listen for sync success: window.addEventListener('pwa:sync-success', (e) => { if (e.detail.formType === 'contact') { showMessage('Your message was sent successfully!'); } }); ``` ### 5. Install Prompt (`src/components/PWAInstallPrompt.astro`) - Listens for `beforeinstallprompt` event (Chrome/Edge/Android) - Shows a non-intrusive banner after 4 seconds on first visit - Banner is suppressed for 7 days after dismissal (stored in `localStorage`) - Hides immediately after successful installation (`appinstalled` event) - Accessible: uses `role="banner"`, `aria-label`, button labels --- ## Security Considerations The CSP in `src/middleware.ts` was updated to include: ``` worker-src 'self' → allows service worker from same origin only manifest-src 'self' → allows manifest.json from same origin only ``` These are deliberately restrictive — no external workers or manifests are permitted. --- ## Offline Functionality Testing ### Manual Testing in Chrome DevTools 1. Open DevTools → **Application** tab 2. **Service Workers** panel: - Confirm `sw.js` is registered and active - Use "Offline" checkbox to simulate offline 3. **Manifest** panel: - Verify all manifest fields are correct - Check "Add to home screen" works 4. **Cache Storage** panel: - Verify `workroot-static-v1` and `workroot-pages-v1` exist with expected entries 5. Navigate to `/contact` while offline → should serve cached page 6. Navigate to a page not in cache while offline → should show `/offline` ### Automated Testing The offline behavior can be tested with Playwright: ```typescript // In a Playwright test: await context.setOffline(true); await page.goto('/contact'); // Should either show cached page or /offline fallback ``` --- ## Updating the Service Worker To force all users to get the new service worker immediately: 1. Increment `CACHE_VERSION` in `public/sw.js` (e.g. `'v1'` → `'v2'`) 2. Old caches will be deleted during the `activate` event 3. `skipWaiting()` + `clients.claim()` ensure immediate takeover --- ## Lighthouse PWA Checklist | Criterion | Status | |-----------|--------| | Registers a service worker | ✅ | | Responds with 200 when offline | ✅ (cached pages + `/offline` fallback) | | `` set | ✅ (BaseLayout) | | `` set | ✅ `#0891b2` | | Web App Manifest with `name`, `short_name`, `icons` | ✅ | | Icons at 192×192 and 512×512 | ⚠️ Only `apple-touch-icon.png` (180×180) — add larger icons for perfect score | | Manifest `display: standalone` | ✅ | | HTTPS in production | ✅ (HSTS enforced) | | Install prompt supported | ✅ | ### Recommended Improvement Add 192×192 and 512×512 PNG icons to `public/` and reference them in `manifest.json` to achieve a perfect Lighthouse PWA score: ```json { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } ``` --- ## Known Limitations - **iOS Safari:** Background sync is not supported. Queued forms will be retried when the user next opens the app with network access (via the SW `sync` event on supported platforms). On iOS, consider showing a manual retry prompt. - **SSR + Service Worker:** Since Astro runs SSR, HTML responses are dynamic. The stale-while-revalidate strategy serves potentially stale HTML while fetching fresh content in the background. This is acceptable for a marketing site. - **API routes are always network-only.** This is intentional — form submissions and the health API must never serve stale responses.