Skip to main content

Getting started

Install

If you are new to React or Next.js, you may want to checkout learn React, learn Next.js and Next.js documentation first, and then start from a simple example.

npm i @ducanh2912/next-pwa && npm i -D webpack
npm i @ducanh2912/next-pwa && npm i -D webpack

Basic usage

Step 1: Wrap your Next config with withPWA

Update or create your next.config.js with

const withPWA = require("@ducanh2912/next-pwa").default({
dest: "public",
});

module.exports = withPWA({
// Your Next.js config
});
const withPWA = require("@ducanh2912/next-pwa").default({
dest: "public",
});

module.exports = withPWA({
// Your Next.js config
});

If your deployment platform requires your production image's size to not exceed a certain limit, you can also install @ducanh2912/next-pwa as one of your devDependencies and do this:

const {
PHASE_DEVELOPMENT_SERVER,
PHASE_PRODUCTION_BUILD,
} = require("next/constants");

/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
};

module.exports = (phase) => {
if (phase === PHASE_DEVELOPMENT_SERVER || phase === PHASE_PRODUCTION_BUILD) {
const withPWA = require("@ducanh2912/next-pwa").default({
dest: "public",
});
return withPWA(nextConfig);
}
return nextConfig;
};
const {
PHASE_DEVELOPMENT_SERVER,
PHASE_PRODUCTION_BUILD,
} = require("next/constants");

/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
};

module.exports = (phase) => {
if (phase === PHASE_DEVELOPMENT_SERVER || phase === PHASE_PRODUCTION_BUILD) {
const withPWA = require("@ducanh2912/next-pwa").default({
dest: "public",
});
return withPWA(nextConfig);
}
return nextConfig;
};

This plugin is written in TypeScript and supports JSDoc. It is recommended to add // @ts-check to the top of your next.config.js file to leverage typechecking. This is especially useful when you use PluginOptions.workboxOptions, as you may unknowningly mix InjectManifest-specific and GenerateSW-specific options up.

After running next build, this will generate two files in your public directory: workbox-*.js and sw.js, which will automatically be served statically.

Step 1.5: Next.js < 9

If you are using Next.js >= 9, then skip the section below.

Otherwise, you'll need to pick one of the two options below before continuing.

Option 1: Hosting static files

Copy files to your static file hosting server, so that they are accessible from the following paths: https://yourdomain.com/sw.js and https://yourdomain.com/workbox-*.js.

An example is using Firebase hosting service to host those files statically. You can automate the copying step with scripts in your deployment workflow.

For security reasons, you must host these files directly from your domain. If the content is delivered using a redirect, the browser will refuse to run the service worker.

Option 2: Using a custom server

Example server:

server.js
const { createServer } = require("http");
const { join } = require("path");
const { parse } = require("url");
const next = require("next");

const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 3000;

const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();

app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
const { pathname } = parsedUrl;
if (
pathname === "/sw.js" ||
/^\/(workbox|worker|fallback)-\w+\.js$/.test(pathname)
) {
const filePath = join(__dirname, ".next", pathname);
app.serveStatic(req, res, filePath);
} else {
handle(req, res, parsedUrl);
}
}).listen(port, () => {
console.log(`> Ready on http://${hostname}:${port}`);
});
});
server.js
const { createServer } = require("http");
const { join } = require("path");
const { parse } = require("url");
const next = require("next");

const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 3000;

const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();

app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
const { pathname } = parsedUrl;
if (
pathname === "/sw.js" ||
/^\/(workbox|worker|fallback)-\w+\.js$/.test(pathname)
) {
const filePath = join(__dirname, ".next", pathname);
app.serveStatic(req, res, filePath);
} else {
handle(req, res, parsedUrl);
}
}).listen(port, () => {
console.log(`> Ready on http://${hostname}:${port}`);
});
});

It is recommended to upgrade your Next.js version instead. Usually, Next.js will provide codemods needed to migrate between major versions (see Next.js codemods). Its releases are often packed with a lot of awesome features, and you shouldn't miss out on them.

Step 2: Add a manifest.json file

Create a manifest.json file in your public folder:

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

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

import type { Metadata, Viewport } from "next";

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,
manifest: "/manifest.json",
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",
};
import type { Metadata, Viewport } from "next";

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,
manifest: "/manifest.json",
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",
};

Tips

  • You may want to ask user to reload when a new service worker is installed

  • When you are debugging the service worker, remember to constantly clean the application cache to reduce some flaky errors.

  • If you are redirecting the user to another route, please note that Workbox by default only caches responses with 200 HTTP status, if you really want to cache redirected page for the route, you can specify it in runtimeCaching by setting options.cacheableResponse.statuses to [200, 302].

  • When debugging issues, sometimes you may want to format your generated sw.js file to figure out what's really going on. In development mode it is not minified though, so it is readable to an extent.

  • You can force next-pwa to generate the SW in production mode by setting workboxOptions.mode to "production". By default, next-pwa generates the SW in development mode for next dev and in production mode for next build and next start, but you may still want to force it to build in production mode even during development because of one of these reasons:

    • Reduced logging noise as the production build doesn't include logging.
    • Improved performance as the production build is better optimized.

    However, if you just want to disable Workbox's logging, simply add self.__WB_DISABLE_DEV_LOGS = true to your worker/index.ts.

  • If you want to get your user's userAgent string, use ua-parser-js!