mcpField
P
pwa-storefront

pwa-storefront

github.com/finsilabs開発6074 回インストール5 日前に更新
リポジトリ

pwa-storefront について

カスタムまたはヘッドレスの EC サイト向けに、プログレッシブ Web アプリのストアフロント構築を案内するスキルです。オフラインカタログ、キャッシュ、ホーム画面への追加、注文通知などを扱います。

主な機能

  1. 01Web App Manifest の作成
  2. 02Workbox によるサービスワーカー登録
  3. 03カタログのキャッシュとオフラインページ
  4. 04Web Push 通知の実装
  5. 05接続状態の検知と画面更新

活用シーン

  1. 01通信が不安定な地域でのオフライン閲覧
  2. 02ストアのホーム画面への追加
  3. 03キャッシュによる再訪時の読み込み短縮
  4. 04注文状況や再入荷の通知

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill pwa-storefront

インストール後、Claude Code で/pwa-storefrontを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

tile.json
evals/push-notifications-and-offline-ux/
criteria.json
task.md
evals/service-worker-caching-strategies/
criteria.json
task.md
evals/web-app-manifest-and-installability/
criteria.json
task.md

詳細

PWA ストアフロント

概要

プログレッシブウェブアプリ(PWA)のストアフロントは、ウェブの広いリーチとネイティブアプリのような機能を組み合わせます。オフラインでの商品カタログ閲覧、プッシュ通知、ホーム画面へのインストール、キャッシュによる高速な再読み込みが可能です。サービスワーカーはネットワークリクエストを横取りし、不安定な接続でもストアが機能し続けるキャッシュ戦略を実装します。このスキルでは、Workbox を使ったサービスワーカーの実装、Web App Manifest の作成、商品カタログのキャッシュ、注文状況更新のプッシュ通知送信について説明します。

このスキルを使う場面

  • モバイルインターネット接続が不安定な地域に顧客がいる場合
  • ネイティブアプリを用意せずに「ホーム画面に追加」を有効にして、再エンゲージメント率を高めたい場合
  • キャッシュからアセットを配信し、ページをすばやく再読み込みしたい場合
  • 注文状況の更新、再入荷のお知らせ、プロモーションのプッシュ通知を送りたい場合
  • ネイティブアプリに匹敵する UX 品質が必要な、モバイルファーストのストアフロントを構築する場合

前提条件とプラットフォームに関する注意事項

このスキルはカスタム/ヘッドレスのストアフロント向けです(Node.js、Python、または同様のバックエンド)。コード例では TypeScript/Node.js を使用していますが、どのスタックにも適応できます。

Shopify:Shopify Hydrogen は Shopify のヘッドレスフレームワークです。Shopify をコマースバックエンドとしてカスタムフロントエンドを使用する場合や、Shopify と他の最適なサービスを組み合わせる場合は、MACH/コンポーザブルのパターンが適用されます。 WooCommerce:WooCommerce は、REST API と WPGraphQL を介してヘッドレスバックエンドとして利用できます。これらのパターンは、フロントエンドを WordPress から切り離す場合に適用されます。 Magento:Magento の GraphQL API と PWA Studio は、ヘッドレスアーキテクチャをサポートします。これらのコンポーザブルなパターンは、MACH スタックのバックエンドサービスとして Magento を使用する場合に適用されます。

必要なもの:

  • Node.js 18+(またはバックエンドの言語に合わせて調整)
  • PostgreSQL(または任意のリレーショナルデータベース)
  • キャッシュ/キュー用の Redis
  • メール送信サービス(SendGrid、AWS SES、または Postmark)
  • CDN(Cloudflare、CloudFront、または Fastly)

主な手順

  1. Web App Manifest を作成する

    マニフェストにより、アプリを Android および iOS にインストールできるようになります(iOS のサポートは一部に限られます)。

    // public/manifest.json
    {
      "name": "My Commerce Store",
      "short_name": "MyStore",
      "description": "Fast, reliable shopping from anywhere",
      "start_url": "/?source=pwa",
      "display": "standalone",
      "background_color": "#ffffff",
      "theme_color": "#1a1a2e",
      "orientation": "portrait-primary",
      "icons": [
        { "src": "/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png" },
        { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
        { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
      ],
      "screenshots": [
        { "src": "/screenshots/home.png", "sizes": "390x844", "type": "image/png", "form_factor": "narrow" }
      ],
      "categories": ["shopping"],
      "share_target": {
        "action": "/search",
        "method": "GET",
        "params": { "title": "q" }
      }
    }
    

    HTML にリンクを追加します:

    <link rel="manifest" href="/manifest.json">
    <meta name="theme-color" content="#1a1a2e">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="default">
    <link rel="apple-touch-icon" href="/icons/icon-192x192.png">
    
  2. Workbox でサービスワーカーを登録する

    npm install workbox-webpack-plugin
    # or for Vite:
    npm install vite-plugin-pwa
    

    vite-plugin-pwa を使用する場合(Vite/Next.js プロジェクトに推奨):

    // vite.config.ts
    import {VitePWA} from 'vite-plugin-pwa';
    
    export default {
      plugins: [
        VitePWA({
          registerType: 'autoUpdate',
          workbox: {
            globPatterns: ['**/*.{js,css,html,svg,png,webp,woff2}'],
            runtimeCaching: [
              {
                urlPattern: /^https:\/\/fonts\.googleapis\.com/,
                handler: 'CacheFirst',
                options: {cacheName: 'google-fonts-cache', expiration: {maxAgeSeconds: 60 * 60 * 24 * 365}},
              },
              {
                urlPattern: /\/api\/products/,
                handler: 'StaleWhileRevalidate',
                options: {
                  cacheName: 'products-cache',
                  expiration: {maxEntries: 500, maxAgeSeconds: 60 * 60 * 24}, // 24h
                  cacheableResponse: {statuses: [0, 200]},
                },
              },
              {
                urlPattern: /\/api\/collections/,
                handler: 'NetworkFirst',
                options: {
                  cacheName: 'collections-cache',
                  networkTimeoutSeconds: 3,
                  expiration: {maxEntries: 50, maxAgeSeconds: 60 * 60},
                },
              },
            ],
          },
          manifest: {/* inline manifest or path */},
        }),
      ],
    };
    
  3. オフラインの商品カタログ用にカスタムサービスワーカーを実装する

    細かく制御するには、サービスワーカーを直接記述します:

    // public/sw.js
    import {precacheAndRoute, cleanupOutdatedCaches} from 'workbox-precaching';
    import {registerRoute} from 'workbox-routing';
    import {StaleWhileRevalidate, CacheFirst, NetworkFirst} from 'workbox-strategies';
    import {ExpirationPlugin} from 'workbox-expiration';
    import {BackgroundSyncPlugin} from 'workbox-background-sync';
    
    // Precache app shell (injected by build tool)
    precacheAndRoute(self.__WB_MANIFEST);
    cleanupOutdatedCaches();
    
    // Product images: Cache-first with 7-day expiry
    registerRoute(
      ({url}) => url.hostname.includes('cdn.shopify.com') || url.pathname.includes('/product-images/'),
      new CacheFirst({
        cacheName: 'product-images',
        plugins: [
          new ExpirationPlugin({maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 7}),
        ],
      })
    );
    
    // Product API: Stale-while-revalidate (show cached, refresh in background)
    registerRoute(
      ({url}) => url.pathname.startsWith('/api/products') || url.pathname.startsWith('/api/collections'),
      new StaleWhileRevalidate({
        cacheName: 'api-products',
        plugins: [
          new ExpirationPlugin({maxEntries: 500, maxAgeSeconds: 60 * 60 * 24}),
        ],
      })
    );
    
    // Background sync for cart operations when offline
    const cartSyncPlugin = new BackgroundSyncPlugin('cart-sync-queue', {
      maxRetentionTime: 24 * 60, // Retry for 24 hours
    });
    
    registerRoute(
      ({url, request}) => url.pathname.startsWith('/api/cart') && request.method !== 'GET',
      new NetworkFirst({plugins: [cartSyncPlugin]}),
      'POST'
    );
    
  4. オフライン時のフォールバックページを表示する

    // In the service worker
    import {setCatchHandler, setDefaultHandler} from 'workbox-routing';
    
    // Precache the offline page during installation
    precacheAndRoute([{url: '/offline', revision: '1'}]);
    
    // Serve offline page for navigation requests when network fails
    setCatchHandler(async ({event}) => {
      if (event.request.destination === 'document') {
        return caches.match('/offline');
      }
      return Response.error();
    });
    
    // app/offline/page.tsx
    export default function OfflinePage() {
      return (
        <div className="flex flex-col items-center justify-center min-h-screen">
          <h1>You're offline</h1>
          <p>Check your connection. Recently viewed products are still available below.</p>
          <RecentlyViewedProducts /> {/* Reads from IndexedDB */}
        </div>
      );
    }
    
  5. Web Push 通知を実装する

    // client: subscribe to push notifications
    async function subscribeToPush() {
      const registration = await navigator.serviceWorker.ready;
      const subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!),
      });
    
      await fetch('/api/push/subscribe', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({subscription, customerId: user.id}),
      });
    }
    
    // server: send push notification for order status update
    import webpush from 'web-push';
    
    webpush.setVapidDetails(
      'mailto:[email protected]',
      process.env.VAPID_PUBLIC_KEY!,
      process.env.VAPID_PRIVATE_KEY!
    );
    
    export async function sendOrderUpdatePush(customerId: string, order: Order) {
      const subscriptions = await db.pushSubscriptions.findByCustomer(customerId);
      await Promise.allSettled(
        subscriptions.map(sub =>
          webpush.sendNotification(sub.data, JSON.stringify({
            title: `Order #${order.number} Update`,
            body: `Your order is now ${order.status}`,
            icon: '/icons/icon-192x192.png',
            url: `/orders/${order.id}`,
            tag: `order-${order.id}`,
          }))
        )
      );
    }
    

    サービスワーカーでプッシュイベントを処理します:

    self.addEventListener('push', (event) => {
      const data = event.data?.json();
      event.waitUntil(
        self.registration.showNotification(data.title, {
          body: data.body,
          icon: data.icon,
          badge: '/icons/badge-72x72.png',
          data: {url: data.url},
          tag: data.tag,
          renotify: true,
        })
      );
    });
    
    self.addEventListener('notificationclick', (event) => {
      event.notification.close();
      event.waitUntil(clients.openWindow(event.notification.data.url));
    });
    
  6. UI でオフライン状態を検出して対応する

    // hooks/use-online-status.ts
    import {useState, useEffect} from 'react';
    
    export function useOnlineStatus() {
      const [isOnline, setIsOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true);
    
      useEffect(() => {
        const setOnline = () => setIsOnline(true);
        const setOffline = () => setIsOnline(false);
        window.addEventListener('online', setOnline);
        window.addEventListener('offline', setOffline);
        return () => { window.removeEventListener('online', setOnline); window.removeEventListener('offline', setOffline); };
      }, []);
    
      return isOnline;
    }
    
    // Usage in a component
    function CartButton() {
      const isOnline = useOnlineStatus();
      return (
        <button disabled={!isOnline} title={isOnline ? undefined : 'You are offline'}>
          Add to Cart
        </button>
      );
    }
    

例

Lighthouse PWA 監査チェックリスト(自動)
# Install Lighthouse CLI
npm install -g lighthouse

# Audit PWA criteria
lighthouse https://mystore.com --preset=desktop --only-categories=pwa --output=json --output-path=./lighthouse-pwa.json

# Key scores to target:
# - "Installable" checks: manifest, service worker, HTTPS
# - "PWA Optimized" checks: themed address bar, offline page, mobile viewport
オフライン閲覧用の IndexedDB 商品カタログキャッシュ
import {openDB} from 'idb';

const db = await openDB('catalog-db', 1, {
  upgrade(db) {
    db.createObjectStore('products', {keyPath: 'id'});
    db.createObjectStore('collections', {keyPath: 'id'});
  },
});

// Store products when user browses online
export async function cacheProductsLocally(products: Product[]) {
  const tx = db.transaction('products', 'readwrite');
  await Promise.all([...products.map(p => tx.store.put(p)), tx.done]);
}

// Retrieve from IDB when offline
export async function getProductFromCache(id: string): Promise<Product | null> {
  return db.get('products', id) ?? null;
}

ベストプラクティス

  • 商品データには StaleWhileRevalidate を使う — サービスワーカーがバックグラウンドで最新データを取得する間、ユーザーにはキャッシュ済みのコンテンツがすぐに表示されます
  • カートやチェックアウトのページは絶対にキャッシュしない — 常に最新の状態である必要があります。/cart、/checkout、アカウントページには NetworkOnly 戦略を使います
  • サービスワーカーのキャッシュ名にバージョンを付ける — アプリを更新するときにキャッシュ名を増やすと、古いアセットが自動的に削除されます
  • Chrome DevTools でオフラインモードをテストする — デプロイ前に、Network タブ →「Offline」スロットリングを使ってオフライン時の動作を確認します
  • VAPID キーは一度だけ生成し、安全に保管する — VAPID の秘密鍵を失うと、既存のプッシュ購読がすべて失われます。シークレットマネージャーに保管してください
  • 状況に応じてプッシュ通知の許可を求める — オプトイン率を高めるため、注文完了や再入荷への関心など、関連する操作の後にのみ通知の許可を求めます
  • 妥当なキャッシュサイズの上限を設定する — maxEntries を指定した ExpirationPlugin を使い、サービスワーカーのキャッシュがデバイスのストレージを使いすぎないようにします

よくある落とし穴

問題解決策
デプロイ後にサービスワーカーが更新されないregisterType: 'autoUpdate' を使用し、サービスワーカー内で skipWaiting() を呼び出してすぐに制御を引き継ぎ、「新しいバージョンが利用可能です」というトーストを表示します
iOS でプッシュ通知が表示されないiOS では、まずユーザーが PWA をホーム画面に追加する必要があります。iOS Safari の Web Push には iOS 16.4 以降と standalone の表示モードが必要です
価格変更後もキャッシュされた API レスポンスが返されるmaxAgeSeconds を適切に設定します。デプロイ時にキャッシュ名を変更して、オンデマンドでキャッシュを無効化します
バックグラウンド同期がエラーを通知せずに失敗するバックグラウンド同期を try/catch で囲んでエラーを記録します。DevTools の Application → Background Sync パネルでテストします
Lighthouse 監査でアプリのインストール可能性が不合格になるHTTPS、512×512 のマスカブルアイコンを含む有効なマニフェスト、登録済みのサービスワーカー、200 を返す start_url を確認します

関連スキル

  • @jamstack-storefront
  • @image-optimization-cdn
  • @edge-commerce
  • @monitoring-alerting-commerce

コメント