💡

Next.jsでopengraph-imageが動かない解決法

に公開

はじめまして、ta(Xアカウント)です。

Next.jsのapp routerでopengraph-image.tsxで、つまづいたところがあったので備忘録として残します。

問題

opengraph-image.tsxはルートグループとの相性が凄い悪い。特にgenerateMetadataを使いバックエンドでopengraph-image.tsxのURLを指定している場合は、ルートグループを使用すると上手く動かない。

理由は、Next.jsが(route group)/XXX/opengraph-image.tsxとして場合に、自動的にハッシュ化された6文字の文字列がopengraph-image-XXXXXXのように付くため、404エラーになってしまう。

理由はGithubのISSUEでも述べられているが、要はルートグループによってあいまいになるパスを識別するためにハッシュ化された文字列が付け足される。

解決策

opengraph-image.tsxをルートグループのない場所に、作成することでハッシュ化された文字列がつけられるのを防ぐことが出来る。

app/
├─ (route group)/
│  └─ p/
│     └─ [postID]/
│        └─ page.tsx
└─ p/
   └─ [postID]/
      └─ opengraph-image.tsx

つまり、このような形でopengraph-image.tsxを作るとp/[postID]/opengraph-imageにアクセスしたときに画像が返される。

ただ、ドキュメントにも載っていなかったので解決するのに非常に苦労した。

Next.jsのソースには書いてあった。

get-metadata-route.ts
/*
 * If there's special convention like (...) or @ in the page path,
 * Give it a unique hash suffix to avoid conflicts
 *
 * e.g.
 * /opengraph-image -> /opengraph-image
 * /(post)/opengraph-image.tsx -> /opengraph-image-[0-9a-z]{6}
 *
 * Sitemap is an exception, it should not have a suffix.
 * Each sitemap contains all the urls of sub routes, we don't have the case of duplicates `/(group)/sitemap.[ext]` and `/sitemap.[ext]` since they should be the same.
 * Hence we always normalize the urls for sitemap and do not append hash suffix, and ensure user-land only contains one sitemap per pathname.
 *
 * /sitemap -> /sitemap
 * /(post)/sitemap -> /sitemap
 */
function getMetadataRouteSuffix(page: string) {
  // Remove the last segment and get the parent pathname
  // e.g. /parent/a/b/c -> /parent/a/b
  // e.g. /parent/opengraph-image -> /parent
  const parentPathname = path.dirname(page)
  // Only apply suffix to metadata routes except for sitemaps
  if (page.endsWith('/sitemap') || page.endsWith('/sitemap.xml')) {
    return ''
  }

  // Calculate the hash suffix based on the parent path
  let suffix = ''
  // Check if there's any special characters in the parent pathname.
  const segments = parentPathname.split('/')
  if (
    segments.some((seg) => isGroupSegment(seg) || isParallelRouteSegment(seg))
  ) {
    // Hash the parent path to get a unique suffix
    suffix = djb2Hash(parentPathname).toString(36).slice(0, 6)
  }
  return suffix
}

Discussion