これまでの Next.js ブログサイト構築シリーズをまとめたページは以下の通りです。
前回の第6回では、フォルダ内にある複数の Markdown ファイルを読み込み、
トップページに新着記事の一覧としてリスト表示させる仕組みを作りました。
しかし今の状態では、一覧に並んだ記事のタイトルをクリックしても、
個別の記事本文を読むことはできません。
本格的なブログとして機能させるためには、
タイトルをクリックした際に、
それぞれの記事専用のページが開く仕組みが必要です。
この記事では、
Next.js の非常に強力な機能である「動的ルーティング(Dynamic Routes)」を使って、
個別記事の URL(例:/posts/test-1)を自動生成し、
記事の本文を表示させる手順を解説します。
記事の前提条件と対象読者
この記事は、以下の環境と進捗を前提として手順を解説しています。
- Next.js のバージョン: Next.js 16.2.1(React 19)環境の App Router を使用しています。
- 対象読者: シリーズ第6回までを完了し、すでに
postsフォルダに Markdown 記事が保存されており、トップページに新着記事の一覧が表示できている方を対象としています。
Next.js を構成するフォルダとファイルの役割
実際の作業に入る前に、これから編集する Next.js の各フォルダやファイルが、どのような役割を持っているのかを整理しておきます。サイトの仕組みを「劇団」に例えると分かりやすくなります。
postsフォルダ(倉庫):
Markdown ファイル(記事の原稿データ)を保管しておくための単なるデータ置き場です。libフォルダ(裏方):
画面には直接出ない、データ取得や計算などの「裏方プログラム」をまとめておく場所です。appフォルダ(表舞台):
読者が見る URL と画面を作る、 Next.js の最重要フォルダです。この中のフォルダ構成が、そのまま Web サイトの URL になります。page.tsx(実際の画面):appフォルダ内に配置され、その URL にアクセスしたときに表示される「実際の画面(UI)のデザイン」を作るファイルです。
この役割分担を踏まえて、裏方に仕事を増やし、表舞台に型枠を作っていくのが今回の作業の流れです。
記事本文データを取得する裏方プログラムの追加
まずは、前回作成したデータ処理専用の裏方プログラム(lib/posts.ts)に、「指定されたファイル名の Markdown から、タイトルと本文を取り出してくる」という新しい仕事を追加します。
Cursor を使って lib/posts.ts を開き、現在書かれているプログラムの一番下(最終行のさらに下)に、以下のコードを追記して保存します。
// ▼▼▼ ここから下を追加 ▼▼▼
// 指定された ID(ファイル名)の記事データを取得する関数
export function getPostData(id: string) {
// 1. 読み込むファイルの正しい場所(パス)を作る
const fullPath = path.join(postsDirectory, `${id}.md`);
// 2. ファイルの中身を読み込む
// ※ fs によるファイルの読み込み処理は、サーバー側でのみ動作します。
const fileContents = fs.readFileSync(fullPath, 'utf8');
// 3. gray-matter を使って、フロントマター(タイトルや日付など)と本文を切り分ける
const matterResult = matter(fileContents);
// 4. React でエラーにならないよう、Date オブジェクトを文字列(YYYY-MM-DD)に変換する
let dateString = matterResult.data.date;
if (dateString instanceof Date) {
dateString = dateString.toISOString().slice(0, 10);
} else {
dateString = String(dateString);
}
// 5. 取得したデータをまとめて返す
return {
id,
content: matterResult.content, // マークダウンの本文
title: matterResult.data.title as string,
date: dateString,
};
}
このコードにより、例えばシステムから test-1 と指示されたら、データ倉庫から test-1.md を探し出して中身を読み取ることができるようになります。
個別記事の「型枠」となるページを作成する
次に、実際の画面(表舞台)となるページを作成します。 Next.js では、フォルダ名を [id] のようにカッコ [] で囲むと、「ここはどんな URL が来ても受け付けます」という動的な型枠になります。
- Cursor の左側のファイル一覧で、
appフォルダの中にpostsフォルダを作成します。 - その
postsフォルダの中に、半角カッコを含めた[id]という名前のフォルダを作成します。 - その
[id]フォルダの中に、page.tsxというファイルを作成します。
ここまでの作業で、ブログのフォルダ構造は以下のようになります。
example-blog/
├── app/
│ ├── page.tsx
│ └── posts/
│ └── [id]/
│ └── page.tsx ← 今回新しく作成した「型枠」ファイル
├── lib/
│ └── posts.ts
└── posts/
├── test-1.md
└── test-2.md
- 作成した
app/posts/[id]/page.tsxに、以下のプログラムを貼り付けて保存します。
import { getPostData } from '../../../lib/posts';
import ReactMarkdown from 'react-markdown';
// Next.js 15 以降、動的ルートの params は Promise として渡されるため
// コンポーネントを async 関数にして await で受け取る必要があります。
export default async function Post({
params,
}: {
// params は { id: string } を含む Promise 型として受け取る
params: Promise<{ id: string }>;
}) {
// 1. Promise を解決して、URL の [id](例: "test-1")を取り出す
const { id } = await params;
// 2. id に対応するマークダウンファイルを読み込み、記事データを取得する
const postData = getPostData(id);
// 3. 取得したタイトル、日付、本文を画面に表示する
return (
<main className="max-w-3xl mx-auto p-8">
{/* タイトルを大きく太字で表示 */}
<h1 className="text-3xl font-bold mb-2">{postData.title}</h1>
{/* 日付をグレーのテキストで表示 */}
<div className="text-gray-400 mb-8">{postData.date}</div>
{/* マークダウン本文を prose スタイル(読みやすい文章レイアウト)で表示 */}
{/* prose-invert はダークテーマ向けの配色、max-w-none は幅制限を解除 */}
<div className="prose prose-invert max-w-none">
<ReactMarkdown>{postData.content}</ReactMarkdown>
</div>
</main>
);
}
Next.js 15 以降の最新仕様では、 URL のパラメータ(今回の [id] の部分)を非同期(Promise)で受け取るルールに変更されています。そのため、コード内で async と await を使って安全に処理を行っています。
トップページから個別記事へリンクを繋ぐ
型枠の準備ができたので、最後にトップページ(新着記事一覧)のタイトルをクリックして、個別記事へジャンプできるようにリンクを設定します。
- Cursor で、トップページのファイル(app/page.tsx)を開きます。
- 中身をすべて消去し、以下のプログラムにまるごと書き換えて保存します。
import { getSortedPostsData } from '../lib/posts';
// ★追加:Next.js 専用の高速なページ遷移を行うための Link コンポーネントを読み込む
import Link from 'next/link';
export default function Home() {
// 1. 裏方プログラム(lib/posts.ts)を呼び出して、全記事のデータを取得し、日付の新しい順に並べたリストを受け取る
const allPostsData = getSortedPostsData();
// 2. 取得したデータをリスト状(カード型)に並べて画面に表示する
return (
<main className="max-w-3xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">新着記事一覧</h1>
<ul className="space-y-4">
{/* allPostsData の中身(id, date, title)を1つずつ順番に取り出して表示する */}
{allPostsData.map(({ id, date, title }) => (
// リストの各項目(カード)。
// マウスが乗った時に背景色が変わるデザイン(hover:bg-gray-700 transition-colors)を追加しています。
<li key={id} className="border border-gray-700 rounded-lg shadow-sm bg-gray-800 hover:bg-gray-700 transition-colors">
{/* 通常の <a> タグではなく Next.js の <Link> コンポーネントを使用します。
これにより、画面全体を再読み込みすることなく、一瞬でページが切り替わります。
リンク先の URL は「/posts/記事のID(ファイル名)」になります。
*/}
<Link href={`/posts/${id}`} className="block p-4">
{/* 日付の表示 */}
<span className="text-gray-400 text-sm">{date}</span>
{/* タイトルの表示 */}
<h2 className="text-xl font-semibold text-white mt-1">
{title}
</h2>
</Link>
</li>
))}
</ul>
</main>
);
}
Next.js でページを移動する際は、通常の HTML の <a> タグではなく、専用の <Link> コンポーネントを使います。これにより、画面全体を読み込み直す通信が発生せず、アプリのように一瞬でページが切り替わるようになります。
動作確認(Next.js の起動とページ遷移)
すべての準備が整いました。設定が正しく反映されているか、ブラウザで確認してみましょう。
開発作業の区切りで毎回システムを終了させている場合は、ブラウザを開く前にターミナルから Next.js を起動する必要があります。
- Cursor の下半分にあるターミナル(黒い画面)を開き、ブログのフォルダに移動してから Next.js を起動します。
cd ~/example-blog
npm run dev
- ターミナルに起動完了のメッセージが表示されたら、ブラウザでブログのトップページにアクセスします。
トップページにアクセスすると、新着記事一覧が並んでいます。それぞれの記事がカード状に表示されており、マウスのカーソルを合わせると背景色が少し明るいグレーにフワッと変わることが確認できるはずです。
そのままタイトルをクリックしてみましょう。画面全体が白く再読み込みされることなく、一瞬で個別記事のページ(例: /posts/test-1 )へ切り替わり、 Markdown の本文が表示されます。ブラウザの「戻る」ボタンを押せば、同じく一瞬でトップページに戻ることも可能です。
まとめ
今回は、 Next.js の「動的ルーティング」機能を使い、一覧ページから個別記事のページへリンクを繋いで本文を表示する仕組みを構築しました。
test-1 や test-2 という URL のページを一つずつ手作業で作るのではなく、 [id] の型枠を使って Next.js が自動的にページを生成してくれるという、モダンな Web フレームワークの強力さを実感できたかと思います。
これで、ブログの核となる「記事一覧」と「個別記事」の画面遷移が完成しました。次回は、サイト全体で共通して表示される「ヘッダー」と「フッター」を作成し、ブログ全体のレイアウトデザインを整えていく工程に進みます。



コメント