# 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.