DEV Community

orjinameh
orjinameh

Posted on

How I Added PWA Support to a Next.js App

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:

  1. manifest.json — tells the browser the app's name, icons, and colors
  2. Service Worker — handles caching and offline support
  3. 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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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);
      })
  );
});
Enter fullscreen mode Exit fullscreen mode

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,
  },
}
Enter fullscreen mode Exit fullscreen mode

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');
        });
      }
    `,
  }}
/>
Enter fullscreen mode Exit fullscreen mode

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 tabManifest 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
Enter fullscreen mode Exit fullscreen mode

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)