ブログの記事ページや商品ページのように、URL ごとに内容が変わるページでは、タイトルや説明文、SNS シェア時の OGP 画像もページごとに変えたいものです。App Router の export const metadata は固定値しか書けないため、記事のタイトルのような「実際にデータを取ってこないと決まらない値」には使えません。そこで登場するのが generateMetadata 関数です。この記事では、動的ルート([slug] など)で generateMetadata を使い、ページの内容に応じてタイトル・description・OGP を切り替える方法を、初心者〜中級者向けに解説します。
目次
静的な metadata では足りない場面
App Router では、layout.tsx や page.tsx から metadata というオブジェクトを export することで、そのページの <title> や <meta name="description"> を設定できます。
import type { Metadata } from "next";
// 値が固定なので const metadata でよい
export const metadata: Metadata = {
title: "運営者について",
description: "このサイトの運営方針とプロフィールを紹介します。",
};
export default function AboutPage() {
return <h1>運営者について</h1>;
}
これで十分なのは、タイトルや説明文があらかじめ決まっているページです。ところが、app/blog/[slug]/page.tsx のような動的ルートでは、URL に含まれる slug によって表示する記事が変わります。記事のタイトルは記事データを取得してみないと分からないため、export const metadata のような静的な値では書けません。この「レンダリング時にデータから決まる値をメタデータにしたい」というときに使うのが generateMetadata です。使い分けはシンプルで、固定値なら metadata、実行時に決まる値なら generateMetadata と覚えておけば大丈夫です。なお、同じファイルで metadata と generateMetadata の両方を export することはできません。
generateMetadata の基本形
generateMetadata は、Metadata 型(またはその Promise)を返す関数です。非同期でデータを取得できるよう async で書くのが基本で、戻り値の型は Promise<Metadata> になります。まずは引数を使わない最小形を見てみます。
import type { Metadata } from "next";
export async function generateMetadata(): Promise<Metadata> {
return {
title: "ブログ記事",
description: "記事の説明文です。",
};
}
export default function Page() {
return <article>記事本文</article>;
}
関数名は必ず generateMetadata にします(Next.js がこの名前を探して呼び出します)。返したオブジェクトは export const metadata に書くものと同じ構造で、title や description、後述する openGraph などを指定できます。これだけではまだ固定値ですが、次に引数を受け取って動的にしていきます。
params でルートの値を受け取って動的にする
generateMetadata は、ページコンポーネントと同じ props(params と searchParams)を第1引数で受け取れます。App Router では params は Promise なので、await してから中身を取り出します。[slug] ルートなら params の中に slug が入っています。
import type { Metadata } from "next";
type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
// params は Promise なので await する
const { slug } = await params;
// slug をもとに記事データを取得する
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
};
}
export default async function Page({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return <article><h1>{post.title}</h1></article>;
}
await params で slug を取り出し、それを使って記事データ(post)を取得し、post.title をそのままページのタイトルに設定しています。これで URL が /blog/hello-world なら「hello-world」の記事タイトルが、/blog/nextjs-tips なら別の記事タイトルが、それぞれ自動で <title> に反映されます。検索キーワードのようにクエリ文字列で内容が変わる場合は、同じ要領で searchParams を await して使えます。
OGP と Twitter カードを設定する
SNS でシェアされたときの見た目を整えるには、openGraph(Facebook や LINE などが参照する OGP)と twitter(X のカード)を設定します。これらも generateMetadata の中で記事データから組み立てられるので、記事ごとに違うサムネイル画像やタイトルを出せます。
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
url: `https://example.com/blog/${slug}`,
images: [
{
url: post.ogImage, // 記事ごとのOGP画像URL
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.ogImage],
},
};
}
openGraph.images には画像の URL に加えて width / height / alt を指定できます。X 向けの twitter.card を "summary_large_image" にすると、大きなサムネイル付きのカードで表示されます。title や description はトップレベルと openGraph の両方に書いていますが、openGraph 側を省くとトップレベルの値が使われます。記事ごとに画像やタイトルを変えたいときだけ明示的に指定すればよい、と考えると分かりやすいです。
親のメタデータを引き継ぐ
generateMetadata の第2引数では、親セグメント(上位の layout.tsx など)で解決されたメタデータを ResolvingMetadata として受け取れます。await すると親の値が取れるので、たとえば親が設定した OGP 画像を引き継ぎつつ、このページ独自の画像を先頭に足す、といった合成ができます。
import type { Metadata, ResolvingMetadata } from "next";
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
// 親で設定済みの OGP 画像を取り出す
const previousImages = (await parent).openGraph?.images || [];
return {
title: post.title,
openGraph: {
// このページの画像を先頭に、親の画像を後ろに並べる
images: [post.ogImage, ...previousImages],
},
};
}
親の layout.tsx でサイト共通のタイトルテンプレート(例: title.template を "%s | My Blog" にする)を設定しておけば、子の generateMetadata で title に記事名だけを返しても、最終的に「記事名 | My Blog」のように整形されます。共通部分は親の metadata、可変部分は子の generateMetadata、と役割を分けると管理しやすくなります。
同じ fetch を書いても二重に取得されない
ここまでの例では、generateMetadata とページ本体の両方で getPost(slug) を呼んでいます。「同じデータを2回取りに行って無駄では?」と気になるかもしれませんが、fetch を使っている場合、Next.js が同一リクエスト内の同じ fetch(同じ URL・オプション)を自動でメモ化(キャッシュ)するため、実際のリクエストは1回で済みます。
// generateMetadata と Page の両方から呼んでよい
export async function getPost(slug: string) {
// 同じ URL・オプションの fetch は自動でメモ化される
const res = await fetch(`https://api.example.com/posts/${slug}`);
if (!res.ok) throw new Error("記事の取得に失敗しました");
return res.json();
}
この仕組みのおかげで、データ取得のロジックを1つの関数にまとめ、generateMetadata とページの両方から気兼ねなく呼び出せます。ネットワークアクセスは1回にまとまるため、パフォーマンスを気にして無理に値を受け渡しする必要はありません。なお、メモ化が効くのは fetch API を使ったリクエストです。データベースクライアントや ORM など fetch を使わない取得を重複させたくない場合は、React の cache 関数で関数をラップすると同様にリクエスト単位でメモ化できます。
メタデータが反映されないときに見直すこと
generateMetadata を書いたのにタイトルや OGP が変わらないときは、いくつか典型的な原因があります。
クライアントコンポーネントに書いている
metadata と generateMetadata はサーバーコンポーネントでしか使えません。ファイルの先頭に "use client" が付いていると、これらの export は無視されます。メタデータを設定したいページはサーバーコンポーネントのまま page.tsx に置き、クライアント機能が必要な部分だけ別コンポーネントに切り出して読み込む形にします。
params を await していない
App Router では params と searchParams は Promise です。await を付けずに params.slug のように直接アクセスすると、期待した文字列が取れず、タイトルが undefined になったり型エラーになったりします。必ず const { slug } = await params; のように await してから使ってください。
<head> に自分でタグを書いている
App Router では <title> や <meta> を手書きせず、Metadata API に任せるのが基本です。generateMetadata と手書きのタグが混在すると、どちらが優先されるか分かりにくくなります。動的なメタデータはすべて generateMetadata の戻り値で表現するようにそろえると、混乱を防げます。
まとめ
generateMetadata は、動的ルートの内容に応じてタイトル・description・OGP を切り替えるための関数です。export async function generateMetadata({ params }): Promise<Metadata> の形で書き、await params で受け取った値をもとにデータを取得して、title や openGraph、twitter を組み立てます。固定値なら export const metadata、実行時に決まる値なら generateMetadata、という使い分けが基本です。第2引数の親メタデータを await すれば OGP 画像の合成やタイトルテンプレートの継承もでき、fetch のメモ化のおかげでデータ取得を重複させても実リクエストは1回に収まります。記事ページや商品ページなど、ページごとにメタ情報を変えたいときにぜひ使ってみてください。