Progressive Web Apps (PWAs) deliver near-native mobile and desktop experiences using standard open web technologies. An installable PWA provides standalone window display, push notifications, offline capability, and home-screen presence without app store friction.
To trigger native browser install prompts and achieve 100% scores on Google Lighthouse, your web app requires two core primitives:
- A valid Web App Manifest (
manifest.json/manifest.webmanifest) detailing metadata, theme colors, and icons. - A registered Service Worker with an offline fetch fallback strategy.
This guide walks you through crafting a production-grade manifest, configuring maskable adaptive icons, and implementing resilient service worker caching.
1. Complete Production manifest.json Specification
Place manifest.json in your static public directory and link it inside your HTML <head>:
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0f172a" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
{
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"name": "DevFlow Developer Suite",
"short_name": "DevFlow",
"description": "High-performance online developer tools and offline utilities",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#0f172a",
"categories": ["developer", "productivity", "utilities"],
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"shortcuts": [
{
"name": "Format JSON",
"short_name": "JSON",
"description": "Open the JSON formatting tool directly",
"url": "/tools/json-formatter?source=pwa_shortcut",
"icons": [{ "src": "/icons/shortcut-json.png", "sizes": "96x96" }]
}
],
"screenshots": [
{
"src": "/screenshots/desktop-preview.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide",
"label": "DevFlow Dashboard"
}
]
}
Tip: Audit your manifest parameters, security policies, and service worker registration with the DevFlow PWA Manifest & Service Worker Audit Tool.
2. Icon Requirements & The Safe Zone Rule
Android, iOS, and desktop operating systems display icons in varying shapes (circles, squircles, rounded rectangles).
The "Maskable" Icon Rule
Standard icons with non-padded edge details get cropped awkwardly on modern Android launchers.
- Canvas Size: Minimum
512x512px. - Safe Zone: Keep all critical graphics and logos within the central 80% circle (a 410px diameter circle centered on a 512px canvas). The outer 10% on every edge is reserved for system mask cropping.
- Generate both
any(transparent background permitted) andmaskable(solid background required) entries inmanifest.json.
You can quickly generate all required icon sizes and apple touch icons using the DevFlow Favicon & Icon Generator.
3. Implementing a Production Service Worker
Service workers operate as background network proxies. Below is a complete vanilla service worker implementing a Stale-While-Revalidate strategy for static assets and a Network-First strategy for API requests:
// public/sw.js
const CACHE_NAME = 'devflow-cache-v1';
const OFFLINE_URL = '/offline.html';
const PRECACHE_ASSETS = [
'/',
'/offline.html',
'/manifest.json',
'/styles/global.css',
'/icons/icon-192x192.png',
];
// 1. Install Event: Cache essential shell assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(PRECACHE_ASSETS);
}).then(() => self.skipWaiting())
);
});
// 2. Activate Event: Clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
}).then(() => self.clients.claim())
);
});
// 3. Fetch Event: Intercept network traffic
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests and browser extensions
if (request.method !== 'GET' || !url.protocol.startsWith('http')) return;
// Static Assets: Cache First with Network Fallback
if (request.destination === 'image' || request.destination === 'style' || request.destination === 'script') {
event.respondWith(
caches.match(request).then((cachedResponse) => {
return cachedResponse || fetch(request).then((networkResponse) => {
if (networkResponse.status === 200) {
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(request, responseClone));
}
return networkResponse;
});
})
);
return;
}
// HTML Pages: Network First, fallback to cached or offline page
if (request.mode === 'navigate') {
event.respondWith(
fetch(request).catch(async () => {
const cachedResponse = await caches.match(request);
if (cachedResponse) return cachedResponse;
return caches.match(OFFLINE_URL);
})
);
}
});
Registering the Service Worker in Your Application
// app/providers/pwa-register.tsx or index.ts
export function registerServiceWorker() {
if (typeof window !== 'undefined' && 'serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then((reg) => {
console.log('[PWA] Service Worker registered with scope:', reg.scope);
})
.catch((err) => {
console.error('[PWA] Service Worker registration failed:', err);
});
});
}
}
4. Troubleshooting Lighthouse PWA Verification Checklist
| Criterion | Requirement | Failure Fix |
|---|---|---|
| HTTPS | Origin must be served over TLS / HTTPS. | Enable SSL certificate in your CDN/hosting provider (e.g. Cloudflare / Vercel). |
Manifest start_url |
Must load successfully within the manifest scope. |
Ensure start_url returns status 200 (not 404 or redirect). |
| Maskable Icon | At least one icon with purpose: "maskable". |
Create a 512x512 PNG with padding and solid background. |
| Viewport Meta | <meta name="viewport" content="width=device-width, initial-scale=1"> |
Verify root HTML layout head tags. |
| Offline Response | Returns HTTP 200 when offline. | Precache /offline.html during service worker install step. |
Frequently Asked Questions
Why is the browser install button not showing up?
Modern browsers only show the native install prompt if the user has engaged with the site for at least 30 seconds, HTTPS is verified, a valid manifest with icons is loaded, and a registered service worker with an offline fetch handler is active.
How do I handle PWA updates when a new version is deployed?
Listen for the updatefound event on the ServiceWorkerRegistration object. When a new service worker is in the waiting state, display a user toast prompting them to reload the application.
Can Next.js App Router support PWAs seamlessly?
Yes. Place manifest.json or manifest.ts in the app/ directory (or public/manifest.json), and place sw.js in the public/ directory so it has root scope (/).