🎩

next/headでhead要素を管理する

2024/08/30に公開

SalesNow 開発部の @sa9sha9 です。今回は小ネタとして、Next.js の next/head を使って head 要素を動的に切り替える方法について備忘録的にまとめました。

前提となる環境

  • React: 18.x
  • Next.js: 13.x
    • Pages Router を採用しています

はじめに

Next.js をお使いの皆様は next/head を使って head タグ内の要素を管理しているかと思います。

今回は next/head を使って動的に head 要素を生成する際に、実装で理解しておくべき点がいくつかあったため備忘録としてまとめました。

【その1】 next/head<Head>next/document<Head> は別物

初歩的ではあるものの、自動インポートなどで間違った方をインポートしていて、うまく動作しないなどが稀にあるので記載します。

両者にはざっくり次のような違いがあります。適切な方を使用しましょう。

モジュール 用途
next/head<Head> ページ単位でのhead要素の設定。 例として _app pages/* components/* などで使用。
next/document<Head> アプリケーション全体で共通のhead要素の設定。 pages/_document のみで使用。

next/document<Head> で設定された各種 head 要素は、各ページで next/head を使って上書きしようとしても上書きされないので注意が必要です。

そのため、各ページで上書きされる可能性のある head 要素はすべて next/head<Head> 中に記述しておきましょう。

【その2】 <Head> 内で同一のkeyを指定すると上書きできる

next/head は key に同一のものを指定すると既存の head 要素を上書きできます。

まず大抵のプロジェクトでは _app など上位の階層に次のような基底となる head 要素が設定されているかと思います。

pages/_app
import Head from 'next/head'
 
import type { AppProps } from 'next/app'
 
export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <title key="title">SalesNow</title>
      </Head>
      <Component {...pageProps} />
    </>
  )
}

そして、pages/* components/* 内で <Head> に次のように同一の key を指定した head 要素を記述することで、基底の head 要素が上書きされ、1つだけがレンダリングされます

pages/dashboard

import Head from 'next/head'
 
function DashboardPage() {
  return (
    <>
      <Head>
        <title key="title">SalesNow | Dashboard</title> // タイトルはこれが表示される
      </Head>
      <p>This is Dashboard</p>
    </>
  )
}
 
export default DashboardPage

https://nextjs.org/docs/pages/api-reference/components/head#avoid-duplicated-tags

【その3】 <Head> 内では直下の子要素にhead要素タグが含まれている必要がある

これが結構ハマりどころなのですが、 next/head では次の制約に違反するとレンダリングされない仕様となっています。

  1. title, meta, その他の要素(e.g. script) が直下の子要素として含まれていること
  2. もしくは <React.Fragment> や配列でラップされた子要素として第 1 階層までに含まれていること

つまり、子コンポーネントを経由した場合2階層以上のReact.Fragmentのネストがある場合はこの制約に引っかかるため、レンダリングされません。

ハマり例 その1

これがどういうケースでハマるかというと、たとえば OGP 専用の head 要素をまとめておこう!という考えで、次のように実装すると動きません。

pages/dashboard
function OgpHeadElements({title, description}) {
  return (
    <>
      <meta key="og:title" content={title} property="og:title" />
      <meta key="og:description" content={description} property="og:description" />
      <meta key="og:locale" content="ja_JP" property="og:locale" />
      <meta key="og:site_name" content="SalesNow" property="og:site_name" />
    </>
  )
}
 
function DashboardPage() {
  return (
    <>
      <Head>
        <title key="title">SalesNow | Dashboard</title>
        <OgpHeadElements /> // これはレンダリングされない
      </Head>
      <p>This is Dashboard</p>
    </>
  )
}

これは 1. の制約である head 要素が <Head> の直接の子要素ではなく、子コンポーネントを経由したものであるためです。

このケースでは、OgpHeadElements 側で Head に包んで、Head の直接の子要素とすることで動きます。

pages/dashboard
function OgpHead({title, description}) {
  return (
    <Head>
      <meta key="og:title" content={title} property="og:title" />
      <meta key="og:description" content={description} property="og:description" />
      <meta key="og:locale" content="ja_JP" property="og:locale" />
      <meta key="og:site_name" content="SalesNow" property="og:site_name" />
    </Head>
  )
}
 
function DashboardPage() {
  return (
    <>
      <Head>
        <title key="title">SalesNow | Dashboard</title>
      </Head>
      <OgpHead title="dashboard" description="dashboardのogp" /> // これはレンダリングされる
      <p>This is Dashboard</p>
    </>
  )
}

ハマり例 その2

また筆者がハマったケースとしては、複数のページで使いまわしている head 要素群のコンポーネントに、特定のページだけに必要な head 要素を extra なタグとして追加したいと考え、次のようなコンポーネントを作成しました。

components/customHead
function CustomHead({extra}: {extra: ReactNode}) {
  return (
    <Head>
      <> // ←これがポイント
        <title key="title">{title}</title>
        <meta key="description" content={description} name="description" />
        {extra}
      </>
    </Head>
  )
}
pages/dashboard
function DashboardPage() {
  return (
    <>
      <CustomHead
        extra={
          <> // ←これがポイント
            <meta key="extra" name="extra" /> // 特定のページだけに必要なmetaタグなどをここで指定する想定
            <meta key="extra2" name="extra2" />
          </>
        }
      />
      <p>This is Dashboard</p>
    </>
  )
}

これは結果として動きません。

これが動かない理由は、2.の制約である「第 1 階層までに含まれていること」という点に違反しているためであり、最終的に Fragment が 2 階層以上を形成していることが原因です。

components/customHead
function CustomHead({extra}: {extra: ReactNode}) {
  return (
    <Head>
      <> // ← 1階層
        <title key="title">{title}</title>
        <meta key="description" content={description} name="description" />
        <> // ← 2階層
          <meta key="extra" name="extra" />
          <meta key="extra2" name="extra2" />
        </>
      </>
    </Head>
  )
}

単純に次のように Fragment が1階層になるように修正すると動きます。

pages/dashboard
function CustomHead({extra}: {extra: ReactNode}) {
  return (
    <Head>
-     <>
      <title key="title">{title}</title>
      <meta key="description" content={description} name="description" />
      {extra}
-     </>
    </Head>
  )
}
pages/dashboard
function DashboardPage() {
  return (
    <>
      <CustomHead
        extra={
          <> // 第1階層までに収まるなら残したままでOK
            <meta key="extra" name="extra" />
            <meta key="extra2" name="extra2" />
          </>
        }
      />
      <p>This is Dashboard</p>
    </>
  )
}

凡ミスではありますが、この挙動にそこそこの時間を溶かしました。

https://nextjs.org/docs/pages/api-reference/components/head#use-minimal-nesting

おしまい

以上、next/head で head 要素を管理することのまとめでした。

もしお手元のアプリケーションで head 要素が適切にレンダリングされていないなどのケースがある場合、これらの制約に引っかかっていないか見直してみるとよいかもしれません!

どなたかのお役に立てば幸いです。

SalesNow Tech Blog

Discussion