Skip to main content

Configurator mode

Why?

Prior to Serwist 9.4, Serwist integrated with Next.js by wrapping withSerwist around your Next.js configuration. This tied Serwist to webpack’s lifecycle, and it could only build the service worker after webpack finished bundling your app, but before Next.js prerendered any route. Because of this timing, Serwist had no visibility into prerendered output and could not automatically precache those routes.

Configurator mode exists to solve this problem. Rather than hooking Serwist directly into the Next.js build, Serwist now has an external build step. In this mode, @serwist/next only generates the @serwist/cli configuration needed to build the service worker. The service worker is then built after Next.js has prerendered everything, allowing Serwist to automatically precache all prerendered routes.

This approach also makes the integration bundler–agnostic. Since configurator mode does not depend on webpack internals, it works with Turbopack as well, eliminating the need for a separate implementation like @serwist/turbopack.

If you are looking to set up Serwist with Next.js the classic way, see the following guides:

Install

Run the following command:

npm i -D @serwist/next @serwist/cli serwist esbuild concurrently

Implementation

Step 1: Configure Serwist

Add serwist.config.js with the following content:

// @ts-check
import { spawnSync } from "node:child_process";
import { serwist } from "@serwist/next/config";

// Using `git rev-parse HEAD` might not the most efficient
// way of determining a revision. You may prefer to use
// the hashes of every extra file you precache.
const revision = spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf-8" }).stdout ?? crypto.randomUUID();

export default serwist({
  swSrc: "app/sw.ts",
  swDest: "public/sw.js",
  // If you want to precache any other page that is not
  // already detected by Serwist, add them here. Otherwise,
  // delete `revision`.
  additionalPrecacheEntries: [{ url: "/precached", revision }],
});

Step 2: Update build scripts

Change your build scripts like so in package.json:

{
  "scripts": {
    // If you don't need the service worker in development, you can set `disable` in `SerwistProvider`
    // (see below) to `process.env.NODE_ENV === "development"` instead.
    "dev": "concurrently -p none 'serwist build --watch' 'next dev'",
    // Alternatively, build the service worker just once before starting the development server.
    "dev:once": "cross-env NODE_ENV=development serwist build && next dev",
    // Add `serwist build` to your build command.
    "build": "next build && serwist build"
  }
}

Step 3: Update .gitignore

If you use Git, update your .gitignore like so:

# Serwist
public/sw*

Otherwise, safely skip this step.

Step 4: Create a service worker

Here is a basic service worker template to get Serwist up and running:

/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { defaultCache } from "@serwist/next/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";

// This declares the value of `injectionPoint` to TypeScript.
// `injectionPoint` is the string that will be replaced by the
// actual precache manifest. By default, this string is set to
// `"self.__SW_MANIFEST"`.
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();

Step 5: Add a web application manifest

Update app/manifest.json (App Router) or public/manifest.json (Pages Router) with the following content:

{
  "name": "My Awesome PWA app",
  "short_name": "PWA App",
  "icons": [
    {
      "src": "/icons/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#FFFFFF",
  "background_color": "#FFFFFF",
  "start_url": "/",
  "display": "standalone",
  "orientation": "portrait"
}

Step 6: Add metadata and SerwistProvider

Add the following content to app/layout.tsx and/or pages/_app.tsx:

// app/serwist.ts
"use client";
export { SerwistProvider } from "@serwist/next/react";
// app/layout.tsx
import type { Metadata, Viewport } from "next";
import type { ReactNode } from "react";
import { SerwistProvider } from "./serwist";

const APP_NAME = "PWA App";
const APP_DEFAULT_TITLE = "My Awesome PWA App";
const APP_TITLE_TEMPLATE = "%s - PWA App";
const APP_DESCRIPTION = "Best PWA app in the world!";

export const metadata: Metadata = {
  applicationName: APP_NAME,
  title: {
    default: APP_DEFAULT_TITLE,
    template: APP_TITLE_TEMPLATE,
  },
  description: APP_DESCRIPTION,
  appleWebApp: {
    capable: true,
    statusBarStyle: "default",
    title: APP_DEFAULT_TITLE,
    // startUpImage: [],
  },
  formatDetection: {
    telephone: false,
  },
  openGraph: {
    type: "website",
    siteName: APP_NAME,
    title: {
      default: APP_DEFAULT_TITLE,
      template: APP_TITLE_TEMPLATE,
    },
    description: APP_DESCRIPTION,
  },
  twitter: {
    card: "summary",
    title: {
      default: APP_DEFAULT_TITLE,
      template: APP_TITLE_TEMPLATE,
    },
    description: APP_DESCRIPTION,
  },
};

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

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en" dir="ltr">
      <head />
      <body>
        <SerwistProvider swUrl="/sw.js" /* disable={process.env.NODE_ENV === "development"} */>
          {children}
        </SerwistProvider>
      </body>
    </html>
  );
}