Next.jsでブログを作る #1 開始と設計
モダンReact + Next.jsシリーズで学んだServer Components / Server Actionsを実戦プロジェクトで手に馴染ませてみます。今回のシリーズは個人ブログを一から作ります — 興味深い点が1つあって、いま読んでいるこのサイト(schoolofweb.net)自体がほぼ同じ構造だということです。ドッグフーディング学習が可能な良い題材です。
5編に分けて段階的に積み上げていきます。
- #1 開始と設計 ← 今回の記事
- #2 記事一覧と詳細ページ
- #3 タグと検索
- #4 コメント (Server Actions)
- #5 SEOとデプロイ (まとめ)
要件定義 #
まず私たちが作るブログが何をできるべきかを書き出しておきます。
コア機能
- 記事一覧ページ (新着順)
- 記事詳細ページ (Markdown本文をレンダリング)
- タグ別記事まとめページ
- 検索機能 (タイトル/本文)
- 記事ごとにコメントを付ける
技術決定 (前もって決めておけば揺るがない)
- 記事本文はMDXファイルで書く (DBなし、ファイルシステムに保存)
- コメントはメモリストアを使用 (実サービスならDBが必要だが学習を単純化するため)
- データフェッチはServer Componentsが直接fsで読む
- mutationはServer Actionsを使用
- デプロイはVercel
これらの決定がシリーズ全体を通して中心を支えてくれます。「これはどこで扱うのか?」「これはどう解くべきなのか?」で迷わずに済みます。
なぜMDXファイルベースなのか? #
ブログを作るときに記事データをどこに置くかの大きな選択肢は3つです。
- DB (PostgreSQL、SQLite、Supabase など) — 多人数運用 / 管理ページ / 動的な記事追加に強み
- MDXファイル — Gitワークフロー、シンプルさ、JSXコンポーネントの埋め込み
- 外部CMS (Contentful、Sanity など) — 非技術者の編集者との協業に強み
このシリーズはMDXファイル方式を選びます。理由は次のとおりです。
- Gitがそのままバックアップ — 記事の履歴がそのまま保存される
- ローカルで編集 — 好きなエディタで記事を書ける
- DBセットアップしない — 学習負担 ↓
- Server Componentsの強みが自然に活きる —
fs.readFileSyncでファイルを直接読むコードがそのまま動く - Reactコンポーネントの埋め込み — 記事の中に
<YouTube />、<Tip>のようなカスタムコンポーネントが使える
このサイトも同じ方式で、多くの個人技術ブログがこのように運営されています。
フォルダ構造の設計 #
コーディングを始める前にフォルダ構造を描いておくと、行き詰まることが減ります。
my-blog/
├── posts/ ← MDX 記事の置き場
│ ├── hello-world.mdx
│ ├── about-rsc.mdx
│ └── learning-react.mdx
├── src/
│ └── app/
│ ├── layout.js ← サイト共通 (ヘッダー/フッター)
│ ├── page.js ← '/' 記事一覧 (新着順)
│ ├── posts/
│ │ └── [slug]/
│ │ └── page.js ← '/posts/[slug]' 記事詳細
│ ├── tags/
│ │ ├── page.js ← '/tags' タグ一覧
│ │ └── [tag]/
│ │ └── page.js ← '/tags/[tag]' タグ別記事
│ ├── search/
│ │ └── page.js ← '/search?q=...' 検索
│ └── lib/
│ └── posts.js ← MDX 読み取り/パース ユーティリティ
├── public/
└── package.json各ルートの役割は次のとおりです。
| ルート | 画面 |
|---|---|
/ | 最新記事一覧 |
/posts/[slug] | 記事詳細 (本文 + コメント) |
/tags | すべてのタグ一覧 |
/tags/[tag] | 特定のタグが付いた記事 |
/search?q=... | 検索結果 |
src/app/lib/posts.jsのようなユーティリティファイルに「MDXファイル読み込み」のような共通ロジックをまとめておくつもりです。ページファイルが膨らみすぎないように分離する、よくあるパターンです。
記事データの形を決める #
各MDXファイルがどんな情報を持つかを決めておくと、コードがすっきりします。
---
title: "こんにちは、ブログ"
date: 2026-05-01
description: "最初の記事です。"
tags: ["日常", "お知らせ"]
published: true
---
## 最初の段落
これは **マークダウン** で書いた本文です。
リストも可能:
- 項目 1
- 項目 2
- 項目 3上の---で囲まれた部分がfrontmatter(メタデータ)で、その下が本文です。frontmatterはYAML形式で、私たちのコードではJavaScriptのオブジェクトにパースして使います。
各フィールドの意味は次のとおりです。
| フィールド | 型 | 説明 |
|---|---|---|
title | string | 記事タイトル |
date | string (YYYY-MM-DD) | 公開日 |
description | string | 一行要約 (一覧とメタに使用) |
tags | string[] | タグ配列 |
draft | boolean | true なら一覧に表示されない (作成中) |
このほかにも必要ならimage、keywordsなどを追加できますが、ひとまず最小限で始めましょう。
Slug #
/posts/[slug]のslugは記事のURL識別子です。私たちはファイル名をスラグとして使います。
posts/hello-world.mdx→/posts/hello-worldposts/about-rsc.mdx→/posts/about-rsc
ルールが単純で良いです。記事ごとに別途IDを振ったり、URLマッピング作業をしたりする必要がありません。
プロジェクトの開始 #
これで本物のコードに入ります。新しいNext.jsプロジェクトを作ります。
npx create-next-app@latest my-blog
cd my-blog前回のシリーズと同じオプションを選択します (App Router、JavaScript、src/ directory推奨)。
必要な依存関係 #
MDXファイルをパースしてコンパイルするライブラリをインストールします。
npm install gray-matter next-mdx-remote各パッケージの役割。
gray-matter—.mdxファイルからfrontmatterと本文を分離next-mdx-remote— 本文MarkdownをReactコンポーネントにコンパイル
オプションで追加してもよいパッケージ。
remark-gfm— GitHub Flavored Markdown(テーブル、チェックボックスなど)サポートrehype-pretty-code— コードブロックのシンタックスハイライト
今回の記事ではひとまずコアの2つだけインストールし、#2で本文をコンパイルしながら追加プラグインを導入します。
最初の記事を作る #
my-blog/posts/フォルダを作って (Next.jsの外に置きます — ルートではなくデータなので)、最初の記事を追加します。
posts/hello-world.mdx。
---
title: "こんにちは、ブログ"
date: 2026-05-01
description: "Next.jsで作った最初のブログ記事です。"
tags: ["お知らせ", "リアクト"]
published: true
---
# こんにちは!
この記事は **MDX** で作成されました。Next.js の Server Component がこのファイルを直接読んで画面に描画します。
## 太字、斜体、コード
マークダウンの基本文法のほとんどが動作します。
- リスト項目 1
- リスト項目 2
- リスト項目 3
`インラインコード` も可能です。
\```js
// コードブロックも
function hello() {
console.log("hello");
}
\```(注意: 上の本文の最後のコードブロック末尾の\``` は、実際のファイルでは`````` と書きます — Markdownインライン表示の都合でエスケープされています)
posts/learning-react.mdx。
---
title: "リアクト学習ノート"
date: 2026-05-10
description: "リアクトを学びながら整理した核心ポイント。"
tags: ["リアクト", "学習"]
published: true
---
リアクトの勉強開始!
## Server Components
基本は Server Component、必要なものだけ Client Component。posts/draft-not-shown.mdx。
---
title: "まだ作成中"
date: 2026-05-15
description: "下書きです。"
tags: ["メモ"]
published: false
---
この記事は draft が true なので一覧に表示されないはずです。3つの記事はどれも似た構造ですね。draft: trueの記事は#2で作る一覧から自動的に除外されます。
MDXパースユーティリティ — 最初のステップ #
src/app/lib/posts.jsにMDXファイルを読み込んでパースする関数の最初の輪郭を作っておきます (#2で本格的に使用)。
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const POSTS_DIR = path.join(process.cwd(), 'posts');
export function getAllSlugs() {
return fs.readdirSync(POSTS_DIR)
.filter(file => file.endsWith('.mdx'))
.map(file => file.replace(/\.mdx$/, ''));
}
export function getPostBySlug(slug) {
const fullPath = path.join(POSTS_DIR, `${slug}.mdx`);
if (!fs.existsSync(fullPath)) return null;
const fileContent = fs.readFileSync(fullPath, 'utf-8');
const { data, content } = matter(fileContent);
return {
slug,
frontmatter: data,
content,
};
}
export function getAllPosts() {
const slugs = getAllSlugs();
const posts = slugs
.map(slug => getPostBySlug(slug))
.filter(post => post && !post.frontmatter.draft);
return posts.sort((a, b) => a.frontmatter.date < b.frontmatter.date ? 1 : -1);
}各関数の役割は次のとおりです。
getAllSlugs()—posts/フォルダの.mdxファイル名をスラグの配列として返すgetPostBySlug(slug)— スラグに該当するファイルを読み、frontmatterと本文を分離getAllPosts()— すべての記事を取得し、draftは除外、公開日の降順でソート
これらの関数はServer Componentからのみ呼び出し可能です (fsを使っているので)。Client Componentでこれを使おうとするとビルドエラーになります — 意図したとおりの安全装置です。
このサイトのapp/lib/posts-util.tsもほぼ同じ構造で動きます。ただ私たちは学習目的なので、シンプル化して始めて、必要なときに段階的に拡張します。
動作確認 #
今の状態ではページがまだないので、devサーバーを立ち上げても意味のある画面は出ません。それでも一度ちゃんと回るかは確認しておくと良いです。
npm run devhttp://localhost:3000でNext.jsのデフォルト画面が見えればOK。次の記事から本物のページを作っていきます。
おわりに #
今回の記事ではブログビルドシリーズの土台を整えました。
- 要件を明確に書き出した (一覧 / 詳細 / タグ / 検索 / コメント)
- データ保存方式を決定した (MDXファイル)
- フォルダ構造とルーティングの絵を描いた
- 記事のfrontmatterの形を決めた
- 最初のMDX記事を作った
posts.jsユーティリティ関数の最初の輪郭をつかんだ
本格的な画面作業は次の記事からです。「Next.jsでブログを作る #2 記事一覧と詳細ページ」では、上で作ったgetAllPostsを活用してホーム画面に記事一覧を描き、/posts/[slug]動的ルートでMDX本文をコンパイルして表示するところまで進みます。