| name | pwa-deployment |
| description | Deploying first-aid-reference as a PWA on various platforms. Use when you need to deploy to Netlify, Vercel, a static server, or Docker; configure the service worker cache strategy; handle app updates; or troubleshoot PWA installation and caching issues. Triggers include "deploy PWA", "service worker", "cache strategy", "app update", "install prompt", "offline not working", or any deployment-related task for first-aid-reference. |
pwa-deployment
Deploy and maintain first-aid-reference as a Progressive Web App. Covers service worker configuration, cache strategy, update handling, and platform-specific deployment.
Build output
After pnpm build, the dist/ folder contains:
dist/
index.html
sw.js # Service worker (generated by Workbox)
manifest.webmanifest # PWA manifest
registerSW.js # SW registration helper
assets/
index-[hash].js # Main bundle
index-[hash].css # Styles
articles-[hash].json # All guide content
categories-[hash].json # Category metadata
icons/
pwa-192x192.png
pwa-512x512.png
Service worker configuration
The service worker is generated by vite-plugin-pwa using Workbox generateSW strategy. Configuration is in vite.config.ts:
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,json,png,svg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 },
},
},
],
},
manifest: {
name: 'first-aid-reference',
short_name: 'First Aid',
theme_color: '#0891b2',
background_color: '#fafaf9',
display: 'standalone',
icons: [
{ src: 'icons/pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'icons/pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' },
],
},
})
Cache strategy
All static assets (JS, CSS, HTML, JSON, images) are precached on service worker install using CacheFirst. This means:
- On first visit: all assets downloaded and cached
- On subsequent visits: assets served from cache instantly
- Background: service worker checks for updates; new assets downloaded to a waiting SW
- Update: when user closes and reopens the app, the new SW activates and updates the cache
Handling updates
registerType: 'autoUpdate' means the new service worker activates automatically without user prompting. The app reloads with fresh content on next launch.
To show an "update available" toast instead of auto-reload, change to registerType: 'prompt' and handle the needRefresh event from useRegisterSW:
import { useRegisterSW } from 'virtual:pwa-register/react';
const { needRefresh, updateServiceWorker } = useRegisterSW({
onRegistered(r) { console.log('SW registered:', r); },
onRegisterError(error) { console.error('SW registration error:', error); },
});
Deploying to Netlify
- Build:
pnpm build
- Deploy:
netlify deploy --prod --dir=dist
Required public/_headers file for security headers:
/*
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Required public/_redirects file for SPA routing:
/* /index.html 200
Deploying to Vercel
- Push to GitHub
- Connect repo in Vercel dashboard
- Set build command:
pnpm build
- Set output directory:
dist
Or CLI:
pnpm add -g vercel
vercel --prod
Add vercel.json for SPA routing and headers:
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
]
}
Deploying with Docker
FROM node:20-alpine AS build
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@latest --activate
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# SPA routing
location / {
try_files $uri $uri/ /index.html;
}
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
# Cache static assets aggressively
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Do not cache service worker or manifest
location ~ \.(webmanifest|sw\.js)$ {
expires -1;
add_header Cache-Control "no-store";
}
}
Deploying to a static server
The dist/ folder is a self-contained static site. Any static file server works:
npx serve dist -p 8080 --single
cd dist && python3 -m http.server 8080
npx http-server dist -p 8080 --spa
Note: for offline to work correctly, the service worker must be served over HTTPS or localhost.
Verifying offline capability
pnpm build && pnpm preview
- Open
http://localhost:4173 in Chrome
- Open DevTools > Application > Service Workers - confirm SW is registered and activated
- Open DevTools > Network - enable "Offline" checkbox
- Reload the page - app should load fully from cache
- Navigate to any article - should load instantly without network
Troubleshooting
Service worker not registering
- Must be served over HTTPS or localhost
- Check browser console for registration errors
- Verify
registerSW.js is included in the build
Content not updating after deploy
- The SW uses cache-first strategy - users see old content until the new SW activates
- With
registerType: 'autoUpdate', the new SW activates on next app launch
- Force update: Clear Site Data in DevTools > Application > Storage
PWA install prompt not appearing
- Requires HTTPS
- Requires a valid manifest with icons
- Chrome requires a user gesture interaction before showing the prompt
- iOS Safari does not support the install prompt - show manual instructions instead
Cache size too large
- The default
globPatterns includes all build output
- Exclude large files by adding to
globIgnores in workbox config
- Use
runtimeCaching for large assets with size limits