できない.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 キャッシュ残存が反映されない典型原因。

公開: 更新:

症状できない · exit 1
npx next build > build.log 2>&1
解決後できた · exit 0
npx next build > build.log 2>&1

2026-09-29 に node:20(Node.js 20.20.2 / npm 10.8.2) で実際に打って取った出力。検証環境の詳細

要約

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

実行例

node:20 のコンテナで 'use client' を付けた page.js に metadata を書いたまま next build すると、終了コード 1 で止まり、ログには You are attempting to export "metadata" from a component marked with "use client", which is disallowed. が残っていた。'use client' を外すとビルドが通り、/about と generateMetadata を使った /blog/hello では書いた title と description が HTML に出た一方、共通コンポーネント側で metadata を export した /contact は layout の My Site のままだった。

$ npx next build > build.log 2>&1
$ echo $?
1
$ grep -o 'You are attempting.*disallowed\.' build.log
You are attempting to export "metadata" from a component marked with "use client", which is disallowed.
$ npx next build > build.log 2>&1
$ echo $?
0
$ curl -s http://localhost:3000/about | grep -o -e '<title>[^<]*</title>' -e '<meta name="description"[^>]*>'
<title>About | dekinai.dev</title>
<meta name="description" content="サイトについて"/>
$ curl -s http://localhost:3000/contact | grep -o -e '<title>[^<]*</title>' -e '<meta name="description"[^>]*>'
<title>My Site</title>
$ curl -s http://localhost:3000/blog/hello | grep -o -e '<title>[^<]*</title>' -e '<meta name="description"[^>]*>'
<title>Hello, Next.js</title>
<meta name="description" content="最初の記事"/>

— 2026-09-29 時点の出力

検証環境Node.js 20.20.2 / npm 10.8.2 / Python 3.11.2 / 2026-09-29 検証(ほか 1 件)
検証日
実行環境
node:20 Debian GNU/Linux 12 (bookworm)
バージョン
  • Node.js 20.20.2
  • npm 10.8.2
  • Python 3.11.2
  • Git 2.39.5

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

解決策

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: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await fetchPost(slug);
  return { title: post.title, description: post.summary };
}

generateMetadata は params / searchParams を受け取り Promise を返す async 関数として書く。
Next.js 15 以降は params 自体も Promise なので、await してから使う(16 で同期アクセスは廃止された)。
同じ params なら request 間で結果がメモ化される。

3. next/head を撤去する

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

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

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

よくある原因

  1. client component に書いている: metadata / generateMetadata は Server Components でしか使えない。
    ファイル冒頭に 'use client' がある状態で export const metadata を書くと、Next.js 15.5 では next build が You are attempting to export "metadata" from a component marked with "use client", which is disallowed. のエラーで止まる。
  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> が表示され続けている。

この手順で直った?