Techに戻る

Next.jsとMDXでテックブログを一から構築する

GitHubにMarkdownを置くだけで自動公開。最小構成を具体例で紹介。

こんにちは!今回は、Next.jsとVercelを使った静的ブログの構築方法について紹介します。GitHubにMarkdownファイルを置くだけで、自動的にブログ記事として公開できる仕組みを最小構成で解説します。

はじめに

個人ブログの運営は、技術的な知識のアウトプットや自分の成長記録として最適です。今回はNext.jsとMDXを使って、モダンでパフォーマンスの高いテックブログを構築した過程を共有します。

技術選定の理由

ブログシステムを構築するにあたり、以下の理由からNext.jsを選びました:

  • Reactベースで開発しやすい
  • App Routerによる直感的なルーティング
  • SSG(静的サイト生成)のサポート
  • 高速なパフォーマンス
  • Vercelでの簡単なデプロイ

コンテンツ管理には、マークダウンの拡張版であるMDXを採用しました。コードのシンタックスハイライトやカスタムコンポーネントの埋め込みが可能で、テック系の記事執筆に最適です。

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

まず、Next.jsプロジェクトを作成します:

npx create-next-app@latest torquenap-blog

セットアップ時に以下のオプションを選択しました:

  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • App Router: Yes

ディレクトリ構造

ブログの基本構造は以下のようにしました:

/content          # マークダウンファイル
  /2021           # 年ごとにディレクトリ分け
  /2022
/app              # Next.js App Router
  /blog           # ブログ関連ページ
  /components     # 共通コンポーネント
  /utils          # ユーティリティ関数
/lib              # マークダウン処理などのコアロジック
/public           # 静的ファイル

マークダウンファイルの処理

マークダウンファイルはフロントマターと本文から構成されます:

---
title: 記事タイトル
tags:
  - タグ1
  - タグ2
category: カテゴリー
publishedAt: '2021-05-29'
excerpt: 記事の要約
slug: article-slug
---

記事の本文...

マークダウンファイルを処理するために、以下のパッケージをインストールしました:

npm install gray-matter remark remark-html remark-gfm remark-rehype rehype-stringify rehype-prism-plus

lib/mdx.tsでマークダウンファイルの読み込みとパースを行います:

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import rehypePrism from 'rehype-prism-plus';

// マークダウンをHTMLに変換する関数
export async function markdownToHtml(markdown: string): Promise<string> {
  const result = await remark()
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypePrism, { showLineNumbers: true })
    .use(rehypeStringify, { allowDangerousHtml: true })
    .process(markdown);
  
  return result.toString();
}

// 全記事の取得
export function getAllPosts() {
  // contentディレクトリから全記事を取得するロジック
  // ...
}

ページの実装

App Routerを使用すると、ディレクトリ構造がそのままURLパスになります。ブログの記事詳細ページはapp/blog/[slug]/page.tsxに作成します:

// app/blog/[slug]/page.tsx
import { getAllPostSlugs, fetchPostWithContent } from '@/lib/mdx';
import { MDXRemote } from 'next-mdx-remote/rsc';

export async function generateStaticParams() {
  const slugs = getAllPostSlugs();
  return slugs.map(slug => ({ slug }));
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetchPostWithContent(params.slug);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div className="metadata">
        <time>{new Date(post.publishedAt).toLocaleDateString('ja-JP')}</time>
        <span>{post.category}</span>
      </div>
      <div className="content">
        <MDXRemote source={post.content} />
      </div>
    </article>
  );
}

タグとカテゴリーの実装

記事のメタデータからタグやカテゴリーを抽出し、それぞれのページを実装します:

// lib/mdx.ts
export function getAllTags() {
  const posts = getAllPosts();
  const tags = {};
  
  posts.forEach(post => {
    post.tags.forEach(tag => {
      tags[tag] = (tags[tag] || 0) + 1;
    });
  });
  
  return Object.entries(tags).map(([name, count]) => ({ name, count }));
}

export function getPostsByTag(tag: string) {
  const posts = getAllPosts();
  return posts.filter(post => 
    post.tags.some(t => t.toLowerCase() === tag.toLowerCase())
  );
}

まとめ

Next.jsとMDXを組み合わせることで、メンテナンスしやすく、パフォーマンスの高いブログサイトを構築できました。静的サイト生成(SSG)の恩恵を受けながら、リッチなマークダウンコンテンツを提供できる点が大きなメリットです。

次回は、このブログでiframeコンテンツを安全に表示する方法について解説します。