next/headでhead要素を管理する
SalesNow 開発部の @sa9sha9 です。今回は小ネタとして、Next.js の next/head を使って head 要素を動的に切り替える方法について備忘録的にまとめました。
前提となる環境
- React: 18.x
- Next.js: 13.x
- Pages Router を採用しています
はじめに
Next.js をお使いの皆様は next/head
を使って head タグ内の要素を管理しているかと思います。
今回は next/head
を使って動的に head 要素を生成する際に、実装で理解しておくべき点がいくつかあったため備忘録としてまとめました。
next/head
の <Head>
と next/document
の <Head>
は別物
【その1】 初歩的ではあるものの、自動インポートなどで間違った方をインポートしていて、うまく動作しないなどが稀にあるので記載します。
両者にはざっくり次のような違いがあります。適切な方を使用しましょう。
モジュール | 用途 |
---|---|
next/head の <Head>
|
ページ単位でのhead要素の設定。 例として _app pages/* components/* などで使用。 |
next/document の <Head>
|
アプリケーション全体で共通のhead要素の設定。 pages/_document のみで使用。 |
next/document
の <Head>
で設定された各種 head 要素は、各ページで next/head
を使って上書きしようとしても上書きされないので注意が必要です。
そのため、各ページで上書きされる可能性のある head 要素はすべて next/head
の <Head>
中に記述しておきましょう。
<Head>
内で同一のkeyを指定すると上書きできる
【その2】 next/head は key に同一のものを指定すると既存の head 要素を上書きできます。
まず大抵のプロジェクトでは _app
など上位の階層に次のような基底となる head 要素が設定されているかと思います。
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つだけがレンダリングされます
import Head from 'next/head'
function DashboardPage() {
return (
<>
<Head>
<title key="title">SalesNow | Dashboard</title> // タイトルはこれが表示される
</Head>
<p>This is Dashboard</p>
</>
)
}
export default DashboardPage
<Head>
内では直下の子要素にhead要素タグが含まれている必要がある
【その3】 これが結構ハマりどころなのですが、 next/head
では次の制約に違反するとレンダリングされない仕様となっています。
-
title
,meta
,その他の要素(e.g. script)
が直下の子要素として含まれていること - もしくは
<React.Fragment>
や配列でラップされた子要素として第 1 階層までに含まれていること
つまり、子コンポーネントを経由した場合や2階層以上のReact.Fragmentのネストがある場合はこの制約に引っかかるため、レンダリングされません。
ハマり例 その1
これがどういうケースでハマるかというと、たとえば OGP 専用の head 要素をまとめておこう!という考えで、次のように実装すると動きません。
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 の直接の子要素とすることで動きます。
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 なタグとして追加したいと考え、次のようなコンポーネントを作成しました。
function CustomHead({extra}: {extra: ReactNode}) {
return (
<Head>
<> // ←これがポイント
<title key="title">{title}</title>
<meta key="description" content={description} name="description" />
{extra}
</>
</Head>
)
}
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 階層以上を形成していることが原因です。
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階層になるように修正すると動きます。
function CustomHead({extra}: {extra: ReactNode}) {
return (
<Head>
- <>
<title key="title">{title}</title>
<meta key="description" content={description} name="description" />
{extra}
- </>
</Head>
)
}
function DashboardPage() {
return (
<>
<CustomHead
extra={
<> // 第1階層までに収まるなら残したままでOK
<meta key="extra" name="extra" />
<meta key="extra2" name="extra2" />
</>
}
/>
<p>This is Dashboard</p>
</>
)
}
凡ミスではありますが、この挙動にそこそこの時間を溶かしました。
おしまい
以上、next/head
で head 要素を管理することのまとめでした。
もしお手元のアプリケーションで head 要素が適切にレンダリングされていないなどのケースがある場合、これらの制約に引っかかっていないか見直してみるとよいかもしれません!
どなたかのお役に立てば幸いです。
Discussion