Next.js で PWA を実装する方法【Vercel × Serwist】

この記事ではNext.js(App Router)で PWA を実装する方法のメモを残しておきます。

Meditationアプリに実装してみた知見です。

https://just-meditation.com


Serwist を使う理由(Workbox の後継)

Serwist は Workbox の設計をベースにした最新 PWA ライブラリで、

  • Next.js App Router 対応
  • Precache の自動注入
  • ランタイムキャッシュの柔軟な設定
  • Workbox より軽量

という特徴があります。


導入手順

個人的にpnpm にはまっているので、pnpm を使っていますが、なんでもいいです。

Serwist のインストール

pnpm add @serwist/next serwist

next.config.ts

next.config.ts が以下のようになっているか確認してください。

// next.config.ts
import type { NextConfig } from "next";
import withSerwistInit from "@serwist/next";

const withSerwist = withSerwistInit({
  swSrc: "app/sw.ts",
  swDest: "public/sw.js",
  disable: process.env.NODE_ENV !== "production",
});

const nextConfig: NextConfig = {
  reactStrictMode: true,
  turbopack: {}, // dev は Turbopack
};

export default withSerwist(nextConfig);

app/manifest.json

{
  "name": "Just Meditation",
  "short_name": "Meditation",
  "start_url": "https://hogehoge.com",
  "display": "standalone",
  "background_color": "#000000",
  "theme_color": "#000000",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

app/sw.ts

/// <reference lib="webworker" />

import { defaultCache } from "@serwist/next/worker";
import {
  Serwist,
  CacheFirst,
  type PrecacheEntry,
  type SerwistGlobalConfig,
  type RuntimeCaching,
} from "serwist";

declare global {
  interface WorkerGlobalScope extends SerwistGlobalConfig {
    __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
  }
}
declare const self: ServiceWorkerGlobalScope;

const extraRuntimeCaching: RuntimeCaching[] = [
  {
    matcher: ({ request }) => request.destination === "audio",
    handler: new CacheFirst({ cacheName: "audio-cache" }),
  },
];

const serwist = new Serwist({
  precacheEntries: self.__SW_MANIFEST,
  skipWaiting: true,
  clientsClaim: true,
  navigationPreload: true,
  runtimeCaching: [...defaultCache, ...extraRuntimeCaching],
});

serwist.addEventListeners();

app/layout.tsx

メタデータに記述が必要です

// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Just Meditation",
  manifest: "/manifest.json",
  icons: [
    { rel: "icon", url: "/icon-192.png" },
    { rel: "apple-touch-icon", url: "/icon-192.png" }
  ],
};

おまけ インストールボタンの実装

インストールボタンのコンポーネントを作成して、Headerに置きました。

// components/InstallPWAButton.tsx
"use client";
import { useEffect, useState } from "react";
import { Button } from "@mui/material";

export default function InstallPWAButton() {
  const [promptEvent, setPromptEvent] = useState<any>(null);

  useEffect(() => {
    window.addEventListener("beforeinstallprompt", (e) => {
      e.preventDefault();
      setPromptEvent(e);
    });
  }, []);

  if (!promptEvent) return null;

  return (
    <Button
      variant="outlined"
      onClick={() => {
        promptEvent.prompt();
      }}
    >
      Install App
    </Button>
  );
}

インストール可能なブラウザでのみ表示されます。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA