Skip to main content

Getting started

This guide uses React, but it applies to any project using webpack. For some frameworks, we also have packages tailored to them:

Install

Run the following command:

npm i -D @serwist/webpack-plugin @serwist/window serwist

Implementation

Step 1: Add Serwist’s webpack plugin

Update or create your webpack configuration file with the following content:

import fs from "node:fs";
import path from "node:path";

import { InjectManifest } from "@serwist/webpack-plugin";
import type { Configuration } from "webpack";

const dev = process.env.NODE_ENV === "development";
const rootDir = fs.realpathSync(process.cwd());
const srcDir = path.join(rootDir, "src");
const destDir = path.join(rootDir, "dist");

const clientEntry = path.resolve(srcDir, "client.ts");

export default {
  target: "web",
  name: "client",
  module: {
    rules: [
      // Insert rules...
    ],
  },
  entry: clientEntry,
  output: {
    publicPath: "/",
    path: path.resolve(destDir, "public"),
    filename: "static/js/[name]-[contenthash:8].js",
    chunkFilename: "static/js/[name]-[contenthash:8].chunk.js",
    assetModuleFilename: "static/media/[name].[hash][ext]",
  },
  plugins: [
    // swDest is automatically resolved to "$\{output.path}/sw.js"
    new InjectManifest({
      swSrc: path.resolve(srcDir, "sw.ts"),
      disablePrecacheManifest: dev,
      // Insert something...
      additionalPrecacheEntries: !dev ? [] : undefined,
    }),
  ],
} satisfies Configuration;

Step 2: Update tsconfig.json

If you use TypeScript, you should add the following content to tsconfig.json in order to get the correct types:

{
  // Other stuff...
  "compilerOptions": {
    // Other options...
    "lib": [
      // Other libs...
      // Add this! Doing so adds WebWorker and ServiceWorker types to the global.
      "webworker"
    ],
  },
}

Otherwise, safely skip this step.

Step 3: Update .gitignore

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

# Serwist
public/sw*
public/swe-worker*

Otherwise, safely skip this step.

Step 4: Create a service worker

Basic service worker template to get Serwist up and running:

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,
  // We leave this up to you :)
  runtimeCaching: [],
});

serwist.addEventListeners();

Step 5: Add a web application manifest

Update public/manifest.json 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: Update your client entrypoint

Next up, update src/App.tsx with the following content:

import { useEffect } from "react";

export default function App() {
  useEffect(() => {
    const loadSerwist = async () => {
      if ("serviceWorker" in navigator) {
        const serwist = new (await import("@serwist/window")).Serwist("/sw.js", { scope: "/", type: "classic" });

        serwist.addEventListener("installed", () => {
          console.log("Serwist installed!");
        });

        void serwist.register();
      }
    };

    loadSerwist();
  }, []);

  return <></>;
}

Step 7: Add metadata

Add the following content to your HTML file:

<head>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>My awesome PWA app</title>
  <meta name="description" content="Best PWA app in the world!">
  <link rel="shortcut icon" href="/favicon.ico">
  <link rel="mask-icon" href="/icons/mask-icon.svg" color="#FFFFFF">
  <meta name="theme-color" content="#ffffff">
  <link rel="apple-touch-icon" href="/icons/touch-icon-iphone.png">
  <link rel="apple-touch-icon" sizes="152x152" href="/icons/touch-icon-ipad.png">
  <link rel="apple-touch-icon" sizes="180x180" href="/icons/touch-icon-iphone-retina.png">
  <link rel="apple-touch-icon" sizes="167x167" href="/icons/touch-icon-ipad-retina.png">
  <link rel="manifest" href="/manifest.json">
  <meta name="twitter:card" content="summary">
  <meta name="twitter:url" content="https://yourdomain.com">
  <meta name="twitter:title" content="My awesome PWA app">
  <meta name="twitter:description" content="Best PWA app in the world!">
  <meta name="twitter:image" content="/icons/twitter.png">
  <meta property="og:type" content="website">
  <meta property="og:title" content="My awesome PWA app">
  <meta property="og:description" content="Best PWA app in the world!">
  <meta property="og:site_name" content="My awesome PWA app">
  <meta property="og:url" content="https://yourdomain.com">
  <meta property="og:image" content="/icons/og.png">
  <link rel="apple-touch-startup-image" href="/images/apple_splash_2048.png" sizes="2048x2732">
  <link rel="apple-touch-startup-image" href="/images/apple_splash_1668.png" sizes="1668x2224">
  <link rel="apple-touch-startup-image" href="/images/apple_splash_1536.png" sizes="1536x2048">
  <link rel="apple-touch-startup-image" href="/images/apple_splash_1125.png" sizes="1125x2436">
  <link rel="apple-touch-startup-image" href="/images/apple_splash_1242.png" sizes="1242x2208">
  <link rel="apple-touch-startup-image" href="/images/apple_splash_750.png" sizes="750x1334">
  <link rel="apple-touch-startup-image" href="/images/apple_splash_640.png" sizes="640x1136">
</head>