Claude
Skills
Sign in
โ† Back

add-pwa

Included with Lifetime
$97 forever

Setup Progressive Web App (PWA) support for a Next.js project using Serwist. Use when user says "setup pwa", "add pwa", "make it a pwa", "progressive web app", or "enable offline support".

Web Dev

What this skill does


# Add PWA Support

Adds Progressive Web App capabilities to a Next.js project using [`@serwist/turbopack`](https://serwist.pages.dev/docs/next/turbo) โ€” the Turbopack-compatible Serwist integration. Uses esbuild via a Route Handler to bundle the service worker, so **no webpack fallback is needed**. Provides installability, offline support, precaching, and runtime caching strategies out of the box.

## What Gets Created

| File | Purpose |
|------|---------|
| `app/manifest.ts` | Web app manifest (name, icons, theme) |
| `app/sw.ts` | Service worker with precaching and offline fallback |
| `app/serwist/[path]/route.ts` | Route handler that bundles and serves the SW via esbuild |
| `app/serwist-provider.tsx` | Re-exports `SerwistProvider` with `"use client"` directive |
| `app/~offline/page.tsx` | Offline fallback page |
| `components/pwa-install-prompt.tsx` | Install prompt banner component |
| `components/pwa-provider.tsx` | PWA context provider (online/offline status) |
| `scripts/generate-icons.ts` | Placeholder icon generator |
| `public/icons/icon-192x192.svg` | Android/Chrome app icon (placeholder) |
| `public/icons/icon-512x512.svg` | Android/Chrome splash icon (placeholder) |
| `public/icons/apple-touch-icon.svg` | iOS home screen icon (placeholder) |

## Prerequisites

- Next.js 15+ project (App Router)
- TypeScript configured

## Installation

```bash
bun add -D @serwist/turbopack esbuild serwist
```

## Setup Steps

### 1. Create Web App Manifest

Create `app/manifest.ts`:

```ts
import type { MetadataRoute } from "next";

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: "My App",
    short_name: "MyApp",
    description: "A Progressive Web App built with Next.js",
    start_url: "/",
    display: "standalone",
    background_color: "#ffffff",
    theme_color: "#000000",
    orientation: "portrait",
    icons: [
      {
        src: "/icons/icon-192x192.svg",
        sizes: "192x192",
        type: "image/svg+xml",
        purpose: "maskable",
      },
      {
        src: "/icons/icon-512x512.svg",
        sizes: "512x512",
        type: "image/svg+xml",
      },
    ],
  };
}
```

**Note:** Next.js automatically serves this at `/manifest.webmanifest` and adds the `<link rel="manifest">` tag. No manual `<link>` tag needed. Replace SVG placeholders with real PNG icons for production.

### 2. Create Service Worker

Create `app/sw.ts`:

```ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { defaultCache } from "@serwist/turbopack/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,
  fallbacks: {
    entries: [
      {
        url: "/~offline",
        matcher({ request }) {
          return request.destination === "document";
        },
      },
    ],
  },
});

serwist.addEventListeners();
```

**Important:** The `/// <reference no-default-lib="true" />` directive removes default type definitions (including `dom`) from the compilation scope. You **must** exclude this file from the main TypeScript compilation. Add `"app/sw.ts"` to the `exclude` array in `tsconfig.json`:

```jsonc
{
  "exclude": ["node_modules", "app/sw.ts"]
}
```

This is safe because `app/sw.ts` is bundled separately by esbuild via the route handler โ€” it does not go through the normal `tsc` / Next.js compilation pipeline.

### 3. Create Serwist Route Handler

Create `app/serwist/[path]/route.ts`:

```ts
import { spawnSync } from "node:child_process";
import { createSerwistRoute } from "@serwist/turbopack";

const revision =
  spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf-8" }).stdout.trim() ||
  crypto.randomUUID();

export const { dynamic, dynamicParams, revalidate, generateStaticParams, GET } =
  createSerwistRoute({
    additionalPrecacheEntries: [{ url: "/~offline", revision }],
    swSrc: "app/sw.ts", // Use "src/app/sw.ts" if project has --src-dir
    useNativeEsbuild: true,
  });
```

This route handler uses esbuild to bundle `app/sw.ts` on-demand, injecting the precache manifest. The compiled service worker is served at `/serwist/sw.js`.

### 4. Create Offline Fallback Page

Create `app/~offline/page.tsx`:

```tsx
export default function OfflinePage() {
  return (
    <div className="flex min-h-screen items-center justify-center bg-background">
      <div className="text-center px-6">
        <div className="text-6xl mb-6">๐Ÿ“ก</div>
        <h1 className="text-2xl font-bold text-foreground mb-2">
          You&apos;re offline
        </h1>
        <p className="text-muted-foreground max-w-sm">
          Check your internet connection and try again. Some features may still
          be available offline.
        </p>
      </div>
    </div>
  );
}
```

### 5. Configure next.config.ts

Update `next.config.ts` to wrap with Serwist:

```ts
import { withSerwist } from "@serwist/turbopack";

export default withSerwist({
  // Your existing Next.js config goes here
});
```

**Important:** If the project has an existing `next.config.ts` with other config (e.g. `reactCompiler: true`), pass it inside the `withSerwist()` call:

```ts
import { withSerwist } from "@serwist/turbopack";

export default withSerwist({
  reactCompiler: true,
  // ...other config
});
```

### 6. Create SerwistProvider Re-export

Create `app/serwist-provider.tsx`:

```tsx
"use client";
export { SerwistProvider } from "@serwist/turbopack/react";
```

Serwist's `SerwistProvider` must be used in a client component. This re-export adds the `"use client"` directive.

### 7. Update Root Layout Metadata & Wire Up Provider

Update `app/layout.tsx` to include PWA metadata and the Serwist provider:

```tsx
import type { Metadata, Viewport } from "next";
import { SerwistProvider } from "./serwist-provider";
import { PWAInstallPrompt } from "@/components/pwa-install-prompt";
import { PWAProvider } from "@/components/pwa-provider";

export const metadata: Metadata = {
  applicationName: "My App",
  title: {
    default: "My App",
    template: "%s | My App",
  },
  description: "A Progressive Web App built with Next.js",
  appleWebApp: {
    capable: true,
    statusBarStyle: "default",
    title: "My App",
  },
  formatDetection: {
    telephone: false,
  },
};

export const viewport: Viewport = {
  themeColor: "#000000",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <SerwistProvider swUrl="/serwist/sw.js">
          <PWAProvider>
            {children}
            <PWAInstallPrompt />
          </PWAProvider>
        </SerwistProvider>
      </body>
    </html>
  );
}
```

**Note:** `themeColor` must be in the `viewport` export, not `metadata` (changed in Next.js 14+). The `swUrl` points to the route handler path, not a static file.

### 8. Generate Placeholder Icons

Create `scripts/generate-icons.ts`:

```ts
import { mkdirSync, writeFileSync } from "node:fs";

function generatePlaceholderSVG(size: number): string {
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
  <rect width="${size}" height="${size}" fill="#000000" rx="${size * 0.1}"/>
  <text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" fill="white" font-family="system-ui" font-size="${size * 0.4}" font-weight="bold">App</text>
</svg>`;
}

mkdirSync("public/icons", { recursive: true });

for (const size of [192, 512]) {
  writeFileSync(
    `public/icons/icon-${size}x${size}.svg`,
    generatePlaceholderSVG(size),
  );
}

writeFileSync("public/icons/apple-touch-ic
Files: 1
Size: 16.8 KB
Complexity: 25/100
Category: Web Dev

Related in Web Dev