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 キャッシュ残存が反映されない典型原因。
公開:
要約
App Router の metadata API は server component の page.tsx / layout.tsx から export した時だけ 動作する。'use client' ファイルに書く、page.tsx 以外の場所で export する、next/head と二重に書く、いずれかの場合に反映されない。
client / server の境界と export 位置を直すと解決する。
よくある原因
- client component に書いている: ファイル冒頭に
'use client'がある状態でexport const metadataを書いても無視される。 - export 位置が違う:
page.tsx/layout.tsx以外(共通コンポーネントや子ファイル)で export しても拾われない。 generateMetadataの書き方ミス: async / 戻り値型を間違え、titleが undefined のまま render される。next/headと二重指定: Pages Router から移行したプロジェクトに<Head>やhead.tsxが残っていて、metadata API と競合している。- キャッシュ: ブラウザや 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 / generateMetadata は page.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 };
}generateMetadata は params / 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 キャッシュをクリアしてから確認する。