mcpField
N
nextjs-development

nextjs-development

github.com/manutej開発62472 回インストール40 週間前に更新
リポジトリ

nextjs-development について

App Router とフルスタック構成で Next.js アプリを開発するためのスキルです。ルーティング、レンダリング、データ取得、API ハンドラー、本番向けの実践を扱い、Web アプリやコンテンツサイトに役立ちます。

主な機能

  1. 01App Router とファイルベースルーティング
  2. 02Server Components と Client Components
  3. 03キャッシュとデータ再検証
  4. 04API Route Handlers とミドルウェア
  5. 05メタデータと SEO のパターン
  6. 06画像最適化と Server Actions

活用シーン

  1. 01フルスタック React アプリの構築
  2. 02段階的な再検証を行う静的サイトの作成
  3. 03API バックエンドとルートハンドラーの実装
  4. 04SEO を重視したコンテンツサイトの開発

インストール

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

$npx skills add https://github.com/manutej/luxor-claude-marketplace --skill nextjs-development

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

同梱ファイルEXAMPLES.mdREADME.md

詳細

Next.js 開発スキル

このスキルでは、公式の Next.js ドキュメントに基づき、App Router、Server Components、データ取得パターン、ルーティング、API ルート、ミドルウェア、フルスタック開発の手法を活用して、最新の Next.js アプリケーションを構築するための包括的なガイダンスを提供します。

このスキルを使用する場面

次のような場合に、このスキルを使用してください。

  • サーバーサイドレンダリング(SSR)を備えたフルスタック React アプリケーションを構築する
  • 増分静的再生成(ISR)を使用した静的サイトを作成する
  • React Server Components を使用した最新の Web アプリケーションを開発する
  • サーバーレスのルートハンドラーで API バックエンドを構築する
  • メタデータと Open Graph を使用して、SEO に最適化されたアプリケーションを実装する
  • 組み込みの最適化機能を備えた、本番環境対応の Web アプリケーションを作成する
  • E コマース、ブログ、ダッシュボード、コンテンツ中心のサイトを構築する
  • 認証、データ取得、複雑なルーティングパターンを実装する
  • 画像、フォント、パフォーマンスを自動的に最適化する
  • エッジコンピューティング機能を備えたサーバーレスアプリケーションをデプロイする

基本概念

App Router

App Router は、React Server Components を基盤とする Next.js の最新のルーティングシステムです。app ディレクトリを使用したファイルベースのルーティングに、拡張された機能を加えます。

基本的なアプリ構成:

app/
├── layout.tsx          # Root layout (required)
├── page.tsx            # Home page
├── loading.tsx         # Loading UI
├── error.tsx           # Error UI
├── not-found.tsx       # 404 page
├── about/
│   └── page.tsx        # /about route
└── blog/
    ├── page.tsx        # /blog route
    ├── [slug]/
    │   └── page.tsx    # /blog/[slug] dynamic route
    └── layout.tsx      # Blog layout

ルートレイアウト(必須):

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

ページコンポーネント:

// app/page.tsx
export default function HomePage() {
  return (
    <main>
      <h1>Welcome to Next.js</h1>
      <p>Building modern web applications</p>
    </main>
  )
}
Server Components

Server Components は、サーバー上でレンダリングされる React コンポーネントです。App Router ではデフォルトで使用され、パフォーマンスを向上させます。

Server Component(デフォルト):

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'force-cache' // Static generation
  })
  return res.json()
}

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

  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

Client Component(必要な場合):

'use client'

import { useState } from 'react'

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  )
}

Server Components と Client Components の組み合わせ:

// app/dashboard/page.tsx (Server Component)
import ClientCounter from './ClientCounter'

async function getData() {
  const res = await fetch('https://api.example.com/data')
  return res.json()
}

export default async function DashboardPage() {
  const data = await getData()

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Server data: {data.value}</p>
      {/* Client component for interactivity */}
      <ClientCounter />
    </div>
  )
}
データ取得

Next.js は、ネイティブの fetch() API を自動キャッシュと再検証に対応するよう拡張しています。

静的データ取得(デフォルト):

async function getStaticData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache' // Default, equivalent to getStaticProps
  })
  return res.json()
}

export default async function Page() {
  const data = await getStaticData()
  return <div>{data.title}</div>
}

動的データ取得:

async function getDynamicData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store' // Equivalent to getServerSideProps
  })
  return res.json()
}

export default async function Page() {
  const data = await getDynamicData()
  return <div>{data.title}</div>
}

再検証(ISR):

async function getRevalidatedData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 } // Revalidate every 60 seconds
  })
  return res.json()
}

export default async function Page() {
  const data = await getRevalidatedData()
  return <div>{data.title}</div>
}

並列データ取得:

async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`)
  return res.json()
}

async function getUserPosts(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}/posts`)
  return res.json()
}

export default async function UserPage({ params }: { params: { id: string } }) {
  // Fetch in parallel
  const [user, posts] = await Promise.all([
    getUser(params.id),
    getUserPosts(params.id)
  ])

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

逐次データ取得:

async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`)
  return res.json()
}

async function getRecommendations(preferences: string[]) {
  const res = await fetch('https://api.example.com/recommendations', {
    method: 'POST',
    body: JSON.stringify({ preferences })
  })
  return res.json()
}

export default async function UserPage({ params }: { params: { id: string } }) {
  // First fetch user
  const user = await getUser(params.id)

  // Then fetch recommendations based on user data
  const recommendations = await getRecommendations(user.preferences)

  return (
    <div>
      <h1>{user.name}</h1>
      <h2>Recommendations</h2>
      <ul>
        {recommendations.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  )
}
ルーティング

Next.js は app ディレクトリで、ファイルシステムベースのルーティングを使用します。

動的ルート:

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>Post: {params.slug}</h1>
}

// Generates static pages for these slugs at build time
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())

  return posts.map((post) => ({
    slug: post.slug,
  }))
}

キャッチオールルート:

// app/docs/[...slug]/page.tsx
export default function DocsPage({ params }: { params: { slug: string[] } }) {
  // /docs/a/b/c -> params.slug = ['a', 'b', 'c']
  return <h1>Docs: {params.slug.join('/')}</h1>
}

オプショナルキャッチオールルート:

// app/shop/[[...slug]]/page.tsx
export default function ShopPage({ params }: { params: { slug?: string[] } }) {
  // /shop -> params.slug = undefined
  // /shop/clothes -> params.slug = ['clothes']
  // /shop/clothes/tops -> params.slug = ['clothes', 'tops']
  return <h1>Shop: {params.slug?.join('/') || 'All'}</h1>
}

ルートグループ:

app/
├── (marketing)/          # Route group (not in URL)
│   ├── about/
│   │   └── page.tsx      # /about
│   └── contact/
│       └── page.tsx      # /contact
└── (shop)/
    ├── products/
    │   └── page.tsx      # /products
    └── cart/
        └── page.tsx      # /cart

並列ルート:

app/
├── @analytics/
│   └── page.tsx
├── @team/
│   └── page.tsx
└── layout.tsx

// app/layout.tsx
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {children}
      {analytics}
      {team}
    </>
  )
}

インターセプトルート:

app/
├── feed/
│   └── page.tsx
├── photo/
│   └── [id]/
│       └── page.tsx
└── @modal/
    └── (.)photo/
        └── [id]/
            └── page.tsx  # Intercepts /photo/[id] when navigating from /feed
レイアウト

レイアウトはページを囲み、ナビゲーションをまたいで状態を保持します。

ネストされたレイアウト:

// app/layout.tsx (Root Layout)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>Global Navigation</nav>
        </header>
        {children}
        <footer>Global Footer</footer>
      </body>
    </html>
  )
}

// app/dashboard/layout.tsx (Dashboard Layout)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="dashboard">
      <aside>Dashboard Sidebar</aside>
      <main>{children}</main>
    </div>
  )
}

// app/dashboard/page.tsx
export default function DashboardPage() {
  return <h1>Dashboard</h1>
}

テンプレート(ナビゲーション時に再レンダリング):

// app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
  return (
    <div>
      {/* This creates a new instance on each navigation */}
      {children}
    </div>
  )
}
読み込み UI

専用の loading.tsx ファイルを使用すると、React Suspense による読み込み状態を作成できます。

読み込み状態:

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="spinner">
      <p>Loading dashboard...</p>
    </div>
  )
}

// app/dashboard/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/data')
  return res.json()
}

export default async function DashboardPage() {
  const data = await getData()
  return <div>{data.content}</div>
}

Suspense によるストリーミング:

// app/page.tsx
import { Suspense } from 'react'

async function SlowComponent() {
  await new Promise(resolve => setTimeout(resolve, 3000))
  return <div>Slow data loaded</div>
}

export default function Page() {
  return (
    <div>
      <h1>Page</h1>
      <Suspense fallback={<div>Loading slow component...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}
エラー処理

専用の error.tsx ファイルを使用すると、エラーバウンダリでエラーを処理できます。

エラーバウンダリ:

// app/error.tsx
'use client' // Error components must be Client Components

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <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>Application Error</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

見つからない場合:

// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <div>
      <h2>Page Not Found</h2>
      <p>Could not find the requested resource</p>
      <Link href="/">Return Home</Link>
    </div>
  )
}
API ルート

ルートハンドラーを使うと、Web Request API と Response API を使用して API エンドポイントを作成できます。

基本的な API ルート:

// app/api/hello/route.ts
export async function GET(request: Request) {
  return Response.json({ message: 'Hello from Next.js!' })
}

動的ルートハンドラー:

// app/api/posts/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const id = params.id
  const post = await db.post.findUnique({ where: { id } })

  if (!post) {
    return new Response('Post not found', { status: 404 })
  }

  return Response.json(post)
}

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } }
) {
  await db.post.delete({ where: { id: params.id } })
  return new Response(null, { status: 204 })
}

ボディ付き POST リクエスト:

// app/api/posts/route.ts
export async function POST(request: Request) {
  const body = await request.json()

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

  return Response.json(post, { status: 201 })
}

ヘッダー付きリクエスト:

// app/api/protected/route.ts
export async function GET(request: Request) {
  const token = request.headers.get('authorization')

  if (!token) {
    return new Response('Unauthorized', { status: 401 })
  }

  const user = await verifyToken(token)
  return Response.json({ user })
}

検索パラメーター:

// app/api/search/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const query = searchParams.get('q')
  const page = searchParams.get('page') || '1'

  const results = await search(query, parseInt(page))

  return Response.json(results)
}

CORS ヘッダー:

// app/api/public/route.ts
export async function GET(request: Request) {
  const data = { message: 'Public API' }

  return Response.json(data, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}

export async function OPTIONS(request: Request) {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}
ミドルウェア

ミドルウェアはリクエストの完了前に実行され、レスポンスを変更できます。

基本的なミドルウェア:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Clone the request headers
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-custom-header', 'custom-value')

  // Return response with modified headers
  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })
}

export const config = {
  matcher: '/api/:path*',
}

認証ミドルウェア:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value

  // Redirect to login if no token
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  // Redirect to dashboard if already logged in
  if (token && request.nextUrl.pathname === '/login') {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/login'],
}

位置情報とリライト:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US'

  // Rewrite based on country
  if (country === 'GB') {
    return NextResponse.rewrite(new URL('/gb' + request.nextUrl.pathname, request.url))
  }

  return NextResponse.next()
}

レート制限:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
})

export async function middleware(request: NextRequest) {
  const ip = request.ip ?? '127.0.0.1'
  const { success } = await ratelimit.limit(ip)

  if (!success) {
    return new Response('Too Many Requests', { status: 429 })
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/api/:path*',
}
メタデータと SEO

Next.js には、ページのメタデータを定義するための Metadata API が用意されています。

静的メタデータ:

// app/about/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'About Us',
  description: 'Learn more about our company',
  openGraph: {
    title: 'About Us',
    description: 'Learn more about our company',
    images: ['/og-image.jpg'],
  },
  twitter: {
    card: 'summary_large_image',
  },
}

export default function AboutPage() {
  return <h1>About Us</h1>
}

動的メタデータ:

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

type Props = {
  params: { slug: string }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
      type: 'article',
      publishedTime: post.publishedAt,
    },
  }
}

export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug)
  return <article>{post.content}</article>
}

アイコン付きメタデータ:

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    default: 'My App',
    template: '%s | My App',
  },
  description: 'My application description',
  icons: {
    icon: '/favicon.ico',
    apple: '/apple-icon.png',
  },
  manifest: '/manifest.json',
}
画像の最適化

Next.js は Image コンポーネントを使用して、画像を自動的に最適化します。

基本的な画像:

import Image from 'next/image'

export default function Page() {
  return (
    <Image
      src="/profile.jpg"
      alt="Profile picture"
      width={500}
      height={500}
    />
  )
}

リモート画像:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
    ],
  },
}

// Component
import Image from 'next/image'

export default function Page() {
  return (
    <Image
      src="https://images.unsplash.com/photo-1234567890"
      alt="Photo"
      width={800}
      height={600}
      priority // Load image with high priority
    />
  )
}

コンテナーに合わせて表示:

import Image from 'next/image'

export default function Page() {
  return (
    <div style={{ position: 'relative', width: '100%', height: '400px' }}>
      <Image
        src="/background.jpg"
        alt="Background"
        fill
        style={{ objectFit: 'cover' }}
      />
    </div>
  )
}

レスポンシブ画像:

import Image from 'next/image'

export default function Page() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1920}
      height={1080}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
  )
}
フォントの最適化

Next.js は next/font を使用して、フォントを自動的に最適化します。

Google Fonts:

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}

ローカルフォント:

// app/layout.tsx
import localFont from 'next/font/local'

const myFont = localFont({
  src: './fonts/my-font.woff2',
  display: 'swap',
  variable: '--font-my-font',
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={myFont.variable}>
      <body>{children}</body>
    </html>
  )
}

ワークフローパターン

新しいページの作成
  1. app ディレクトリに新しいフォルダーを作成します
  2. page.tsx ファイルを追加します
  3. デフォルトコンポーネントをエクスポートします
  4. 必要に応じて、レイアウト、読み込み、エラーファイルを追加します
// app/products/page.tsx
export default function ProductsPage() {
  return <h1>Products</h1>
}

// app/products/layout.tsx
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <nav>Products Navigation</nav>
      {children}
    </div>
  )
}

// app/products/loading.tsx
export default function Loading() {
  return <div>Loading products...</div>
}
サーバーアクション

サーバーアクションを使用すると、クライアントコンポーネントからサーバー側のコードを直接実行できます。

サーバーアクションを使ったフォーム:

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

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

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

  revalidatePath('/posts')
}

// app/new-post/page.tsx
import { createPost } from '../actions'

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

useTransition を使ったサーバーアクション:

// app/actions.ts
'use server'

export async function updateUser(userId: string, data: UserData) {
  await db.user.update({
    where: { id: userId },
    data,
  })
  return { success: true }
}

// app/profile/page.tsx
'use client'

import { useTransition } from 'react'
import { updateUser } from '../actions'

export default function ProfilePage() {
  const [isPending, startTransition] = useTransition()

  const handleSubmit = (formData: FormData) => {
    startTransition(async () => {
      await updateUser('user-id', {
        name: formData.get('name') as string,
      })
    })
  }

  return (
    <form action={handleSubmit}>
      <input name="name" />
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
    </form>
  )
}
認証パターン

ミドルウェアを使った基本認証:

// lib/auth.ts
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

export async function getSession() {
  const session = cookies().get('session')?.value
  if (!session) return null
  return await verifySession(session)
}

export async function requireAuth() {
  const session = await getSession()
  if (!session) {
    redirect('/login')
  }
  return session
}

// app/dashboard/page.tsx
import { requireAuth } from '@/lib/auth'

export default async function DashboardPage() {
  const session = await requireAuth()

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
    </div>
  )
}

// app/api/login/route.ts
import { cookies } from 'next/headers'

export async function POST(request: Request) {
  const { email, password } = await request.json()

  const user = await validateCredentials(email, password)

  if (!user) {
    return Response.json({ error: 'Invalid credentials' }, { status: 401 })
  }

  const session = await createSession(user)
  cookies().set('session', session, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // 1 week
  })

  return Response.json({ success: true })
}
データベースパターン

サーバーコンポーネントでの Prisma:

// lib/db.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = global as unknown as { prisma: PrismaClient }

export const prisma = globalForPrisma.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

// app/posts/page.tsx
import { prisma } from '@/lib/db'

async function getPosts() {
  return await prisma.post.findMany({
    orderBy: { createdAt: 'desc' },
    include: { author: true },
  })
}

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

  return (
    <div>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>By {post.author.name}</p>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  )
}
キャッシュ戦略

オンデマンドでの再検証:

// app/actions.ts
'use server'

import { revalidatePath, revalidateTag } from 'next/cache'

export async function createPost(data: PostData) {
  await db.post.create({ data })

  // Revalidate specific path
  revalidatePath('/posts')

  // Or revalidate by tag
  revalidateTag('posts')
}

// Fetch with cache tags
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] }
  })
  return res.json()
}

時間ベースの再検証:

async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 } // Revalidate every hour
  })
  return res.json()
}

ルートセグメントの設定:

// app/blog/page.tsx
export const revalidate = 3600 // Revalidate every hour
export const dynamic = 'force-static' // Force static generation
export const fetchCache = 'force-cache' // Force cache for all fetch requests

export default async function BlogPage() {
  const posts = await getPosts()
  return <div>{/* ... */}</div>
}

ベストプラクティス

1. デフォルトでサーバーコンポーネントを使用する

サーバーコンポーネントはデフォルトで使用され、パフォーマンスが向上します。必要な場合にのみクライアントコンポーネントを使用してください。

// ✅ Good - Server Component
async function getData() {
  const res = await fetch('https://api.example.com/data')
  return res.json()
}

export default async function Page() {
  const data = await getData()
  return <div>{data.title}</div>
}

// ✅ Good - Client Component only when needed
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}
2. 必要な場所でデータを取得する

データを props 経由で複数の階層に渡さないでください。必要とするコンポーネント内でデータを取得してください。

// ✅ Good - Fetch where needed
async function Header() {
  const user = await getUser()
  return <div>Welcome, {user.name}</div>
}

async function Posts() {
  const posts = await getPosts()
  return <div>{/* render posts */}</div>
}

export default function Page() {
  return (
    <>
      <Header />
      <Posts />
    </>
  )
}
3. 並列データ取得を使用する

可能な場合はデータを並列に取得して、読み込み時間を短縮してください。

// ✅ Good - Parallel fetching
export default async function Page() {
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts()
  ])

  return <div>{/* ... */}</div>
}
4. 画像を最適化する

自動最適化のために、常に Image コンポーネントを使用してください。

// ✅ Good
import Image from 'next/image'

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  priority
/>

// ❌ Bad
<img src="/hero.jpg" alt="Hero" />
5. SEO に Metadata API を使用する

すべてのページにメタデータを定義してください。

// ✅ Good
export const metadata = {
  title: 'My Page',
  description: 'Page description',
}

export default function Page() {
  return <div>Content</div>
}
6. エラーバウンダリを実装する

エラー処理のために error.tsx ファイルを追加してください。

// app/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>
  )
}
7. 読み込み状態を使用する

読み込み UI のために loading.tsx ファイルを追加してください。

// app/loading.tsx
export default function Loading() {
  return <div>Loading...</div>
}
8. API にルートハンドラーを実装する

pages ディレクトリの API ルートではなく、ルートハンドラーを使用してください。

// ✅ Good - app/api/users/route.ts
export async function GET(request: Request) {
  const users = await db.user.findMany()
  return Response.json(users)
}
9. 共通ロジックにミドルウェアを使用する

認証、リダイレクト、リライトをミドルウェアに実装してください。

// middleware.ts
export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}
10. フォントを最適化する

自動フォント最適化には next/font を使用してください。

// ✅ Good
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export default function RootLayout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

一般的なパターン

ブログパターン
// app/blog/page.tsx
import Link from 'next/link'

async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }
  })
  return res.json()
}

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

  return (
    <div>
      <h1>Blog</h1>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>
            <Link href={`/blog/${post.slug}`}>{post.title}</Link>
          </h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  if (!res.ok) return null
  return res.json()
}

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  return posts.map((post) => ({ slug: post.slug }))
}

export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)
  if (!post) return {}

  return {
    title: post.title,
    description: post.excerpt,
  }
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound()
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}
E コマースパターン
// app/products/page.tsx
import { Suspense } from 'react'
import ProductGrid from './ProductGrid'
import Filters from './Filters'

export default function ProductsPage({
  searchParams,
}: {
  searchParams: { category?: string; sort?: string }
}) {
  return (
    <div>
      <h1>Products</h1>
      <Filters />
      <Suspense fallback={<div>Loading products...</div>}>
        <ProductGrid searchParams={searchParams} />
      </Suspense>
    </div>
  )
}

// app/products/ProductGrid.tsx
async function getProducts(category?: string, sort?: string) {
  const params = new URLSearchParams()
  if (category) params.set('category', category)
  if (sort) params.set('sort', sort)

  const res = await fetch(`https://api.example.com/products?${params}`, {
    next: { revalidate: 300 }
  })
  return res.json()
}

export default async function ProductGrid({
  searchParams,
}: {
  searchParams: { category?: string; sort?: string }
}) {
  const products = await getProducts(searchParams.category, searchParams.sort)

  return (
    <div className="grid">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

// app/products/[id]/page.tsx
async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`)
  return res.json()
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id)

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>${product.price}</p>
      <AddToCartButton productId={product.id} />
    </div>
  )
}
ダッシュボードパターン
// app/dashboard/layout.tsx
import { requireAuth } from '@/lib/auth'
import Sidebar from './Sidebar'

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const session = await requireAuth()

  return (
    <div className="dashboard">
      <Sidebar user={session.user} />
      <main>{children}</main>
    </div>
  )
}

// app/dashboard/page.tsx
import { Suspense } from 'react'
import Stats from './Stats'
import RecentActivity from './RecentActivity'
import Chart from './Chart'

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>

      <Suspense fallback={<div>Loading stats...</div>}>
        <Stats />
      </Suspense>

      <div className="grid">
        <Suspense fallback={<div>Loading chart...</div>}>
          <Chart />
        </Suspense>

        <Suspense fallback={<div>Loading activity...</div>}>
          <RecentActivity />
        </Suspense>
      </div>
    </div>
  )
}

まとめ

この Next.js 開発スキルでは、以下を扱います。

  1. App Router:ファイルベースのシステムによる最新のルーティング
  2. サーバーコンポーネント:パフォーマンス向上のための、デフォルトのサーバーサイドレンダリング
  3. データ取得:キャッシュと再検証に対応した拡張 fetch API
  4. ルーティング:動的ルート、キャッチオールルート、ルートグループ、並列ルート
  5. レイアウト:ネストされたレイアウト、テンプレート、レイアウトの構成
  6. 読み込み状態:Suspense による自動読み込み UI
  7. エラー処理:エラーバウンダリと not-found ページ
  8. API ルート:Web API を使ったルートハンドラー
  9. ミドルウェア:リクエストのインターセプトと変更
  10. メタデータ:Metadata API による SEO の最適化
  11. 画像の最適化:Image コンポーネントによる自動画像最適化
  12. フォントの最適化:next/font による自動フォント読み込み
  13. サーバーアクション:クライアントコンポーネントからのサーバー側の変更処理
  14. 認証:セッション管理と保護されたルート
  15. ベストプラクティス:パフォーマンス最適化、キャッシュ戦略、一般的なパターン

すべてのパターンは公式 Next.js ドキュメント(信頼スコア:10)に基づいており、最新の Next.js 13+ App Router 開発プラクティスを示しています。

コメント