Next.jsとSupabaseでオンライ学習プラットフォーム（LMS）を構築する | エピソード9: Progressive Web App（PWA）への変換

こんにちは！前回のエピソードでは、LMSのパフォーマンスを最適化し、Core Web Vitalsのスコアを向上させました。今回は、LMSをProgressive Web App（PWA）に変換して、モバイルでのネイティブアプリのような体験を提供します。`next-pwa`を使ってオフライン機能を追加し、`manifest.json`とサービスワーカーを設定、プッシュ通知をOneSignalで統合します。これで、ユーザーはアプリをホーム画面に追加し、オフラインでも利用できるようになります！

## このエピソードのゴール

- `next-pwa`を使ってPWA機能を追加。
- `manifest.json`とサービスワーカーを設定し、オフライン対応を実現。
- OneSignalでプッシュ通知を統合。
- モバイルデバイスでPWAの動作を確認。

## 必要なもの

- 前回のプロジェクト（`next-lms`）がセットアップ済み。
- Supabaseプロジェクト（既存のテーブル設定済み）。
- `next-pwa`と`@onesignal/onesignal-react`パッケージ。
- OneSignalアカウント（無料プランで十分）。
- 基本的なTypeScript、React、Next.jsの知識。

---

## ステップ1: `next-pwa`のセットアップ

`next-pwa`をインストールし、PWA機能を有効化します。

1. **パッケージのインストール**：
   以下のコマンドで`next-pwa`をインストール：

```bash
npm install next-pwa
```

2. **Next.js設定の更新**：
   `next.config.js`を更新してPWAを有効化：

```js
const withPWA = require('next-pwa')({
  dest: 'public',
  disable: process.env.NODE_ENV === 'development', // 開発環境では無効
  register: true,
  skipWaiting: true,
});

module.exports = withPWA({
  reactStrictMode: true,
  images: {
    domains: ['via.placeholder.com'], // 必要に応じてSupabase Storageのドメインを追加
  },
});
```

このコードは：
- PWAのキャッシュ先を`public`フォルダに設定。
- 開発環境ではPWAを無効化。
- サービスワーカーを自動登録。

3. **サービスワーカーのカスタマイズ**：
   `public/workbox-*.js`が自動生成されるが、カスタムキャッシュ戦略が必要な場合、`public/sw.js`を作成（今回はデフォルトを使用）。

---

## ステップ2: `manifest.json`の設定

PWAのメタデータを定義する`manifest.json`を追加します。

1. **`manifest.json`の作成**：
   `public/manifest.json`を作成：

```json
{
  "name": "Next.js LMS",
  "short_name": "LMS",
  "description": "Next.jsとSupabaseで構築されたオンライ学習プラットフォーム",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#f3f4f6",
  "theme_color": "#2563eb",
  "icons": [
    {
      "src": "/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
```

2. **アイコンの準備**：
   - 192x192と512x512のPNGアイコン（例: `icon-192x192.png`, `icon-512x512.png`）を`public`フォルダに追加。
   - アイコンはブランドロゴやアプリのテーマに合わせたデザインを使用（例: LMSのロゴ）。

3. **マニフェストのリンク**：
   `src/app/layout.tsx`を更新して`manifest.json`をリンク：

```tsx
import '../styles/globals.css';
import Link from 'next/link';
import { supabaseServer } from '@/lib/supabase';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'], display: 'swap' });

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const supabase = supabaseServer();
  const { data: { user } } = await supabase.auth.getUser();

  return (
    <html lang="ja" className={inter.className}>
      <head>
        <link rel="manifest" href="/manifest.json" />
        <meta name="theme-color" content="#2563eb" />
        <link rel="apple-touch-icon" href="/icon-192x192.png" />
      </head>
      <body className="bg-gray-100">
        <header className="bg-primary text-white p-4">
          <div className="container mx-auto flex justify-between items-center">
            <h1 className="text-2xl font-bold">
              <Link href="/">Next.js LMS</Link>
            </h1>
            <nav className="flex gap-4">
              <Link href="/courses" className="hover:underline">コース</Link>
              {user ? (
                <>
                  <Link href="/dashboard" className="hover:underline">ダッシュボード</Link>
                  <form
                    action={async () => {
                      'use server';
                      await supabase.auth.signOut();
                    }}
                    className="inline"
                  >
                    <button className="hover:underline">ログアウト</button>
                  </form>
                </>
              ) : (
                <>
                  <Link href="/login" className="hover:underline">ログイン</Link>
                  <Link href="/register" className="hover:underline">登録</Link>
                </>
              )}
            </nav>
          </div>
        </header>
        {children}
      </body>
    </html>
  );
}
```

このコードは：
- `manifest.json`をリンク。
- テーマカラーとApple Touch Iconを指定。

---

## ステップ3: オフライン対応の確認

サービスワーカーがオフラインキャッシュを提供します。

1. **キャッシュ戦略の確認**：
   - `next-pwa`はデフォルトで静的アセット（HTML、CSS、JS、画像）をキャッシュ。
   - `public/sw.js`でカスタムキャッシュが必要な場合、以下を参考に追加：

```js
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});
```

2. **オフライン動作のテスト**：
   - 開発サーバーを停止し、ブラウザのキャッシュをクリア。
   - Chrome DevToolsの「Application」→「Service Workers」でキャッシュを確認。
   - オフラインモード（DevToolsの「Network」→「Offline」）でページをリロードし、コース一覧やホームが表示されることを確認。

---

## ステップ4: OneSignalでプッシュ通知の統合

OneSignalを使ってプッシュ通知を追加します。

1. **OneSignalアカウントの設定**：
   - [OneSignal](https://onesignal.com/)でアカウントを作成。
   - 新しいアプリを作成し、Web Pushを設定。
   - アプリIDを取得。

2. **パッケージのインストール**：
   OneSignalのReact SDKをインストール：

```bash
npm install @onesignal/onesignal-react
```

3. **OneSignalの初期化**：
   `src/app/_app.tsx`（またはグローバルコンポーネント）を作成し、OneSignalを初期化：

```tsx
'use client';

import { useEffect } from 'react';
import OneSignal from '@onesignal/onesignal-react';

export default function OneSignalInit() {
  useEffect(() => {
    OneSignal.init({
      appId: 'あなたのOneSignalアプリID',
      allowLocalhostAsSecureOrigin: true, // 開発環境用
    }).then(() => {
      OneSignal.showSlidedownPrompt();
    });

    return () => {
      OneSignal.removeAllListeners();
    };
  }, []);

  return null;
}
```

4. **初期化コンポーネントの統合**：
   `src/app/layout.tsx`に`OneSignalInit`を追加：

```tsx
import '../styles/globals.css';
import Link from 'next/link';
import { supabaseServer } from '@/lib/supabase';
import { Inter } from 'next/font/google';
import OneSignalInit from '@/components/OneSignalInit';

const inter = Inter({ subsets: ['latin'], display: 'swap' });

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const supabase = supabaseServer();
  const { data: { user } } = await supabase.auth.getUser();

  return (
    <html lang="ja" className={inter.className}>
      <head>
        <link rel="manifest" href="/manifest.json" />
        <meta name="theme-color" content="#2563eb" />
        <link rel="apple-touch-icon" href="/icon-192x192.png" />
      </head>
      <body className="bg-gray-100">
        <OneSignalInit />
        <header className="bg-primary text-white p-4">
          <div className="container mx-auto flex justify-between items-center">
            <h1 className="text-2xl font-bold">
              <Link href="/">Next.js LMS</Link>
            </h1>
            <nav className="flex gap-4">
              <Link href="/courses" className="hover:underline">コース</Link>
              {user ? (
                <>
                  <Link href="/dashboard" className="hover:underline">ダッシュボード</Link>
                  <form
                    action={async () => {
                      'use server';
                      await supabase.auth.signOut();
                    }}
                    className="inline"
                  >
                    <button className="hover:underline">ログアウト</button>
                  </form>
                </>
              ) : (
                <>
                  <Link href="/login" className="hover:underline">ログイン</Link>
                  <Link href="/register" className="hover:underline">登録</Link>
                </>
              )}
            </nav>
          </div>
        </header>
        {children}
      </body>
    </html>
  );
}
```

5. **プッシュ通知のテスト**：
   - OneSignalダッシュボードでテスト通知を作成（例: 「新しいコースが追加されました！」）。
   - ブラウザで通知許可プロンプトが表示され、許可後に通知を受信。
   - モバイルデバイスで通知が正しく表示されることを確認。

---

## ステップ5: モバイルでのPWA動作確認

PWAがモバイルでネイティブアプリのように動作することを確認します。

1. **ビルドとサーバー起動**：
   - プロジェクトをビルドし、プロダクションサーバーを起動：

```bash
npm run build
npm run start
```

2. **モバイルデバイスでのテスト**：
   - スマートフォンで`http://localhost:3000`にアクセス（ローカルネットワーク経由）。
   - ブラウザの「ホーム画面に追加」を選択し、アプリをインストール。
   - アプリを起動し、以下の点を確認：
     - フルスクリーン表示（`display: standalone`）。
     - オフラインモードでコース一覧が表示。
     - プッシュ通知が受信可能。
   - アイコンとテーマカラーが正しく反映。

3. **LighthouseのPWA監査**：
   - Lighthouseの「Progressive Web App」カテゴリで監査を実行。
   - 「Installable」「Service Worker」「Manifest」などの項目が緑（合格）であることを確認。

---

## ステップ6: 動作確認

1. 開発サーバーを起動（またはビルド後）：

```bash
npm run dev
```

2. ブラウザで`http://localhost:3000`にアクセス：
   - PWAインストールプロンプトが表示（Chrome/Edge）。
   - `manifest.json`が正しく読み込まれ、アイコンとテーマカラーが反映。
   - オフラインモードでページが表示。
   - OneSignalの通知許可プロンプトが表示され、通知を受信。
3. モバイルデバイスで：
   - ホーム画面に追加後、アプリがネイティブのように動作。
   - 通知が正しく表示。
4. LighthouseでPWAスコアを確認（90以上を目指す）。

エラーがあれば、`manifest.json`の設定、OneSignalのアプリID、またはサービスワーカーのキャッシュを確認してください。

---

## まとめと次のステップ

このエピソードでは、LMSをProgressive Web App（PWA）に変換しました。`next-pwa`でオフライン機能を追加し、`manifest.json`とサービスワーカーを設定、OneSignalでプッシュ通知を統合しました。これで、ユーザーはモバイルでネイティブアプリのような体験を得られます！

次回の最終エピソードでは、LMSをVercelにデプロイし、セキュリティを強化、CI/CDを設定します。さらに、AIによるコース推薦や学習分析などの拡張アイデアも提案しますので、引き続きお楽しみに！

---

この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです！次回のエピソードもお楽しみに！