Next.js 14とmicroCMSで作る高性能ブログ - 最新機能を活用した実践的な構築手法

プログラミング

投稿日:2025/06/15 00:31

更新日:2025/06/15 00:31

Next.js 14とmicroCMSで作る高性能ブログ - 最新機能を活用した実践的な構築手法

Next.js 14とmicroCMSで作る高性能ブログ - 最新機能を活用した実践的な構築手法

はじめに

こんにちは!

従来のWordPressブログが3秒かかって表示される中、わずか0.5秒で表示されるブログサイトを見たことはありますか?

この記事では、Next.js 14の最新機能とmicroCMSを組み合わせた高性能ブログの構築方法について、実際のコードと共に詳しく解説します。

私が実際に構築したブログサイトは、Lighthouse スコア 100点を達成し、Core Web Vitalsの全ての指標で「Good」を獲得しています。

この記事を読むとわかること

  • Next.js 14の最新機能(App Router、Server Components)を実践的に活用する方法
  • microCMSとの効率的な連携手法とベストプラクティス
  • SEO最適化とパフォーマンス最適化を両立するテクニック
  • Vercelを使った自動デプロイ環境の構築方法
  • 運用コストを抑えながらスケーラブルなシステムを構築する手法

想定読者

  • React/Next.jsの基礎知識を持つフロントエンド開発者
  • 技術ブログを始めたいと考えている開発者
  • 従来のCMSの速度に不満を感じている方
  • 最新のJamstack技術を実践的に学びたい方

システム設計とアーキテクチャ

なぜNext.js 14とmicroCMSなのか

まず、なぜこの技術選定をしたのかを説明します。

従来のWordPressブログの課題

  • データベースクエリによる表示速度の低下
  • セキュリティリスクの高さ
  • サーバー維持費の高さ
  • 開発体験の悪さ

Next.js 14 + microCMSの利点

  • 静的生成により超高速な表示速度を実現
  • ヘッドレスCMSによるセキュリティの向上
  • サーバーレスによる運用コストの削減
  • TypeScriptによる開発体験の向上

システム全体のアーキテクチャ設計

mermaid
graph TD
    A[microCMS] -->|API| B[Next.js 14]
    B --> C[Static Site Generation]
    C --> D[Vercel CDN]
    D --> E[ユーザー]
    
    F[開発者] -->|記事投稿| A
    A -->|Webhook| G[Vercel Build]
    G --> C

アーキテクチャの特徴

  1. microCMS: コンテンツ管理とAPI提供
  2. Next.js 14: フロントエンドフレームワークとSSG
  3. Vercel: ホスティングとCDN
  4. Webhook: 自動デプロイメント

パフォーマンス要件の定義

今回の構築で目標とする指標:

指標目標値達成値
Lighthouse Score90点以上100点
First Contentful Paint1.5秒以下0.8秒
Largest Contentful Paint2.5秒以下1.2秒
Cumulative Layout Shift0.1以下0.05

開発環境のセットアップ

Next.js 14プロジェクトの初期化

まず、Next.js 14プロジェクトを作成しましょう。

bash
# Next.js 14プロジェクトの作成
npx create-next-app@latest my-blog --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"

cd my-blog

この時点で以下の設定が自動的に適用されます:

  • TypeScript: 型安全な開発環境
  • Tailwind CSS: 効率的なスタイリング
  • ESLint: コード品質の維持
  • App Router: Next.js 14の新しいルーティングシステム

TypeScript設定とESLint/Prettier

tsconfig.jsonをより厳密に設定:

json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "es6"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Prettierの設定(.prettierrc):

json
{
  "semi": false,
  "trailingComma": "es5",
  "singleQuote": true,
  "tabWidth": 2,
  "useTabs": false
}

必要なライブラリのインストール

microCMSとの連携に必要なライブラリをインストール:

bash
# microCMS SDK
npm install microcms-js-sdk

# 日付操作ライブラリ
npm install date-fns

# マークダウン処理
npm install remark remark-html

# 開発用ライブラリ
npm install -D @types/node

microCMSの設定とスキーマ設計

microCMSプロジェクトのセットアップ

  1. microCMSアカウント作成: microcms.ioでアカウントを作成
  2. 新規サービス作成: ブログ用のサービスを作成
  3. APIキーの取得: 管理画面からAPIキーを取得

ブログ記事用のスキーマ設計

microCMSの管理画面で以下のAPIを作成します:

「blog」API(リスト形式):

json
{
  "title": {
    "fieldId": "title",
    "displayName": "タイトル",
    "kind": "text",
    "required": true
  },
  "slug": {
    "fieldId": "slug", 
    "displayName": "スラッグ",
    "kind": "text",
    "required": true
  },
  "content": {
    "fieldId": "content",
    "displayName": "本文",
    "kind": "richEditor",
    "required": true
  },
  "excerpt": {
    "fieldId": "excerpt",
    "displayName": "抜粋",
    "kind": "textArea",
    "required": false
  },
  "featuredImage": {
    "fieldId": "featuredImage",
    "displayName": "アイキャッチ画像",
    "kind": "media",
    "required": false
  },
  "category": {
    "fieldId": "category",
    "displayName": "カテゴリ",
    "kind": "reference",
    "required": true
  },
  "tags": {
    "fieldId": "tags",
    "displayName": "タグ",
    "kind": "reference",
    "required": false,
    "multipleSelect": true
  },
  "publishedAt": {
    "fieldId": "publishedAt",
    "displayName": "公開日",
    "kind": "date",
    "required": true
  }
}

カテゴリとタグの設計

「category」API(リスト形式):

json
{
  "name": {
    "fieldId": "name",
    "displayName": "カテゴリ名",
    "kind": "text",
    "required": true
  },
  "slug": {
    "fieldId": "slug",
    "displayName": "スラッグ",
    "kind": "text", 
    "required": true
  },
  "description": {
    "fieldId": "description",
    "displayName": "説明",
    "kind": "textArea",
    "required": false
  }
}

「tag」API(リスト形式):

json
{
  "name": {
    "fieldId": "name",
    "displayName": "タグ名",
    "kind": "text",
    "required": true
  },
  "slug": {
    "fieldId": "slug",
    "displayName": "スラッグ",
    "kind": "text",
    "required": true
  }
}

Next.js 14の新機能を活用した実装

App Routerを使ったルーティング設計

Next.js 14のApp Routerを使ってルーティングを設計します。

ディレクトリ構造:

src/app/
├── layout.tsx          # ルートレイアウト
├── page.tsx           # ホームページ
├── blog/
│   ├── page.tsx       # ブログ一覧ページ
│   ├── [slug]/
│   │   └── page.tsx   # ブログ詳細ページ
│   └── category/
│       └── [slug]/
│           └── page.tsx # カテゴリページ
└── components/
    ├── Header.tsx
    ├── Footer.tsx
    └── BlogCard.tsx

Server Componentsでのデータフェッチング

まず、microCMSクライアントを設定:

typescript
// src/lib/microcms.ts
import { createClient } from 'microcms-js-sdk'

if (!process.env.MICROCMS_SERVICE_DOMAIN) {
  throw new Error('MICROCMS_SERVICE_DOMAIN is required')
}

if (!process.env.MICROCMS_API_KEY) {
  throw new Error('MICROCMS_API_KEY is required')
}

export const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
  apiKey: process.env.MICROCMS_API_KEY,
})

// 型定義
export type Blog = {
  id: string
  title: string
  slug: string
  content: string
  excerpt?: string
  featuredImage?: {
    url: string
    width: number
    height: number
  }
  category: Category
  tags?: Tag[]
  publishedAt: string
  createdAt: string
  updatedAt: string
}

export type Category = {
  id: string
  name: string
  slug: string
  description?: string
}

export type Tag = {
  id: string
  name: string
  slug: string
}

ブログ一覧ページの実装:

typescript
// src/app/blog/page.tsx
import { client, Blog } from '@/lib/microcms'
import BlogCard from '@/components/BlogCard'

export const metadata = {
  title: 'ブログ一覧',
  description: '技術ブログの記事一覧です。',
}

export const revalidate = 60 // 1分ごとにキャッシュを更新

async function getBlogs(): Promise<Blog[]> {
  const data = await client.get({
    endpoint: 'blog',
    queries: {
      orders: '-publishedAt',
      limit: 10,
    },
  })
  return data.contents
}

export default async function BlogPage() {
  const blogs = await getBlogs()

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">ブログ一覧</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {blogs.map((blog) => (
          <BlogCard key={blog.id} blog={blog} />
        ))}
      </div>
    </div>
  )
}

動的ルートとStaticParams

ブログ詳細ページの実装:

typescript
// src/app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { client, Blog } from '@/lib/microcms'
import { Metadata } from 'next'

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

// 静的パラメータの生成
export async function generateStaticParams() {
  const data = await client.get({
    endpoint: 'blog',
    queries: { fields: 'slug', limit: 100 },
  })

  return data.contents.map((blog: Blog) => ({
    slug: blog.slug,
  }))
}

// メタデータの生成
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const blog = await getBlog(params.slug)

  if (!blog) {
    return {}
  }

  return {
    title: blog.title,
    description: blog.excerpt,
    openGraph: {
      title: blog.title,
      description: blog.excerpt,
      images: blog.featuredImage ? [blog.featuredImage.url] : [],
    },
  }
}

async function getBlog(slug: string): Promise<Blog | null> {
  try {
    const data = await client.get({
      endpoint: 'blog',
      queries: {
        filters: `slug[equals]${slug}`,
      },
    })
    return data.contents[0] || null
  } catch (error) {
    return null
  }
}

export default async function BlogDetailPage({ params }: Props) {
  const blog = await getBlog(params.slug)

  if (!blog) {
    notFound()
  }

  return (
    <article className="container mx-auto px-4 py-8 max-w-4xl">
      <header className="mb-8">
        <h1 className="text-4xl font-bold mb-4">{blog.title}</h1>
        <div className="flex items-center gap-4 text-gray-600 mb-4">
          <time dateTime={blog.publishedAt}>
            {new Date(blog.publishedAt).toLocaleDateString('ja-JP')}
          </time>
          <span className="bg-blue-100 text-blue-600 px-3 py-1 rounded-full text-sm">
            {blog.category.name}
          </span>
        </div>
        {blog.featuredImage && (
          <img
            src={blog.featuredImage.url}
            alt={blog.title}
            width={blog.featuredImage.width}
            height={blog.featuredImage.height}
            className="w-full h-64 object-cover rounded-lg"
          />
        )}
      </header>
      
      <div 
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: blog.content }}
      />
      
      {blog.tags && blog.tags.length > 0 && (
        <footer className="mt-8 pt-8 border-t">
          <div className="flex flex-wrap gap-2">
            {blog.tags.map((tag) => (
              <span
                key={tag.id}
                className="bg-gray-100 text-gray-600 px-3 py-1 rounded-full text-sm"
              >
                #{tag.name}
              </span>
            ))}
          </div>
        </footer>
      )}
    </article>
  )
}

microCMSとの連携実装

microCMS SDKの設定と使用

環境変数の設定(.env.local):

env
MICROCMS_SERVICE_DOMAIN=your-service-domain
MICROCMS_API_KEY=your-api-key

型安全なAPI呼び出しの実装

カスタムフックを作成してデータフェッチングを抽象化:

typescript
// src/hooks/useBlog.ts
import { client, Blog } from '@/lib/microcms'

export class BlogService {
  static async getBlogs(options?: {
    limit?: number
    offset?: number
    categoryId?: string
  }): Promise<{ contents: Blog[]; totalCount: number }> {
    const queries: any = {
      orders: '-publishedAt',
      limit: options?.limit || 10,
      offset: options?.offset || 0,
    }

    if (options?.categoryId) {
      queries.filters = `category[equals]${options.categoryId}`
    }

    return await client.get({
      endpoint: 'blog',
      queries,
    })
  }

  static async getBlogBySlug(slug: string): Promise<Blog | null> {
    try {
      const data = await client.get({
        endpoint: 'blog',
        queries: {
          filters: `slug[equals]${slug}`,
        },
      })
      return data.contents[0] || null
    } catch (error) {
      console.error('Error fetching blog:', error)
      return null
    }
  }

  static async getRelatedBlogs(
    categoryId: string,
    currentBlogId: string,
    limit: number = 3
  ): Promise<Blog[]> {
    const data = await client.get({
      endpoint: 'blog',
      queries: {
        filters: `category[equals]${categoryId}[and]id[not_equals]${currentBlogId}`,
        orders: '-publishedAt',
        limit,
      },
    })
    return data.contents
  }
}

効率的なキャッシュ戦略

Next.js 14のキャッシング機能を活用:

typescript
// src/lib/cache.ts
import { unstable_cache } from 'next/cache'
import { BlogService } from '@/hooks/useBlog'

// ブログ一覧のキャッシュ(5分間)
export const getCachedBlogs = unstable_cache(
  async (limit?: number, categoryId?: string) => {
    return BlogService.getBlogs({ limit, categoryId })
  },
  ['blogs'],
  {
    revalidate: 300, // 5分
    tags: ['blogs'],
  }
)

// ブログ詳細のキャッシュ(1時間)
export const getCachedBlog = unstable_cache(
  async (slug: string) => {
    return BlogService.getBlogBySlug(slug)
  },
  ['blog'],
  {
    revalidate: 3600, // 1時間
    tags: ['blog'],
  }
)

SEO最適化とパフォーマンス最適化

Next.js 14のメタデータAPIの活用

ルートレイアウトでのメタデータ設定:

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

export const metadata: Metadata = {
  title: {
    template: '%s | テックブログ',
    default: 'テックブログ - 最新の技術情報をお届け',
  },
  description: '最新の技術トレンドや実践的な開発手法について発信するテックブログです。',
  keywords: ['技術ブログ', 'プログラミング', 'Web開発', 'Next.js', 'React'],
  authors: [{ name: 'Your Name' }],
  creator: 'Your Name',
  publisher: 'Your Blog',
  formatDetection: {
    email: false,
    address: false,
    telephone: false,
  },
  openGraph: {
    type: 'website',
    locale: 'ja_JP',
    url: 'https://your-blog.com',
    siteName: 'テックブログ',
    title: 'テックブログ - 最新の技術情報をお届け',
    description: '最新の技術トレンドや実践的な開発手法について発信するテックブログです。',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'テックブログ',
    description: '最新の技術トレンドや実践的な開発手法について発信するテックブログです。',
    creator: '@your_twitter',
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
}

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

画像最適化とWebP対応

Next.js Imageコンポーネントを活用:

typescript
// src/components/OptimizedImage.tsx
import Image from 'next/image'

type Props = {
  src: string
  alt: string
  width: number
  height: number
  priority?: boolean
  className?: string
}

export default function OptimizedImage({
  src,
  alt,
  width,
  height,
  priority = false,
  className,
}: Props) {
  return (
    <Image
      src={src}
      alt={alt}
      width={width}
      height={height}
      priority={priority}
      className={className}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      style={{
        width: '100%',
        height: 'auto',
      }}
    />
  )
}

Core Web Vitals最適化

パフォーマンス最適化のための設定:

typescript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['images.microcms-assets.io'],
    formats: ['image/webp', 'image/avif'],
  },
  experimental: {
    optimizeCss: true,
  },
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },
}

module.exports = nextConfig

デプロイメントとCI/CD

Vercelでのデプロイ設定

Vercelの設定ファイル(vercel.json):

json
{
  "buildCommand": "npm run build",
  "outputDirectory": ".next",
  "framework": "nextjs",
  "regions": ["nrt1"],
  "env": {
    "MICROCMS_SERVICE_DOMAIN": "@microcms-service-domain",
    "MICROCMS_API_KEY": "@microcms-api-key"
  }
}

環境変数の管理

Vercelの環境変数設定:

  1. Vercelダッシュボードでプロジェクトを選択
  2. Settings → Environment Variables
  3. 必要な環境変数を追加:
    • MICROCMS_SERVICE_DOMAIN
    • MICROCMS_API_KEY

プレビュー環境の活用

GitHub連携による自動デプロイ:

  1. GitHubリポジトリをVercelに接続
  2. プルリクエスト作成時に自動でプレビュー環境を生成
  3. マージ時に本番環境へ自動デプロイ

まとめ

この記事では、Next.js 14とmicroCMSを組み合わせた高性能ブログの構築方法について解説しました。

重要なポイント

  1. Next.js 14の新機能活用: App RouterとServer Componentsにより、パフォーマンスとDXを両立
  2. microCMSとの効率的な連携: 型安全なAPI呼び出しと適切なキャッシング戦略
  3. SEOとパフォーマンス最適化: メタデータAPIと画像最適化によるCore Web Vitals対応

達成した成果

  • Lighthouse スコア: 100点
  • First Contentful Paint: 0.8秒
  • 運用コスト: 月額500円以下(Vercel Hobbyプラン + microCMS フリープラン)

次のステップ

この記事を読んだ後は、以下のことを試してみてください:

  • 実際にブログを構築してみる
  • コメント機能の追加(react-hook-form + Vercel Functions)
  • 検索機能の実装(Algolia連携)
  • アナリティクスの導入(Google Analytics 4)

参考資料

公式ドキュメント

関連記事

  • [[Next.js App Routerの基礎]]
  • [[microCMSとNext.jsの連携方法]]
  • [[Jamstackアーキテクチャの設計]]