19 | >
20 | );
21 |
22 | export default A;
23 |
--------------------------------------------------------------------------------
/examples/cache-on-front-end-nav/pages/b.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Link from "next/link";
3 |
4 | const B = () => (
5 | <>
6 |
7 | next-pwa example | Route b
8 |
9 |
This is route /b
10 |
11 | Go to Home
12 |
13 |
14 | Go to route /a
15 |
16 |
17 | Go to route /b
18 |
19 | >
20 | );
21 |
22 | export default B;
23 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/examples/cache-on-front-end-nav/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Link from "next/link";
3 |
4 | const Index = () => (
5 | <>
6 |
7 | next-pwa example | Home
8 |
9 |
;
6 | }
7 |
8 | const APP_NAME = "next-pwa example";
9 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
10 |
11 | export const metadata: Metadata = {
12 | title: "PWA 💖 Next.js",
13 | description: APP_DESCRIPTION,
14 | twitter: {
15 | card: "summary_large_image",
16 | creator: "@imamdev_",
17 | images: "https://example.com/og.png",
18 | },
19 | applicationName: APP_NAME,
20 | appleWebApp: {
21 | capable: true,
22 | title: APP_NAME,
23 | statusBarStyle: "default",
24 | },
25 | formatDetection: {
26 | telephone: false,
27 | },
28 | themeColor: "#FFFFFF",
29 | viewport:
30 | "minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover",
31 | manifest: "/manifest.json",
32 | icons: [
33 | { rel: "apple-touch-icon", url: "/icons/apple-touch-icon.png" },
34 | { rel: "shortcut icon", url: "/favicon.ico" },
35 | ],
36 | keywords: ["nextjs", "pwa", "next-pwa"],
37 | };
38 |
--------------------------------------------------------------------------------
/docs/public/favicons/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@imbios/next-pwa Documentation",
3 | "short_name": "@imbios/next-pwa Docs",
4 | "icons": [
5 | {
6 | "src": "/android-icon-36x36.png",
7 | "sizes": "36x36",
8 | "type": "image/png",
9 | "density": "0.75"
10 | },
11 | {
12 | "src": "/android-icon-48x48.png",
13 | "sizes": "48x48",
14 | "type": "image/png",
15 | "density": "1.0"
16 | },
17 | {
18 | "src": "/android-icon-72x72.png",
19 | "sizes": "72x72",
20 | "type": "image/png",
21 | "density": "1.5"
22 | },
23 | {
24 | "src": "/android-icon-96x96.png",
25 | "sizes": "96x96",
26 | "type": "image/png",
27 | "density": "2.0"
28 | },
29 | {
30 | "src": "/android-icon-144x144.png",
31 | "sizes": "144x144",
32 | "type": "image/png",
33 | "density": "3.0"
34 | },
35 | {
36 | "src": "/android-icon-192x192.png",
37 | "sizes": "192x192",
38 | "type": "image/png",
39 | "density": "4.0"
40 | }
41 | ],
42 | "theme_color": "#000000",
43 | "background_color": "#000000",
44 | "display": "standalone"
45 | }
46 |
--------------------------------------------------------------------------------
/examples/web-push/README.md:
--------------------------------------------------------------------------------
1 | # next-pwa - web push example
2 |
3 | [TOC]
4 |
5 | This example demonstrates how to use `next-pwa` plugin to implement web push with custom worker.
6 |
7 | **NOTE**
8 |
9 | In real world, you may want to send the subscription data to your server once user agree to subscribe web push. Store the data associate with the user. So that you can initiate a web push notification from your server to the specific users.
10 |
11 | ## Usage
12 |
13 | [](https://gitpod.io/#https://github.com/shadowwalker/next-pwa/)
14 |
15 | ```bash
16 | cd examples/web-push
17 | yarn install
18 | yarn vapid
19 | ```
20 |
21 | Create a `.env` file, and put the public key generated from the previous steps
22 |
23 | ```
24 | WEB_PUSH_EMAIL=user@example.com
25 | WEB_PUSH_PRIVATE_KEY=
26 | NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY=
27 | ```
28 |
29 | Build and start
30 |
31 | ```bash
32 | yarn build
33 | yarn start
34 | ```
35 |
36 | ## Recommend `.gitignore`
37 |
38 | ```
39 | **/public/workbox-*.js
40 | **/public/sw.js
41 | **/public/worker-*.js
42 | ```
43 |
--------------------------------------------------------------------------------
/examples/next-i18next/public/locales/de/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "h1": "Ein einfaches Beispiel",
3 | "change-locale": "Sprache wechseln zu \"{{changeTo}}\"",
4 | "to-second-page": "Zur zweiten Seite",
5 | "error-with-status": "Auf dem Server ist ein Fehler ({{statusCode}}) aufgetreten",
6 | "error-without-status": "Auf dem Server ist ein Fehler aufgetreten",
7 | "title": "Hauptseite | next-i18next",
8 | "blog": {
9 | "appDir": {
10 | "question": "Verwendest du das neue Next.js 13 mit app directory?",
11 | "answer": "Dann schau dir <1>diesen Blogbeitrag1> an.",
12 | "link": "https://locize.com/blog/next-13-app-dir-i18n/"
13 | },
14 | "optimized": {
15 | "question": "Möchtest du einige Superkräfte entfesseln, um für alle Seiten optimierte Übersetzungen zu haben?",
16 | "answer": "Dann schaue dir vielleicht <1>diesen Blogbeitrag1> an.",
17 | "link": "https://locize.com/blog/next-i18next-de/"
18 | },
19 | "ssg": {
20 | "question": "Möchtest du SSG (next export) verwenden?",
21 | "answer": "Dann schaue dir vielleicht <1>diesen Blogbeitrag1> an.",
22 | "link": "https://locize.com/blog/next-i18n-statisch/"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | name: Node.js Package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | concurrency: ${{ github.workflow }}-${{ github.ref }}
8 |
9 | jobs:
10 | publish-npm:
11 | name: Publish to NPM
12 | timeout-minutes: 15
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: 📂 Checkout
16 | uses: actions/checkout@v3
17 | - name: 📦 Install PNPM
18 | uses: pnpm/action-setup@v2
19 | with:
20 | version: 8
21 | - name: 📦 Install Node.js
22 | uses: actions/setup-node@v3
23 | with:
24 | cache: "pnpm"
25 | node-version-file: .node-version
26 | registry-url: https://registry.npmjs.org/
27 | scope: "@imbios"
28 | - name: 📦 Install Dependencies
29 | run: pnpm install
30 | - name: Publish to npm
31 | id: changesets
32 | uses: changesets/action@v1
33 | with:
34 | # This expects you to have a script called publish-packages which does a build for your packages and calls changeset publish
35 | publish: pnpm publish-packages
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | NPM_TOKEN: ${{secrets.NPM_AUTOMATION_TOKEN}}
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 ShadowWalker w@weiw.io
4 | Copyright (c) 2023 Imamuzzaki Abu Salam imamuzzaki@gmail.com
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/examples/next-i18next/pages/second-page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
3 |
4 | import { useTranslation } from "next-i18next";
5 | import { serverSideTranslations } from "next-i18next/serverSideTranslations";
6 |
7 | import { Header } from "../components/Header";
8 | import { Footer } from "../components/Footer";
9 |
10 | type Props = {
11 | // Add custom props here
12 | };
13 |
14 | const SecondPage = (
15 | _props: InferGetServerSidePropsType
16 | ) => {
17 | const { t } = useTranslation(["common", "second-page"]);
18 |
19 | return (
20 | <>
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | >
29 | );
30 | };
31 |
32 | export const getServerSideProps: GetServerSideProps = async ({
33 | locale,
34 | }) => ({
35 | props: {
36 | ...(await serverSideTranslations(locale ?? "en", [
37 | "second-page",
38 | "footer",
39 | ])),
40 | },
41 | });
42 |
43 | export default SecondPage;
44 |
--------------------------------------------------------------------------------
/docs/pages/offline-fallbacks.mdx:
--------------------------------------------------------------------------------
1 | # Offline Fallbacks
2 |
3 | Offline fallbacks are useful when the fetch failed from both cache and network, a precached resource is served instead of present an error from browser.
4 |
5 | To get started simply add a `/_offline` page such as `pages/_offline.js` or `pages/_offline.jsx` or `pages/_offline.ts` or `pages/_offline.tsx`. Then you are all set! When the user is offline, all pages which are not cached will fallback to '/\_offline'.
6 |
7 | **[Use this example to see it in action](https://github.com/ImBIOS/next-pwa/tree/master/examples/offline-fallback-v2)**
8 |
9 | `next-pwa` helps you precache those resources on the first load, then inject a fallback handler to `handlerDidError` plugin to all `runtimeCaching` configs, so that precached resources are served when fetch failed.
10 |
11 | You can also setup `precacheFallback.fallbackURL` in your [runtimeCaching config entry](https://developer.chrome.com/docs/workbox/reference/workbox-build/#type-RuntimeCaching) to implement similar functionality. The difference is that above method is based on the resource type, this method is based matched url pattern. If this config is set in the runtimeCaching config entry, resource type based fallback will be disabled automatically for this particular url pattern to avoid conflict.
12 |
--------------------------------------------------------------------------------
/examples/lifecycle/README.md:
--------------------------------------------------------------------------------
1 | # next-pwa - lifecycle and register workflow control example
2 |
3 | [TOC]
4 |
5 | This example demonstrates how to use the `next-pwa` plugin to turn a `next.js` based web application into a progressive web application (PWA) painlessly.
6 |
7 | This example demonstrates how to control the service worker registration workflow (instead of automatically registering the service worker) and add an event listener to handle the lifecycle events. It gives you more control through the PWA lifecycle. The key here is to set the `register` option in `next.config.js` to `false` then call `window.workbox.register()` to register the service worker on your own.
8 |
9 | **UPDATE**
10 |
11 | This example also demonstrates how to [prompt the user to reload the page when a new version is available](https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users).
12 |
13 | ## Usage
14 |
15 | [](https://gitpod.io/#https://github.com/shadowwalker/next-pwa/)
16 |
17 | ```bash
18 | cd examples/lifecycle
19 | yarn install
20 | yarn build
21 | yarn start
22 | ```
23 |
24 | ## Recommended `.gitignore`
25 |
26 | ```
27 | **/public/precache.*.js
28 | **/public/sw.js
29 | ```
30 |
--------------------------------------------------------------------------------
/examples/offline-fallback-v2/README.md:
--------------------------------------------------------------------------------
1 | # next-pwa - offline fallback example
2 |
3 | [TOC]
4 |
5 | This example demonstrates how to use `next-pwa` to implement fallback routes for page, image or font when fetch error. Fetch error usually happens when **offline**. (Note fetch is successful even when server returns error codes `404, 400, 500, ...`)
6 |
7 | Simply add a `/_offline` page such as `pages/_offline.js` or `pages/_offline.jsx` or `pages/_offline.tsx`. Then you are all set! No configuration needed for this.
8 |
9 | You can configure fallback routes for other type of resources
10 |
11 | ```
12 | pwa: {
13 | // ...
14 | fallbacks: {
15 | image: '/static/images/fallback.png',
16 | // document: '/other-offline', // if you want to fallback to a custom page other than /_offline
17 | // font: '/static/font/fallback.woff2',
18 | // audio: ...,
19 | // video: ...,
20 | },
21 | // ...
22 | }
23 | ```
24 |
25 | ## Usage
26 |
27 | [](https://gitpod.io/#https://github.com/shadowwalker/next-pwa/)
28 |
29 | ```bash
30 | cd examples/offline-fallback-v2
31 | yarn install
32 | yarn build
33 | yarn start
34 | ```
35 |
36 | ## Recommend `.gitignore`
37 |
38 | ```
39 | **/public/workbox-*.js
40 | **/public/sw.js
41 | **/public/fallback-*.js
42 | ```
43 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:base"],
4 | "branchPrefix": "renovate/",
5 | "baseBranches": ["master"],
6 | "assigneesFromCodeOwners": true,
7 | "packageRules": [
8 | {
9 | "groupName": "lint",
10 | "matchPackageNames": [
11 | "eslint-config-next",
12 | "eslint-config-prettier",
13 | "eslint-config-turbo",
14 | "@next/eslint-plugin-next",
15 | "@typescript-eslint/eslint-plugin",
16 | "@typescript-eslint/parser",
17 | "eslint",
18 | "eslint-config-prettier",
19 | "eslint-plugin-import",
20 | "eslint-plugin-react",
21 | "eslint-plugin-react-hooks",
22 | "eslint-plugin-tailwindcss",
23 | "eslint-plugin-typescript-sort-keys",
24 | "eslint-plugin-unicorn",
25 | "prettier",
26 | "prettier-plugin-tailwindcss"
27 | ]
28 | },
29 | {
30 | "matchPackagePatterns": ["*"],
31 | "matchUpdateTypes": ["minor", "patch"],
32 | "groupName": "all non-major dependencies",
33 | "groupSlug": "all-minor-patch",
34 | "automerge": true,
35 | "labels": ["dependencies"]
36 | },
37 | {
38 | "matchPackagePatterns": ["*"],
39 | "matchUpdateTypes": ["major"],
40 | "labels": ["dependencies", "breaking"]
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/examples/web-push/worker/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | self.addEventListener("push", function (event) {
4 | const data = JSON.parse(event.data.text());
5 | event.waitUntil(
6 | registration.showNotification(data.title, {
7 | body: data.message,
8 | icon: "/icons/android-chrome-192x192.png",
9 | })
10 | );
11 | });
12 |
13 | self.addEventListener("notificationclick", function (event) {
14 | event.notification.close();
15 | event.waitUntil(
16 | clients
17 | .matchAll({ type: "window", includeUncontrolled: true })
18 | .then(function (clientList) {
19 | if (clientList.length > 0) {
20 | let client = clientList[0];
21 | for (let i = 0; i < clientList.length; i++) {
22 | if (clientList[i].focused) {
23 | client = clientList[i];
24 | }
25 | }
26 | return client.focus();
27 | }
28 | return clients.openWindow("/");
29 | })
30 | );
31 | });
32 |
33 | // self.addEventListener('pushsubscriptionchange', function(event) {
34 | // event.waitUntil(
35 | // Promise.all([
36 | // Promise.resolve(event.oldSubscription ? deleteSubscription(event.oldSubscription) : true),
37 | // Promise.resolve(event.newSubscription ? event.newSubscription : subscribePush(registration))
38 | // .then(function(sub) { return saveSubscription(sub) })
39 | // ])
40 | // )
41 | // })
42 |
--------------------------------------------------------------------------------
/examples/cache-on-front-end-nav/pages/_app.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useRouter } from "next/router";
3 |
4 | const _App = ({ Component, pageProps }) => {
5 | const [isOnline, setIsOnline] = useState(true);
6 | useEffect(() => {
7 | if (
8 | typeof window !== "undefined" &&
9 | "ononline" in window &&
10 | "onoffline" in window
11 | ) {
12 | setIsOnline(window.navigator.onLine);
13 | if (!window.ononline) {
14 | window.addEventListener("online", () => {
15 | setIsOnline(true);
16 | });
17 | }
18 | if (!window.onoffline) {
19 | window.addEventListener("offline", () => {
20 | setIsOnline(false);
21 | });
22 | }
23 | }
24 | }, []);
25 |
26 | const router = useRouter();
27 | useEffect(() => {
28 | if (
29 | typeof window !== "undefined" &&
30 | "serviceWorker" in navigator &&
31 | window.workbox !== undefined &&
32 | isOnline
33 | ) {
34 | // skip index route, because it's already cached under `start-url` caching object
35 | if (router.route !== "/") {
36 | const wb = window.workbox;
37 | wb.active.then((worker) => {
38 | wb.messageSW({ action: "CACHE_NEW_ROUTE" });
39 | });
40 | }
41 | }
42 | }, [isOnline, router.route]);
43 |
44 | return ;
45 | };
46 |
47 | export default _App;
48 |
--------------------------------------------------------------------------------
/examples/cookie/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { useRouter } from "next/router";
3 | import Cookies from "js-cookie";
4 | import nextCookies from "next-cookies";
5 |
6 | const Index = ({ user }) => {
7 | const router = useRouter();
8 |
9 | const handleLogoutClick = () => {
10 | Cookies.remove("user");
11 | router.replace("/login");
12 | };
13 |
14 | const handleLoginClick = () => {
15 | router.replace("/login");
16 | };
17 |
18 | return (
19 | <>
20 |
21 | next-pwa example
22 |
23 |
Next.js + PWA = AWESOME!
24 | {user ? (
25 | <>
26 |
User ID: {user}
27 |
28 | >
29 | ) : (
30 |
31 | )}
32 | >
33 | );
34 | };
35 |
36 | export const getServerSideProps = (context) => {
37 | const { user } = nextCookies(context);
38 | if (!user) {
39 | console.log("❌ User Not Login, Redirect To Login Page");
40 | context.res.setHeader("location", "/login");
41 | context.res.statusCode = 302;
42 | context.res.end();
43 | return { props: {} };
44 | } else {
45 | console.log(`✅ User (id=${user}) Already Login, Show Home Page.`);
46 | return {
47 | props: {
48 | user,
49 | },
50 | };
51 | }
52 | };
53 |
54 | export default Index;
55 |
--------------------------------------------------------------------------------
/examples/offline-fallback/README.md:
--------------------------------------------------------------------------------
1 | # next-pwa - offline fallback example
2 |
3 | [TOC]
4 |
5 | > **Checkout this simple and easy way to implement offline fallbacks without inject manifest: **
6 | >
7 | > **[offline-fallback-v2](https://github.com/shadowwalker/next-pwa/tree/master/examples/offline-fallback-v2)**
8 |
9 | This example demonstrates how to use `next-pwa` to implement fallback route, image or font when fetch error. Fetch error usually happens when **offline**. (Note fetch is successful even when server returns error codes `404, 400, 500, ...`)
10 |
11 | This example uses **Inject Manifest** module from `workbox`. The advantage of using this module is you get more control over your service worker. The disadvantage is that it's more complicated and needs to write more code.
12 |
13 | The idea of implementing comprehensive fallbacks can be found [here](https://developers.google.com/web/tools/workbox/guides/advanced-recipes#comprehensive_fallbacks).
14 |
15 | > In the future, using inject manifest may not be needed. When [this proposal](https://github.com/GoogleChrome/workbox/issues/2569) is completed in workbox v6.
16 |
17 | ## Usage
18 |
19 | [](https://gitpod.io/#https://github.com/shadowwalker/next-pwa/)
20 |
21 | ```bash
22 | cd examples/offline-fallback
23 | yarn install
24 | yarn build
25 | yarn start
26 | ```
27 |
28 | ## Recommend `.gitignore`
29 |
30 | ```
31 | **/public/workbox-*.js
32 | **/public/sw.js
33 | ```
34 |
--------------------------------------------------------------------------------
/packages/next-pwa/fallback.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | self.fallback = async (request) => {
4 | // https://developer.mozilla.org/en-US/docs/Web/API/RequestDestination
5 | switch (request.destination) {
6 | case "document":
7 | if (process.env.__PWA_FALLBACK_DOCUMENT__)
8 | return caches.match(process.env.__PWA_FALLBACK_DOCUMENT__, {
9 | ignoreSearch: true,
10 | });
11 | case "image":
12 | if (process.env.__PWA_FALLBACK_IMAGE__)
13 | return caches.match(process.env.__PWA_FALLBACK_IMAGE__, {
14 | ignoreSearch: true,
15 | });
16 | case "audio":
17 | if (process.env.__PWA_FALLBACK_AUDIO__)
18 | return caches.match(process.env.__PWA_FALLBACK_AUDIO__, {
19 | ignoreSearch: true,
20 | });
21 | case "video":
22 | if (process.env.__PWA_FALLBACK_VIDEO__)
23 | return caches.match(process.env.__PWA_FALLBACK_VIDEO__, {
24 | ignoreSearch: true,
25 | });
26 | case "font":
27 | if (process.env.__PWA_FALLBACK_FONT__)
28 | return caches.match(process.env.__PWA_FALLBACK_FONT__, {
29 | ignoreSearch: true,
30 | });
31 | case "":
32 | if (
33 | process.env.__PWA_FALLBACK_DATA__ &&
34 | request.url.startsWith("/_next/data/") &&
35 | request.url.indexOf(".json") !== -1
36 | )
37 | return caches.match(process.env.__PWA_FALLBACK_DATA__, {
38 | ignoreSearch: true,
39 | });
40 | default:
41 | return Response.error();
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/examples/next-image/images/nextjs-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/custom-ts-worker/worker/index.ts:
--------------------------------------------------------------------------------
1 | import { util } from "./util";
2 |
3 | declare let self: ServiceWorkerGlobalScope;
4 |
5 | // To disable all workbox logging during development, you can set self.__WB_DISABLE_DEV_LOGS to true
6 | // https://developers.google.com/web/tools/workbox/guides/configure-workbox#disable_logging
7 | //
8 | // self.__WB_DISABLE_DEV_LOGS = true
9 |
10 | util();
11 |
12 | // listen to message event from window
13 | self.addEventListener("message", (event) => {
14 | // HOW TO TEST THIS?
15 | // Run this in your browser console:
16 | // window.navigator.serviceWorker.controller.postMessage({command: 'log', message: 'hello world'})
17 | // OR use next-pwa injected workbox object
18 | // window.workbox.messageSW({command: 'log', message: 'hello world'})
19 | console.log(event?.data);
20 | });
21 |
22 | self.addEventListener("push", (event) => {
23 | const data = JSON.parse(event?.data.text() || "{}");
24 | event?.waitUntil(
25 | self.registration.showNotification(data.title, {
26 | body: data.message,
27 | icon: "/icons/android-chrome-192x192.png",
28 | })
29 | );
30 | });
31 |
32 | self.addEventListener("notificationclick", (event) => {
33 | event?.notification.close();
34 | event?.waitUntil(
35 | self.clients
36 | .matchAll({ type: "window", includeUncontrolled: true })
37 | .then(function (clientList) {
38 | if (clientList.length > 0) {
39 | let client = clientList[0];
40 | for (let i = 0; i < clientList.length; i++) {
41 | if (clientList[i].focused) {
42 | client = clientList[i];
43 | }
44 | }
45 | return client.focus();
46 | }
47 | return self.clients.openWindow("/");
48 | })
49 | );
50 | });
51 |
--------------------------------------------------------------------------------
/examples/minimal/index.js:
--------------------------------------------------------------------------------
1 | const { join } = require("path");
2 | const { parse } = require("url");
3 | const fs = require("fs");
4 | const fastify = require("fastify")({
5 | logger: false,
6 | });
7 | const Next = require("next");
8 | const nextConfig = require("./next.config");
9 |
10 | const port = parseInt(process.env.PORT, 10) || 3000;
11 | const dev = process.env.NODE_ENV !== "production";
12 |
13 | const swJs = fs.readFileSync(join(__dirname, ".next", "sw.js"));
14 | let workboxJs;
15 |
16 | fastify.register(require("fastify-compress"));
17 |
18 | fastify.register((fastify, options, next) => {
19 | const app = Next({ dev, conf: nextConfig });
20 | const handle = app.getRequestHandler();
21 | app
22 | .prepare()
23 | .then(() => {
24 | fastify.get("/sw.js", (request, reply) => {
25 | reply.type("application/javascript").send(swJs);
26 | });
27 |
28 | fastify.get("/workbox-*.js", (request, reply) => {
29 | const { pathname } = parse(request.raw.url, true);
30 | if (!workboxJs)
31 | workboxJs = fs.readFileSync(join(__dirname, ".next", pathname));
32 | reply.type("application/javascript").send(workboxJs);
33 | });
34 |
35 | fastify.all("/*", (request, reply) => {
36 | return handle(request.raw, reply.raw).then(() => {
37 | reply.sent = true;
38 | });
39 | });
40 |
41 | fastify.setNotFoundHandler((request, reply) => {
42 | return app.render404(request.raw, reply.raw).then(() => {
43 | reply.sent = true;
44 | });
45 | });
46 |
47 | next();
48 | })
49 | .catch((err) => next(err));
50 | });
51 |
52 | fastify.listen(port, (err) => {
53 | if (err) throw err;
54 | console.log(`> Ready on http://localhost:${port}`);
55 | });
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-pwa-project",
3 | "version": "1.0.0",
4 | "author": "Imamuzzaki Abu Salam ",
5 | "devDependencies": {
6 | "@changesets/changelog-github": "0.4.8",
7 | "@changesets/cli": "2.26.2",
8 | "@commitlint/cli": "17.6.7",
9 | "@commitlint/config-conventional": "17.6.7",
10 | "concurrently": "8.2.0",
11 | "config-prettier": "workspace:*",
12 | "dotenv-cli": "7.2.1",
13 | "eslint": "8.46.0",
14 | "eslint-config-custom": "workspace:*",
15 | "husky": "8.0.3",
16 | "lint-staged": "13.2.3",
17 | "prettier": "2.8.8",
18 | "syncpack": "9.8.6",
19 | "turbo": "1.10.12"
20 | },
21 | "engines": {
22 | "pnpm": "8",
23 | "node": ">=16.0.0",
24 | "npm": "please-use-pnpm",
25 | "yarn": "please-use-pnpm"
26 | },
27 | "license": "MIT",
28 | "packageManager": "pnpm@8.6.12",
29 | "private": true,
30 | "repository": "https://github.com/ImBIOS/next-pwa",
31 | "scripts": {
32 | "prepare": "husky install",
33 | "build": "dotenv -- turbo run build",
34 | "test": "turbo run test",
35 | "clean": "turbo run clean && rm -rf node_modules",
36 | "lint": "turbo run lint -- --fix && pnpm run lint:packages",
37 | "lint:packages": "concurrently \"pnpm:lint:packages:*\"",
38 | "lint:packages:semver": "syncpack lint-semver-ranges",
39 | "lint:packages:mismatches": "syncpack fix-mismatches",
40 | "format": "prettier --write .",
41 | "publish-packages": "turbo run build lint test && changeset version && changeset publish"
42 | },
43 | "workspaces": [
44 | "packages/*"
45 | ],
46 | "lint-staged": {
47 | "*.{js,jsx,ts,tsx}": [
48 | "eslint --ext js,jsx,ts,tsx --quiet --fix --",
49 | "prettier --write"
50 | ],
51 | "*.{md,mdx,mjs,yml,yaml,css}": [
52 | "prettier --write"
53 | ]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/examples/cookie/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/lifecycle/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/minimal/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/next-13/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Head, Html, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/next-image/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/web-push/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/custom-worker/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/offline-fallback-v2/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/offline-fallback/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/cache-on-front-end-nav/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/custom-worker-webpack/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | const APP_NAME = "next-pwa example";
4 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
5 |
6 | class _Document extends Document {
7 | static async getInitialProps(ctx) {
8 | return await Document.getInitialProps(ctx);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
27 | {/* */}
28 |
29 |
34 |
35 |
36 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default _Document;
58 |
--------------------------------------------------------------------------------
/examples/next-i18next/public/app.css:
--------------------------------------------------------------------------------
1 | #__next {
2 | font-family: "Open Sans", sans-serif;
3 | text-align: center;
4 | background-image: linear-gradient(
5 | to left top,
6 | #ffffff,
7 | #f5f5f5,
8 | #eaeaea,
9 | #e0e0e0,
10 | #d6d6d6
11 | );
12 | display: flex;
13 | flex-direction: column;
14 | margin: 0;
15 | min-height: 100vh;
16 | min-width: 100vw;
17 | }
18 |
19 | h1,
20 | h2 {
21 | font-family: "Oswald", sans-serif;
22 | }
23 |
24 | h1 {
25 | font-size: 3rem;
26 | margin: 5rem 0;
27 | }
28 | h2 {
29 | min-width: 18rem;
30 | font-size: 2rem;
31 | opacity: 0.3;
32 | }
33 | h3 {
34 | font-size: 1.5rem;
35 | opacity: 0.5;
36 | }
37 |
38 | p {
39 | line-height: 1.65em;
40 | }
41 | p:nth-child(2) {
42 | font-style: italic;
43 | opacity: 0.65;
44 | margin-top: 1rem;
45 | }
46 |
47 | a.github {
48 | position: fixed;
49 | top: 0.5rem;
50 | right: 0.75rem;
51 | font-size: 4rem;
52 | color: #888;
53 | opacity: 0.8;
54 | }
55 | a.github:hover {
56 | opacity: 1;
57 | }
58 |
59 | button {
60 | display: inline-block;
61 | vertical-align: bottom;
62 | outline: 0;
63 | text-decoration: none;
64 | cursor: pointer;
65 | background-color: rgba(255, 255, 255, 0.5);
66 | box-sizing: border-box;
67 | font-size: 1em;
68 | font-family: inherit;
69 | border-radius: 3px;
70 | transition: box-shadow 0.2s ease;
71 | user-select: none;
72 | line-height: 2.5em;
73 | min-height: 40px;
74 | padding: 0 0.8em;
75 | border: 0;
76 | color: inherit;
77 | position: relative;
78 | transform: translateZ(0);
79 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
80 | margin: 0.8rem;
81 | }
82 |
83 | button:hover,
84 | button:focus {
85 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4);
86 | }
87 |
88 | main {
89 | display: flex;
90 | flex-direction: column;
91 | flex: 1;
92 | justify-content: center;
93 | align-items: center;
94 | }
95 | footer {
96 | background-color: rgba(255, 255, 255, 0.5);
97 | width: 100vw;
98 | padding: 3rem 0;
99 | }
100 |
--------------------------------------------------------------------------------
/examples/custom-ts-worker/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, {
2 | DocumentContext,
3 | Head,
4 | Html,
5 | Main,
6 | NextScript,
7 | } from "next/document";
8 |
9 | const APP_NAME = "next-pwa example";
10 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
11 |
12 | class _Document extends Document {
13 | static async getInitialProps(ctx: DocumentContext) {
14 | return await Document.getInitialProps(ctx);
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
33 | {/* */}
34 |
35 |
40 |
41 |
42 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
63 | export default _Document;
64 |
--------------------------------------------------------------------------------
/docs/pages/tips.mdx:
--------------------------------------------------------------------------------
1 | # Tips
2 |
3 | 1. [Common UX pattern to ask user to reload when new service worker is installed](https://github.com/ImBIOS/next-pwa/blob/master/examples/lifecycle/pages/index.js#L26-L38)
4 | 2. Use a convention like `{command: 'doSomething', message: ''}` object when `postMessage` to service worker. So that on the listener, it could do multiple different tasks using `if...else...`.
5 | 3. When you are debugging service worker, constantly `clean application cache` to reduce some flaky errors.
6 | 4. If you are redirecting the user to another route, please note [workbox by default only cache response with 200 HTTP status](https://developer.chrome.com/docs/workbox/modules/workbox-cacheable-response#what_are_the_defaults), if you really want to cache redirected page for the route, you can specify it in `runtimeCaching` such as `options.cacheableResponse.statuses=[200,302]`.
7 | 5. When debugging issues, you may want to format your generated `sw.js` file to figure out what's really going on.
8 | 6. Force `next-pwa` to generate worker box production build by specify the option `mode: 'production'` in your `pwa` section of `next.config.js`. Though `next-pwa` automatically generate the worker box development build during development (by running `next`) and worker box production build during production (by running `next build` and `next start`). You may still want to force it to production build even during development of your web app for following reason:
9 | 1. Reduce logging noise due to production build doesn't include logging.
10 | 2. Improve performance a bit due to production build is optimized and minified.
11 | 7. If you just want to disable worker box logging while keeping development build during development, [simply put `self.__WB_DISABLE_DEV_LOGS = true` in your `worker/index.js` (create one if you don't have one)](https://github.com/shadowwalker/next-pwa/blob/c48ef110360d0138ad2dacd82ab96964e3da2daf/examples/custom-worker/worker/index.js#L6).
12 | 8. It is common developers have to use `userAgent` string to determine if users are using Safari/iOS/MacOS or some other platform, [ua-parser-js](https://www.npmjs.com/package/ua-parser-js) library is a good friend for that purpose.
13 |
--------------------------------------------------------------------------------
/examples/custom-ts-worker/README.md:
--------------------------------------------------------------------------------
1 | # next-pwa - custom worker example
2 |
3 | [TOC]
4 |
5 | This example demonstrates how to use `next-pwa` plugin to turn a `next.js` based web application into a progressive web application easily. It demonstrates how to add custom worker code to the service worker generated by workbox.
6 |
7 | ## New Method
8 |
9 | Simply create a `worker/index.ts` and start implementing your service worker. `next-pwa` will detect this file automatically, and bundle the file into `dest` as `worker-*.js` using `webpack`. It's also automatically injected into `sw.js` generated.
10 |
11 | In this way, you get benefit of code splitting and size minimization automatically. Yes! `require` modules works! Yes! you can share codes between web app and the service worker!
12 |
13 | > - In dev mode, `worker/index.ts` is not watched, so it will not hot reload.
14 |
15 | ### Custom Worker Directory
16 |
17 | You can customize the directory of your custom worker file by setting the `customWorkerDir` relative to the `basedir` in the `pwa` section of your `next.config.js`:
18 |
19 | ```javascript
20 | const withPWA = require("@imbios/next-pwa")({
21 | customWorkerDir: "serviceworker",
22 | // ...
23 | });
24 |
25 | module.exports = withPWA({
26 | // next.js config
27 | });
28 | ```
29 |
30 | In this example, `next-pwa` would look for `serviceworker/index.ts`.
31 |
32 | ## Old Method (Still Works)
33 |
34 | Basically you need to create a file such as `worker.js` in `public` folder, then add an option `importScripts` to `pwa` object in `next.config.js`:
35 |
36 | ```javascript
37 | const withPWA = require("@imbios/next-pwa")({
38 | dest: "public",
39 | importScripts: ["/worker.js"],
40 | });
41 |
42 | module.exports = withPWA({
43 | // next.js config
44 | });
45 | ```
46 |
47 | Then service worker generated will automatically import your code and run it before other workbox code.
48 |
49 | ## Usage
50 |
51 | [](https://gitpod.io/#https://github.com/shadowwalker/next-pwa/)
52 |
53 | ```bash
54 | cd examples/custom-ts-server
55 | yarn install
56 | yarn build
57 | yarn start
58 | ```
59 |
60 | ## Recommend `.gitignore`
61 |
62 | ```
63 | **/public/workbox-*.js
64 | **/public/sw.js
65 | **/public/worker-*.js
66 | ```
67 |
--------------------------------------------------------------------------------
/examples/custom-worker-webpack/README.md:
--------------------------------------------------------------------------------
1 | # next-pwa - custom worker example
2 |
3 | [TOC]
4 |
5 | This example demonstrates how to use `next-pwa` plugin to turn a `next.js` based web application into a progressive web application easily. It demonstrates how to add custom worker code to the service worker generated by workbox.
6 |
7 | ## New Method
8 |
9 | Simply create a `worker/index.js` and start implementing your service worker. `next-pwa` will detect this file automatically, and bundle the file into `dest` as `worker-*.js` using `webpack`. It's also automatically injected into `sw.js` generated.
10 |
11 | In this way, you get benefit of code splitting and size minimization automatically. Yes! `require` modules works! Yes! you can share codes between web app and the service worker!
12 |
13 | > - Typescript support for `worker/index.ts` current not supported.
14 | >
15 | > - In dev mode, `worker/index.js` is not watch, so it will not hot reload.
16 |
17 | ### Custom Worker Directory
18 |
19 | You can customize the directory of your custom worker file by setting the `customWorkerDir` relative to the `basedir` in the `pwa` section of your `next.config.js`:
20 |
21 | ```javascript
22 | const withPWA = require('next-pwa')({
23 | customWorkerDir: 'serviceworker'
24 | ...
25 | })
26 | module.exports = withPWA({
27 | // next.js config
28 | })
29 | ```
30 |
31 | In this example, `next-pwa` would look for `serviceworker/index.js`.
32 |
33 | ## Old Method (Still Works)
34 |
35 | Basically you need to create a file such as `worker.js` in `public` folder, then add an option `importScripts` to `pwa` object in `next.config.js`:
36 |
37 | ```javascript
38 | const withPWA = require("next-pwa")({
39 | dest: "public",
40 | importScripts: ["/worker.js"],
41 | });
42 | module.exports = withPWA({
43 | // next.js config
44 | });
45 | ```
46 |
47 | Then service worker generated will automatically import your code and run it before other workbox code.
48 |
49 | ## Usage
50 |
51 | [](https://gitpod.io/#https://github.com/ImBIOS/next-pwa/)
52 |
53 | ```bash
54 | cd examples/custom-server
55 | yarn install
56 | yarn build
57 | yarn start
58 | ```
59 |
60 | ## Recommend `.gitignore`
61 |
62 | ```
63 | **/public/workbox-*.js
64 | **/public/sw.js
65 | **/public/worker-*.js
66 | ```
67 |
--------------------------------------------------------------------------------
/examples/custom-worker/README.md:
--------------------------------------------------------------------------------
1 | # next-pwa - custom worker example
2 |
3 | [TOC]
4 |
5 | This example demonstrates how to use `next-pwa` plugin to turn a `next.js` based web application into a progressive web application easily. It demonstrates how to add custom worker code to the service worker generated by workbox.
6 |
7 | ## New Method
8 |
9 | Simply create a `worker/index.js` and start implementing your service worker. `next-pwa` will detect this file automatically, and bundle the file into `dest` as `worker-*.js` using `webpack`. It's also automatically injected into `sw.js` generated.
10 |
11 | In this way, you get benefit of code splitting and size minimization automatically. Yes! `require` modules works! Yes! you can share codes between web app and the service worker!
12 |
13 | > - Typescript support for `worker/index.ts` current not supported.
14 | >
15 | > - In dev mode, `worker/index.js` is not watch, so it will not hot reload.
16 |
17 | ### Custom Worker Directory
18 |
19 | You can customize the directory of your custom worker file by setting the `customWorkerDir` relative to the `basedir` in the `pwa` section of your `next.config.js`:
20 |
21 | ```javascript
22 | const withPWA = require('@imbios/next-pwa')({
23 | customWorkerDir: 'serviceworker'
24 | ...
25 | })
26 |
27 | module.exports = withPWA({
28 | // next.js config
29 | })
30 | ```
31 |
32 | In this example, `next-pwa` would look for `serviceworker/index.js`.
33 |
34 | ## Old Method (Still Works)
35 |
36 | Basically you need to create a file such as `worker.js` in `public` folder, then add an option `importScripts` to `pwa` object in `next.config.js`:
37 |
38 | ```javascript
39 | const withPWA = require("@imbios/next-pwa")({
40 | dest: "public",
41 | importScripts: ["/worker.js"],
42 | });
43 |
44 | module.exports = withPWA({
45 | // next.js config
46 | });
47 | ```
48 |
49 | Then service worker generated will automatically import your code and run it before other workbox code.
50 |
51 | ## Usage
52 |
53 | [](https://gitpod.io/#https://github.com/shadowwalker/next-pwa/)
54 |
55 | ```bash
56 | cd examples/custom-server
57 | yarn install
58 | yarn build
59 | yarn start
60 | ```
61 |
62 | ## Recommend `.gitignore`
63 |
64 | ```
65 | **/public/workbox-*.js
66 | **/public/sw.js
67 | **/public/worker-*.js
68 | ```
69 |
--------------------------------------------------------------------------------
/examples/cache-on-front-end-nav/README.md:
--------------------------------------------------------------------------------
1 | # next-pwa - cache on front end navigation example
2 |
3 | [TOC]
4 |
5 | > **Since `next-pwa@5.2.1`, you can set `cacheOnFrontEndNav: true` in your `pwa` config to achieve the same result in this example, no other code needed.**
6 |
7 | This example demonstrates how to use `next-pwa` plugin to solve the issue when users refresh on a front end navigated route while offline and saw browser's connection lost page. This is an edge case which should not happen very often in normal network connectivity areas, however, this example should help you improve the users experience.
8 |
9 | For context, `next.js` embraces both SSR and front end routing (typical SPA) to deliver smooth users experience. However, when a user navigate on the web app through `next/router` or `next/link` (Link component), the navigation is made through front end routing. Which means there is no HTTP GET request made to the server for that route, it only swap the react component to the new page and change the url showed on the url bar. This "fake" navigation is usually desired because it means users do not have to wait for network delay.
10 |
11 | However the problem appears when it comes to caching, because there is not request that service worker could intercept, nothing is cached. Users may landing on your home page and navigate to different pages without doing any caching. When the network is lost and users reload these pages using refresh button or re-open the browser, a network lost page from the browser is present, bummer! This behavior could leave users very confused as they could navigated the web app without problem in online and offline, when it come back offline, the web app stopped working.
12 |
13 | So we have to enforce a cache for each front-end caching. Yes, it adds additional network traffic which seems conflict of what we are trying to achieve with front end routing. But since the cache happens in service worker, it should have trivial impact on user experience or more specifically, page load.
14 |
15 | I personally feel it's a trade off for you to decide whether you want to improve this user experience.
16 |
17 | ## Related Issue
18 |
19 | [A fix for errored offline refreshes #95](https://github.com/shadowwalker/next-pwa/issues/95) (this example idea is based on what @bahumbert suggested)
20 |
21 | ## Usage
22 |
23 | [](https://gitpod.io/#https://github.com/shadowwalker/next-pwa/)
24 |
25 | ```bash
26 | cd examples/cache-on-front-end-nav
27 | yarn install
28 | yarn build
29 | yarn start
30 | ```
31 |
32 | ## Recommend `.gitignore`
33 |
34 | ```
35 | **/public/workbox-*.js
36 | **/public/sw.js
37 | **/public/worker-*.js
38 | ```
39 |
--------------------------------------------------------------------------------
/examples/next-i18next/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import type { DocumentProps } from "next/document";
2 | import Document, { Head, Html, Main, NextScript } from "next/document";
3 | import i18nextConfig from "../next-i18next.config";
4 |
5 | const APP_NAME = "next-pwa example";
6 | const APP_DESCRIPTION = "This is an example of using next-pwa plugin";
7 |
8 | type Props = DocumentProps & {
9 | // add custom document props
10 | };
11 |
12 | class MyDocument extends Document {
13 | render() {
14 | const currentLocale =
15 | this.props.__NEXT_DATA__.locale ?? i18nextConfig.i18n.defaultLocale;
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 | {/* TIP: set viewport head meta tag in _app.js, otherwise it will show a warning */}
32 | {/* */}
33 |
34 |
39 |
40 |
41 |
45 |
46 |
50 |
54 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 | }
68 |
69 | export default MyDocument;
70 |
--------------------------------------------------------------------------------
/packages/next-pwa/register.js:
--------------------------------------------------------------------------------
1 | import { Workbox } from "workbox-window";
2 |
3 | if (
4 | typeof window !== "undefined" &&
5 | "serviceWorker" in navigator &&
6 | typeof caches !== "undefined"
7 | ) {
8 | if (__PWA_START_URL__) {
9 | caches.has("start-url").then(function (has) {
10 | if (!has) {
11 | caches
12 | .open("start-url")
13 | .then((c) =>
14 | c.put(__PWA_START_URL__, new Response("", { status: 200 }))
15 | );
16 | }
17 | });
18 | }
19 |
20 | window.workbox = new Workbox(window.location.origin + __PWA_SW__, {
21 | scope: __PWA_SCOPE__,
22 | });
23 |
24 | if (__PWA_START_URL__) {
25 | window.workbox.addEventListener("installed", async ({ isUpdate }) => {
26 | if (!isUpdate) {
27 | const cache = await caches.open("start-url");
28 | const response = await fetch(__PWA_START_URL__);
29 | let _response = response;
30 | if (response.redirected) {
31 | _response = new Response(response.body, {
32 | status: 200,
33 | statusText: "OK",
34 | headers: response.headers,
35 | });
36 | }
37 |
38 | await cache.put(__PWA_START_URL__, _response);
39 | }
40 | });
41 | }
42 |
43 | window.workbox.addEventListener("installed", async () => {
44 | const data = window.performance
45 | .getEntriesByType("resource")
46 | .map((e) => e.name)
47 | .filter(
48 | (n) =>
49 | n.startsWith(`${window.location.origin}/_next/data/`) &&
50 | n.endsWith(".json")
51 | );
52 | const cache = await caches.open("next-data");
53 | data.forEach((d) => cache.add(d));
54 | });
55 |
56 | if (__PWA_ENABLE_REGISTER__) {
57 | window.workbox.register();
58 | }
59 |
60 | if (__PWA_CACHE_ON_FRONT_END_NAV__ || __PWA_START_URL__) {
61 | const cacheOnFrontEndNav = function (url) {
62 | if (!window.navigator.onLine) return;
63 | if (__PWA_CACHE_ON_FRONT_END_NAV__ && url !== __PWA_START_URL__) {
64 | return caches.open("others").then((cache) =>
65 | cache.match(url, { ignoreSearch: true }).then((res) => {
66 | if (!res) return cache.add(url);
67 | return Promise.resolve();
68 | })
69 | );
70 | } else if (__PWA_START_URL__ && url === __PWA_START_URL__) {
71 | return fetch(__PWA_START_URL__).then(function (response) {
72 | if (!response.redirected) {
73 | return caches
74 | .open("start-url")
75 | .then((cache) => cache.put(__PWA_START_URL__, response));
76 | }
77 | return Promise.resolve();
78 | });
79 | }
80 | };
81 |
82 | const pushState = history.pushState;
83 | history.pushState = function () {
84 | pushState.apply(history, arguments);
85 | cacheOnFrontEndNav(arguments[2]);
86 | };
87 |
88 | const replaceState = history.replaceState;
89 | history.replaceState = function () {
90 | replaceState.apply(history, arguments);
91 | cacheOnFrontEndNav(arguments[2]);
92 | };
93 |
94 | window.addEventListener("online", () => {
95 | cacheOnFrontEndNav(window.location.pathname);
96 | });
97 | }
98 |
99 | if (__PWA_RELOAD_ON_ONLINE__) {
100 | window.addEventListener("online", () => {
101 | location.reload();
102 | });
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/examples/web-push/pages/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import Head from "next/head";
3 |
4 | const base64ToUint8Array = (base64) => {
5 | const padding = "=".repeat((4 - (base64.length % 4)) % 4);
6 | const b64 = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/");
7 |
8 | const rawData = window.atob(b64);
9 | const outputArray = new Uint8Array(rawData.length);
10 |
11 | for (let i = 0; i < rawData.length; ++i) {
12 | outputArray[i] = rawData.charCodeAt(i);
13 | }
14 | return outputArray;
15 | };
16 |
17 | const Index = () => {
18 | const [isSubscribed, setIsSubscribed] = useState(false);
19 | const [subscription, setSubscription] = useState(null);
20 | const [registration, setRegistration] = useState(null);
21 |
22 | useEffect(() => {
23 | if (
24 | typeof window !== "undefined" &&
25 | "serviceWorker" in navigator &&
26 | window.workbox !== undefined
27 | ) {
28 | // run only in browser
29 | navigator.serviceWorker.ready.then((reg) => {
30 | reg.pushManager.getSubscription().then((sub) => {
31 | if (
32 | sub &&
33 | !(
34 | sub.expirationTime &&
35 | Date.now() > sub.expirationTime - 5 * 60 * 1000
36 | )
37 | ) {
38 | setSubscription(sub);
39 | setIsSubscribed(true);
40 | }
41 | });
42 | setRegistration(reg);
43 | });
44 | }
45 | }, []);
46 |
47 | const subscribeButtonOnClick = async (event) => {
48 | event.preventDefault();
49 | const sub = await registration.pushManager.subscribe({
50 | userVisibleOnly: true,
51 | applicationServerKey: base64ToUint8Array(
52 | process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY
53 | ),
54 | });
55 | // TODO: you should call your API to save subscription data on server in order to send web push notification from server
56 | setSubscription(sub);
57 | setIsSubscribed(true);
58 | console.log("web push subscribed!");
59 | console.log(sub);
60 | };
61 |
62 | const unsubscribeButtonOnClick = async (event) => {
63 | event.preventDefault();
64 | await subscription.unsubscribe();
65 | // TODO: you should call your API to delete or invalidate subscription data on server
66 | setSubscription(null);
67 | setIsSubscribed(false);
68 | console.log("web push unsubscribed!");
69 | };
70 |
71 | const sendNotificationButtonOnClick = async (event) => {
72 | event.preventDefault();
73 | if (subscription == null) {
74 | console.error("web push not subscribed");
75 | return;
76 | }
77 |
78 | await fetch("/api/notification", {
79 | method: "POST",
80 | headers: {
81 | "Content-type": "application/json",
82 | },
83 | body: JSON.stringify({
84 | subscription,
85 | }),
86 | });
87 | };
88 |
89 | return (
90 | <>
91 |
92 | next-pwa example
93 |
94 |
Next.js + PWA = AWESOME!
95 |
98 |
101 |
104 | >
105 | );
106 | };
107 |
108 | export default Index;
109 |
--------------------------------------------------------------------------------
/examples/lifecycle/pages/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import Head from "next/head";
3 |
4 | const Index = () => {
5 | // This hook only run once in browser after the component is rendered for the first time.
6 | // It has same effect as the old componentDidMount lifecycle callback.
7 | useEffect(() => {
8 | if (
9 | typeof window !== "undefined" &&
10 | "serviceWorker" in navigator &&
11 | window.workbox !== undefined
12 | ) {
13 | const wb = window.workbox;
14 | // add event listeners to handle any of PWA lifecycle event
15 | // https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-window.Workbox#events
16 | wb.addEventListener("installed", (event) => {
17 | console.log(`Event ${event.type} is triggered.`);
18 | console.log(event);
19 | });
20 |
21 | wb.addEventListener("controlling", (event) => {
22 | console.log(`Event ${event.type} is triggered.`);
23 | console.log(event);
24 | });
25 |
26 | wb.addEventListener("activated", (event) => {
27 | console.log(`Event ${event.type} is triggered.`);
28 | console.log(event);
29 | });
30 |
31 | // A common UX pattern for progressive web apps is to show a banner when a service worker has updated and waiting to install.
32 | // NOTE: MUST set skipWaiting to false in next.config.js pwa object
33 | // https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users
34 | const promptNewVersionAvailable = (event) => {
35 | // `event.wasWaitingBeforeRegister` will be false if this is the first time the updated service worker is waiting.
36 | // When `event.wasWaitingBeforeRegister` is true, a previously updated service worker is still waiting.
37 | // You may want to customize the UI prompt accordingly.
38 | if (
39 | confirm(
40 | "A newer version of this web app is available, reload to update?"
41 | )
42 | ) {
43 | wb.addEventListener("controlling", (event) => {
44 | window.location.reload();
45 | });
46 |
47 | // Send a message to the waiting service worker, instructing it to activate.
48 | wb.messageSkipWaiting();
49 | } else {
50 | console.log(
51 | "User rejected to reload the web app, keep using old version. New version will be automatically load when user open the app next time."
52 | );
53 | }
54 | };
55 |
56 | wb.addEventListener("waiting", promptNewVersionAvailable);
57 |
58 | // ISSUE - this is not working as expected, why?
59 | // I could only make message event listenser work when I manually add this listenser into sw.js file
60 | wb.addEventListener("message", (event) => {
61 | console.log(`Event ${event.type} is triggered.`);
62 | console.log(event);
63 | });
64 |
65 | /*
66 | wb.addEventListener('redundant', event => {
67 | console.log(`Event ${event.type} is triggered.`)
68 | console.log(event)
69 | })
70 |
71 | wb.addEventListener('externalinstalled', event => {
72 | console.log(`Event ${event.type} is triggered.`)
73 | console.log(event)
74 | })
75 |
76 | wb.addEventListener('externalactivated', event => {
77 | console.log(`Event ${event.type} is triggered.`)
78 | console.log(event)
79 | })
80 | */
81 |
82 | // never forget to call register as auto register is turned off in next.config.js
83 | wb.register();
84 | }
85 | }, []);
86 |
87 | return (
88 | <>
89 |
90 | next-pwa example
91 |
92 |
Next.js + PWA = AWESOME!!
93 | >
94 | );
95 | };
96 |
97 | export default Index;
98 |
--------------------------------------------------------------------------------
/docs/theme.config.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { DocsThemeConfig, useConfig } from "nextra-theme-docs";
3 |
4 | const config: DocsThemeConfig = {
5 | logo: @imbios/next-pwa,
6 | project: {
7 | link: "https://github.com/ImBIOS/next-pwa",
8 | },
9 | chat: {
10 | // Change to Discord if needed
11 | link: "https://github.com/ImBIOS/next-pwa/discussions",
12 | },
13 | docsRepositoryBase: "https://github.com/ImBIOS/next-pwa",
14 | head: () => {
15 | const { asPath, defaultLocale, locale } = useRouter();
16 | const { frontMatter } = useConfig();
17 | const url =
18 | "https://next-pwa.imam.dev" +
19 | (defaultLocale === locale ? asPath : `/${locale}${asPath}`);
20 |
21 | return (
22 | <>
23 |
24 |
28 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
86 |
92 |
98 |
104 |
105 |
106 |
110 |
111 |
116 | >
117 | );
118 | },
119 | footer: {
120 | text: "@imbios/next-pwa Docs",
121 | },
122 | };
123 |
124 | export default config;
125 |
--------------------------------------------------------------------------------
/examples/app-dir/readme.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) template to use when reporting a [bug in the Next.js repository](https://github.com/vercel/next.js/issues) with the `app/` directory.
2 |
3 | ## Getting Started
4 |
5 | These are the steps you should follow when creating a bug report:
6 |
7 | - Bug reports must be verified against the `next@canary` release. The canary version of Next.js ships daily and includes all features and fixes that have not been released to the stable version yet. Think of canary as a public beta. Some issues may already be fixed in the canary version, so please verify that your issue reproduces before opening a new issue. Issues not verified against `next@canary` will be closed after 30 days.
8 | - Make sure your issue is not a duplicate. Use the [GitHub issue search](https://github.com/vercel/next.js/issues) to see if there is already an open issue that matches yours. If that is the case, upvoting the other issue's first comment is desireable as we often prioritize issues based on the number of votes they receive. Note: Adding a "+1" or "same issue" comment without adding more context about the issue should be avoided. If you only find closed related issues, you can link to them using the issue number and `#`, eg.: `I found this related issue: #3000`.
9 | - If you think the issue is not in Next.js, the best place to ask for help is our [Discord community](https://nextjs.org/discord) or [GitHub discussions](https://github.com/vercel/next.js/discussions). Our community is welcoming and can often answer a project-related question faster than the Next.js core team.
10 | - Make the reproduction as minimal as possible. Try to exclude any code that does not help reproducing the issue. E.g. if you experience problems with Routing, including ESLint configurations or API routes aren't necessary. The less lines of code is to read through, the easier it is for the Next.js team to investigate. It may also help catching bugs in your codebase before publishing an issue.
11 | - Don't forget to create a new repository on GitHub and make it public so that anyone can view it and reproduce it.
12 |
13 | ## How to use this template
14 |
15 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
16 |
17 | ```bash
18 | npx create-next-app --example reproduction-template-app-dir reproduction-app
19 | ```
20 |
21 | ```bash
22 | yarn create next-app --example reproduction-template-app-dir reproduction-app
23 | ```
24 |
25 | ```bash
26 | pnpm create next-app --example reproduction-template-app-dir reproduction-app
27 | ```
28 |
29 | ## Learn More
30 |
31 | To learn more about Next.js, take a look at the following resources:
32 |
33 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
34 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
35 | - [How to Contribute to Open Source (Next.js)](https://www.youtube.com/watch?v=cuoNzXFLitc) - a video tutorial by Lee Robinson
36 | - [Triaging in the Next.js repository](https://github.com/vercel/next.js/blob/canary/contributing.md#triaging) - how we work on issues
37 | - [StackBlitz](https://stackblitz.com/fork/github/vercel/next.js/tree/canary/examples/reproduction-template) - Edit this repository on StackBlitz
38 | - [CodeSandbox](https://codesandbox.io/s/github/vercel/next.js/tree/canary/examples/reproduction-template) - Edit this repository on CodeSandbox
39 |
40 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
41 |
42 | ## Deployment
43 |
44 | If your reproduction needs to be deployed, the easiest way is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
45 |
46 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
47 |
--------------------------------------------------------------------------------
/packages/next-pwa/build-custom-worker.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const path = require("path");
4 | const fs = require("fs");
5 | const webpack = require("webpack");
6 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
7 | const TerserPlugin = require("terser-webpack-plugin");
8 |
9 | const buildCustomWorker = ({
10 | id,
11 | basedir,
12 | customWorkerDir,
13 | destdir,
14 | plugins,
15 | minify,
16 | webpack: customWebpack,
17 | }) => {
18 | let workerDir = undefined;
19 |
20 | if (fs.existsSync(path.join(basedir, customWorkerDir))) {
21 | workerDir = path.join(basedir, customWorkerDir);
22 | } else if (fs.existsSync(path.join(basedir, "src", customWorkerDir))) {
23 | workerDir = path.join(basedir, "src", customWorkerDir);
24 | }
25 |
26 | if (!workerDir) return;
27 |
28 | const name = `worker-${id}.js`;
29 | const customWorkerEntries = ["ts", "js"]
30 | .map((ext) => path.join(workerDir, `index.${ext}`))
31 | .filter((entry) => fs.existsSync(entry));
32 |
33 | if (customWorkerEntries.length === 0) return;
34 |
35 | if (customWorkerEntries.length > 1) {
36 | console.warn(
37 | `> [PWA] WARNING: More than one custom worker found (${customWorkerEntries.join(
38 | ","
39 | )}), not building a custom worker`
40 | );
41 | return;
42 | }
43 |
44 | const customWorkerEntry = customWorkerEntries[0];
45 | console.log(`> [PWA] Custom worker found: ${customWorkerEntry}`);
46 | console.log(`> [PWA] Build custom worker: ${path.join(destdir, name)}`);
47 | const baseConfig = {
48 | mode: "none",
49 | target: "webworker",
50 | entry: {
51 | main: customWorkerEntry,
52 | },
53 | resolve: {
54 | extensions: [".ts", ".js"],
55 | fallback: {
56 | module: false,
57 | dgram: false,
58 | dns: false,
59 | path: false,
60 | fs: false,
61 | os: false,
62 | crypto: false,
63 | stream: false,
64 | http2: false,
65 | net: false,
66 | tls: false,
67 | zlib: false,
68 | child_process: false,
69 | },
70 | },
71 | module: {
72 | rules: [
73 | {
74 | test: /\.(t|j)s$/i,
75 | use: [
76 | {
77 | loader: "babel-loader",
78 | options: {
79 | presets: [
80 | [
81 | "next/babel",
82 | {
83 | "transform-runtime": {
84 | corejs: false,
85 | helpers: true,
86 | regenerator: false,
87 | useESModules: true,
88 | },
89 | "preset-env": {
90 | modules: false,
91 | targets: "chrome >= 56",
92 | },
93 | },
94 | ],
95 | ],
96 | },
97 | },
98 | ],
99 | },
100 | ],
101 | },
102 | output: {
103 | path: destdir,
104 | filename: name,
105 | },
106 | plugins: [
107 | new CleanWebpackPlugin({
108 | cleanOnceBeforeBuildPatterns: [
109 | path.join(destdir, "worker-*.js"),
110 | path.join(destdir, "worker-*.js.map"),
111 | ],
112 | }),
113 | ].concat(plugins),
114 | optimization: minify
115 | ? {
116 | minimize: true,
117 | minimizer: [new TerserPlugin()],
118 | }
119 | : undefined,
120 | };
121 |
122 | let config = baseConfig;
123 | if (typeof customWebpack === "function") {
124 | console.log("> [PWA] Using provided webpack config to build custom worker");
125 | config = customWebpack(baseConfig);
126 | }
127 |
128 | webpack(config).run((error, status) => {
129 | if (error || status.hasErrors()) {
130 | console.error(`> [PWA] Failed to build custom worker`);
131 | console.error(status.toString({ colors: true }));
132 | process.exit(-1);
133 | }
134 | });
135 |
136 | return name;
137 | };
138 |
139 | module.exports = buildCustomWorker;
140 |
--------------------------------------------------------------------------------
/examples/custom-ts-worker/types/service-worker.d.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:file-header
2 | /**
3 | * Copyright (c) 2016, Tiernan Cridland
4 | *
5 | * Permission to use, copy, modify, and/or distribute this software for any purpose with or without
6 | * fee is hereby
7 | * granted, provided that the above copyright notice and this permission notice appear in all
8 | * copies.
9 | *
10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
11 | * SOFTWARE INCLUDING ALL
12 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
13 | * SPECIAL, DIRECT,
14 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
15 | * PROFITS, WHETHER
16 | * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
17 | * WITH THE USE OR
18 | * PERFORMANCE OF THIS SOFTWARE.
19 | *
20 | * Typings for Service Worker
21 | * @author Tiernan Cridland
22 | * @email tiernanc@gmail.com
23 | * @license: ISC
24 | */
25 | interface ExtendableEvent extends Event {
26 | waitUntil(fn: Promise): void;
27 | }
28 |
29 | interface PushSubscriptionChangeEvent extends ExtendableEvent {
30 | readonly newSubscription?: PushSubscription;
31 | readonly oldSubscription?: PushSubscription;
32 | }
33 |
34 | // Client API
35 |
36 | declare class Client {
37 | frameType: ClientFrameType;
38 | id: string;
39 | url: string;
40 | focused: boolean;
41 | focus(): void;
42 | postMessage(message: any): void;
43 | }
44 |
45 | interface Clients {
46 | claim(): Promise;
47 | get(id: string): Promise;
48 | matchAll(options?: ClientMatchOptions): Promise>;
49 | openWindow(url: string): Promise;
50 | }
51 |
52 | interface ClientMatchOptions {
53 | includeUncontrolled?: boolean;
54 | type?: ClientMatchTypes;
55 | }
56 |
57 | interface WindowClient {
58 | focused: boolean;
59 | visibilityState: WindowClientState;
60 | focus(): Promise;
61 | navigate(url: string): Promise;
62 | }
63 |
64 | type ClientFrameType = "auxiliary" | "top-level" | "nested" | "none";
65 | type ClientMatchTypes = "window" | "worker" | "sharedworker" | "all";
66 | type WindowClientState = "hidden" | "visible" | "prerender" | "unloaded";
67 |
68 | // Fetch API
69 |
70 | interface FetchEvent extends ExtendableEvent {
71 | clientId: string | null;
72 | request: Request;
73 | respondWith(response: Promise | Response): Promise;
74 | }
75 |
76 | interface InstallEvent extends ExtendableEvent {
77 | activeWorker: ServiceWorker;
78 | }
79 |
80 | interface ActivateEvent extends ExtendableEvent {}
81 |
82 | // Notification API
83 |
84 | interface NotificationEvent extends ExtendableEvent {
85 | action: string;
86 | notification: Notification;
87 | }
88 |
89 | // Push API
90 |
91 | interface PushEvent extends ExtendableEvent {
92 | data: PushMessageData;
93 | }
94 |
95 | interface PushMessageData {
96 | arrayBuffer(): ArrayBuffer;
97 | blob(): Blob;
98 | json(): any;
99 | text(): string;
100 | }
101 |
102 | // Sync API
103 |
104 | interface SyncEvent extends ExtendableEvent {
105 | lastChance: boolean;
106 | tag: string;
107 | }
108 |
109 | interface ExtendableMessageEvent extends ExtendableEvent {
110 | data: any;
111 | source: Client | Object;
112 | }
113 |
114 | // ServiceWorkerGlobalScope
115 |
116 | interface ServiceWorkerGlobalScope {
117 | caches: CacheStorage;
118 | clients: Clients;
119 | registration: ServiceWorkerRegistration;
120 |
121 | addEventListener(
122 | event: "activate",
123 | fn: (event?: ExtendableEvent) => any
124 | ): void;
125 | addEventListener(
126 | event: "message",
127 | fn: (event?: ExtendableMessageEvent) => any
128 | ): void;
129 | addEventListener(event: "fetch", fn: (event?: FetchEvent) => any): void;
130 | addEventListener(
131 | event: "install",
132 | fn: (event?: ExtendableEvent) => any
133 | ): void;
134 | addEventListener(event: "push", fn: (event?: PushEvent) => any): void;
135 | addEventListener(
136 | event: "notificationclick",
137 | fn: (event?: NotificationEvent) => any
138 | ): void;
139 | addEventListener(event: "sync", fn: (event?: SyncEvent) => any): void;
140 |
141 | fetch(request: Request | string): Promise;
142 | skipWaiting(): Promise;
143 | }
144 |
--------------------------------------------------------------------------------
/examples/next-i18next/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { useRouter } from "next/router";
3 | import type { GetStaticProps, InferGetStaticPropsType } from "next";
4 |
5 | import { useTranslation, Trans } from "next-i18next";
6 | import { serverSideTranslations } from "next-i18next/serverSideTranslations";
7 |
8 | import { Header } from "../components/Header";
9 | import { Footer } from "../components/Footer";
10 |
11 | type Props = {
12 | // Add custom props here
13 | };
14 |
15 | const Homepage = (_props: InferGetStaticPropsType) => {
16 | const router = useRouter();
17 | const { t, i18n } = useTranslation("common");
18 |
19 | const onToggleLanguageClick = (newLocale: string) => {
20 | const { pathname, asPath, query } = router;
21 | router.push({ pathname, query }, asPath, { locale: newLocale });
22 | };
23 |
24 | const clientSideLanguageChange = (newLocale: string) => {
25 | i18n.changeLanguage(newLocale);
26 | };
27 |
28 | const changeTo = router.locale === "en" ? "de" : "en";
29 | // const changeTo = i18n.resolvedLanguage === 'en' ? 'de' : 'en'
30 |
31 | return (
32 | <>
33 |
34 |
35 |
84 |
85 |
86 |
87 | {/* alternative language change without using Link component
88 |
91 | */}
92 | {/* alternative language change without using Link component, but this will change language only on client side
93 | */}
96 |
97 |
98 |
99 |
100 |
101 |
102 | >
103 | );
104 | };
105 |
106 | // or getServerSideProps: GetServerSideProps = async ({ locale })
107 | export const getStaticProps: GetStaticProps = async ({ locale }) => ({
108 | props: {
109 | ...(await serverSideTranslations(locale ?? "en", ["common", "footer"])),
110 | },
111 | });
112 |
113 | export default Homepage;
114 |
--------------------------------------------------------------------------------
/docs/pages/index.mdx:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Welcome to the Next.js PWA module, a customizable and easy-to-use solution for creating Progressive Web Apps (PWA) with Next.js. This module is designed to help developers convert their Next.js applications into fully functional PWAs with minimal effort. This plugin is powered by [workbox](https://developer.chrome.com/docs/workbox/) and other good stuff. With the Next.js PWA module, you can enjoy the benefits of a PWA such as offline access, push notifications, and home screen installation.
4 |
5 | This module is built on top of the service worker API and provides a range of features to optimize your application's performance, improve user engagement, and enhance the user experience. The module is designed to work seamlessly with Next.js, which means you can easily integrate it into your existing Next.js projects.
6 |
7 | Getting started with the Next.js PWA module is straightforward, and this documentation will guide you through the process of setting up and configuring your PWA. You will learn how to install the module, configure your service worker, and customize your PWA's features to suit your application's needs.
8 |
9 | Whether you are new to PWAs or an experienced developer, the Next.js PWA module is a powerful tool that can help you take your application to the next level. Let's get started!
10 |
11 | ## What is `@imbios/next-pwa`?
12 |
13 | @imbios/next-pwa is a Next.js module that enables you to convert your Next.js application into a Progressive Web App (PWA) with ease. This module is built on top of the service worker API and provides a range of features to optimize your application's performance, improve user engagement, and enhance the user experience.
14 |
15 | With @imbios/next-pwa, you can enjoy all the benefits of a PWA, including offline access, push notifications, and home screen installation. This module is designed to work seamlessly with Next.js, which means you can easily integrate it into your existing Next.js projects.
16 |
17 | The @imbios/next-pwa module provides a simple and flexible API that allows you to customize your PWA's features to suit your application's needs. You can configure your service worker, add support for offline caching, customize your manifest, and much more.
18 |
19 | Whether you are building a new application or want to convert an existing Next.js application into a PWA, @imbios/next-pwa is a powerful tool that can help you achieve your goals with minimal effort.
20 |
21 | ## Features
22 |
23 | - 🚀 Next.js appDir support
24 | - 0️⃣ Zero config for registering and generating service worker
25 | - ✨ Optimized precache and runtime cache
26 | - 💯 Maximize lighthouse score
27 | - 🎈 Easy to understand examples
28 | - 📴 Completely offline support with fallbacks [example](https://github.com/ImBIOS/next-pwa/tree/master/examples/offline-fallback-v2) 🆕
29 | - 📦 Use [workbox](https://developer.chrome.com/docs/workbox/) and [workbox-window](https://developer.chrome.com/docs/workbox/modules/workbox-window) v6
30 | - 🍪 Work with cookies out of the box
31 | - 🔉 Default range requests for audios and videos
32 | - ☕ No custom server needed for Next.js 9+ [example](https://github.com/ImBIOS/next-pwa/tree/master/examples/next-13)
33 | - 🔧 Handle PWA lifecycle events opt-in [example](https://github.com/ImBIOS/next-pwa/tree/master/examples/lifecycle)
34 | - 📐 Custom worker to run extra code with code splitting and **typescript** support [example](https://github.com/ImBIOS/next-pwa/tree/master/examples/custom-ts-worker)
35 | - 📜 [Public environment variables](https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser) available in custom worker as usual
36 | - 🐞 Debug service worker with confidence in development mode without caching
37 | - 🌏 Internationalization (a.k.a I18N) with `next-i18next` [example](https://github.com/ImBIOS/next-pwa/tree/master/examples/next-i18next)
38 | - 🛠 Configurable by the same [workbox configuration options](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin) for [GenerateSW](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin/#generatesw-plugin) and [InjectManifest](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin/#injectmanifest-plugin)
39 | - 🚀 Spin up a [GitPod](https://gitpod.io/#https://github.com/ImBIOS/next-pwa/) and try out examples in rocket speed
40 | - ⚡ Support [blitz.js](https://blitzjs.com/) (simply add to `blitz.config.js`)
41 | - 🔩 (Experimental) precaching `.module.js` when `next.config.js` has `experimental.modern` set to `true`
42 |
43 | > **NOTE 1** - `@imbios/next-pwa` should only work with `next.js` 9.1+, and static files should only be served through `public` directory. This will make things simpler.
44 | >
45 | > **NOTE 2** - If you encounter error `TypeError: Cannot read property **'javascript' of undefined**` during build, [please consider upgrade to webpack5 in `next.config.js`](https://github.com/shadowwalker/next-pwa/issues/198#issuecomment-817205700).
46 |
--------------------------------------------------------------------------------
/packages/next-pwa/build-fallback-worker.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const path = require("path");
4 | const fs = require("fs");
5 | const webpack = require("webpack");
6 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
7 | const TerserPlugin = require("terser-webpack-plugin");
8 |
9 | const getFallbackEnvs = ({ fallbacks, basedir, id, pageExtensions }) => {
10 | let { document, data } = fallbacks;
11 |
12 | if (!document) {
13 | let pagesDir = undefined;
14 |
15 | if (fs.existsSync(path.join(basedir, "pages"))) {
16 | pagesDir = path.join(basedir, "pages");
17 | } else if (fs.existsSync(path.join(basedir, "src", "pages"))) {
18 | pagesDir = path.join(basedir, "src", "pages");
19 | }
20 |
21 | if (!pagesDir) return;
22 |
23 | const offlines = pageExtensions
24 | .map((ext) => path.join(pagesDir, `_offline.${ext}`))
25 | .filter((entry) => fs.existsSync(entry));
26 | if (offlines.length === 1) {
27 | document = "/_offline";
28 | }
29 | }
30 |
31 | if (data && data.endsWith(".json")) {
32 | data = path.posix.join("/_next/data", id, data);
33 | }
34 |
35 | const envs = {
36 | __PWA_FALLBACK_DOCUMENT__: document || false,
37 | __PWA_FALLBACK_IMAGE__: fallbacks.image || false,
38 | __PWA_FALLBACK_AUDIO__: fallbacks.audio || false,
39 | __PWA_FALLBACK_VIDEO__: fallbacks.video || false,
40 | __PWA_FALLBACK_FONT__: fallbacks.font || false,
41 | __PWA_FALLBACK_DATA__: data || false,
42 | };
43 |
44 | if (Object.values(envs).filter((v) => !!v).length === 0) return;
45 |
46 | console.log(
47 | "> [PWA] Fallback to precache routes when fetch failed from cache or network:"
48 | );
49 | if (envs.__PWA_FALLBACK_DOCUMENT__)
50 | console.log(`> [PWA] document (page): ${envs.__PWA_FALLBACK_DOCUMENT__}`);
51 | if (envs.__PWA_FALLBACK_IMAGE__)
52 | console.log(`> [PWA] image: ${envs.__PWA_FALLBACK_IMAGE__}`);
53 | if (envs.__PWA_FALLBACK_AUDIO__)
54 | console.log(`> [PWA] audio: ${envs.__PWA_FALLBACK_AUDIO__}`);
55 | if (envs.__PWA_FALLBACK_VIDEO__)
56 | console.log(`> [PWA] video: ${envs.__PWA_FALLBACK_VIDEO__}`);
57 | if (envs.__PWA_FALLBACK_FONT__)
58 | console.log(`> [PWA] font: ${envs.__PWA_FALLBACK_FONT__}`);
59 | if (envs.__PWA_FALLBACK_DATA__)
60 | console.log(
61 | `> [PWA] data (/_next/data/**/*.json): ${envs.__PWA_FALLBACK_DATA__}`
62 | );
63 |
64 | return envs;
65 | };
66 |
67 | const buildFallbackWorker = ({
68 | id,
69 | fallbacks,
70 | basedir,
71 | destdir,
72 | minify,
73 | pageExtensions,
74 | }) => {
75 | const envs = getFallbackEnvs({ fallbacks, basedir, id, pageExtensions });
76 | if (!envs) return;
77 |
78 | const name = `fallback-${id}.js`;
79 | const fallbackJs = path.join(__dirname, `fallback.js`);
80 |
81 | webpack({
82 | mode: "none",
83 | target: "webworker",
84 | entry: {
85 | main: fallbackJs,
86 | },
87 | resolve: {
88 | extensions: [".js"],
89 | fallback: {
90 | module: false,
91 | dgram: false,
92 | dns: false,
93 | path: false,
94 | fs: false,
95 | os: false,
96 | crypto: false,
97 | stream: false,
98 | http2: false,
99 | net: false,
100 | tls: false,
101 | zlib: false,
102 | child_process: false,
103 | },
104 | },
105 | module: {
106 | rules: [
107 | {
108 | test: /\.js$/i,
109 | use: [
110 | {
111 | loader: "babel-loader",
112 | options: {
113 | presets: [
114 | [
115 | "next/babel",
116 | {
117 | "transform-runtime": {
118 | corejs: false,
119 | helpers: true,
120 | regenerator: false,
121 | useESModules: true,
122 | },
123 | "preset-env": {
124 | modules: false,
125 | targets: "chrome >= 56",
126 | },
127 | },
128 | ],
129 | ],
130 | },
131 | },
132 | ],
133 | },
134 | ],
135 | },
136 | output: {
137 | path: destdir,
138 | filename: name,
139 | },
140 | plugins: [
141 | new CleanWebpackPlugin({
142 | cleanOnceBeforeBuildPatterns: [
143 | path.join(destdir, "fallback-*.js"),
144 | path.join(destdir, "fallback-*.js.map"),
145 | ],
146 | }),
147 | new webpack.EnvironmentPlugin(envs),
148 | ],
149 | optimization: minify
150 | ? {
151 | minimize: true,
152 | minimizer: [new TerserPlugin()],
153 | }
154 | : undefined,
155 | }).run((error, status) => {
156 | if (error || status.hasErrors()) {
157 | console.error(`> [PWA] Failed to build fallback worker`);
158 | console.error(status.toString({ colors: true }));
159 | process.exit(-1);
160 | }
161 | });
162 |
163 | return { fallbacks, name, precaches: Object.values(envs).filter((v) => !!v) };
164 | };
165 |
166 | module.exports = buildFallbackWorker;
167 |
--------------------------------------------------------------------------------
/examples/offline-fallback/service-worker.js:
--------------------------------------------------------------------------------
1 | import { skipWaiting, clientsClaim } from "workbox-core";
2 | import { ExpirationPlugin } from "workbox-expiration";
3 | import {
4 | NetworkOnly,
5 | NetworkFirst,
6 | CacheFirst,
7 | StaleWhileRevalidate,
8 | } from "workbox-strategies";
9 | import {
10 | registerRoute,
11 | setDefaultHandler,
12 | setCatchHandler,
13 | } from "workbox-routing";
14 | import {
15 | matchPrecache,
16 | precacheAndRoute,
17 | cleanupOutdatedCaches,
18 | } from "workbox-precaching";
19 |
20 | skipWaiting();
21 | clientsClaim();
22 |
23 | // must include following lines when using inject manifest module from workbox
24 | // https://developers.google.com/web/tools/workbox/guides/precache-files/workbox-build#add_an_injection_point
25 | const WB_MANIFEST = self.__WB_MANIFEST;
26 | // Precache fallback route and image
27 | WB_MANIFEST.push({
28 | url: "/fallback",
29 | revision: "1234567890",
30 | });
31 | precacheAndRoute(WB_MANIFEST);
32 |
33 | cleanupOutdatedCaches();
34 | registerRoute(
35 | "/",
36 | new NetworkFirst({
37 | cacheName: "start-url",
38 | plugins: [
39 | new ExpirationPlugin({
40 | maxEntries: 1,
41 | maxAgeSeconds: 86400,
42 | purgeOnQuotaError: !0,
43 | }),
44 | ],
45 | }),
46 | "GET"
47 | );
48 | registerRoute(
49 | /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
50 | new CacheFirst({
51 | cacheName: "google-fonts",
52 | plugins: [
53 | new ExpirationPlugin({
54 | maxEntries: 4,
55 | maxAgeSeconds: 31536e3,
56 | purgeOnQuotaError: !0,
57 | }),
58 | ],
59 | }),
60 | "GET"
61 | );
62 | registerRoute(
63 | /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,
64 | new StaleWhileRevalidate({
65 | cacheName: "static-font-assets",
66 | plugins: [
67 | new ExpirationPlugin({
68 | maxEntries: 4,
69 | maxAgeSeconds: 604800,
70 | purgeOnQuotaError: !0,
71 | }),
72 | ],
73 | }),
74 | "GET"
75 | );
76 | // disable image cache, so we could observe the placeholder image when offline
77 | registerRoute(
78 | /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
79 | new NetworkOnly({
80 | cacheName: "static-image-assets",
81 | plugins: [
82 | new ExpirationPlugin({
83 | maxEntries: 64,
84 | maxAgeSeconds: 86400,
85 | purgeOnQuotaError: !0,
86 | }),
87 | ],
88 | }),
89 | "GET"
90 | );
91 | registerRoute(
92 | /\.(?:js)$/i,
93 | new StaleWhileRevalidate({
94 | cacheName: "static-js-assets",
95 | plugins: [
96 | new ExpirationPlugin({
97 | maxEntries: 32,
98 | maxAgeSeconds: 86400,
99 | purgeOnQuotaError: !0,
100 | }),
101 | ],
102 | }),
103 | "GET"
104 | );
105 | registerRoute(
106 | /\.(?:css|less)$/i,
107 | new StaleWhileRevalidate({
108 | cacheName: "static-style-assets",
109 | plugins: [
110 | new ExpirationPlugin({
111 | maxEntries: 32,
112 | maxAgeSeconds: 86400,
113 | purgeOnQuotaError: !0,
114 | }),
115 | ],
116 | }),
117 | "GET"
118 | );
119 | registerRoute(
120 | /\.(?:json|xml|csv)$/i,
121 | new NetworkFirst({
122 | cacheName: "static-data-assets",
123 | plugins: [
124 | new ExpirationPlugin({
125 | maxEntries: 32,
126 | maxAgeSeconds: 86400,
127 | purgeOnQuotaError: !0,
128 | }),
129 | ],
130 | }),
131 | "GET"
132 | );
133 | registerRoute(
134 | /\/api\/.*$/i,
135 | new NetworkFirst({
136 | cacheName: "apis",
137 | networkTimeoutSeconds: 10,
138 | plugins: [
139 | new ExpirationPlugin({
140 | maxEntries: 16,
141 | maxAgeSeconds: 86400,
142 | purgeOnQuotaError: !0,
143 | }),
144 | ],
145 | }),
146 | "GET"
147 | );
148 | registerRoute(
149 | /.*/i,
150 | new NetworkFirst({
151 | cacheName: "others",
152 | networkTimeoutSeconds: 10,
153 | plugins: [
154 | new ExpirationPlugin({
155 | maxEntries: 32,
156 | maxAgeSeconds: 86400,
157 | purgeOnQuotaError: !0,
158 | }),
159 | ],
160 | }),
161 | "GET"
162 | );
163 |
164 | // following lines gives you control of the offline fallback strategies
165 | // https://developers.google.com/web/tools/workbox/guides/advanced-recipes#comprehensive_fallbacks
166 |
167 | // Use a stale-while-revalidate strategy for all other requests.
168 | setDefaultHandler(new StaleWhileRevalidate());
169 |
170 | // This "catch" handler is triggered when any of the other routes fail to
171 | // generate a response.
172 | setCatchHandler(({ event }) => {
173 | // The FALLBACK_URL entries must be added to the cache ahead of time, either
174 | // via runtime or precaching. If they are precached, then call
175 | // `matchPrecache(FALLBACK_URL)` (from the `workbox-precaching` package)
176 | // to get the response from the correct cache.
177 | //
178 | // Use event, request, and url to figure out how to respond.
179 | // One approach would be to use request.destination, see
180 | // https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c
181 | switch (event.request.destination) {
182 | case "document":
183 | // If using precached URLs:
184 | return matchPrecache("/fallback");
185 | // return caches.match('/fallback')
186 | break;
187 | case "image":
188 | // If using precached URLs:
189 | return matchPrecache("/static/images/fallback.png");
190 | // return caches.match('/static/images/fallback.png')
191 | break;
192 | case "font":
193 | // If using precached URLs:
194 | // return matchPrecache(FALLBACK_FONT_URL);
195 | //return caches.match('/static/fonts/fallback.otf')
196 | //break
197 | default:
198 | // If we don't have a fallback, just return an error response.
199 | return Response.error();
200 | }
201 | });
202 |
--------------------------------------------------------------------------------