🦢swan's room
2026.02.10Next.jsMDXTypeScript

Next.js App RouterでMDXブログを構築する

はじめに

個人ブログを Next.js の App Router で構築しました。MDX を使うことで、Markdown の手軽さと React コンポーネントの表現力を両立できます。

この記事では、実際のセットアップ手順と設計判断を紹介します。

技術選定

今回のブログでは以下の技術を採用しました。

  • Next.js 16 (App Router) — SSG + Cache Components
  • MDX — Markdown + JSX
  • Tailwind CSS v4 — CSS-first configuration
  • gray-matter — フロントマター解析

MDX の設定

@next/mdx を使って、App Router にネイティブ統合します。

// next.config.ts
import createMDX from "@next/mdx";

const nextConfig = {
  pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
};

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [],
  },
});

export default withMDX(nextConfig);

プロジェクトルートに mdx-components.tsx を配置するのを忘れないでください。App Router では必須です。

フロントマターの型定義

gray-matter で解析したフロントマターに型安全性を持たせます。

type Post = {
  slug: string;
  title: string;
  description: string;
  date: string;
  tags: string[];
  published: boolean;
  content: string;
};

記事の取得

ファイルシステムから MDX ファイルを読み取り、フロントマターを解析します。

import fs from "fs/promises";
import path from "path";
import matter from "gray-matter";

const BLOG_DIR = path.join(process.cwd(), "content/blog");

export const getAllPosts = async (): Promise<Post[]> => {
  const files = await fs.readdir(BLOG_DIR);
  const posts = await Promise.all(
    files
      .filter((f) => f.endsWith(".mdx"))
      .map(async (file) => {
        const content = await fs.readFile(
          path.join(BLOG_DIR, file),
          "utf-8"
        );
        const { data } = matter(content);
        return { slug: file.replace(/\.mdx$/, ""), ...data };
      })
  );
  return posts.sort((a, b) => (a.date > b.date ? -1 : 1));
};

シンタックスハイライト

rehype-pretty-codeshiki を組み合わせることで、VS Code と同等のシンタックスハイライトを実現できます。テーマは GitHub の Light/Dark を使い分けています。

まとめ

App Router + MDX の組み合わせは、個人ブログには最適な選択肢だと感じました。Server Components のおかげで、ビルド時にすべてが静的生成され、クライアントには最小限の JavaScript しか配信されません。