hyme

Next.js の generateMetadata 内で notFound を実行するとルートの not-found.tsx を参照する

掲題のとおりなのですが、ドキュメントについても記載されていなかったためメモです。

バージョンについて

Next.js 13.4.1 で確認しています。

$ pnpm why next
Legend: production dependency, optional only, dev only

...

dependencies:
next 13.4.1

generateMetadata とは

<meta> タグの記述を動的に(あるいは非同期処理を用いながら)設定するための関数です。Next.js の App Router で機能します。

わかりやすい使用例としては、CMSなどから投稿データを取得してきて、それを <title> タグに挿入するときなどです。

type Props = {
  params: {
    id: string
  }
}

export async function generateMetadata({
  params: { id },
}: Props): Promise<Metadata> {
  const post = await fetchPost(id)
  return {
    title: post.title,
    description: post.description,
  }
}

export default async function PostDetailPage({ params: { id } }: Props) {
  // ...
}

詳しくは公式ドキュメントへ↓

notFound とは

こちらも Next.js の App Router で追加されたもので、いわゆる 404 Not Found ページを呼び出すための関数です。

使い方としては単純に実行するだけです。

type Props = {
  params: {
    id: string
  }
}

export default async function PostDetailPage({ params: { id } }: Props) {
  const post = await fetchPost(id)
  if (!post) {
    // return は不要。返し値は never 型となっている。
    notFound()
  }
  return <div>{post.content}</div>
}

詳しくは公式ドキュメントへ↓

なにがおこったか

このサイトを App Router に移行するときに not-found.tsx をつくってみたときに、以下のような構造で全体の Not Found ページと投稿の Not Found ページを別々のファイルとしてつくっていました。

app/
  not-found.tsx // ページ全体でパスの不一致で出す用
  layout.tsx
  page.tsx
  post/
    [uid]/
      not-found.tsx // 投稿の uid がなかったときに表示する用
      page.tsx

app/not-found.tsx は↓

const NotFound = () => {
  return (
    <html>
      <head>
        <title>Page Not Found.</title>
        <meta name="description" content="Page Not Found." />
      </head>
      <body>
        <h1>Page Not Found.</h1>
        <a href="/">Home</a>
      </body>
    </html>
  )
}

export default NotFound

app/post/[uid]/not-found.tsx は↓

const UidNotFound = () => {
  return (
    <html>
      <head>
        <title>Uid Not Found.</title>
        <meta name="description" content="Uid Not Found." />
      </head>
      <body>
        <h1>Uid Not Found.</h1>
        <a href="/">Home</a>
      </body>
    </html>
  )
}

export default UidNotFound

この状態で、 app/post/[uid]/page.tsxnotFound へ飛ばす処理を書いてみました。

type Props = {
  params: {
    id: string
  }
}

export async function generateMetadata({ params: { id } }: Props) {
  const postMetadata = await fetchPostMetadata(id).catch((err) => {
    console.error(err)
    // 1. 投稿メタデータが取得できない = 存在しないとみなして 404 ページへ
    notFound()
  })
  if (!postMetadata) {
    // 2. 投稿メタデータがないため 404 ページへ
    notFound()
  }
  const metadata: Metadata = {
    title: postMetadata.title,
    description: postMetadata.description,
  }
  return metadata
}

export default async function PostDetailPage({ params: { id } }: Props) {
  const post = await fetchPost(id).catch((err) => {
    console.error(err)
    // 3. 投稿データが取得できない = 存在しないとみなして 404 ページへ
    notFound()
  })
  if (!post) {
    // 4. 投稿データがないため 404 ページへ
    notFound()
  }
  return <div>{post.content}</div>
}

この状態で、 generateMetadata 内の notFound (1と2)が実行されたときには、 「Page Not Found.」が表示されます

つまり、 app/not-found.tsx がレンダリングされました。

回避策

app/post/[uid]/not-found.tsx を表示するために、今回は notFoundgenerateMetadata で実行しないように変更しました。

投稿データがないケースと投稿メタデータがないケースはほとんど一致するので、今回はこれで回避するという形をとりました。

fetchPostMetadatacatch は、エラーログの出力のみに変更しました。

後日談

Next.js リポジトリ上でも notFound の issue は割と上がっていたので、他にも色々あるかもしれないです。