2025年人気No.1のNext.jsで、最新ツールとウェブを作る！ | エピソード5 Markdownでブログを構築

## 目標
- Markdownファイルを使って静的コンテンツを扱う方法を学ぶ。
- 必要なライブラリをインストールし、MarkdownをHTMLに変換する。
- ブログの投稿リストと詳細ページを作成する。

---

## 1. Markdownを使う準備
Markdownは軽量で書きやすいフォーマットで、ブログに最適です。Next.jsでMarkdownを扱うために、ライブラリを導入します。

### ライブラリのインストール：
ターミナルで以下を実行：
```
npm install gray-matter remark remark-html
```
- **gray-matter**: Markdownのメタデータ（フロントマター）を解析。
- **remark** & **remark-html**: MarkdownをHTMLに変換。

---

## 2. Markdownファイルの作成
プロジェクトに投稿データを保存するディレクトリを作成します。

### 手順：
1. `posts/`フォルダをプロジェクトルートに作成。
2. 以下のようなMarkdownファイルを作成（例：`posts/sample-post.md`）：
   ```markdown
   ---
   title: "初めての投稿"
   date: "2025-03-31"
   ---
   これはNext.jsで作ったブログのサンプル投稿です。  
   Markdownで簡単に書けますね！
   ```

3. もう1つ追加（`posts/second-post.md`）：
   ```markdown
   ---
   title: "2つ目の投稿"
   date: "2025-04-01"
   ---
   2番目の投稿です。Next.jsとMarkdownの組み合わせが最高！
   ```

---

## 3. Markdownを読み込む関数
投稿リストと詳細を取得する関数を作成します。

### 手順：
1. `lib/posts.ts`ファイルを作成し、以下を記述：
   ```ts
   import fs from 'fs';
   import path from 'path';
   import matter from 'gray-matter';
   import { remark } from 'remark';
   import html from 'remark-html';

   const postsDirectory = path.join(process.cwd(), 'posts');

   // すべての投稿のメタデータを取得
   export function getAllPosts() {
     const fileNames = fs.readdirSync(postsDirectory);
     const allPostsData = fileNames.map((fileName) => {
       const id = fileName.replace(/\.md$/, '');
       const fullPath = path.join(postsDirectory, fileName);
       const fileContents = fs.readFileSync(fullPath, 'utf8');
       const matterResult = matter(fileContents);

       return {
         id,
         ...(matterResult.data as { title: string; date: string }),
       };
     });
     return allPostsData.sort((a, b) => (a.date < b.date ? 1 : -1));
   }

   // 個別の投稿データを取得
   export async function getPostData(id: string) {
     const fullPath = path.join(postsDirectory, `${id}.md`);
     const fileContents = fs.readFileSync(fullPath, 'utf8');
     const matterResult = matter(fileContents);

     const processedContent = await remark()
       .use(html)
       .process(matterResult.content);
     const contentHtml = processedContent.toString();

     return {
       id,
       contentHtml,
       ...(matterResult.data as { title: string; date: string }),
     };
   }
   ```

---

## 4. 投稿リストの表示
ホーム画面に投稿リストを表示します。

### `app/page.tsx`の編集：
```tsx
import { getAllPosts } from '../lib/posts';

export default async function Home() {
  const posts = getAllPosts();

  return (
    <div className="min-h-screen flex flex-col items-center bg-gray-100 py-8">
      <h1 className="text-4xl font-bold text-blue-600 mb-8">ブログ投稿</h1>
      <ul className="space-y-4 max-w-2xl w-full">
        {posts.map((post) => (
          <li key={post.id} className="bg-white p-4 rounded shadow">
            <a href={`/posts/${post.id}`} className="text-xl font-semibold text-blue-500 hover:underline">
              {post.title}
            </a>
            <p className="text-gray-500">{post.date}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

---

## 5. 投稿詳細ページの作成
動的ルートを使って、個別の投稿ページを作成します。

### `app/posts/[id]/page.tsx`の作成：
```tsx
import { getPostData } from '../../../lib/posts';

export default async function Post({ params }: { params: { id: string } }) {
  const postData = await getPostData(params.id);

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-3xl font-bold text-blue-600 mb-4">{postData.title}</h1>
      <p className="text-gray-500 mb-6">{postData.date}</p>
      <div
        className="prose"
        dangerouslySetInnerHTML={{ __html: postData.contentHtml }}
      />
    </div>
  );
}
```

**注意**：
- `dangerouslySetInnerHTML`を使用するので、信頼できるコンテンツのみを扱うようにしてください。

---

## 実践：シンプルなブログ
- **Home**: 投稿リストを表示（タイトルと日付）。
- **投稿詳細**: クリックするとMarkdownの内容をHTMLとして表示。

**確認**：
- `npm run dev`でサーバーを起動し、`http://localhost:3000`と`/posts/sample-post`を確認。

---

## エピソード5の終了
- Markdownを使って静的ブログを構築しました。
- 投稿リストと詳細ページを実装しました。

**次回のエピソード**：データベースを統合し、動的なブログにアップグレードします。

---

この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです！次回のエピソードもお楽しみに！