Use Cases (更新: 2026/6/2)

Claude CodeでPWAを実装する完全ガイド

Claude CodeでPWAを実装する手順。manifest、Service Worker、オフライン対応、検証まで初心者向けに解説。

Claude CodeでPWAを実装する完全ガイド

PWA(Progressive Web App)は、Webサイトを「インストールできるアプリ」に近づけるための実装パターンです。ホーム画面に追加できる、通信が不安定でも最低限の画面を出せる、更新後も古いキャッシュに引っ張られにくい、という体験をWebだけで作れます。

ただし、PWAは「manifestを置けば完成」ではありません。manifest.webmanifest、アイコン、Service Worker、オフラインページ、キャッシュ戦略、インストール可否の確認、LighthouseやDevToolsでの検証までを一つの作業として扱う必要があります。ここを雑にすると、初回は動いても更新時に古いHTMLが残ったり、404のアイコンでインストール条件を満たせなかったりします。

この記事では、Claude Codeに任せやすい粒度に分けながら、初心者でもコピペして試せるPWA実装を作ります。Claude Codeの基本操作がまだ不安な場合は、先にClaude Code入門ガイドを読んでおくと、この記事のプロンプトをそのまま使いやすくなります。

公式情報としては、PWA全体はweb.devのPWA学習ガイド、インストール条件はMDNのinstallable PWAガイド、オフライン設計はMDNのPWAベストプラクティス、Claude Code自体はClaude Code公式ドキュメントで確認できます。

PWA化の全体像

PWA化で作る部品は多く見えますが、流れは単純です。ブラウザがmanifestを読んでアプリ名、開始URL、アイコン、テーマカラーを理解し、Service Workerがネットワークとキャッシュの間に入り、オフライン時のfallbackを返します。

ユーザー
  -> index.html
  -> manifest.webmanifest でアプリ情報を読む
  -> register-sw.js で Service Worker を登録
  -> sw.js が静的ファイル、HTML、画像を戦略別に扱う
  -> 通信不可なら offline.html を返す

初心者が最初に決めるべきことは3つです。

決めること失敗すると起きること
開始URL/ または /app/scope外になり、Service Workerが効かない
キャッシュ対象HTML、CSS、JS、画像、offline.html404をキャッシュして更新されない
オフライン時の挙動offline.html、直近の閲覧ページ、APIエラー白画面や古いデータ表示になる

「全部をキャッシュする」ほど危険です。記事、LP、教材サイトならHTMLはNetwork First、画像やフォントはCache First、CSS/JSはStale While Revalidateから始めると管理しやすいです。キャッシュ戦略の考え方はClaude Codeでキャッシュ戦略を実装する記事とも相性がよいです。

Claude Codeに渡す作業単位

Claude Codeには「PWAにして」とだけ頼むより、成果物と検証条件を分けて渡す方が安定します。特にService Workerは影響範囲が大きいので、既存ルーティング、ビルド出力先、publicディレクトリ、デプロイ先のパスを確認させてから編集させます。

既存のVite/ReactアプリをPWA化してください。

要件:
- manifest.webmanifestをpublic直下に追加
- 192x192、512x512、maskable 512x512のアイコンを参照
- public/offline.htmlを追加
- public/sw.jsでService Workerを実装
- src/register-sw.jsから登録
- HTML navigationはNetwork First、画像はCache First、CSS/JS/fontはStale While Revalidate
- POSTや外部オリジンはキャッシュしない
- LighthouseとChrome DevTools Applicationパネルで確認できるチェックリストを最後に出す

注意:
- 空のfetchハンドラでinstallabilityだけを満たそうとしない
- 古いキャッシュ削除と更新通知を入れる
- 変更ファイルと手動確認手順を説明する

このプロンプトの狙いは、Claude Codeに「コードを書く」だけでなく「壊れやすい点を確認する」仕事も持たせることです。Masaが教材LPで試したときも、最初の失敗はstart_urlとService Workerのscope不一致でした。人間が見落としやすい前提条件をプロンプトに入れると、レビューの精度が上がります。

manifestとアイコンを作る

まずpublic/manifest.webmanifestを作ります。nameはインストール画面やアプリ一覧で表示される正式名、short_nameはホーム画面など狭い場所で使われる短い名前です。scopeはService Workerが管理する範囲、start_urlは起動時に開くURLです。

{
  "id": "/",
  "name": "ClaudeCodeLab PWA Demo",
  "short_name": "CCLab",
  "description": "Claude Codeで作るオフライン対応のPWAデモ",
  "start_url": "/?source=pwa",
  "scope": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0f766e",
  "orientation": "portrait-primary",
  "prefer_related_applications": false,
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-maskable-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "shortcuts": [
    {
      "name": "オフラインで読む",
      "short_name": "Offline",
      "url": "/?shortcut=offline",
      "icons": [
        {
          "src": "/icons/icon-192.png",
          "sizes": "192x192"
        }
      ]
    }
  ]
}

次にHTML側からmanifestを参照します。Viteならindex.html、AstroやNext.jsなら共通レイアウトやhead設定に入れます。

<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0f766e" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />

アイコンは最低でも192x192と512x512を用意します。Androidではmaskable iconが使われる場面があるので、重要なロゴを端まで広げず、中央に余白を残した512x512 PNGを作ると安全です。Claude Codeにアイコン生成まで頼む場合も、最終的にはブラウザで404になっていないかを確認してください。manifestのJSONは文法ミスに厳しいため、末尾カンマを入れないことも大事です。

Service Workerを実装する

次にpublic/sw.jsを作ります。ここではアプリシェル、オフラインページ、manifest、アイコンをインストール時にキャッシュし、古いキャッシュはactivate時に削除します。

const VERSION = "2026-06-02";
const STATIC_CACHE = `static-${VERSION}`;
const RUNTIME_CACHE = `runtime-${VERSION}`;

const APP_SHELL = [
  "/",
  "/offline.html",
  "/manifest.webmanifest",
  "/icons/icon-192.png",
  "/icons/icon-512.png",
  "/icons/icon-maskable-512.png"
];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(STATIC_CACHE)
      .then((cache) => cache.addAll(APP_SHELL))
      .then(() => self.skipWaiting())
  );
});

self.addEventListener("activate", (event) => {
  const allowedCaches = [STATIC_CACHE, RUNTIME_CACHE];

  event.waitUntil(
    caches
      .keys()
      .then((keys) =>
        Promise.all(
          keys
            .filter((key) => !allowedCaches.includes(key))
            .map((key) => caches.delete(key))
        )
      )
      .then(() => self.clients.claim())
  );
});

self.addEventListener("fetch", (event) => {
  const { request } = event;

  if (request.method !== "GET") return;

  const url = new URL(request.url);
  if (url.origin !== self.location.origin) return;

  if (request.mode === "navigate") {
    event.respondWith(networkFirstPage(request));
    return;
  }

  if (request.destination === "image") {
    event.respondWith(cacheFirst(request));
    return;
  }

  if (["style", "script", "font"].includes(request.destination)) {
    event.respondWith(staleWhileRevalidate(request));
  }
});

async function networkFirstPage(request) {
  const cache = await caches.open(RUNTIME_CACHE);

  try {
    const response = await fetch(request);
    if (response.ok) {
      await cache.put(request, response.clone());
    }
    return response;
  } catch {
    const cached = await cache.match(request);
    return (
      cached ||
      (await caches.match("/offline.html")) ||
      new Response("Offline", {
        status: 503,
        headers: { "Content-Type": "text/plain; charset=utf-8" }
      })
    );
  }
}

async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;

  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(RUNTIME_CACHE);
    await cache.put(request, response.clone());
  }
  return response;
}

async function staleWhileRevalidate(request) {
  const cache = await caches.open(RUNTIME_CACHE);
  const cached = await cache.match(request);

  const networkPromise = fetch(request)
    .then((response) => {
      if (response.ok) {
        cache.put(request, response.clone());
      }
      return response;
    })
    .catch(() => undefined);

  if (cached) return cached;

  return (
    (await networkPromise) ||
    new Response("Network error", {
      status: 504,
      headers: { "Content-Type": "text/plain; charset=utf-8" }
    })
  );
}

この実装は、POSTや外部APIをキャッシュしません。ログイン状態、決済、在庫、問い合わせフォームのようなデータを雑にキャッシュすると、ユーザーに古い情報や他人向けの情報を見せる危険があります。PWA化は便利ですが、キャッシュは保存であり、保存すべきでないものを保存しない設計が重要です。

オフラインfallbackを用意する

public/offline.htmlは、通信が切れたときに表示する最後の砦です。凝ったUIでなくて構いません。ユーザーに「何が起きたか」「何ならできるか」を短く伝えます。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>オフラインです</title>
    <style>
      body {
        margin: 0;
        font-family: system-ui, sans-serif;
        background: #f8fafc;
        color: #0f172a;
        display: grid;
        min-height: 100vh;
        place-items: center;
      }
      main {
        max-width: 36rem;
        padding: 2rem;
      }
      a {
        color: #0f766e;
        font-weight: 700;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>現在オフラインです</h1>
      <p>通信が戻ったら再読み込みしてください。直近に開いたページは表示できる場合があります。</p>
      <p><a href="/">トップへ戻る</a></p>
    </main>
  </body>
</html>

オフラインページには広告タグや外部フォントを入れない方が安全です。ネットワークがない前提の画面なので、依存が増えるほど白画面の原因になります。また、cache.addAll(APP_SHELL)は1つでも404が混ざると失敗します。アイコンやoffline.htmlを追加した直後は、DevToolsのNetworkタブで200を確認してください。

Service Worker登録と更新通知

Service Workerはブラウザに登録されて初めて動きます。src/register-sw.jsのようなファイルを作り、アプリのエントリーポイントから読み込みます。

export async function registerServiceWorker() {
  if (!("serviceWorker" in navigator)) {
    console.info("Service Worker is not supported in this browser.");
    return;
  }

  window.addEventListener("load", async () => {
    try {
      const registration = await navigator.serviceWorker.register("/sw.js", {
        scope: "/"
      });

      registration.addEventListener("updatefound", () => {
        const worker = registration.installing;
        if (!worker) return;

        worker.addEventListener("statechange", () => {
          if (worker.state === "installed" && navigator.serviceWorker.controller) {
            showUpdateNotice();
          }
        });
      });
    } catch (error) {
      console.error("Service Worker registration failed:", error);
    }
  });
}

function showUpdateNotice() {
  const button = document.querySelector("[data-refresh-app]");
  if (!button) return;

  button.hidden = false;
  button.addEventListener(
    "click",
    () => {
      window.location.reload();
    },
    { once: true }
  );
}

Reactならmain.jsxmain.tsxで呼び出します。

import { registerServiceWorker } from "./register-sw.js";

registerServiceWorker();

更新通知を入れる理由は、Service Workerが「すぐに全タブへ反映される」とは限らないからです。古いタブが残っていると、ユーザーは古いHTMLと新しいJSの組み合わせを踏むことがあります。業務アプリや教材サイトでは、更新ボタンを明示するだけで問い合わせが減ります。

インストール導線を作る

Chrome系ブラウザでは、条件を満たしたときにbeforeinstallpromptイベントが発火する場合があります。ただし、これはすべてのブラウザで使える標準的な合図ではありません。表示できる環境ではボタンを出し、使えない環境ではブラウザのメニューから追加してもらう、という控えめな実装にします。

let deferredPrompt = null;

window.addEventListener("beforeinstallprompt", (event) => {
  event.preventDefault();
  deferredPrompt = event;

  const installButton = document.querySelector("[data-install-app]");
  if (installButton) {
    installButton.hidden = false;
  }
});

document.querySelector("[data-install-app]")?.addEventListener("click", async () => {
  if (!deferredPrompt) return;

  deferredPrompt.prompt();
  const choice = await deferredPrompt.userChoice;
  console.info("Install prompt result:", choice.outcome);

  deferredPrompt = null;
});

window.addEventListener("appinstalled", () => {
  console.info("PWA was installed.");
});

インストールボタンはCV導線にもなります。たとえば教材サイトなら「ホーム画面に追加して続きを読む」、SaaSなら「毎日使うダッシュボードをアプリ化する」、イベントサイトなら「会場でオフラインでも開けるようにする」という文脈で置くと自然です。ただし、押し売りのモーダルにすると逆効果なので、記事末尾やダッシュボードの右上のように邪魔にならない場所に置きます。

LighthouseとDevToolsで検証する

現在のChromeでは、古いLighthouseのPWAカテゴリだけに頼る検証は適切ではありません。Chrome公式はPWA監査の扱いが変わったことを説明しており、実務ではDevToolsのApplicationパネルでmanifest、Service Worker、Cache Storage、offline挙動を直接確認し、LighthouseはPerformance、Accessibility、Best Practices、SEOの確認に使うのが現実的です。背景はChrome DevelopersのPWA install criteria更新記事も参考になります。

ローカルで確認するなら、ビルド済みファイルをHTTPSまたはlocalhostで配信します。localhostはService Workerの検証に使えます。

npm run build
npx serve dist -l 4173
npx lighthouse http://localhost:4173 --view --only-categories=performance,accessibility,best-practices,seo

手動チェックリストは次の通りです。

チェック見る場所合格ライン
manifestが読めるDevTools > Application > Manifestname、start_url、iconsにエラーがない
Service Workerが登録されるApplication > Service Workerssw.jsがactivatedになっている
オフライン表示NetworkをOfflineにして再読み込みoffline.htmlか直近ページが表示される
キャッシュApplication > Cache Storagestaticとruntimeが意図通り分かれている
LighthouseLighthouseレポートパフォーマンスとSEOの悪化がない

Claude Codeに検証を頼む場合は「ブラウザで見て」と曖昧に言わず、「DevTools Applicationパネルのどこを確認したか」「Network Offlineで何が表示されたか」「Lighthouseの主要スコア」を報告させます。可能ならPlaywrightでオンライン/オフラインのE2Eテストも足すと、更新でPWAが壊れたときに気づけます。

使いどころと収益化CTA

PWAはすべてのサイトに必要ではありません。効果が出やすいのは、再訪問が多く、通信状態に左右され、ホーム画面に置く理由があるサービスです。

ユースケースPWA化の価値収益導線の例
技術ブログ・教材サイト通勤中や移動中に続きを読める有料テンプレート、講座、相談へ誘導
社内ダッシュボード毎日同じURLを開く手間を減らす導入支援、運用設計、研修
イベント・会場案内回線が混む場所でも地図や予定を見られるスポンサー枠、追加資料、問い合わせ
EC・予約サイト商品画像や閲覧履歴を軽く表示できる再訪問、カート復帰、会員登録

ClaudeCodeLabでPWA導入を収益につなげるなら、単に「アプリ化できます」と言うより、読了率、再訪率、CTAクリック、問い合わせ数を一緒に測るべきです。実装テンプレートやClaude Code用プロンプトを整えたい場合は、教材一覧からテンプレートを確認できます。チーム導入や既存サイトのPWA化では、Service Workerの設計レビュー、キャッシュ対象の棚卸し、Lighthouse/DevTools検証まで含めて相談すると失敗が減ります。

よくある落とし穴

1つ目は、scopestart_urlの不一致です。/app/配下で動くサイトなのにstart_url/にすると、期待した画面がService Workerの管理外になることがあります。

2つ目は、HTMLをCache Firstにして更新されなくなることです。LPや記事なら多少古くても許される場面はありますが、アプリ本体のHTMLは新しいJS/CSSとの組み合わせが重要です。最初はNetwork Firstを選ぶ方が安全です。

3つ目は、APIレスポンスを雑にキャッシュすることです。ユーザー別データ、決済、在庫、権限に関わるレスポンスは、Service Workerではなくサーバー側のHTTPキャッシュ、ETag、認証設計と一緒に考えます。

4つ目は、アイコンの404です。manifest自体は読めても、アイコンが404ならインストール画面で問題になります。/icons/icon-512.pngのような絶対パスは、サブディレクトリ配信時に壊れやすいのでデプロイ先のベースパスを確認します。

5つ目は、Service Worker更新の見落としです。変更したはずなのにブラウザが古いsw.jsを持っている、という状態はよくあります。検証時はApplicationパネルからUnregisterし、Cache Storageを消して、初回訪問と更新後の両方を確認します。

まとめと実際に試した結果

PWA実装は、manifest、アイコン、Service Worker、offline fallback、キャッシュ戦略、インストール導線、検証を一つずつ積む作業です。Claude Codeを使うと、ファイル追加やコード生成は速くなりますが、キャッシュ対象、更新タイミング、デプロイパスの判断は人間がレビューする必要があります。

この記事で紹介した内容を実際に試した結果、Masaの小さなVite製教材LPでは、manifestとService Workerの追加自体は短時間で終わりました。一方で、最も時間を使ったのはアイコンパスの確認、Network Offlineでの再読み込み、古いキャッシュ削除の確認でした。PWAは実装より検証の方が差が出ます。まずはoffline.htmlが確実に出る最小構成から始め、必要なページだけを段階的にキャッシュするのが安全です。

#Claude Code #PWA #Service Worker #オフライン #モバイル
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。