Next.js 15.2 以降で動的メタデータが <body> に出力される問題とその回避方法
はじめに
Next.js 15.2 以降で、サーバーサイドの情報をもとに動的にメタデータを生成しているページだけ、メタタグが <head> ではなく <body> に出力されてしまうという現象が確認されました。
静的ページでは正常に <head> に入るため、原因がコードなのか、Next.js の仕様なのか、切り分けが少し難しい問題でした。
同じ状況に遭遇した方の参考になればと思い、原因と回避策をまとめます。
問題の概要
発生していた状況は次のようなものでした。
- サーバーサイドでデータ取得 →
generateMetadata()で動的メタデータを作成 - そのページだけ meta タグが
<head>内に入らず<body>に表示される - 動的メタデータを使っていないページは問題なし
つまり、動的メタデータを扱うページだけメタタグが意図しない位置に出力されるという状態です。
原因を調べていく中で、Next.js 15.2 の「ストリーミングメタデータ」という仕様変更が関係していることがわかりました。
原因
Next.js 15.2 以降では、メタデータもストリーミングの対象になりました。
この仕様では、
- メタデータ生成が非同期で遅れている
- Next.js は UI のストリーミングを優先して開始する
- メタデータが後から到着すると
<body>に追記される
という流れになります。
また、app/template.tsx やレイアウト内で <Suspense> を使用している場合、レンダリング開始が早まりやすく、ズレが起きやすいようです。
試したことと結果
<Suspense> を外す(一般的には有効らしいが今回は無効)
ネット上では「<Suspense> を外すと解決する」という情報が多かったのですが、自分の環境では取り除いても状況は変わりませんでした。
サーバーサイドの処理がやや重めで、メタデータ生成が完了するより UI のストリーミング開始のほうが先に走るという構造になっており、これが本質的な原因だったようです。
最終的に効果があった回避策:UI のレンダリングを少し遅らせる
最終的に採用したのは、UI のレンダリングを意図的に少し遅らせるという方法です。
非常にシンプルですが、確実に安定しました。
// page.tsx
const delay = (ms: number) =>
new Promise(resolve => setTimeout(resolve, ms));
export default async function Page() {
// メタデータ生成が完了するのを少し待つ
await delay(120); // 100〜300ms 程度で十分
const data = await fetchDetail();
return <DetailPage data={data} />;
}
この遅延によって、
- メタデータ生成が先に完了する
- その後 UI がストリーミングされる
- メタタグがしっかり
<head>に収まる
という理想的な流れになり、問題が解消しました。
遅延時間もごく短いため、実際の体感速度への影響はほぼありませんでした。
まとめ
- Next.js 15.2 以降では、「ストリーミングメタデータ」の影響で、メタデータが <head> ではなく
<body>に出力されるケースがあります。 - これは メタデータ生成と UI のストリーミング開始タイミングのずれ によって発生します。
-
<Suspense>を外すことで解消されることが多いものの、常に有効とは限りません。 - UI 側にわずかな delay を挟み、ストリーミング開始を遅らせる方法が、より安定した対策として有効でした。
- 動的メタデータを使用するページでは、OGP や favicon が正しく
<head>に出力されているか、念のため確認することをおすすめします。
個人的な感想
今回の件を調査する中で、Next.js がよりストリーミング中心のアーキテクチャに移行していることを強く感じました。
パフォーマンスを優先するという方向性自体は非常に良いのですが、メタデータのような "レンダリング順が重要な要素" が後回しになるケースでは、今回のような影響が出る可能性があります。
従来の SSR の感覚のままだと気づきにくい部分でもあり、今後はストリーミングを前提にした設計や実装を意識していく必要がありそうです。