DEV Community

Md .Rakibul Islam
Md .Rakibul Islam

Posted on

πŸ”₯ Progressive Web App (PWA) Setup Guide for Next.js 15 β€” Complete Step-by-Step Walkthrough

πŸš€ 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

  1. Prerequisites
  2. Installation
  3. Manifest Configuration
  4. Next.js Configuration
  5. Middleware Configuration
  6. Service Worker Registration
  7. Install Prompt Component
  8. Layout Integration
  9. Testing
  10. 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
Enter fullscreen mode Exit fullscreen mode

πŸ“„ 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" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

🧠 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',
});

Enter fullscreen mode Exit fullscreen mode

βœ… 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).*)',
  ],
};

Enter fullscreen mode Exit fullscreen mode

⚠️ 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;
}

Enter fullscreen mode Exit fullscreen mode

πŸ“² 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;
}

Enter fullscreen mode Exit fullscreen mode

🧩 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>
  );
}

Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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
Enter fullscreen mode Exit fullscreen mode

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

🧩 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)