この記事の要点
headを自分で書くのではなく、metadataオブジェクトかgenerateMetadataを export する。タグは Next.js が生成する。title.templateは子セグメントにだけ効く。同じ階層のpageには適用されないのでdefaultが必須。- メタデータは親から子へ浅くマージされる。子が
openGraphを書くと、親のopenGraphは丸ごと置き換わる。 favicon.ico、opengraph-image、robots.ts、sitemap.tsはファイルを置くだけで機能する。ImageResponseを使うと、JSX と CSS で OG 画像を動的に生成できる。ただし対応する CSS は限定的。
検索やソーシャル共有で正しく扱われるかどうかは、head 内のタグにかかっています。App Router では、これらを手書きするのではなく、ページやレイアウトから設定値をエクスポートする方式をとります。この記事では、その仕組みと SEO 上つまずきやすい点を扱います。
1静的なメタデータ
変化しない値は metadata オブジェクトをエクスポートします。layout と page のどちらからでも書けます。
// app/blog/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'ブログ',
description: '最新の投稿一覧です',
}
export default function Layout() {}
文字コードとビューポートの 2 つは、何も書かなくても常に出力されます。自分で meta charset を書く必要はありません。
なお、metadata と generateMetadata はサーバーコンポーネントでのみ使えます。ページを対話的にしたい場合は、page.tsx はサーバーのまま残し、対話部分を別ファイルのクライアントコンポーネントに切り出してください。また、同じセグメントから両方をエクスポートすることはできません。
2データに応じたメタデータ
記事タイトルのように内容が可変な場合は generateMetadata を使います。
// app/blog/[slug]/page.tsx
import { getPost } from '@/app/lib/data'
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.description,
}
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return <div>{post.title}</div>
}
同じデータをメタデータと本文の両方で使うと、取得が 2 回走るように見えます。fetch は自動でメモ化されますが、DB 問い合わせなどは React.cache で包むと 1 回にまとめられます。上の例の getPost がそれです。
3タイトルのテンプレート
「ページ名 | サイト名」のような形をまとめて適用するには title.template を使います。
// app/layout.tsx
export const metadata: Metadata = {
title: {
template: '%s | Acme',
default: 'Acme', // template を使うときは必須
},
}
// app/about/page.tsx
export const metadata: Metadata = {
title: 'About',
}
// 出力: <title>About | Acme</title>
ここには重要な制約があります。template は子セグメントにだけ効き、同じ階層には効きません。そのため default が必須になっています。テンプレートを無視して単独のタイトルを出したい場合は title.absolute を使います。
ルートレイアウトで
template と default を定義し、各ページは title に短い名前だけを書く。page.tsx に template を書く。ページは末端なので子がなく、何の効果もない。4マージの規則に注意
メタデータはルートから順に評価され、同じキーは後勝ちで浅くマージされます。これが実務で最も事故を生む挙動です。
逆に、子が openGraph をまったく書かなければ、親のものがそのまま継承されます。
5正規 URL と metadataBase
OG 画像や正規 URL(canonical)は絶対 URL が必要です。毎回書くのは冗長なので、ルートレイアウトで metadataBase を設定します。
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
alternates: {
canonical: '/',
},
openGraph: {
images: '/og-image.png',
},
}
これで相対パスが自動的に絶対 URL に展開されます。設定せずに相対パスを書くとビルドエラーになるので、早い段階で入れておくのが安全です。
検索エンジンへの指示は robots で書けます。特定のページだけインデックスさせたくない場合に使います。
export const metadata: Metadata = {
robots: {
index: false,
follow: true,
},
}
6ファイルを置くだけで効くもの
| ファイル | 役割 |
|---|---|
app/favicon.ico | ブラウザのタブや検索結果に出るアイコン |
app/icon.png / app/apple-icon.png | 各種プラットフォーム向けアイコン |
app/opengraph-image.png | SNS 共有時に出る画像 |
app/robots.ts | robots.txt を生成する |
app/sitemap.ts | sitemap.xml を生成する |
これらは階層ごとに置けます。app/blog/opengraph-image.png を置けば、ブログ配下だけ別の画像になります。より深い場所のファイルが優先されます。
ファイルベースの指定は、metadata オブジェクトでの指定より優先されます。両方書いている場合はファイル側が勝ちます。
7OG 画像を動的に作る
記事タイトル入りの OG 画像を自動生成したい場合は、opengraph-image.tsx を置いて ImageResponse を使います。
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPost } from '@/app/lib/data'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return new ImageResponse(
(
<div
style={{
fontSize: 64,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{post.title}
</div>
)
)
}
ここで使える CSS は限定的で、flexbox と一部のプロパティのみです。display: grid は動きません。凝ったレイアウトを組もうとすると空振りするので、シンプルな構成にとどめるのが無難です。
バージョン 16 では、この画像生成関数が受け取る params も Promise になっています。await を忘れないでください。
8メタデータのストリーミング
事前生成できないページでは、Next.js は本文を先に送り、generateMetadata が解決した時点でタグを追加します。これにより初期表示が速くなります。
JavaScript を実行して DOM 全体を見るクローラー(Googlebot など)はこの形でも正しく解釈します。一方、JavaScript を実行しない種類のボットに対しては、従来どおり描画をブロックして head 内にタグを出す挙動が維持されます。どのユーザーエージェントをその扱いにするかは htmlLimitedBots で調整できますが、既定のままで問題ないケースがほとんどです。
9つまずきやすいところ
- クライアントコンポーネントで
metadataを export した:無視されます。サーバーコンポーネント側へ移します。 templateが効かない:同じ階層には効きません。親のレイアウトに書きます。- OG の description が消える:子で
openGraphを定義すると親のものは丸ごと置き換わります。共通部分を変数化して展開してください。 - 相対パスでビルドエラー:
metadataBaseを設定していません。 themeColorが効かない:バージョン 14 でmetadataから分離され、viewport側の設定に移りました。
検索とシェアの準備ができたら、次は表示速度に直結する画像とフォントに進みます。
子ページはありません
人気ページ
- 1 Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法
- 2 tomcat の起動 / 停止ログと catalina.log・catalina.out の違い
- 3 JavaScript で base URL を取得する方法|window.location.origin
- 4 YouTube Data API v3 エラー一覧|403・400・404 の原因と対処
- 5 Laravel エラー一覧|500/Blade/DB 接続/ルーティングの代表エラー
- 6 3Dグラフィックスとは|モデリング/レンダリング/主要ソフトウェア (Blender / Maya)
- 7 Spring Frameworkのアノテーション一覧
- 8 【Spring】@Valueアノテーションとは
- 9 CATALINA_HOME の確認方法 (Linux / Mac)
- 10 【Spring】@Autowiredアノテーションとは
最近更新/作成されたページ
- プロジェクトをTomcatプロジェクトとして認識させる方法 2026-10-07 22:32:50
- MySQLの1366 Incorrect string value|Laravelの文字コード・絵文字エラー 2026-10-07 21:54:03
- curlの証明書ホスト名不一致|旧エラー51・現行60の確認と対処 2026-10-07 21:54:03
- LaravelのMassAssignmentException|fillableの原因と安全な対処 2026-10-07 21:54:03
- Eclipse で Tomcat の起動ログがコンソールに出ない時の確認手順 2026-10-07 21:54:02
- MySQLにおける中央値(Median)の導き方(バージョン8未満) 2026-10-07 13:49:45
- getInputForward 2026-10-07 13:41:15
- JSONから配列に変換 2026-10-07 13:41:15
- ビューから値をモデルに格納しコントローラーで受け取る方法 2026-10-07 13:23:41
- Laravelのテーブル作成と定義変更|マイグレーション・up/down・注意点 2026-10-07 13:23:41
- NumPy 配列に要素を追加する方法 (append / concatenate) 2026-10-07 13:23:41
- MariaDB・MySQLで現在日時を取得する方法|NOW・タイムゾーン・保存型 2026-10-07 13:13:36
- 【django】テンプレートで定数を使用する方法 2026-10-07 13:10:15
- Spring BootにおけるApplication.propertiesの環境依存設定の分割方法 2026-10-07 12:09:35
- Not supported for DML operations【Springエラー】 2026-10-07 11:09:38