12.

Next.js の metadata|title・OG 画像・sitemap 設定

編集

この記事の要点

  • head を自分で書くのではなく、metadata オブジェクトか generateMetadata を export する。タグは Next.js が生成する。
  • title.template子セグメントにだけ効く。同じ階層の page には適用されないので default が必須。
  • メタデータは親から子へ浅くマージされる。子が openGraph を書くと、親の openGraph は丸ごと置き換わる。
  • favicon.icoopengraph-imagerobots.tssitemap.ts はファイルを置くだけで機能する。
  • ImageResponse を使うと、JSX と CSS で OG 画像を動的に生成できる。ただし対応する CSS は限定的。

検索やソーシャル共有で正しく扱われるかどうかは、head 内のタグにかかっています。App Router では、これらを手書きするのではなく、ページやレイアウトから設定値をエクスポートする方式をとります。この記事では、その仕組みと SEO 上つまずきやすい点を扱います。

1静的なメタデータ

変化しない値は metadata オブジェクトをエクスポートします。layoutpage のどちらからでも書けます。

// app/blog/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'ブログ',
  description: '最新の投稿一覧です',
}

export default function Layout() {}

文字コードとビューポートの 2 つは、何も書かなくても常に出力されます。自分で meta charset を書く必要はありません。

なお、metadatagenerateMetadataサーバーコンポーネントでのみ使えます。ページを対話的にしたい場合は、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 を使います。

意図どおり
ルートレイアウトで templatedefault を定義し、各ページは title に短い名前だけを書く。
効かない書き方
page.tsxtemplate を書く。ページは末端なので子がなく、何の効果もない。

4マージの規則に注意

メタデータはルートから順に評価され、同じキーは後勝ちで浅くマージされます。これが実務で最も事故を生む挙動です。

app/layout.tsx openGraph: {  title: 'Acme',  description: 'Acme is a...' } app/blog/page.tsx openGraph: {  title: 'Blog' } ※ description は書いていない 実際の出力 og:title は Blog になる og:description は消える(openGraph ごと置き換わるため)
入れ子のオブジェクトは「足し算」ではなく「置き換え」。共通部分は別ファイルの変数に切り出してスプレッドで混ぜるのが定石。

逆に、子が 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.pngSNS 共有時に出る画像
app/robots.tsrobots.txt を生成する
app/sitemap.tssitemap.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 側の設定に移りました。

検索とシェアの準備ができたら、次は表示速度に直結する画像とフォントに進みます。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 開発環境の構築とプロジェクトの初期構成
  2. App Router のルーティングとファイル規約
  3. レイアウトと特殊ファイルの使い分け
  4. サーバーコンポーネントとクライアントコンポーネント
  5. サーバー側でのデータ取得と並列化
  6. ストリーミングで体感速度を上げる
  7. キャッシュの仕組みと use cache
  8. 再検証の使い分け
  9. フォーム送信とデータ更新の実装
  10. エラーハンドリングとエラー境界
  11. Route Handlers で API を作る
  12. メタデータ API で SEO を整える
  13. 画像とフォントの最適化
  14. 本番ビルドとセルフホスト
  15. バージョン16 への移行と注意点