nextjs-pwa

Build Progressive Web Apps with Next.js: service workers, offline support, caching strategies, push notifications, install prompts, and web app manifest. Use when creating PWAs, adding offline capability, configuring service workers, implementing push notifications, handling install prompts, or optimizing PWA performance. Triggers: PWA, progressive web app, service worker, offline, cache strategy, web manifest, push notification, installable app, Serwist, next-pwa, workbox, background sync.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "nextjs-pwa" with this command: npx skills add jakerains/agentskills/jakerains-agentskills-nextjs-pwa

Next.js PWA Skill

Quick Reference

TaskApproachReference
Add PWA to Next.js appSerwist (recommended)This file → Quick Start
Add PWA without dependenciesManual SWreferences/service-worker-manual.md
Configure cachingSerwist defaultCache or customreferences/caching-strategies.md
Add offline supportApp shell + IndexedDBreferences/offline-data.md
Push notificationsVAPID + web-pushreferences/push-notifications.md
Fix iOS issuesSafari/WebKit workaroundsreferences/ios-quirks.md
Debug SW / LighthouseDevTools + common fixesreferences/troubleshooting.md
Migrate from next-pwaSerwist migrationreferences/serwist-setup.md

Quick Start — Serwist (Recommended)

Serwist is the actively maintained successor to next-pwa, built for App Router.

1. Install

npm install @serwist/next && npm install -D serwist

2. Create app/manifest.ts

import type { MetadataRoute } from "next";

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: "My App",
    short_name: "App",
    description: "My Progressive Web App",
    start_url: "/",
    display: "standalone",
    background_color: "#ffffff",
    theme_color: "#000000",
    icons: [
      { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
      { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
    ],
  };
}

3. Create app/sw.ts (service worker)

import { defaultCache } from "@serwist/next/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";

declare global {
  interface WorkerGlobalScope extends SerwistGlobalConfig {
    __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
  }
}

declare const self: ServiceWorkerGlobalScope;

const serwist = new Serwist({
  precacheEntries: self.__SW_MANIFEST,
  skipWaiting: true,
  clientsClaim: true,
  navigationPreload: true,
  runtimeCaching: defaultCache,
});

serwist.addEventListeners();

4. Update next.config.ts

import withSerwist from "@serwist/next";

const nextConfig = {
  // your existing config
};

export default withSerwist({
  swSrc: "app/sw.ts",
  swDest: "public/sw.js",
  disable: process.env.NODE_ENV === "development",
})(nextConfig);

That's it — 4 files for a working PWA. Run next build and test with Lighthouse.


Quick Start — Manual (No Dependencies)

Use this when you want zero dependencies or are using output: "export".

1. Create app/manifest.ts

Same as above.

2. Create public/sw.js

const CACHE_NAME = "app-v1";
const PRECACHE_URLS = ["/", "/offline"];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
  );
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
    )
  );
  self.clients.claim();
});

self.addEventListener("fetch", (event) => {
  if (event.request.mode === "navigate") {
    event.respondWith(
      fetch(event.request).catch(() => caches.match("/offline"))
    );
    return;
  }
  event.respondWith(
    caches.match(event.request).then((cached) => cached || fetch(event.request))
  );
});

3. Register SW in layout

// app/components/ServiceWorkerRegistration.tsx
"use client";

import { useEffect } from "react";

export function ServiceWorkerRegistration() {
  useEffect(() => {
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("/sw.js");
    }
  }, []);
  return null;
}

Add <ServiceWorkerRegistration /> to your root layout.


Decision Framework

ScenarioRecommendation
App Router, wants caching out of the boxSerwist
Static export (output: "export")Manual SW
Migrating from next-pwaSerwist (drop-in successor)
Need push notificationsEither — see references/push-notifications.md
Need granular cache controlSerwist with custom routes
Zero dependencies requiredManual SW
Minimal PWA (just installable)Manual SW

Web App Manifest

Next.js 13.3+ supports app/manifest.ts natively. This generates /manifest.webmanifest at build time.

Key fields

{
  name: "Full App Name",              // install dialog, splash screen
  short_name: "App",                  // home screen label (≤12 chars)
  description: "What the app does",
  start_url: "/",                     // entry point on launch
  display: "standalone",              // standalone | fullscreen | minimal-ui | browser
  orientation: "portrait",            // optional: lock orientation
  background_color: "#ffffff",        // splash screen background
  theme_color: "#000000",             // browser chrome color
  icons: [
    { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
    { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
    { src: "/icon-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
  ],
  screenshots: [                      // optional: richer install UI
    { src: "/screenshot-wide.png", sizes: "1280x720", type: "image/png", form_factor: "wide" },
    { src: "/screenshot-narrow.png", sizes: "640x1136", type: "image/png", form_factor: "narrow" },
  ],
}

Manifest tips

  • Always include both 192x192 and 512x512 icons (Lighthouse requirement)
  • Add a maskable icon for Android adaptive icons
  • screenshots enable the richer install sheet on Android/desktop Chrome
  • theme_color should match your <meta name="theme-color"> in layout

Service Worker Essentials

Lifecycle

  1. Install — SW downloaded, install event fires, precache assets
  2. Waiting — New SW waits for all tabs to close (unless skipWaiting)
  3. Activate — Old caches cleaned up, SW takes control
  4. Fetch — SW intercepts network requests

Update flow

When a new SW is detected:

  • skipWaiting: true — immediately activates (may break in-flight requests)
  • Without skipWaiting — waits for all tabs to close, then activates
  • Notify users of updates with workbox-window or manual controllerchange listener

Registration scope

  • SW at /sw.js controls all pages under /
  • SW at /app/sw.js only controls /app/*
  • Always place SW at root unless you have a specific reason not to

Caching Strategies Quick Reference

StrategyUse ForSerwist Class
Cache FirstStatic assets, fonts, imagesCacheFirst
Network FirstAPI data, HTML pagesNetworkFirst
Stale While RevalidateSemi-static content (CSS/JS)StaleWhileRevalidate
Network OnlyAuth endpoints, real-time dataNetworkOnly
Cache OnlyPrecached content onlyCacheOnly

Serwist's defaultCache provides sensible defaults. For custom strategies, see references/caching-strategies.md.


Offline Support Basics

App shell pattern

Precache the app shell (layout, styles, scripts) so the UI loads instantly offline. Dynamic content loads from cache or shows a fallback.

Online/offline detection hook

"use client";
import { useSyncExternalStore } from "react";

function subscribe(callback: () => void) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true // SSR: assume online
  );
}

Offline fallback page

Create app/offline/page.tsx and precache /offline in your SW. When navigation fails, serve this page.

For IndexedDB, background sync, and advanced offline patterns, see references/offline-data.md.


Install Prompt Handling

beforeinstallprompt (Chrome/Edge/Android)

"use client";
import { useState, useEffect } from "react";

export function InstallPrompt() {
  const [deferredPrompt, setDeferredPrompt] = useState<any>(null);

  useEffect(() => {
    const handler = (e: Event) => {
      e.preventDefault();
      setDeferredPrompt(e);
    };
    window.addEventListener("beforeinstallprompt", handler);
    return () => window.removeEventListener("beforeinstallprompt", handler);
  }, []);

  if (!deferredPrompt) return null;

  return (
    <button
      onClick={async () => {
        deferredPrompt.prompt();
        const { outcome } = await deferredPrompt.userChoice;
        if (outcome === "accepted") setDeferredPrompt(null);
      }}
    >
      Install App
    </button>
  );
}

iOS detection

iOS doesn't fire beforeinstallprompt. Detect iOS and show manual instructions:

function isIOS() {
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
}

function isStandalone() {
  return window.matchMedia("(display-mode: standalone)").matches
    || (navigator as any).standalone === true;
}

Show a banner: "Tap Share then Add to Home Screen" for iOS Safari users.


Push Notifications Quick Start

1. Generate VAPID keys

npx web-push generate-vapid-keys

2. Subscribe in client

async function subscribeToPush() {
  const reg = await navigator.serviceWorker.ready;
  const sub = await reg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
  });
  await fetch("/api/push/subscribe", {
    method: "POST",
    body: JSON.stringify(sub),
  });
}

3. Handle in SW

self.addEventListener("push", (event) => {
  const data = event.data?.json() ?? { title: "Notification" };
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: "/icon-192.png",
    })
  );
});

For server-side sending, VAPID setup, and full implementation, see references/push-notifications.md.


Troubleshooting Cheat Sheet

ProblemFix
SW not updatingAdd skipWaiting: true or hard refresh (Shift+Cmd+R)
App not installableCheck manifest: needs name, icons, start_url, display
Stale content after deployBump cache version or use content-hashed URLs
SW registered in devDisable in dev: disable: process.env.NODE_ENV === "development"
iOS not showing installiOS has no install prompt — show manual instructions
Lighthouse PWA failsCheck HTTPS, valid manifest, registered SW, offline page
Next.js rewrite conflictsEnsure SW is served from /sw.js, not rewritten

For detailed debugging steps, see references/troubleshooting.md.


Assets & Templates

  • assets/manifest-template.ts — Complete app/manifest.ts with all fields
  • assets/sw-serwist-template.ts — Serwist SW with custom routes and offline fallback
  • assets/sw-manual-template.js — Manual SW with all strategies
  • assets/next-config-serwist.ts — next.config.ts with withSerwist

Generator Script

python scripts/generate_pwa_config.py <project-name> --approach serwist|manual [--push] [--offline]

Scaffolds PWA files based on chosen approach and features.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

onnx-webgpu-converter

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

elevenlabs

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

vercel-workflow

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

shot-list

No summary provided by upstream source.

Repository SourceNeeds Review
nextjs-pwa | V50.AI