できない.dev

Next.js App Router で metadata(title / description)が反映されない

App Router の `metadata` / `generateMetadata` は server component の `page.tsx` / `layout.tsx` でのみ動く。
`'use client'` ファイルに書く、`page.tsx` 以外で export する、`next/head` と二重指定、CDN キャッシュ残存が反映されない典型原因。

#nextjs#metadata#app-router#seo#head

公開:

要約

App Router の metadata API は server component の page.tsx / layout.tsx から export した時だけ 動作する。'use client' ファイルに書く、page.tsx 以外の場所で export する、next/head と二重に書く、いずれかの場合に反映されない。
client / server の境界と export 位置を直すと解決する。

よくある原因

  1. client component に書いている: ファイル冒頭に 'use client' がある状態で export const metadata を書いても無視される。
  2. export 位置が違う: page.tsx / layout.tsx 以外(共通コンポーネントや子ファイル)で export しても拾われない。
  3. generateMetadata の書き方ミス: async / 戻り値型を間違え、title が undefined のまま render される。
  4. next/head と二重指定: Pages Router から移行したプロジェクトに <Head>head.tsx が残っていて、metadata API と競合している。
  5. キャッシュ: ブラウザや CDN(Vercel など)のキャッシュで古い <title> が表示され続けている。

解決策

1. server component で書く

// app/about/page.tsx ('use client' は付けない)
import type { Metadata } from "next";
 
export const metadata: Metadata = {
  title: "About | dekinai.dev",
  description: "サイトについて",
};
 
export default function Page() {
  return <h1>About</h1>;
}

公式の generateMetadata リファレンス でも、metadata / generateMetadatapage.tsx または layout.tsx のみが対象、と明記されている。

2. 動的タイトルは generateMetadata

// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
 
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await fetchPost(params.slug);
  return { title: post.title, description: post.summary };
}

generateMetadataparams / searchParams を受け取り Promise を返す async 関数として書く。
同じ params なら request 間で結果がメモ化される。

3. next/head を撤去する

App Router の Metadata 最適化ガイド のとおり、App Router では next/head は使わず metadata API に一本化する。<Head> 残骸が DOM 上の <title> を二重に出力し、最後勝ち判定で意図しない側が表示される。

4. キャッシュをクリアする

ブラウザ側は強制リロード(Cmd+Shift+R / Ctrl+F5)でキャッシュ無視リロードする。
Vercel など CDN を挟んでいる場合は Deployments → Redeploy で SSR / static キャッシュをクリアしてから確認する。

この記事は役立ちましたか?