While contributing to Accuguide — an open source platform that helps people discover accessible places and services — I implemented Progressive Web App (PWA) support so users can install it on their phone like a native app.
In this article I'll walk through exactly what a PWA needs, how Next.js handles it, and every file I created or modified.
What is a PWA?
A Progressive Web App is a website that can be installed on a device and behaves like a native app. When installed:
- It gets its own icon on the home screen
- It opens without browser UI (no address bar)
- It can work offline
- It loads faster on repeat visits
What a PWA Needs
Three things are required:
-
manifest.json— tells the browser the app's name, icons, and colors - Service Worker — handles caching and offline support
- Meta tags — links the manifest and configures mobile display
Step 1: Create public/manifest.json
This file describes your app to the browser:
{
"name": "Accuguide - Discover accessibility",
"short_name": "Accuguide",
"description": "Accuguide helps you discover accessible places and services near you.",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"orientation": "portrait",
"icons": [
{
"src": "/images/logo.png",
"sizes": "any",
"type": "image/png",
"purpose": "any maskable"
}
]
}
Key fields explained:
| Field | Purpose |
|---|---|
name |
Full app name shown on splash screen |
short_name |
Name shown under home screen icon |
start_url |
Page to open when app is launched |
display: standalone |
Hides browser UI when installed |
background_color |
Splash screen background |
theme_color |
Status bar color on mobile |
icons |
App icon for home screen |
Step 2: Create public/sw.js (Service Worker)
The service worker runs in the background and handles caching:
const CACHE_NAME = 'accuguide-v1';
const STATIC_ASSETS = [
'/',
'/search',
'/info/about',
'/info/donate',
'/info/volunteer',
'/help/faq',
'/help/resources',
];
// Install: cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
})
);
self.skipWaiting();
});
// Activate: 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))
);
})
);
self.clients.claim();
});
// Fetch: network first, fallback to cache
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
});
Three lifecycle events:
-
install— runs once when SW is first registered. Caches all static pages. -
activate— runs after install. Deletes old caches from previous versions. -
fetch— intercepts every network request. Tries network first, falls back to cache if offline.
Why network first?
Since Accuguide shows real-time data (places, reviews), we always want fresh data when online. Cache is only a fallback when the user is offline.
Step 3: Update src/app/layout.tsx
Next.js handles meta tags through the metadata export. Add these fields:
export const metadata: Metadata = {
// ... existing metadata ...
manifest: '/manifest.json',
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: 'Accuguide',
},
formatDetection: {
telephone: false,
},
}
What each field does:
-
manifest— links the manifest.json file -
appleWebApp.capable— enables "Add to Home Screen" on iOS -
appleWebApp.statusBarStyle— controls iOS status bar appearance -
formatDetection.telephone: false— prevents iOS from auto-linking phone numbers
Then register the service worker at the bottom of the layout:
<Script
id="register-sw"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js');
});
}
`,
}}
/>
Why afterInteractive?
This tells Next.js to load the script after the page is interactive — so it doesn't block the initial render.
Why check 'serviceWorker' in navigator?
Not all browsers support service workers. This check prevents errors on unsupported browsers.
How to Test It
Open Chrome DevTools → Application tab → Manifest to verify your manifest is loaded correctly.
You should see:
- App name and short name
- Icons
- Display mode: standalone
Under Service Workers you should see your sw.js registered and running.
On mobile, Chrome will show an "Add to Home Screen" banner automatically!
The Key Lesson
PWA support in Next.js needs just 3 things:
public/manifest.json ← describes the app
public/sw.js ← handles caching and offline
src/app/layout.tsx ← links manifest + registers SW
No external libraries needed! Next.js + native browser APIs are enough.
Summary
| File | Change |
|---|---|
public/manifest.json |
Created — app metadata for browser |
public/sw.js |
Created — service worker for caching |
src/app/layout.tsx |
Updated — manifest link + SW registration |
If you found this helpful, check out the Accuguide repo and my GitHub profile.
Have questions or spotted something I missed? Drop a comment below!
Top comments (0)