mcpField
P
pwa-setup

pwa-setup

github.com/dadbodgeoff開発79240 回インストール10 週間前に更新
リポジトリ

pwa-setup について

pwa-setupは、インストール可能なアプリ風のブラウザー体験を構成する開発スキルです。マニフェスト、モバイル表示、安全領域、インストールプロンプトを扱い、Next.jsの例も含みます。

主な機能

  1. 01Web App Manifestの設定
  2. 02モバイルメタデータの設定
  3. 03ノッチ端末の安全領域対応
  4. 04インストールプロンプトのHookとバナー
  5. 05PWAアイコンの要件

活用シーン

  1. 01Webアプリのインストール対応
  2. 02モバイルユーザー向けホーム画面追加
  3. 03ノッチ端末向けレイアウト調整

インストール

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

$npx skills add https://github.com/dadbodgeoff/drift --skill pwa-setup

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

詳細

PWA のセットアップ

アプリのようなブラウザー体験を実現するためのプログレッシブウェブアプリの設定です。

このスキルを使う場面

  • ユーザーがウェブアプリを「インストール」したい場合
  • モバイルユーザーがホーム画面からアクセスしたい場合
  • ネイティブアプリを使わずに、アプリのような動作を実現したい場合
  • ノッチのあるデバイス(iPhone など)に対応する場合

基本概念

PWA には以下が必要です。

  1. ウェブアプリマニフェスト - アプリのメタデータとアイコン
  2. モバイル向けメタタグ - ビューポートとテーマの設定
  3. セーフエリア - ノッチのあるデバイスへの対応
  4. インストールプロンプト - カスタムのインストール体験

実装

TypeScript(Next.js)
// app/manifest.ts
import type { MetadataRoute } from 'next';

export default function manifest(): MetadataRoute.Manifest {
  return {
    name: "My SaaS App",
    short_name: "MySaaS",
    description: "Your app description here",
    start_url: '/dashboard',
    display: 'standalone',
    background_color: '#0f172a',
    theme_color: '#14b8a6',
    orientation: 'portrait-primary',
    
    icons: [
      {
        src: '/icons/icon-192.png',
        sizes: '192x192',
        type: 'image/png',
        purpose: 'any',
      },
      {
        src: '/icons/icon-512.png',
        sizes: '512x512',
        type: 'image/png',
        purpose: 'any',
      },
      {
        src: '/icons/icon-maskable.png',
        sizes: '512x512',
        type: 'image/png',
        purpose: 'maskable',
      },
    ],
    
    shortcuts: [
      { name: 'Dashboard', url: '/dashboard', description: 'Go to dashboard' },
      { name: 'Settings', url: '/settings', description: 'App settings' },
    ],
    
    categories: ['productivity', 'utilities'],
  };
}
ルートレイアウトのメタデータ
// app/layout.tsx
import type { Metadata, Viewport } from 'next';

export const metadata: Metadata = {
  title: "My SaaS App",
  description: "Your app description",
  
  appleWebApp: {
    capable: true,
    statusBarStyle: 'black-translucent',
    title: "MySaaS",
  },
  
  applicationName: "MySaaS",
  
  openGraph: {
    title: "My SaaS App",
    description: "Your app description",
    type: 'website',
    siteName: "MySaaS",
  },
};

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  maximumScale: 1,
  userScalable: false,
  themeColor: '#14b8a6',
  viewportFit: 'cover',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className="bg-neutral-900 text-neutral-cream min-h-screen">
        {children}
      </body>
    </html>
  );
}
セーフエリアの CSS
/* globals.css */

/* Safe area for bottom navigation (notched devices) */
.safe-area-bottom {
  padding-bottom: env(safe-area-inset-bottom, 0);
}

/* Safe area for top header */
.safe-area-top {
  padding-top: env(safe-area-inset-top, 0);
}

/* Full safe area padding */
.safe-area-all {
  padding-top: env(safe-area-inset-top, 0);
  padding-right: env(safe-area-inset-right, 0);
  padding-bottom: env(safe-area-inset-bottom, 0);
  padding-left: env(safe-area-inset-left, 0);
}
インストールプロンプトのフック
// hooks/useInstallPrompt.ts
'use client';

import { useState, useEffect } from 'react';

interface BeforeInstallPromptEvent extends Event {
  prompt: () => Promise<void>;
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}

export function useInstallPrompt() {
  const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
  const [isInstalled, setIsInstalled] = useState(false);

  useEffect(() => {
    if (window.matchMedia('(display-mode: standalone)').matches) {
      setIsInstalled(true);
      return;
    }

    const handler = (e: Event) => {
      e.preventDefault();
      setInstallPrompt(e as BeforeInstallPromptEvent);
    };

    window.addEventListener('beforeinstallprompt', handler);
    return () => window.removeEventListener('beforeinstallprompt', handler);
  }, []);

  const promptInstall = async () => {
    if (!installPrompt) return false;

    await installPrompt.prompt();
    const { outcome } = await installPrompt.userChoice;

    if (outcome === 'accepted') {
      setIsInstalled(true);
      setInstallPrompt(null);
    }

    return outcome === 'accepted';
  };

  return {
    canInstall: !!installPrompt && !isInstalled,
    isInstalled,
    promptInstall,
  };
}
インストールバナーコンポーネント
// components/InstallBanner.tsx
'use client';

import { useInstallPrompt } from '@/hooks/useInstallPrompt';

export function InstallBanner() {
  const { canInstall, promptInstall } = useInstallPrompt();

  if (!canInstall) return null;

  return (
    <div className="fixed bottom-20 left-4 right-4 bg-primary-600 text-white p-4 rounded-lg shadow-lg md:hidden">
      <p className="text-sm mb-2">Install our app for a better experience</p>
      <button
        onClick={promptInstall}
        className="w-full py-2 bg-white text-primary-600 rounded font-medium"
      >
        Install App
      </button>
    </div>
  );
}
セーフエリアに対応したモバイルナビゲーション
// components/MobileNav.tsx
export function MobileNav() {
  return (
    <nav className="fixed bottom-0 left-0 right-0 bg-neutral-800 border-t border-neutral-700 z-30 md:hidden safe-area-bottom">
      <div className="flex justify-around py-2">
        <NavItem href="/dashboard" icon={HomeIcon} label="Home" />
        <NavItem href="/search" icon={SearchIcon} label="Search" />
        <NavItem href="/settings" icon={SettingsIcon} label="Settings" />
      </div>
    </nav>
  );
}

アイコンの要件

public/
├── icons/
│   ├── icon-192.png      # Standard icon
│   ├── icon-512.png      # Large icon
│   └── icon-maskable.png # Maskable (with safe zone padding)
├── favicon.ico
└── apple-touch-icon.png  # 180x180 for iOS
マスカブルアイコンのセーフゾーン
┌─────────────────────┐
│                     │
│   ┌───────────┐     │
│   │   LOGO    │     │  ← Content in center 80%
│   └───────────┘     │
│                     │
└─────────────────────┘

使用例

PWA のテスト
  1. Chrome DevTools → アプリケーション → マニフェスト
  2. Lighthouse → PWA 監査
  3. モバイル → ホーム画面に追加
スタンドアロンモードの検出
function isStandalone(): boolean {
  return window.matchMedia('(display-mode: standalone)').matches ||
         (window.navigator as any).standalone === true;
}

ベストプラクティス

  1. セーフゾーン付きのマスカブルアイコンを使う
  2. ブランドに合うように theme_color を設定する
  3. ノッチのあるデバイス向けにセーフエリアを処理する
  4. 適切なタイミングでインストールプロンプトを表示する
  5. 実際のモバイルデバイスでテストする

よくある間違い

  • マスカブルアイコンがない(Android で見栄えが悪くなる)
  • セーフエリアへの対応がない(ノッチの下にコンテンツが表示される)
  • インストールプロンプトをすぐに表示する(煩わしい)
  • start_url が間違っている(別のページが開く)
  • apple-touch-icon がない(iOS のフォールバック)

関連パターン

  • design-tokens - 一貫性のあるテーマ設定
  • mobile-components - レスポンシブコンポーネント

コメント