6.7 KiB
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
/contactand/servicesfrom the home screen - Icons: Uses existing
apple-touch-icon.pngandfavicon.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-v1workroot-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:
- Form handler calls
window.queueFormSync(url, method, body, formType) - The payload is stored in IndexedDB (
workroot-pwaDB,workroot-sync-queuestore) - A background sync tag
form-syncis registered with the browser - When connectivity is restored, the service worker automatically retries all queued submissions
- On success, clients receive a
pwa:sync-successcustom event they can listen to
Usage in form pages:
// 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
beforeinstallpromptevent (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 (
appinstalledevent) - 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
- Open DevTools → Application tab
- Service Workers panel:
- Confirm
sw.jsis registered and active - Use "Offline" checkbox to simulate offline
- Confirm
- Manifest panel:
- Verify all manifest fields are correct
- Check "Add to home screen" works
- Cache Storage panel:
- Verify
workroot-static-v1andworkroot-pages-v1exist with expected entries
- Verify
- Navigate to
/contactwhile offline → should serve cached page - Navigate to a page not in cache while offline → should show
/offline
Automated Testing
The offline behavior can be tested with Playwright:
// 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:
- Increment
CACHE_VERSIONinpublic/sw.js(e.g.'v1'→'v2') - Old caches will be deleted during the
activateevent skipWaiting()+clients.claim()ensure immediate takeover
Lighthouse PWA Checklist
| Criterion | Status |
|---|---|
| Registers a service worker | ✅ |
| Responds with 200 when offline | ✅ (cached pages + /offline fallback) |
<meta name="viewport"> set |
✅ (BaseLayout) |
<meta name="theme-color"> 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:
{
"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
syncevent 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.