mcpField
V
vercel-development

vercel-development

github.com/mindrallyクラウド・DevOps287719 回インストール3 週間前に更新
リポジトリ

vercel-development について

Vercel でアプリを開発・デプロイするチーム向けのガイダンスです。Next.js、React Server Components、Vercel AI SDK を中心に、実装、性能、設定、デプロイを扱います。

主な機能

  1. 01Next.js App Router の指針
  2. 02React Server Components の実践
  3. 03TypeScript と API ルートのパターン
  4. 04Vercel AI SDK 連携
  5. 05性能とアクセシビリティの指針
  6. 06Vercel の設定とデプロイ

活用シーン

  1. 01Next.js アプリの構成設計
  2. 02ストリーミングチャット UI の構築
  3. 03アプリ性能の最適化
  4. 04Vercel デプロイの準備

インストール

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

$npx skills add https://github.com/mindrally/skills --skill vercel-development

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

詳細

Vercel 開発のベストプラクティス

概要

このスキルでは、Next.js、React Server Components、Edge Functions、Vercel AI SDK に重点を置き、Vercel 上でアプリケーションを開発およびデプロイするための包括的なガイドラインを提供します。

基本原則

  • 正確な例を用いて、簡潔で技術的な TypeScript コードを記述します
  • 関数型および宣言型のプログラミングパターンを使用し、クラスは避けます
  • 'use client'、'useEffect'、'setState' の使用を最小限に抑え、React Server Components(RSC)を優先します
  • モバイルファーストのアプローチで Tailwind CSS を使用し、レスポンシブデザインを実装します
  • Core Web Vitals とパフォーマンスを最適化します

プロジェクト構成

my-app/
├── app/                    # App Router pages and layouts
│   ├── (auth)/            # Route groups
│   ├── api/               # API routes
│   ├── layout.tsx         # Root layout
│   └── page.tsx           # Home page
├── components/            # React components
│   ├── ui/               # UI primitives
│   └── features/         # Feature components
├── lib/                   # Utility functions
├── hooks/                 # Custom React hooks
├── types/                 # TypeScript types
├── public/               # Static assets
└── vercel.json           # Vercel configuration

Next.js App Router のガイドライン

ファイル命名規則
  • ディレクトリ名には小文字とハイフンを使用します(例:components/auth-wizard)
  • コンポーネントと関数には名前付きエクスポートを優先します
  • ルートページには page.tsx、レイアウトには layout.tsx を使用します
  • 読み込み状態には loading.tsx、エラーバウンダリーには error.tsx を使用します
サーバーコンポーネント(デフォルト)
// app/users/page.tsx
import { getUsers } from '@/lib/data';

export default async function UsersPage() {
  const users = await getUsers();

  return (
    <main>
      <h1>Users</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </main>
  );
}
クライアントコンポーネント(必要な場合)
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

TypeScript の標準

型定義
// Use interfaces over types for object shapes
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

// Use types for unions and complex types
type Status = 'pending' | 'active' | 'inactive';

// Avoid enums; use const objects instead
const STATUS = {
  PENDING: 'pending',
  ACTIVE: 'active',
  INACTIVE: 'inactive',
} as const;

type StatusValue = typeof STATUS[keyof typeof STATUS];
コンポーネントの Props
interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  onClick?: () => void;
}

export function Button({
  children,
  variant = 'primary',
  size = 'md',
  disabled = false,
  onClick,
}: ButtonProps) {
  // Implementation
}

API ルート

ルートハンドラー
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

export async function GET(request: NextRequest) {
  const users = await getUsers();
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const validated = CreateUserSchema.parse(body);

    const user = await createUser(validated);
    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Validation failed', details: error.errors },
        { status: 400 }
      );
    }
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}
Edge Runtime
// app/api/edge-function/route.ts
export const runtime = 'edge';

export async function GET(request: Request) {
  return new Response(JSON.stringify({ message: 'Hello from the edge!' }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

Vercel AI SDK の統合

ストリーミングチャット UI
'use client';

import { useChat } from 'ai/react';

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: '/api/chat',
  });

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto">
        {messages.map(message => (
          <div key={message.id} className={message.role === 'user' ? 'text-right' : ''}>
            <p>{message.content}</p>
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Type a message..."
          disabled={isLoading}
        />
      </form>
    </div>
  );
}
AI API ルート
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';

export async function POST(request: Request) {
  const { messages } = await request.json();

  const result = await streamText({
    model: openai('gpt-4-turbo'),
    messages,
    system: 'You are a helpful assistant.',
  });

  return result.toDataStreamResponse();
}
AI のエラー処理
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';

export async function POST(request: Request) {
  try {
    const { messages } = await request.json();

    const result = await streamText({
      model: openai('gpt-4-turbo'),
      messages,
    });

    return result.toDataStreamResponse();
  } catch (error) {
    // Handle rate limiting
    if (error.message?.includes('rate limit')) {
      return new Response('Rate limit exceeded. Please try again later.', {
        status: 429,
      });
    }

    // Handle quota exceeded
    if (error.message?.includes('quota')) {
      return new Response('API quota exceeded.', { status: 402 });
    }

    // Fallback to alternative model
    console.error('Primary model failed:', error);
    return new Response('Service temporarily unavailable.', { status: 503 });
  }
}

データ取得

サーバーサイドでのデータ取得
// Fetch data in Server Components
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 }, // Cache for 1 hour
  });

  if (!res.ok) {
    throw new Error('Failed to fetch data');
  }

  return res.json();
}

export default async function Page() {
  const data = await getData();
  return <div>{/* Render data */}</div>;
}
URL 状態管理
// Use URL query parameters for server state
import { useSearchParams, useRouter } from 'next/navigation';

export function Filters() {
  const searchParams = useSearchParams();
  const router = useRouter();

  const updateFilter = (key: string, value: string) => {
    const params = new URLSearchParams(searchParams);
    params.set(key, value);
    router.push(`?${params.toString()}`);
  };

  return (/* Filter UI */);
}

パフォーマンス最適化

画像の最適化
import Image from 'next/image';

export function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority // Load immediately for LCP
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}
動的インポート
import dynamic from 'next/dynamic';

// Lazy load heavy components
const HeavyChart = dynamic(() => import('@/components/heavy-chart'), {
  loading: () => <div>Loading chart...</div>,
  ssr: false, // Disable SSR if needed
});
Suspense バウンダリー
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<Loading />}>
        <AsyncComponent />
      </Suspense>
    </div>
  );
}

エラー処理

エラーバウンダリー
// app/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}
グローバルエラーハンドラー
// app/global-error.tsx
'use client';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={reset}>Try again</button>
      </body>
    </html>
  );
}

Vercel の設定

vercel.json
{
  "framework": "nextjs",
  "regions": ["iad1"],
  "crons": [
    {
      "path": "/api/cron/cleanup",
      "schedule": "0 0 * * *"
    }
  ],
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Access-Control-Allow-Origin", "value": "*" }
      ]
    }
  ]
}
環境変数
// Use environment variables for sensitive data
const apiKey = process.env.API_KEY;
const publicUrl = process.env.NEXT_PUBLIC_APP_URL;

// Validate required env vars
if (!apiKey) {
  throw new Error('API_KEY environment variable is required');
}

デプロイ

プレビューデプロイ
  • すべてのプルリクエストにプレビューデプロイが作成されます
  • テストとレビューにはプレビュー URL を使用します
  • プレビューリンクを関係者と共有します
本番デプロイ
# Connect repo to Vercel (one-click from GitHub)
# Or use Vercel CLI
vercel --prod
Edge Config
import { get } from '@vercel/edge-config';

export async function getFeatureFlag(flag: string) {
  const flags = await get('featureFlags');
  return flags?.[flag] ?? false;
}

UI とスタイル設定

Tailwind CSS のセットアップ
// Use mobile-first responsive design
export function Card({ children }: { children: React.ReactNode }) {
  return (
    <div className="p-4 md:p-6 lg:p-8 rounded-lg bg-white shadow-sm">
      {children}
    </div>
  );
}
Shadcn UI コンポーネント
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';

export function ConfirmDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="destructive">Delete</Button>
      </DialogTrigger>
      <DialogContent>
        <p>Are you sure?</p>
      </DialogContent>
    </Dialog>
  );
}

アクセシビリティ

ベストプラクティス
export function AccessibleButton() {
  return (
    <button
      aria-label="Close dialog"
      aria-expanded={isOpen}
      aria-controls="dialog-content"
      className="focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
    >
      <XIcon aria-hidden="true" />
    </button>
  );
}

避けるべきよくある落とし穴

  1. 'use client' を不必要に使用する
  2. 適切なエラーバウンダリーを実装しない
  3. Core Web Vitals の最適化を無視する
  4. TypeScript を厳格に使用しない
  5. 環境変数をハードコードする
  6. 非同期コンポーネントに Suspense バウンダリーを設けない
  7. 画像を最適化しない
  8. アクセシビリティ要件を無視する

コメント