コードレビューで見つかった問題を、勉強のためにまとめたものです(修正済み)。
この記事でわかること
- Next.js の動的ルートで「一覧に出さない記事」が URL 直叩きで見えてしまうパターン
generateStaticParams/dynamicParamsの責務の誤解と、その対策
概念
「一覧に出さなければ誰も辿り着けないから大丈夫」と考えがちだが、URL を直接叩けば動的レンダリングで返ってきてしまいます。generateStaticParams はビルド最適化の仕組みであって、アクセス制御ではありません。
修正前のコード
ブログ機能実装時、generateStaticParams() が published 記事のみをビルド対象にしていたが、getPost(slug) 関数がファイルの存在確認(ファイルが読めれば返す)しかしておらず、status: "draft" のチェックがなかった。
// 修正前
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params;
let post;
try {
post = getPost(slug); // status チェックなし → dynamicParams=true なので一覧外の slug でもここに到達し、draft も返る
} catch {
notFound(); // Next.js の関数。404ページを返す
}
return <BlogLayout post={post} />; // draft でもそのまま表示
}URL /creation-notes/blog/draft-article-slug を直接叩けばドラフト記事が表示されてしまいます。generateMetadata も同様にドラフトの OGP を生成してしまいます。
なぜ危険か
- 情報漏洩: まだ公開予定でない記事、内部情報、不完全な内容が外部に見えます
- SEO クロール: Googlebot が動的アクセスするケースもあり、下書きがインデックスされる可能性があります
- 意図しない公開: 「一覧には出していないから大丈夫」という誤った安心感につながります
対策
draft を弾くロジックは 記事取得の入り口1か所に集約 するのが安全。綴Photo は getPost の中で status を見て、published でなければ例外を投げる方式を採った。
// blog.ts — 取得の入り口で一元管理
// SHOW_DRAFTS: 本番ビルド(next build)では false、ローカル(next dev)では true になる
const SHOW_DRAFTS = process.env.NODE_ENV !== "production";
export function getPost(slug: string) {
// ... ファイルを読む ...
// frontmatter に status が書いてなければ "published" 扱い(?? は「左が空なら右を使う」演算子)
const status = data.status ?? "published";
// 「本番」かつ「draft」のときだけ弾く=ローカルでは draft も読めてプレビューできる
if (!SHOW_DRAFTS && status !== "published") {
throw new Error(`Post "${slug}" is not published`);
}
return { meta, content };
}SHOW_DRAFTS を挟んでいるのは、ローカル(next dev)では draft を確認できて、本番ビルドでは弾くためのスイッチ。これで一覧(getAllPosts)も個別ページも、同じ判定を通ります。
ページ側 / generateMetadata は、getPost が投げた例外を拾って notFound()(やフォールバック)に倒すだけでよいです。
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params;
let post;
try {
post = getPost(slug); // draft はここで例外になる
} catch {
notFound();
}
return <BlogBody post={post} />;
}別のやり方として、
getPostは素通しにして ページ側でif (post.meta.status !== 'published') notFound()と個別チェックする手もある。ただし page とgenerateMetadataの両方に同じチェックを書くことになり、ページが増えるほど書き漏れやすい。
この記事は2026年4月時点の自分の理解で書いています。Next.js の
generateStaticParams/dynamicParamsの挙動は変化が速い領域なので、最新の公式ドキュメントも合わせて参照してください。