【Next.js】output: “export”のSSGアプリをPWA化する方法|どれを使うべき?@serwist/next、@ducanh2912/next-pwa、@next-pwa

firebase-prograshi(プロぐらし)-kv Firebase
記事内に広告が含まれていることがあります。

Next.jsアプリをPWA(Progressive Web App)化するには、主に「Web App Manifestの作成」と「Service Workerの設定」の2段階が必要です。

現在は、Next.jsの標準機能が充実しているため、シンプルなPWAであれば外部ライブラリなしでも構築可能ですが、オフラインキャッシュなどを高度に制御したい場合は Serwist(next-pwa の後継)などのライブラリを使うのが一般的です。

ここでは、最も推奨されるライブラリを使用した方法と、標準機能での実装方法を解説します。


Web App Manifestの作成(必須)

PWAとして認識させるための設定ファイルとして必須になるのが、Web App Manifestの作成です

Next.js (App Router) では、app ディレクトリ直下に manifest.ts (または manifest.json) を作成します

// app/manifest.ts
// Next.js の Metadata API を使った PWA マニフェスト生成ファイル。
// /manifest.webmanifest として自動配信される(output: "export" 時は静的ファイルとして生成)。
// アイコンは public/ 直下に配置し、sizes・purpose を正確に指定すること。
// output: "export" を使う場合、このファイルに dynamic = "force-static" の指定が必須。

import { MetadataRoute } from "next";

// output: "export"(静的エクスポート)との互換性確保に必要
export const dynamic = "force-static";

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: 'アプリの正式名称', // カスタム
    short_name: '略称',       // カスタム(ホーム画面用)
    description: 'アプリの詳しい説明文をここに記載します。',  //PCでnameの後ろに表示される。文章の重複注意
    start_url: '/',
    display: 'standalone',
    background_color: '#ffffff', // アプリ起動時の背景色
    theme_color: '#3b82f6',     // ブランドカラー(例:Tailwindのblue-500)
    icons: [
      {
        src: '/icon-192x192.png',
        sizes: '192x192',
        type: 'image/png',
        purpose: 'maskable', // アイコンが綺麗に収まるようになります
      },
      {
        src: '/icon-512x512.png',
        sizes: '512x512',
        type: 'image/png',
        purpose: 'maskable',
      },
      {
        src: '/icon-192x192.png',
        sizes: '192x192',
        type: 'image/png',
        purpose: 'any', // purpose: "any" のアイコンも追加すると、トリミング非対応環境でも綺麗に表示される
      },
    ],
  }
}


1. アプリの名前(name / short_name)

  • name: アプリの正式名称です。インストール時の確認画面などに表示されます。
  • short_name: スマホのホーム画面のアイコン下に表示される短い名前です。全角6文字(半角12文字)以内に収めると、名前が途切れず綺麗に表示されます。

2. アプリの説明(description)

  • アプリがどのようなものかを記述します。ブラウザがインストールを促す際などに参照されることがあります。

3. テーマカラーと背景色(theme_color / background_color)

  • theme_color: ブラウザのアドレスバーや、スマホのステータスバーの色に影響します。アプリのブランドカラーを指定するのが一般的です。
  • background_color: アプリが起動する瞬間のスプラッシュ画面の背景色です。#ffffff(白)のままでも問題ありませんが、ダークモードを主体とするなら暗い色に変更します。

4. アイコン(icons)

ここが最も重要で、実際に画像を用意して public フォルダに配置する必要があります。

  • 最低限、192×192512×512 の2種類が必要です。
  • purpose: 'maskable' という設定を追加することをおすすめします。これにより、Androidなどのデバイスでアイコンが丸や四角に切り取られても、背景が隙間なく表示されるようになります。
Point

アイコンはpublic配下に配置し、publicを省略したパスを記述します

アイコンのファイル名に指定はありません。srcで正しくファイパスを指定することが重要です。

5. 起動時の挙動(display)

  • デフォルトの standalone は、ブラウザのアドレスバーを隠し、独立したアプリのように見せる設定です。基本はこのままでOKです。


@serwist/nextのインストール(Service Workerの実装)

npm install @serwist/next

オフライン対応やバックグラウンド処理を簡単に行うには、現在主流となっているSerwist(@serwist/next)をインストールします。

npm install @serwist/next
Serwistとは?

SerwistはService Workerのライブラリです。一般的に「サーウィスト」と読まれています。

Service Worker(サービス・ワーカー)の「Serw」と、〜をする人/もの(-ist)を組み合わせた造語です。


脆弱性の対処:npm audit fix

「npm install @serwist/next」を実行したときに、以下のように脆弱性が報告されることがあります。

169 packages are looking for funding
  run `npm fund` for details

14 vulnerabilities (8 low, 2 moderate, 4 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

「14 vulnerabilities (8 low, 2 moderate, 4 high)」は、使用しているライブラリの中に脆弱性(セキュリティ上の弱点)が見つかったことを示しています。

  • Low / Moderate: 低〜中程度の問題。
  • High: 早めに対処した方が良い重要な問題。


脆弱性の対処法

npmが自動で直せるものを試すのが一般的です。

  1. npm audit fix を実行する
    これだけで、互換性を壊さない範囲でライブラリを安全なバージョンに自動更新してくれます。
  2. それでも残る場合
    npm audit fix --force を実行すると、メジャーバージョンの更新(設定変更が必要になる可能性がある修正)も含めて強制的にアップデートします。
注意点

npm audit fix --force を実行する前に必ずコミットしてください。

PWAの設定(Serwistなど)を始めたばかりであれば、コードが壊れるリスクは低いですが、壊れるリスクもあります。

なお、私の環境の場合、–forceを実行したことでNext.jsがメジャーアップデートされエラーが増えました。(–forceなしの場合、8 low, 1 highのみに大幅に改善)


(補足)@next-pwaと@ducanh2912/next-pwa

Next.jsでPWAを実装する際のサービスワーカーのためのライブラリには、「@serwist/next」以外にも「@next-pwa」と「@ducanh2912/next-pwa」があります。

これらは歴史的なもので、最も古いのが「@next-pwa」。そのメンテが滞り新たに出てきたのが「@ducanh2912/next-pwa」、そして最新が「@serwist/next」です。

なので、余程の理由がない限り「@serwist/next」を使います。


next.config.ts の設定

next.config.tsの書き方

Next.jsのビルドプロセスにPWA専用の自動処理を組み込む設定をします。

next.config.tsに以下を記述します。

import withSerwistInit from "@serwist/next";

const withSerwist = withSerwistInit({
  swSrc: "app/sw.ts", // サービスワーカーのソースファイル
  swDest: "public/sw.js", // 生成される場所
});

export default withSerwist({
  // 通常のNext.js設定をここに記述
});

swSrc: "app/sw.ts" (Source)
開発用のサービスワーカーファイルです。型定義などが使える便利なファイルですが、ブラウザはそのままでは読めません。

swDest: "public/sw.js"
ビルド時に生成する本番用のJavaScriptファイルです。これを public フォルダに自動出力することで、ブラウザが https://ドメイン/sw.js として読み込めるようになります。


next.config.tsの具体例

具体例だと以下のようになります。

// next.config.ts
// Next.js + Serwist (PWA) の設定ファイル。
// output: "export"(静的エクスポート)でも SW のビルド・登録・ランタイムキャッシュは動作する。
// ただし __SW_MANIFEST への HTML ページの自動注入は未対応のため、
// オフライン対応でページをプリキャッシュしたい場合は additionalPrecacheEntries で手動指定する。
// 開発中は disable: true を設定してキャッシュによるデバッグ困難を回避することを強く推奨。

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

// 1. Serwist(Service Worker)の設定
const withSerwist = withSerwistInit({
  swSrc: "src/app/sw.ts",
  swDest: "public/sw.js",
 
  // 開発中は SW を無効化(キャッシュによる「コード変えたのに反映されない」を防ぐ)
  disable: process.env.NODE_ENV === "development",
  
  // output: "export" では __SW_MANIFEST に HTML が含まれないため、
  // オフラインで表示したいページは以下のように手動でプリキャッシュエントリを追加する
  // ※オフラインでの使用を想定していない場合は不要
  // additionalPrecacheEntries: [
  //   { url: "/", revision: crypto.randomUUID() },
  //   { url: "/offline", revision: crypto.randomUUID() },
  // ],
});

// 2. Next.js 本体の設定
const nextConfig: NextConfig = {
  output: "export",       // 静的エクスポート
  trailingSlash: true,    
  images: {
    unoptimized: true,    // 静的エクスポート時は Next.js の画像最適化が使えないため必須
  },
};

// 3. Serwist でラップして export
export default withSerwist(nextConfig);

export default は最後に1回だけ
JavaScriptのルールとして一つのファイルで export default は一度しか使えません。

関数の引数にオブジェクトを渡す
withSerwist(...) のカッコの中には、定義した nextConfig という「設定の塊(オブジェクト)」をそのまま渡しています。(その中で importconst を宣言することはできません)


サービスワーカーの中身作成 (app/sw.ts)

PWAの心臓部である、サービスワーカーの挙動を定義する本体ファイル「sw.ts」を作成しますです。

ブラウザがオフラインでも動くようにファイルを保存(キャッシュ)したり、ネットワークが不安定な時にどう振る舞うかを指示する「司令塔」のような役割を果たします。

// next.config.ts
// Next.js + Serwist (PWA) の設定ファイル。
// output: "export"(静的エクスポート)でも SW のビルド・登録・ランタイムキャッシュは動作する。
// ただし __SW_MANIFEST への HTML ページの自動注入は未対応のため、
// オフライン対応でページをプリキャッシュしたい場合は additionalPrecacheEntries で手動指定する。
// 【重要】Serwist は Turbopack 未対応のため、本番ビルド時のみ有効化している。
// 開発時は `next dev --webpack` を使うか、turbopack: {} を残したまま disable: true で無効化する。

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

// 1. Serwist(Service Worker)の設定
// Serwist は Webpack ベースのため、本番ビルド(next build)時のみ有効化する。
// 開発時に Turbopack を使いたい場合は disable を NODE_ENV !== "production" にする。
const withSerwist = withSerwistInit({
  // Service Worker のソースファイル(app/sw.ts に配置)
  swSrc: "app/sw.ts",
  // ビルド後の出力先(public/sw.js として生成される)
  swDest: "public/sw.js",
  // 本番ビルド以外では SW を無効化(Turbopack との競合 & キャッシュ起因のデバッグ困難を防ぐ)
  disable: process.env.NODE_ENV !== "production",
  // output: "export" では __SW_MANIFEST に HTML が含まれないため、
  // オフラインで表示したいページは以下のように手動でプリキャッシュエントリを追加する
  // additionalPrecacheEntries: [
  //   { url: "/", revision: crypto.randomUUID() },
  //   { url: "/offline", revision: crypto.randomUUID() },
  // ],
});

// 2. Next.js 本体の設定
const nextConfig: NextConfig = {
  output: "export",       // 静的エクスポート(GitHub Pages / S3 等の静的ホスティング向け)
  trailingSlash: true,    // 静的エクスポート時に /about/ のようなスラッシュ付きパスを生成
  images: {
    unoptimized: true,    // 静的エクスポート時は Next.js の画像最適化が使えないため必須
  },
  // Next.js 16 からデフォルトで Turbopack が有効になる。
  // Serwist(Webpack プラグイン)との競合エラーを回避するため空の turbopack 設定を追加。
  // next build は内部的に Webpack を使うため、本番ビルドには影響しない。
  turbopack: {},
};

// 3. Serwist でラップして export
export default withSerwist(nextConfig);
  • declare const self: ServiceWorkerGlobalScope;
    TypeScriptに対して「このファイルはサービスワーカーとして動く特別な環境だよ」と教えています。これによって、サービスワーカー特有の命令(self)がエラーにならずに使えるようになります。
  • new Serwist({ ... })
    Serwistというライブラリの機能を使って、PWAのルールセットを作成しています。


Serwist は Turbopack 未対応なので、Webpack を使うよう設定する必要があるため、「next.config.ts に turbopack: {} 」を追加します。

この空の turbopack 設定を追加することで「Webpack config があるのに turbopack config がない」というエラーが消えます。

また「disable: process.env.NODE_ENV !== “production”」を追加することで、next build(本番ビルド)でのみ Serwist を有効化します。開発時は Turbopack との競合を避けるため常に無効になります。


主要なプロパティの意味

プロパティ役割
precacheEntries最重要設定です。ビルド時に生成されたHTML、JS、CSSなどのリスト(self.__SW_MANIFEST)を、アプリ起動時にあらかじめ一括ダウンロード(プリキャッシュ)します。これにより、オフラインでも即座にページが開けるようになります。
skipWaiting: true新しいサービスワーカー(アプリアップデート)がある場合、古いものをすぐに捨てて、即座に最新版に切り替える設定です。
clientsClaim: trueサービスワーカーが有効になった瞬間から、現在開いているすべてのタブをコントロール下に置く設定です。
navigationPreloadブラウザがサービスワーカーを起動するのと並行して、ネットワークリクエストを開始する高速化技術です。
runtimeCaching「動的キャッシュ」の設定です。プリキャッシュに含まれていないデータ(画像や外部APIなど)にアクセスした際、その場で保存して次回から高速化します。ここでは defaultCacheOnFrontEndNav(Next.js内の遷移を最適化する設定)が使われています。


serwist.addEventListeners();

この一行で、上記で設定したルールをブラウザに登録します。これにより、インストール、アクティベート、リクエストの横取り(フェッチ)といったサービスワーカーのライフサイクルイベントがすべて有効になります。


注意点:output: 'export'(静的書き出し)の場合

output: 'export' を設定している場合、precacheEntries が非常に重要になります。

Next.jsが書き出した out フォルダ内のすべての静的ファイルが self.__SW_MANIFEST に含まれることで、「インターネットがなくても、一度訪れたページや画像がすべて表示される」 という強力なPWA体験が実現します。


package.jsonで本番環境のビルドは「–webpack」を指定する

Serwist は Turbopack 未対応なので、Webpack を使うよう設定する必要があります。このため、sw.jsに「next.config.ts に turbopack: {} 」を追加しました。

ただし、デフォルトのnpm run buildではturbopackでビルドが実行されます。そこでオプションをつけて、本番環境はwebpackを使うようにします。


  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },

  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build --webpack",
    "start": "next start",
    "lint": "eslint"
  },


tsconfig.jsonにServiceWorkerGlobalScopeを追加する

上記のsw.jsを作成したときに「Cannot find name ‘ServiceWorkerGlobalScope’. Did you mean ‘WorkerGlobalScope’?」と表示されることがあります。

このエラーは tsconfig.json の lib に “webworker” が含まれていないために発生します。tsconfig.json に “webworker” を追加することで解決します。

具体的には以下の3つを行います。

  1. lib に “webworker” を追加
    ServiceWorkerGlobalScope が認識されてエラーが消えます
  2. types に “@serwist/next/typings” を追加
    Serwist固有の型(window.serwist など)が補完されます
  3. exclude に “public/sw.js” を追加
    ビルド後の生成ファイルをTS解析対象から除外します
{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext", "webworker"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    },
    "types": ["@serwist/next/typings"]
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".next/dev/types/**/*.ts",
    "**/*.mts",
    "src/app/summary/components/ShowHideButtons"
  ],
  "exclude": ["node_modules", "public/sw.js"]
}


サービスワーカー(sw.js)をブラウザに登録する処理を書く(layout.tsx)

ここまでの状態でnpm run buildを実行するとサービスワーカーのファイル「out/sw.js」が生成されます。

このサービスワーカーをブラウザに登録する処理を別途書く必要があります。

コンポーネントを作成して「layout.tsx 」に追記します。

サービスワーカーの登録処理を書いたSerwistRegisterコンポーネントを作成します。

// @/components/SerwistRegister.tsx
// Service Worker(Serwist)をブラウザに登録するクライアントコンポーネント。
// layout.tsx に配置して全ページで一度だけ実行される。
// HTTPS または localhost 環境でのみ SW を登録する(SW は安全なコンテキストが必須)。
'use client';

import { useEffect } from 'react';

export default function SerwistRegister() {
  useEffect(() => {
    if (typeof window === 'undefined') return;
    if (!('serviceWorker' in navigator)) return;

    const isSecureContext =
      window.location.protocol === 'https:' ||
      window.location.hostname === 'localhost' ||
      window.location.hostname === '127.0.0.1';

    if (!isSecureContext) return;

    navigator.serviceWorker
      .register('/sw.js', { scope: '/' })
      .then((registration) => {
        console.log('[SW] Registered:', registration.scope);
      })
      .catch((error) => {
        console.error('[SW] Registration failed:', error);
      });
  }, []);

  return null;
}


作成したコンポーネントをlayout.tsxで読み込ませます。

    <html lang="ja">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
        suppressHydrationWarning
      >
        {/* Service Worker を登録するクライアントコンポーネント */}
        <SerwistRegister />
        <AuthProvider>
          <Header />
          {children}
        </AuthProvider>
      </body>
    </html>
  );


動作確認

npm run build を実行し、npx serve@latest out で起動します(開発モードではService Workerが動かない設定にすることが多いため)。


outにファイルが生成される

npm run build を実行すると、以下のように表示されsw.jsを作成します。

コンパイル後のoutディレクトリを確認すると、「manifest.webmanifest」と「sw.js」が生成されています。

これで、PWAを使う準備は完了です。

npx serve@latest out で起動します。


Dev Toolでチェックする(Application)

Chromeのデベロッパーツールを開き、Applicationタブを開きます。

Manifestに登録したアプリの情報が表示され、かつ、Service workersのStatusが緑色で「activated and is running 」と表示されていれば成功です。


PWA化が上手くいっていれば、アドレスバーに「インストール」アイコンが表示されます。

タイトルとURLをコピーしました