π Progressive Web App (PWA) Setup Guide for Next.js 15
This comprehensive, production-ready guide walks you through configuring a fully functional Progressive Web App (PWA) in Next.js 15 β covering manifest setup, service workers, middleware fixes, and install prompts across Android, iOS, and desktop browsers.
π§ Table of Contents
- Prerequisites
- Installation
- Manifest Configuration
- Next.js Configuration
- Middleware Configuration
- Service Worker Registration
- Install Prompt Component
- Layout Integration
- Testing
- Troubleshooting
π§© Prerequisites
Make sure you have:
- A Next.js 15.x project set up
- Node.js 18+ installed
- Basic understanding of React hooks
βοΈ Step 1: Installation
Install the required PWA dependency:
npm install next-pwa
π Step 2: Manifest Configuration
Create a new file:
public/manifest.json
{
"name": "Your App Name",
"short_name": "App Short Name",
"description": "Your app description",
"id": "/",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#6366F1",
"lang": "en",
"dir": "ltr",
"categories": ["productivity", "business"],
"icons": [
{ "src": "/icon192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/icon512_maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" },
{ "src": "/icon512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }
]
}
π§ Note:
All icons should live inside your /public folder.
| File | Size | Purpose |
|---|---|---|
| icon192.png | 192x192 | Required |
| icon512_maskable.png | 512x512 | Maskable icon |
| icon512.png | 512x512 | Primary icon |
βοΈ Step 3: Next.js Configuration
Edit next.config.mjs:
import withPWAInit from 'next-pwa';
const isDev = process.env.NODE_ENV === 'development';
const withPWA = withPWAInit({
dest: 'public',
register: false, // Manual registration
skipWaiting: true,
disable: false,
sw: 'sw.js',
scope: '/',
swcMinify: !isDev,
buildExcludes: [/middleware-manifest\.json$/],
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: { maxEntries: 4, maxAgeSeconds: 365 * 24 * 60 * 60 }
},
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: { maxEntries: 64, maxAgeSeconds: 24 * 60 * 60 }
},
},
],
});
export default withPWA({
reactStrictMode: true,
output: 'standalone',
});
β Key Settings Explained:
register: false β manual registration gives more control
disable: false β PWA active in both dev & prod
runtimeCaching β adds caching strategies for fonts, images, etc.
π§± Step 4: Middleware Configuration
By default, Next.js middleware may block your PWA files.
Fix that by editing src/middleware.js:
import { NextResponse } from 'next/server';
import { cookies } from "next/headers";
export async function middleware(request) {
const url = request.nextUrl.clone();
// Allow PWA core files
const pwaFiles = ['/manifest.json', '/sw.js', '/workbox'];
if (pwaFiles.some(file => url.pathname.includes(file))) {
return NextResponse.next();
}
// Example auth logic
const cookieStore = await cookies();
const token = cookieStore.get('accessToken');
const isLoggedIn = Boolean(token);
if (!isLoggedIn && url.pathname !== '/sign-in') {
url.pathname = '/sign-in';
return NextResponse.redirect(url);
}
if (isLoggedIn && url.pathname === '/sign-in') {
url.pathname = '/';
return NextResponse.redirect(url);
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|public|images).*)',
],
};
β οΈ Without this, manifest.json and sw.js will be blocked, breaking your PWA.
π§ Step 5: Service Worker Registration
Create:
src/components/ui/pwa-service-worker-register.jsx
'use client';
import { useEffect } from 'react';
export default function PWAServiceWorkerRegister() {
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js', { scope: '/' })
.then(reg => console.log('SW registered:', reg))
.catch(err => console.error('SW registration failed:', err));
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (window.confirm('New version available! Reload?')) {
window.location.reload();
}
});
}
}, []);
return null;
}
π² Step 6: Install Prompt Component
Create:
src/components/ui/pwa-install-prompt.jsx
'use client';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
export default function PWAInstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState(null);
const [showInstall, setShowInstall] = useState(false);
const [isStandalone, setIsStandalone] = useState(false);
const [isIOS, setIsIOS] = useState(false);
useEffect(() => {
setIsIOS(/iPad|iPhone|iPod/.test(navigator.userAgent));
setIsStandalone(window.matchMedia('(display-mode: standalone)').matches);
const handleBeforeInstall = (e) => {
e.preventDefault();
setDeferredPrompt(e);
setShowInstall(true);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstall);
return () => window.removeEventListener('beforeinstallprompt', handleBeforeInstall);
}, []);
if (isStandalone) return null;
if (isIOS) {
return (
<div className="fixed bottom-4 right-4 bg-indigo-600 text-white p-4 rounded-lg shadow-lg max-w-sm">
<h3 className="font-semibold mb-2">Install this App</h3>
<p className="text-sm mb-3">Tap the share icon β βAdd to Home Screenβ</p>
</div>
);
}
if (showInstall) {
return (
<button
onClick={async () => {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
toast[outcome === 'accepted' ? 'success' : 'info'](`User ${outcome} install`);
setShowInstall(false);
}}
className="fixed bottom-4 right-4 bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg shadow-lg flex items-center gap-2"
>
π² Install App
</button>
);
}
return null;
}
π§© Step 7: Layout Integration
Edit src/app/layout.jsx:
import "./globals.css";
import PWAServiceWorkerRegister from "@/components/ui/pwa-service-worker-register";
import PWAInstallPrompt from "@/components/ui/pwa-install-prompt";
export const metadata = {
title: "Your App Name",
description: "Your app description",
manifest: "/manifest.json",
icons: {
icon: "/icon512.png",
apple: "/icon512_maskable.png",
},
};
export const viewport = {
themeColor: "#6366F1",
};
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta name="theme-color" content="#6366F1" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/icon512_maskable.png" />
</head>
<body>
<main>{children}</main>
<PWAInstallPrompt />
<PWAServiceWorkerRegister />
</body>
</html>
);
}
π§ͺ Step 8: Testing
Run your app and open DevTools β Application Tab
β
Manifest Tab: Shows your app name, start URL, and icons
β
Service Workers Tab: Shows βactivated and runningβ
β
Console: Logs should include
Then check:
http://localhost:3000/manifest.json β should load JSON
http://localhost:3000/sw.js β should load the service worker file
π Step 9: Production Build
npm run build && npm run start
Verify:
- sw.js is present in /public
- Site served over HTTPS
- Install prompt works in production
π§― Step 10: Troubleshooting
π§ Note:
These are common PWA setup issues and how to resolve them.
| Issue | Common Cause | Solution |
|---|---|---|
| Manifest not loading | Middleware blocking | See Step 4 |
| SW not registering | Disabled or wrong path | Check next.config.mjs
|
| Install button missing | Already installed or invalid manifest | Clear cache + fix manifest |
| PWA files blocked | Middleware redirect | Allow /sw.js & /manifest.json
|
β PWA Checklist
- next-pwa installed
- manifest.json configured
- Icons (192, 512) created
- Middleware allowing PWA files
- Service worker registered
- Install prompt visible
- PWA installable on Chrome/Android/iOS
π Browser Support
| Platform | Support |
|---|---|
| Chrome / Edge | β Full |
| Firefox | β οΈ Limited |
| Safari | β οΈ Limited (no install prompt API) |
| Chrome Android | β Full |
| iOS Safari | β οΈ Manual add to home screen |
π References
- Next.js PWA Plugin (
next-pwa) - MDN Web App Manifest Docs
- Service Worker API
- Chrome PWA Best Practices
π§© Summary
By following this guide, youβve built a fully functional, installable PWA in Next.js 15 β complete with:
- Manifest setup
- Middleware exception handling
- Manual SW registration
- Cross-platform install prompts
You can now deploy a production-ready PWA with offline caching, installability, and better UX β all without extra complexity.
π‘ If this helped you, drop a β€οΈ or share your experience building PWAs in Next.js 15!
Top comments (0)