mcpField
N
nextjs

nextjs

github.com/blencorp開発111102 回インストール49 週間前に更新
リポジトリ

nextjs について

Next.js 15+ の App Router を使う開発者向けの開発ガイドです。コンポーネント、データ取得、ルーティング、パフォーマンスのパターンを扱います。

主な機能

  1. 01App Router のファイル規約
  2. 02サーバー・クライアントコンポーネントの指針
  3. 03非同期・並列データ取得
  4. 04ルートハンドラーと Server Actions
  5. 05ストリーミングと静的生成

活用シーン

  1. 01ページとネストしたレイアウトの構成
  2. 02サーバーコンポーネントでのデータ取得
  3. 03API ルートとフォーム処理の構築

インストール

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

$npx skills add https://github.com/blencorp/claude-code-kit --skill nextjs

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

同梱ファイル

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

skill-rules-fragment.json
resources/
data-fetching.md
performance.md
routing.md
server-actions.md

詳細

Next.js 開発ガイドライン

App Router、Server Components、最新のデータ取得を使用する Next.js 15 以降の開発パターンです。

基本原則

  1. サーバー優先のアーキテクチャ:Server Components をデフォルトとし、必要な場合にのみ Client Components を使用します
  2. ファイルベースのルーティング:ページ、レイアウト、ルートハンドラーには App Router の規約を使用します
  3. データ取得:Server Components で async/await を使用し、必要な場所でデータを取得します
  4. 型安全性:ルートパラメーター、検索パラメーター、データ型に TypeScript を活用します
  5. パフォーマンス:ストリーミング、並列データ取得、静的生成によって最適化します

App Router の構成

ファイル規約
app/
├── layout.tsx              # Root layout (required)
├── page.tsx               # Home page
├── loading.tsx            # Loading UI
├── error.tsx              # Error boundary
├── not-found.tsx          # 404 page
├── posts/
│   ├── layout.tsx         # Posts layout
│   ├── page.tsx          # /posts
│   ├── [id]/
│   │   └── page.tsx      # /posts/123
│   └── new/
│       └── page.tsx      # /posts/new
└── api/
    └── posts/
        └── route.ts      # API route handler
ページコンポーネント
// app/posts/page.tsx
import { getPosts } from '@/lib/api';

export const metadata = {
  title: 'Posts',
  description: 'Browse all blog posts'
};

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <a href={`/posts/${post.id}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}
動的ルート
// app/posts/[id]/page.tsx
import { getPost } from '@/lib/api';
import { notFound } from 'next/navigation';

interface PageProps {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export async function generateMetadata({ params }: PageProps) {
  const { id } = await params;
  const post = await getPost(id);
  return {
    title: post.title,
    description: post.excerpt
  };
}

export default async function PostPage({ params }: PageProps) {
  const { id } = await params;
  const post = await getPost(id);

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

サーバーコンポーネントとクライアントコンポーネント

サーバーコンポーネント(デフォルト)

次の用途に使用します:

  • データ取得
  • バックエンドリソースへのアクセス
  • 機密情報をサーバー上に保持する
  • クライアント側の JavaScript を削減する
// app/posts/page.tsx (Server Component by default)
import { db } from '@/lib/db';

export default async function PostsPage() {
  // Direct database access
  const posts = await db.post.findMany();

  return <PostList posts={posts} />;
}
クライアントコンポーネント

次の用途に使用します:

  • イベントリスナー(onClick、onChange など)
  • 状態とライフサイクル(useState、useEffect)
  • ブラウザー専用 API
  • カスタムフック
// components/SearchBar.tsx
'use client'; // Required directive

import { useState } from 'react';
import { useRouter } from 'next/navigation';

export function SearchBar() {
  const [query, setQuery] = useState('');
  const router = useRouter();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    router.push(`/search?q=${query}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search posts..."
      />
      <button type="submit">Search</button>
    </form>
  );
}
コンポジションパターン
// app/posts/page.tsx (Server Component)
import { getPosts } from '@/lib/api';
import { SearchBar } from '@/components/SearchBar'; // Client Component

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <div>
      <SearchBar /> {/* Client Component for interactivity */}
      <PostList posts={posts} /> {/* Can be Server Component */}
    </div>
  );
}

データ取得

基本パターン
// Server Component with async/await
export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // Cache for 1 hour
  }).then(res => res.json());

  return <PostList posts={posts} />;
}
並列データ取得
export default async function DashboardPage() {
  // Fetch in parallel
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats()
  ]);

  return (
    <div>
      <UserProfile user={user} />
      <PostList posts={posts} />
      <Stats data={stats} />
    </div>
  );
}
Suspense によるストリーミング
import { Suspense } from 'react';

export default function PostsPage() {
  return (
    <div>
      <h1>Posts</h1>
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
    </div>
  );
}

async function Posts() {
  const posts = await getPosts(); // Slow data fetch
  return <PostList posts={posts} />;
}

レイアウト

ルートレイアウト(必須)
// app/layout.tsx
import './globals.css';

export const metadata = {
  title: {
    default: 'My Blog',
    template: '%s | My Blog'
  }
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>{/* Navigation */}</nav>
        </header>
        <main>{children}</main>
        <footer>{/* Footer */}</footer>
      </body>
    </html>
  );
}
ネストされたレイアウト
// app/posts/layout.tsx
export default function PostsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <aside>
        <PostsSidebar />
      </aside>
      <div>{children}</div>
    </div>
  );
}

ルートハンドラー(API ルート)

基本的なハンドラー
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';

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

export async function POST(request: NextRequest) {
  const body = await request.json();
  const post = await createPost(body);
  return NextResponse.json({ post }, { status: 201 });
}
動的ルートハンドラー
// app/api/posts/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const post = await getPost(params.id);

  if (!post) {
    return NextResponse.json(
      { error: 'Post not found' },
      { status: 404 }
    );
  }

  return NextResponse.json({ post });
}

サーバーアクション

基本的なサーバーアクション
// app/actions/posts.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  const post = await db.post.create({
    data: { title, content }
  });

  revalidatePath('/posts');
  redirect(`/posts/${post.id}`);
}
フォームでの使用
import { createPost } from '@/app/actions/posts';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}

ナビゲーション

Link コンポーネント
import Link from 'next/link';

export function PostCard({ post }: { post: Post }) {
  return (
    <Link href={`/posts/${post.id}`} prefetch={true}>
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
    </Link>
  );
}
プログラムによるナビゲーション
'use client';

import { useRouter } from 'next/navigation';

export function PostActions({ postId }: { postId: string }) {
  const router = useRouter();

  const handleDelete = async () => {
    await deletePost(postId);
    router.push('/posts');
    router.refresh(); // Refresh server components
  };

  return <button onClick={handleDelete}>Delete</button>;
}
ルーターフック
'use client';

import { usePathname, useSearchParams } from 'next/navigation';

const pathname = usePathname(); // /posts/123
const searchParams = useSearchParams(); // ?q=hello
const query = searchParams.get('q');

メタデータ

// Static metadata
export const metadata = {
  title: 'All Posts',
  description: 'Browse our collection of blog posts'
};

// Dynamic metadata
export async function generateMetadata({ params }: PageProps) {
  const { id } = await params;
  const post = await getPost(id);
  return {
    title: post.title,
    description: post.excerpt
  };
}

エラー処理

// app/posts/error.tsx
'use client';

export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

// app/posts/[id]/not-found.tsx
export default function NotFound() {
  return <div>Post Not Found</div>;
}

静的生成と ISR

// Generate static pages at build time
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ id: post.id }));
}

// Revalidate every hour (ISR)
export const revalidate = 3600;

export default async function PostPage({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params;
  const post = await getPost(id);
  return <Post data={post} />;
}

追加リソース

詳しくは、以下を参照してください:

コメント