Next.jsのApp RouterでStatic Exportsをする時はDynamic Routesのバグに気を付ける必要がある

created at
updated at
technology Next.js TypeScript SSG App_Router

表題の通り、2024/11/17時点の最新版であるNext.js 15.0.3ではApp RouterStatic Exports(いわゆるSSG)を使用するとDynamic Routesで複数のバグがあるので回避策を入れる必要がある。

next devでDynamic Routesを使う時はパーセントエンコードが必要
Static ExportsでDynamic Routesを使用する時は generateStaticParams() でパスを定義しておく必要があるが、next dev では generateStaticParams() の結果はパーセントエンコードしないとエラーが出てしまう。これは仕様に記載されていないし、next build の時は逆にパーセントエンコードするとエラーになってしまうので、next dev の時にバグっているっぽい。
そのため、次のようにして next dev の場合と next build の場合で generateStaticParams() の返り値を切り替える必要がある
_
tsexport async function generateStaticParams(): Promise<
  {
    slug: string[]
  }[]
> {
  const slugs = await getSlugs()

  return slugs.map((slug) => ({
    slug: [
      process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD
        ? slug
        : encodeURIComponent(slug),
    ],
  }))
}
process.env.NEXT_PHASE はNext.jsが自動的に定義してくれる環境変数で、next build 時は PHASE_PRODUCTION_BUILD が設定される事になっている。この環境変数を使って next build 時はそのまま、それ以外の時(= next dev)は encodeURIComponent() を通してパーセントエンコードしたものを返すようにすればよい。
この事象はDynamic Routes with generated segment values containing characters that need to be URI encoded return 404 in dev · Issue #63002 · vercel/next.js · GitHub で報告されており、fix: uri encode dynamic routes in dev by samcx · Pull Request #71588 · vercel/next.js · GitHubで修正されたっぽいが、2024/11/17時点ではリリースされていないので↑のような回避策を取る必要がある…

next devでDynamic Routesのcatch-allを使う時は/で分割してはいけない
Dynamic Routesでcatch-allを使う時に仕様ではparamsは / ごとに分割した文字列の配列を渡す必要があるのだが、上記のバグ関連で next dev では パスが/複数/あります のように / を含んでいる場合に %E3%83%91%E3%82%B9%E3%81%8C/%E8%A4%87%E6%95%B0/%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99 のような形で / 以外をパーセントエンコードしつつ1つの文字列として返さないといけないバグがある。
もし文字列を / で分割してしまったり、/ もパーセントエンコードしてしまうと、パス不一致のエラーが出てしまうので、次のようにして回避する必要がある
_
tsexport async function generateStaticParams(): Promise<
  {
    slug: string[]
  }[]
> {
  const slugs = await getSlugs()

  return slugs.map((slug) => ({
    slug: [
      process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD
        ? page.title.split('/')
        : [
            page.title
              .split('/')
              .map((p) => encodeURIComponent(p))
              .join('/'),
          ],
    ],
  }))
}
こちらはIssueが無さそうだが、多分上記のバグ修正でなおりそうな気がする…

結論
アセット収集機能とかもないので、SSGしたいならNext.js以外を使ったほうがいいです