言語を切り替える
テーマを切り替える

Next.js 15 実践:週末で本番級ブログシステムを構築した方法

はじめに

Next.js 15 がリリースされて間もなく、公式ドキュメントを何度も読み、チュートリアル動画もたくさん見ました。それでも机上の空論のまま——Server Actions や App Router の概念は分かるのに、実プロジェクトでの使い方が見えませんでした。週末、もうチュートリアルはやめて、本物のプロジェクトに取り組むことに。フルスタックのブログシステムです。フロント、バック、DB、デプロイまで。2 日後、Vercel へのデプロイに成功し、Lighthouse 性能スコアは 96 点でした。

この記事では、その過程を共有します。コードをコピペするだけのチュートリアルではなく、なぜそうしたか、どこでつまずいたかを理解できる内容にしました。技術スタックは Next.js 15 + Server Actions + Prisma + PostgreSQL。いずれも本番で使えるコードです。

なぜ Next.js 15 なのか?

技術選定は、かなり悩みました。
Next.js 15 が出た当初、コミュニティでは「またアップデートか、追いつけない」という声もありました。正直、Next.js 14 もまだ完全には使いこなせていない中、抵抗感がありました。でも新機能を詳しく見るうちに、今回のアップグレードは本物だと分かりました。

Server Actions:API Routes の煩わしさから解放

以前のフルスタック開発で一番面倒だったのが、API Routes の記述でした。api/posts/route.ts を作り、POST を定義し、リクエストボディを処理し、レスポンスを返す……毎回同じ流れで、うんざりしていました。

Server Actions はこのやり方を変えます。関数の前に 'use server' を付けるだけで、コンポーネントから直接呼び出せます。最初は「バックエンドのコードをフロントに書いているだけ?」と思いました。一度試すと——本当に楽でした。

例えば、以前は記事作成をこう書いていました:

// 旧方式:API Route が必要
// app/api/posts/route.ts
export async function POST(request: Request) {
  const body = await request.json()
  // 処理ロジック...
}
// フロントから fetch
const response = await fetch('/api/posts', {
  method: 'POST',
  body: JSON.stringify(data)
})

今はこうです:

// app/actions/post-actions.ts
'use server'
export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')
  // DB を直接操作
  return await prisma.post.create({
    data: { title, content }
  })
}

コンポーネントからは、ローカル関数を呼ぶ感覚です。たとえるなら、以前は自分でレストランまでテイクアウトを取りに行っていた(API Routes)のが、今はデリバリーで届く(Server Actions)イメージです。

Turbopack で開発速度が倍以上に

Next.js 15 で Turbopack が実験段階から安定版になりました。公式はローカルサーバー起動が 76.7% 高速、コード更新が 96.3% 高速と言います。最初はマーケ数字かと思いましたが、実際に使うと——誇張ではありませんでした。

76.7%
サーバー起動速度向上
公式テストデータ
96.3%
コード更新速度向上
ホットリロードはほぼ一瞬
2 秒
プロジェクト起動時間
30 以上のコンポーネントを 7〜8 秒から 2 秒以内に

私のプロジェクトは 30 以上のコンポーネントがあり、Webpack だと起動に 7〜8 秒かかっていました。Turbopack では 2 秒以内です。コードを変えると、ホットリロードはほぼ一瞬。開発体験の向上は、計り知れません。

なぜこのスタックがブログ向きなのか?

Next.js をブログに選んだ理由は、主に 3 つです。

SSR/SSG の天然の強み——ブログで最重要なのは SEO です。Next.js のサーバーサイドレンダリングと静的生成は、この用途のために設計されています。Google のクローラーは完成した HTML をそのまま取得でき、純クライアントレンダリングの React より圧倒的に有利です。

Prisma の型安全——以前は Mongoose で MongoDB を書いていましたが、型定義は手動メンテで、すぐバグります。Prisma はスキーマから TypeScript 型を生成するので、補完が効き、ミスが減ります。

Server Actions で開発が簡潔に——API Routes を書かなくていい分、コード量は少なくとも 30% 減ります。このブログを従来方式で書いたら、route.ts がいくつも増えていたはずです。

技術スタック選定とアーキテクチャ設計

「なぜ」が分かったら、次は「どうやって」です。

私の完全な技術スタック

  • フロントエンド: Next.js 15 + TypeScript + Tailwind CSS
  • バックエンド: Next.js Server Actions + NextAuth.js
  • データベース: PostgreSQL + Prisma ORM
  • デプロイ: Vercel

MongoDB では?と思うかもしれません。最初は私もそう考えていました。MongoDB は柔軟です。でも Prisma の PostgreSQL サポートの方が充実しており、記事・ユーザー・コメントのように関係が明確なデータには、リレーショナル DB の方が向いていると判断しました。

ディレクトリ構造

my-blog/
├── app/
│   ├── (auth)/          # 認証関連ページ
│   ├── blog/            # ブログ関連ページ
│   │   └── [slug]/      # 動的ルート
│   ├── dashboard/       # ユーザーダッシュボード
│   ├── actions/         # Server Actions を集約
│   └── api/auth/        # NextAuth 設定
├── components/          # 再利用コンポーネント
├── lib/
│   ├── prisma.ts        # Prisma シングルトン(重要!)
│   └── utils.ts         # ユーティリティ
├── prisma/
│   └── schema.prisma    # DB スキーマ
└── public/              # 静的アセット

この構造は何度か改訂しました。最初は Server Actions を各ページに散らばらせていましたが、メンテが大変で、actions ディレクトリにまとめたらすっきりしました。

データベーススキーマ設計

DB 設計では大きなつまずきがありました。初版はタグ、カテゴリ、閲覧数など盛りすぎて、半ばで「要らないテーブルが多い」と気づき、半日かけて簡素化しました。

最終的なコア Schema はこうです:

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  image     String?
  posts     Post[]
  createdAt DateTime @default(now())
}
model Post {
  id          String   @id @default(cuid())
  title       String
  slug        String   @unique
  content     String   @db.Text
  published   Boolean  @default(false)
  authorId    String
  author      User     @relation(fields: [authorId], references: [id])
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  @@index([slug])
  @@index([authorId])
}

2 つの @@index に注目してください。性能最適化の要です。ブログ一覧では slug で検索し、ユーザーページでは authorId で絞り込みます。インデックスを付けるだけで、クエリ速度は数倍になります。

コア機能の実装詳細

アーキテクチャが決まったら、いよいよコードです。

3.1 環境構築と初期化

Next.js 15 プロジェクトの作成は簡単です:

npx create-next-app@latest my-blog
cd my-blog
npm install prisma @prisma/client zod next-auth

Prisma の初期化:

npx prisma init

prisma/schema.prisma.env が生成されます。.env に PostgreSQL 接続を設定します:

DATABASE_URL="postgresql://username:password@localhost:5432/myblog?schema=public"

ローカル開発は Docker で PostgreSQL を立てるのが手軽です:

docker run --name blog-postgres -e POSTGRES_PASSWORD=mypassword -p 5432:5432 -d postgres

3.2 Prisma データベース連携

Schema を書いたらマイグレーションを実行:

npx prisma migrate dev --name init

ここで注意——Prisma Client のシングルトンパターンです。初めて Vercel にデプロイしたとき、この設定を忘れていて、半日で “Too many connections” エラーが出ました。DDoS かと思うほどでした。

原因は、Next.js 開発環境のホットリロードが、更新のたびに新しい Prisma Client インスタンスを作り、接続プールを枯渇させていたことです。正しい実装は次のとおり:

// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

少し回りくどく見えますが、開発環境では Prisma インスタンスが 1 つだけになることを保証します。本番への影響はありません。Prisma 公式の推奨パターンです。

3.3 Server Actions の実践

記事作成がコア機能です。Server Actions の書き方を見てみましょう:

// app/actions/post-actions.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
// Zod でバリデーション
const PostSchema = z.object({
  title: z.string().min(1, 'タイトルは必須です').max(100),
  content: z.string().min(10, '本文は 10 文字以上必要です'),
  slug: z.string().regex(/^[a-z0-9-]+$/, 'slug は小文字・数字・ハイフンのみ')
})
export async function createPost(formData: FormData) {
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
    slug: formData.get('slug')
  })
  if (!validatedFields.success) {
    return { error: 'バリデーションに失敗しました' }
  }
  try {
    const post = await prisma.post.create({
      data: {
        ...validatedFields.data,
        authorId: 'user-id-here' // 実運用では session から取得
      }
    })
    revalidatePath('/blog')
    return { success: true, post }
  } catch (error) {
    return { error: '記事の作成に失敗しました' }
  }
}

コンポーネント側:

// app/dashboard/new-post/page.tsx
import { createPost } from '@/app/actions/post-actions'
export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="タイトル" />
      <textarea name="content" placeholder="本文" />
      <input name="slug" placeholder="URL slug" />
      <button type="submit">公開</button>
    </form>
  )
}

useStatefetchonSubmit も不要です。フォームが Server Action を直接呼び、Next.js がシリアライズ、ネットワーク、エラー処理を担当します。

Server Actions と API Routes の違いが最初は分かりにくかったのですが、Server Actions は「デリバリーで届く」、API Routes は「自分で店に行く」イメージです。多くの場面では Server Actions で足ります。

3.4 ユーザー認証システム

認証には NextAuth.js を使いました。設定はシンプルです:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import GitHubProvider from 'next-auth/providers/github'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@/lib/prisma'
export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!
    })
  ]
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

GitHub OAuth は Developer Settings でアプリを作り、Client ID と Secret を .env に入れるだけ。10 分程度。JWT 認証を自前で書いていた頃が遠い昔に感じます。

3.5 SSR/SSG 最適化戦略

性能最適化の要です。最初は全部 SSR にしていましたが、ブログ一覧を開くたび DB を叩くのは無駄だと気づき、ハイブリッド戦略に切り替えました。

ブログ一覧——SSG + ISR

// app/blog/page.tsx
import { prisma } from '@/lib/prisma'
export const revalidate = 3600
export default async function BlogList() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' }
  })
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content.slice(0, 150)}...</p>
        </article>
      ))}
    </div>
  )
}

ビルド時に静的 HTML を生成し、1 時間ごとに再生成します。ユーザーは静的ファイルをそのまま受け取るので、非常に速いです。

ブログ詳細——動的 SSR

// app/blog/[slug]/page.tsx
export default async function Post({ params }: { params: { slug: string } }) {
  const post = await prisma.post.findUnique({
    where: { slug: params.slug }
  })
  if (!post) notFound()
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

アクセスのたび DB から最新内容を取得。更新が多い記事向きです。

性能比較データ

〜200ms
SSG ページ読み込み
静的生成で最速
〜800ms
SSR ページ読み込み
動的コンテンツ向き
〜1500ms
純 CSR 読み込み
クライアント描画、SEO は弱い

差は歴然です。Next.js がブログ向きな理由——SEO と性能を両立できるからです。

デプロイと最適化

コードができたら、いよいよデプロイです。

Vercel デプロイの流れ

GitHub にプッシュしたあと、Vercel でリポジトリをインポート。Next.js プロジェクトと判定されれば、ほぼ自動設定されます。手動で必要なのは環境変数です:

DATABASE_URL="本番 DB 接続文字列"
GITHUB_ID="OAuth Client ID"
GITHUB_SECRET="OAuth Secret"
NEXTAUTH_URL="https://your-domain.vercel.app"
NEXTAUTH_SECRET="ランダム文字列"

本番 DB には Supabase の無料 PostgreSQL を使いました。月 500MB 枠があり、個人ブログなら十分です。

Deploy をクリックして約 2 分。自分のドメインで初めてアクセスできたとき、画面を数秒見つめて、思わずスクショを SNS に載せました。

本番環境の最適化

デプロイ成功はゴールではありません。あと少し最適化があります。

Prisma 接続プール設定

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

Serverless では関数呼び出しごとに新接続が作られるため、directUrl でプールを再利用します。

画像最適化

import Image from 'next/image'
<Image
  src="/avatar.jpg"
  alt="ユーザーアバター"
  width={100}
  height={100}
/>

Next.js の Image コンポーネントは WebP 変換とオンデマンド読み込みを自動で行い、性能向上がはっきり分かります。

最終性能スコア

96 点
Lighthouse デスクトップ性能
モバイル 92 点、SEO 100 点。2 日でゼロから本番級性能へ

Lighthouse で計測したところ、デスクトップ 96 点、モバイル 92 点、SEO 100 点。2 日でゼロから本番級まで持っていけた達成感は、言葉にしにくいものでした。

拡張の方向性と学習リソース

現状のブログは MVP です。まだ足せる機能はたくさんあります。

  • Markdown エディタ: react-md-editor の統合
  • コメント: Giscus(GitHub Discussions ベース)
  • 検索: Algolia、または Prisma の全文検索
  • ダークモード: next-themes で比較的簡単

次は Markdown エディタを入れて、執筆を楽にしたいと思っています。

おすすめ学習リソース

十回読むより、一度手を動かす方が効きます。理論は分かっても、実戦で初めて身につきます。

まとめ

この週末プロジェクトで学んだこと:

  1. Server Actions の強さ: 多くの API Routes を代替でき、開発効率は段違い
  2. Prisma の型安全: TypeScript サポートが揃い、コードを書く安心感がある
  3. SSG/SSR の柔軟性: シーンに合わせて描画戦略を選べば、性能と SEO を両立できる
  4. Vercel デプロイの手軽さ: コードから公開まで、5 分もかからない

正直、最大の収穫は技術そのものより自信でした。フルスタック開発も、手を動かせば意外と難しくない——そう感じられました。

自分のブログを作りたいなら、迷わず始めてください。つまずくのは普通です。私もドキュメントを見ながら無数の穴に落ちました。でも公開できた瞬間、すべて報われます。

この記事が、少しでも遠回りを減らす助けになれば嬉しいです。コメントで質問があれば、できる限り返します。あなたの作品も楽しみにしています。

Next.js 15 で本番級ブログシステムを構築する完全フロー

環境構築からデプロイまで。Server Actions、Prisma、SSG/SSR 最適化などコア機能を含む手順

Estimated time: PT16H

  1. 1

    Step 1: 環境構築とプロジェクト初期化

    Next.js 15 プロジェクト作成:
  2. 2

    Step 2: Prisma DB スキーマ設定

    コア Schema 設計:
  3. 3

    Step 3: Server Actions コア機能

    Server Actions ファイル作成:
  4. 4

    Step 4: ユーザー認証設定

    NextAuth.js:
  5. 5

    Step 5: SSR/SSG 描画戦略の最適化

    ブログ一覧は SSG + ISR:app/blog/page.tsx で export const revalidate = 3600(1 時間ごと再生成)。prisma.post.findMany で公開記事を createdAt 降順取得。ビルド時に静的 HTML を生成し、1 時間ごとに再生成。ユーザーは静的ファイルを受け取り、読み込み約 200ms。記事詳細は動的 SSR:app/blog/[slug]/page.tsx でアクセスごとに DB から最新を取得。prisma.post.findUnique({ where: { slug: params.slug } })。更新が多い記事向き、読み込み約 800ms。性能比較:SSG 〜200ms、SSR 〜800ms、純 CSR 〜1500ms。差は歴然。
  6. 6

    Step 6: Vercel デプロイと最適化

    GitHub にプッシュ:プロジェクトを GitHub リポジトリへ。Vercel でインポート:GitHub リポジトリを Vercel に連携。Next.js と判定されれば自動設定。環境変数:DATABASE_URL(本番 DB)、GITHUB_ID、GITHUB_SECRET、NEXTAUTH_URL(https://your-domain.vercel.app)、NEXTAUTH_SECRET(ランダム文字列)。本番 DB は Supabase 無料 PostgreSQL 推奨(月 500MB、個人ブログ向き)。Prisma 接続プール:prisma/schema.prisma の datasource に directUrl = env("DIRECT_URL")。Serverless では関数ごとに新接続が作られるため、directUrl でプール再利用。画像最適化:Next.js Image コンポーネントで WebP 変換、オンデマンド読み込み。Deploy 後約 2 分で公開。Lighthouse:デスクトップ 96 点、モバイル 92 点、SEO 100 点。

FAQ

Next.js 15 の Server Actions と API Routes の違いは?どちらを使うべき?
Server Actions は「デリバリーで届く」、API Routes は「自分で店に取りに行く」イメージです。前者は手軽、後者は柔軟。

Server Actions の利点:
1) コード量が 30% 減り、API Routes を書く必要がない
2) コンポーネントから直接呼び出せ、`useState`、`fetch`、`onSubmit` の処理が不要
3) Next.js がシリアライズ、ネットワーク、エラー処理を自動で担当
4) 型安全で、TypeScript の型をそのまま使える

使用例:関数の前に `'use server'` を付け、コンポーネントから `<form action={createPost}>` のように呼び出す。

API Routes の利点:
• より柔軟で、複雑なリクエスト処理に対応
• ミドルウェアをサポート
• カスタム HTTP レスポンスが必要な場面向き

多くのケースでは Server Actions で十分。複雑な HTTP 処理が必要なときだけ API Routes を検討しましょう。
Turbopack の性能向上はどの程度?実際の体感は?
Next.js 15 で Turbopack が実験段階から安定版になりました。

公式データ:
• ローカルサーバー起動が 76.7% 高速
• コード更新が 96.3% 高速

実際の体感:
• 私のプロジェクトは 30 以上のコンポーネントがあり、Webpack だと起動に 7〜8 秒。Turbopack では 2 秒以内
• コードを変えると、ホットリロードはほぼ一瞬
• 開発体験の向上は、言葉にしにくいほど大きい

Turbopack は Rust 製で Webpack より速く、大規模プロジェクトほど差が出ます。まだ Webpack のままなら、Next.js 15 へ上げて Turbopack を試す価値があります。
Prisma Client のシングルトンパターンはなぜ重要?設定しないとどうなる?
Prisma Client のシングルトンは、Prisma 公式が推奨するベストプラクティスです。

問題のシナリオ:
• Next.js の開発環境はホットリロードのたびに新しい Prisma Client インスタンスを作る
• 接続プールがすぐに枯渇し、「Too many connections」エラーになる
• 初めて Vercel にデプロイしたとき、この設定を忘れていて、半日でサイトが落ちた。DDoS 攻撃かと思った

正しい設定:
• `lib/prisma.ts` でグローバルシングルトンを実装:
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}

このコードで、開発環境では Prisma インスタンスが 1 つだけになり、本番環境への影響はありません。覚えておいてください。これは Prisma 公式の推奨パターンです。
SSG、SSR、ISR の違いは?どう選ぶ?
SSG(Static Site Generation):
• ビルド時に静的 HTML を生成。最速(〜200ms)
• 更新頻度が低いページ向き(ブログ一覧、About など)

SSR(Server-Side Rendering):
• リクエストのたびにサーバーで HTML を生成
• リアルタイムデータが必要なページ向き(記事詳細、ユーザーページ)
• 読み込みは約 800ms

ISR(Incremental Static Regeneration):
• SSG の速度と SSR の柔軟性を組み合わせ、一定間隔で再生成
• たまに更新するページ向き(例:ブログ一覧を 1 時間ごとに更新)

純 CSR(Client-Side Rendering):
• ブラウザで描画。SEO が弱く、読み込みは〜1500ms
• ブログには非推奨

選び方:
• ブログ一覧は SSG + ISR(`export const revalidate = 3600`)
• 記事詳細は動的 SSR
• About は純 SSG

性能比較:SSG 〜200ms、SSR 〜800ms、純 CSR 〜1500ms。差は歴然です。
なぜ PostgreSQL を選び、MongoDB ではない?Prisma の PostgreSQL サポートは?
PostgreSQL を選んだ理由:
1) Prisma の PostgreSQL サポートが充実。型安全、マイグレーション、クエリ最適化が揃っている
2) 記事・ユーザー・コメントのように関係がはっきりしたデータには、リレーショナル DB の方が向いている
3) PostgreSQL の全文検索が強く、ブログ検索に適する
4) 本番環境の安定性とトランザクションサポートが優れている

Prisma の利点:
• スキーマから TypeScript 型を自動生成。補完が効き、ミスが減る
• 以前 Mongoose で MongoDB を書いていたが、型定義は手動メンテで、すぐバグる
• Prisma の型安全さは、コードを書く安心感につながる

複雑なリレーションデータなら、PostgreSQL + Prisma は有力な選択です。
Vercel への Next.js デプロイで注意点は?本番環境の最適化は?
Vercel デプロイの流れ:
1) コードを GitHub にプッシュ
2) Vercel で GitHub リポジトリをインポート。Next.js プロジェクトと判定されれば自動設定
3) 環境変数を設定(DATABASE_URL、GITHUB_ID、GITHUB_SECRET、NEXTAUTH_URL、NEXTAUTH_SECRET)
4) Deploy をクリック。約 2 分で公開

本番環境の最適化:

1) Prisma 接続プール:
• `prisma/schema.prisma` の datasource に `directUrl = env("DIRECT_URL")` を追加
• Serverless では関数呼び出しごとに新接続が作られるため、`directUrl` でプールを再利用

2) 画像最適化:
• Next.js の Image コンポーネントで WebP 変換、オンデマンド読み込み。効果は明確

3) 本番 DB は Supabase の無料 PostgreSQL がおすすめ(月 500MB、個人ブログなら十分)

最終スコア:
• Lighthouse で計測したところ、デスクトップ性能 96 点、モバイル 92 点、SEO 100 点
• 2 日でゼロから本番級の性能まで持っていけたとき、達成感はひとしお

6分で読めます · 公開日: 2025年11月24日 · 更新日: 2026年6月8日

関連記事

コメント

GitHubアカウントでログインしてコメントできます