nextjs-pwa
Next.js PWA Skill
Quick Reference
| Task | Approach | Reference |
|---|---|---|
| Add PWA to Next.js app | Serwist (recommended) | This file → Quick Start |
| Add PWA without dependencies | Manual SW | references/service-worker-manual.md |
| Configure caching | Serwist defaultCache or custom | references/caching-strategies.md |
| Add offline support | App shell + IndexedDB | references/offline-data.md |
| Push notifications | VAPID + web-push | references/push-notifications.md |
| Fix iOS issues | Safari/WebKit workarounds | references/ios-quirks.md |
| Debug SW / Lighthouse | DevTools + common fixes | references/troubleshooting.md |
| Migrate from next-pwa | Serwist migration | references/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
| Scenario | Recommendation |
|---|---|
| App Router, wants caching out of the box | Serwist |
Static export (output: "export") |
Manual SW |
| Migrating from next-pwa | Serwist (drop-in successor) |
| Need push notifications | Either — see references/push-notifications.md |
| Need granular cache control | Serwist with custom routes |
| Zero dependencies required | Manual 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
maskableicon for Android adaptive icons screenshotsenable the richer install sheet on Android/desktop Chrometheme_colorshould match your<meta name="theme-color">in layout
Service Worker Essentials
Lifecycle
- Install — SW downloaded,
installevent fires, precache assets - Waiting — New SW waits for all tabs to close (unless
skipWaiting) - Activate — Old caches cleaned up, SW takes control
- 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-windowor manualcontrollerchangelistener
Registration scope
- SW at
/sw.jscontrols all pages under/ - SW at
/app/sw.jsonly controls/app/* - Always place SW at root unless you have a specific reason not to
Caching Strategies Quick Reference
| Strategy | Use For | Serwist Class |
|---|---|---|
| Cache First | Static assets, fonts, images | CacheFirst |
| Network First | API data, HTML pages | NetworkFirst |
| Stale While Revalidate | Semi-static content (CSS/JS) | StaleWhileRevalidate |
| Network Only | Auth endpoints, real-time data | NetworkOnly |
| Cache Only | Precached content only | CacheOnly |
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
| Problem | Fix |
|---|---|
| SW not updating | Add skipWaiting: true or hard refresh (Shift+Cmd+R) |
| App not installable | Check manifest: needs name, icons, start_url, display |
| Stale content after deploy | Bump cache version or use content-hashed URLs |
| SW registered in dev | Disable in dev: disable: process.env.NODE_ENV === "development" |
| iOS not showing install | iOS has no install prompt — show manual instructions |
| Lighthouse PWA fails | Check HTTPS, valid manifest, registered SW, offline page |
| Next.js rewrite conflicts | Ensure 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 fieldsassets/sw-serwist-template.ts— Serwist SW with custom routes and offline fallbackassets/sw-manual-template.js— Manual SW with all strategiesassets/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.
More from jakerains/agentskills
shot-list
Generate professional shot lists from screenplays and scripts. Use when user uploads a screenplay (.fountain, .fdx, .txt, .pdf, .docx) or describes scenes for production planning. Parses scripts to extract scenes, helps determine camera setups, shot types, framing, and movement through collaborative discussion, then generates beautifully formatted PDF shot lists for production. Triggers include requests to create shot lists, plan shots, break down scripts for filming, or organize camera coverage.
27elevenlabs
Complete ElevenLabs AI audio platform: text-to-speech (TTS), speech-to-text (STT/Scribe), voice cloning, voice design, sound effects, music generation, dubbing, voice changer, voice isolator, and conversational voice agents. Use when working with audio generation, voice synthesis, transcription, audio processing, or building voice-enabled applications. Triggers: generate speech, clone voice, transcribe audio, create sound effects, compose music, dub video, change voice, isolate vocals, build voice agent, ElevenLabs API/SDK/CLI/MCP.
9onnx-webgpu-converter
Convert HuggingFace transformer models to ONNX format for browser inference with Transformers.js and WebGPU. Use when given a HuggingFace model link to convert to ONNX, when setting up optimum-cli for ONNX export, when quantizing models (fp16, q8, q4) for web deployment, when configuring Transformers.js with WebGPU acceleration, or when troubleshooting ONNX conversion errors. Triggers on mentions of ONNX conversion, Transformers.js, WebGPU inference, optimum export, model quantization for browser, or running ML models in the browser.
8skill-seekers
Convert documentation websites, GitHub repositories, and PDFs into Claude AI skills. Use when creating Claude skills from docs, scraping documentation, packaging websites into skills, or converting repos/PDFs to Claude knowledge.
7vercel-workflow
Build durable workflows with Vercel Workflow DevKit using "use workflow" and "use step" directives. Use for long-running tasks, background jobs, AI agents, webhooks, scheduled tasks, retries, and workflow orchestration. Supports Next.js, Vite, Astro, Express, Fastify, Hono, Nitro, Nuxt, SvelteKit.
7apple-foundation-models
Build Apple Intelligence features with Foundation Models and Image Playground on iOS 26+, iPadOS 26+, macOS 26+, Mac Catalyst 26+, and visionOS 26+. Use when implementing SystemLanguageModel, LanguageModelSession, guided generation with @Generable/@Guide, tool calling, streaming responses, prompt design, safety and guardrail handling, model availability checks, content tagging, context-window limits, local on-device inference, routing to larger-model paths, adapters, and ImagePlayground/ImageCreator APIs. Covers model capabilities and limitations, structured output, error handling, and SwiftUI integration patterns.
7