CONTENT
ここから
14. 記事詳細ページを作る
app/posts/[slug]/page.tsx を作成します。
// app/posts/[slug]/page.tsx
import { CategoryBadge } from '@/components/CategoryBadge';
import { getPostBySlug, getSortedPosts } from '@/data/posts';
import type { Metadata } from 'next';
import Link from 'next/link';
import { notFound } from 'next/navigation';
type PostPageProps = {
params: Promise<{
slug: string;
}>;
};
/**
* 役割: 静的生成する記事slug一覧をNext.jsへ渡す
* 入力: なし
* 出力: slug配列
*/
export function generateStaticParams() {
return getSortedPosts().map((post) => ({
slug: post.slug,
}));
}
/**
* 役割: 記事詳細ページのメタデータを生成する
* 入力: URLパラメータ
* 出力: titleとdescriptionを含むメタデータ
*/
export async function generateMetadata({
params,
}: PostPageProps): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
return {
title: '記事が見つかりません',
};
}
return {
title: `${post.title} | 岐阜ローカルエンタメ通信`,
description: post.description,
};
}
/**
* 役割: 記事詳細ページを表示する
* 入力: URLパラメータ
* 出力: 記事本文、タグ、戻るリンクを含む詳細ページ
*/
export default async function PostPage({ params }: PostPageProps) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
notFound();
}
return (
<main className="mx-auto max-w-3xl px-5 py-14">
<Link
href="/"
className="text-sm font-bold text-slate-500 transition hover:text-slate-950"
>
← 記事一覧へ戻る
</Link>
<article className="mt-8">
<div className="mb-5 flex flex-wrap items-center gap-3">
<CategoryBadge category={post.category} />
<time className="text-xs font-bold text-slate-400">
{post.publishedAt}
</time>
<span className="text-xs font-bold text-slate-400">
{post.readingMinutes}分で読める
</span>
</div>
<h1 className="text-4xl font-black leading-tight tracking-tight text-slate-950 md:text-5xl">
{post.title}
</h1>
<p className="mt-6 text-lg leading-9 text-slate-600">
{post.description}
</p>
<div className="mt-8 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-slate-100 px-3 py-1 text-xs font-bold text-slate-500"
>
#{tag}
</span>
))}
</div>
<div className="mt-12 space-y-7 border-t border-slate-200 pt-10">
{post.body.map((paragraph) => (
<p
key={paragraph}
className="text-base leading-9 text-slate-700 md:text-lg"
>
{paragraph}
</p>
))}
</div>
</article>
</main>
);
}
[slug]** とは何か**
[slug] は、URLの一部を変数として受け取る仕組みです。
例えば、次のURLがあるとします。
/posts/gifu-riverside-night-market
このとき、slug には以下が入ります。
gifu-riverside-night-market
そして、このslugを使って記事データを探します。
const post = getPostBySlug(slug);
これで記事詳細ページが表示できます。
15. 表示確認をする
ターミナルで以下を実行します。
npm run dev
ブラウザで開きます。
http://localhost:3000
確認するポイントは以下です。
| 確認項目 | OKの状態 |
|---|---|
| トップページ | 記事カードが表示される |
| 検索 | 「大垣」などで絞り込める |
| 詳細ページ | 記事をクリックすると移動する |
| 戻るリンク | 詳細ページから一覧に戻れる |
| スマホ表示 | 1列で自然に表示される |