Next.js App Router の generateStaticParams で日本語パスが404になる:二重エンコード問題の罠
encodeURIComponent を親切心でかけたら、本番だけ全滅した。
Next.js App Router で動的ルートを使っているとき、日本語を含むパスのページがVercel本番環境でのみ404になるケースがある。前の記事で日本語スラッグの全般的な問題を書いたが、generateStaticParams での二重エンコードは少し別の文脈で踏むことがあるので独立して書いておく。
踏んだ状況
app/[category]/[slug]/page.tsx のような構造で、スラッグがMarkdownのfrontmatterから来ていた。コンテンツ管理の都合でスラッグに日本語が混入していた。
generateStaticParams の実装はこうだった。
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({
category: post.category,
slug: encodeURIComponent(post.slug),
}));
}
URLに日本語が入ることを考えて encodeURIComponent をかけた。むしろ正しい処置をしているつもりだった。
何が起きているか
Next.js の App Router は、generateStaticParams が返した値をビルド時にそのままファイルパスとして使う。そこに encodeURIComponent 済みの値を渡すと、Next.js がもう一度エンコードする。
例えば ギター というスラッグなら:
encodeURIComponent('ギター')→%E3%82%AE%E3%82%BF%E3%83%BC- Next.js が再エンコード →
%25E3%2582%25AE%25E3%2582%25BF%25E3%2583%25BC
生成されるファイルのパスと、ブラウザが実際にリクエストするパスが食い違う。ローカルの開発サーバーはファジーにマッチングするので気づかない。Vercelの本番では静的ファイルがそのまま配信されるので、完全一致しないと404になる。
encodeURIComponent をかけないのが正解だ。
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({
category: post.category,
slug: post.slug, // 生の値をそのまま
}));
}
page.tsx 側で使うときも同じ
generateStaticParams から渡ってくる params を使う側でも同様だ。
// app/[category]/[slug]/page.tsx
export default async function Page({
params,
}: {
params: { category: string; slug: string };
}) {
// params.slug は生の値で来る(例:'ギター')
// decodeURIComponent は不要
const post = getPostBySlug(params.slug);
}
params.slug をそのままコンテンツ取得の検索キーとして使える。decodeURIComponent で解除しようとすると、今度はローカルで壊れる。
整理
| 場所 | やること |
|---|---|
| generateStaticParams の返り値 | 生の値そのまま。エンコード不要 |
| page.tsx の params を使う側 | 生の値として受け取る。デコード不要 |
| リンクの href に日本語スラッグを書く | これも生の値でいい。Next.js の Link が処理する |
Next.js はエンコード・デコードを自動でやる。人間が手を出すと壊れる。
根本的な対処
そもそも日本語スラッグは管理が面倒なので、最初からASCIIにしておくのが楽だ。frontmatterに slug フィールドを別途設けてASCIIで書くか、ファイル名をASCIIにして自動生成する設計にしておくと、この問題自体に踏まない。
※ 本記事にはアフィリエイトリンクが含まれます。