React-helmetを脱いでGatsby Head APIを冠る

2023/06/17 11 minute read

Bye, React-helmet to Gatsby Head API

もくじ

このブログサイトを公開する前からGatsby.jsを触りはじめていたとはいえ、最初からv5で、多分早いうちに gatsby-plugin-react-helmetが非推奨だと知った。

かといって、開発環境で興に乗って楽しんでいるときに、大急ぎで対応する気はなかったのだが。
Git branchを3つ使い分けていて・・というかmainと開発環境と2つ使い分けていて、途中で1つ永久保存版に封印したbranchがあった。

その封印ブランチで、<html lang="ja"> となおしていたのをmainに反映し忘れていて、後日、<html lang="en"> が残っているのに気づき、「カッときてやりました」😆

自分のミスなのにw なんじゃこりゃ〜と松田優作になって、Headごと挿げ替えることに。
どちらかというと「なんだとー!」の発音で「enだとー!」と苦笑した勢いだけでスタート。

咄嗟の思い立ちでいくつかのサイトを参考にさせていただきました。

列挙:

gatsby-plugin-react-helmetが廃止予定に!ヘッダはAPIを使おう【Gatsby.js】 | 謎の技術研究部

非推奨になるreact-helmetからGatsby Head APIに書き換える方法 | ベル15の開発ブログ

Gatsby Docs:

Gatsby Head API | Gatsby

Adding an SEO Component | Gatsby

そして何よりも、人気starterのsource(gatsby-starter-blog)

gatsby-starter-blog/src/components/seo.js at master · gatsbyjs/gatsby-starter-blog · GitHub

このサイトで使っているスターターはContentful / starter-gatsby-blog なのですが、リンクした components/seo.js は、「gatsby-starter-blog」のcomponents/seo.js とそっくり。

前者はReact-helmet版で、後者はGatsby Head API版

さすがの人気スターターでGatsby, Inc.提供だから、すでにHead API対応済みで参考になるどころじゃない。

そのまま写経で通用するくらい、変数名や手法も同じなので、GitHubでページごとのソースを確認しながら自分のサイトに適用していきました。


今後、Gatsby Head APIへ移行する方の参考になりそうな要点を書き出してみます。

まず大事な <title>Document</title> 部分。

実際は - Pageタイトル | Siteタイトル - とパイプなどで繋いで2つの値を入れる要素。

固定ページなら  About | Stylo de Cerise 
ブログのPostページなど、テンプレートから動的につくられるページなら  記事タイトル | Stylo de Cerise  のようにブラウザで表示されるドキュメント・タイトル。

旧react-helmet版では

components/seo.js
title={title}
defaultTitle={defaultTitle}
titleTemplate={defaultTitle ? `%s | ${defaultTitle}` : null}

title= でページのタイトルを取得し
defaultTitle= にgatsby-config.jsで記載済みのsiteMetadata.titleを代入
titleTemplate= でdefaultTitleを文字列 | パイプ付に整形し

titletitleTemplate がtitle:タグに置かれている模様。

<title>{title}{titleTemplate}</title>

SEOコンポーネント全体では、27-29行のところ

components/seo.js
import * as React from 'react'
import { Helmet } from 'react-helmet'
import { useStaticQuery, graphql } from 'gatsby'

const Seo = ({ description = '', lang = 'ja', meta = [], title, image }) => {
  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            title
            description
          }
        }
      }
    `
  )

  const metaDescription = description || site.siteMetadata.description
  const defaultTitle = site.siteMetadata?.title

  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={title}
      defaultTitle={defaultTitle}
      titleTemplate={defaultTitle ? `%s | ${defaultTitle}` : null}
      meta={[
        {
          name: `description`,
          content: metaDescription,
        },
        {
          name: `image`,
          content: image,
        },
        {
          property: `og:title`,
          content: title,
        },
        {
          property: `og:description`,
          content: metaDescription,
        },
        {
          property: `og:type`,
          content: `website`,
        },
        {
          property: `og:image`,
          content: image,
        },
        {
          name: `twitter:card`,
          content: `summary_large_image`,
        },
        {
          name: `twitter:creator`,
          content: site.siteMetadata?.social?.twitter || ``,
        },
        {
          name: `twitter:title`,
          content: title,
        },
        {
          name: `twitter:description`,
          content: metaDescription,
        },
      ].concat(meta)}
    />
  )
}

export default Seo

Gatsby Head APIでtitleを繋ぐ

components/seo.js
const metaDescription = description || site.siteMetadata.description
const defaultTitle = site.siteMetadata?.title

return (
	<>
		<title>{defaultTitle ? `${title} | ${defaultTitle}` : title}</title>

...

同じ変数名defaultTitleへ siteMetadata.titleを代入したあと6行目で、Helmet版と似たような置き換えをしています。

${title} は、

<Seo title="About" />

といった書き方でSeoコンポーネントに渡していた値です。
ページdescriptionや、BlogPost「Hero Image」をimage=で渡しているケースが多いかと思います。

<Seo
	title={post.title}
	description={plainTextDescription}
	image={`http:${post.heroImage.resize.src}`}
/>

上の例は、BlogPostテンプレートで
<Layout ・・・の直下に書いていたものですが、Gatsby Head APIの場合は、

export const Head = でHTML <head> に直接書きだす形になります。

templates/blog-post.js
export const Head = ({ data: { contentfulBlogPost: post } }) => {
  const plainTextDescription = documentToPlainTextString(
    JSON.parse(post.description.raw)
  )

  return (
    <Seo
      title={post.title}
      description={plainTextDescription}
      image={`http:${post.heroImage.resize.src}`}
    />
  )
}

挿入箇所は実際のソースを見た方が早いですが

export default BlogPostTemplate の直前に配置してあった・・・。

うちは export default BlogPostTemplate 直後、GraphQL前に置いたり、ページ末に置いたり、ページによってまちまちと雑なことしてた。(あとで揃えよう💧)

固定ページとテンプレート

もともとtwitter:cardやら、SEO方面は何もしてないので、Seoコンポーネントの使用はごく典型的な例が多いです。

固定ページ:タイトルのみ
export const Head = () => <Seo title="Blog" />

という最小例から、Markdown用テンプレートや

templates/note-post.js
export const Head = ({ data: { markdownRemark: post } }) => {
  return (
    <Seo
      title={post.frontmatter.title}
      description={post.frontmatter.description || post.excerpt}
    />
  )
}

タグ一覧のテンプレートでは、pageContextで渡されたTagの表示名「Gatsby.js」などをtitleに入れるようにしたり。

templates/tag-index.js
export const Head =  ({ pageContext }) => {
	const names = 'Tags:' + pageContext.name;
  return (
    <Seo
      title={names}
    />
  )
}
OUTPUT
<title>Tags:Gatsby.js | SiteTitle</title>

metadataの書き方

react-helmet:seo.js
meta={[
        {
          name: `description`,
          content: metaDescription,
        },
......
].concat(meta)}

React-helmetプラグイン版で、ずらりと配列されていた部分。

HTML <head> に何を書くか、なので古式ゆかしくmetaタグ書きをしていく中に、適宜変数を与える感じです。

<meta name="description" content={metaDescription} />
<meta property="og:title" content={title} />
<meta property="og:description" content={metaDescription} />
<meta property="og:type" content="website" />

これも「gatsby-starter-blog」からマルっとコピーしてきて、該当するHelmet側を1つずつ消していき・・・とやってみると、まったく同じ要素数だったので自分で書き足すこともなかったです。


この複写時に、自分でHelmetをゴリゴリ書き換えていた人は、その設定を消してしまわないよう気をつければ良いのでしょう。

意外にも og:typeなど、最初からwebsiteで固定値になってましたが、createPageから、テンプレートを使う際に website / blog / article を分けて入れるなど、固定値・変数を使い分けて設定可能。

うちの場合は、使っていないのをまとめてザクっと消したくらいで、妙に短くなったかも💧

Gatsby Head API版の全体像は

タイトルとディスクリプション重視だなぁ・・・

特記事項は、アンインストールしたHelmetをimportから外して4行目のconst Seo = が変わった点。
23行目からのreturn ()には、 <Helmet ..... /> の代わりに

<> ..... </> Reactフラグメントで囲い
最後に変数受け取り用の {children} を4行目と合わせて追加。

seo.js
import * as React from 'react'
import { useStaticQuery, graphql } from 'gatsby'

const Seo = ({ description, title, children }) => {
  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            title
            description
            siteUrl
            developer
          }
        }
      }
    `
  )

  const metaDescription = description || site.siteMetadata.description
  const defaultTitle = site.siteMetadata?.title

  return (
    <>
		<title>{defaultTitle ? `${title} | ${defaultTitle}` : title}</title>
		<meta name="description" content={metaDescription} />
		<meta property="og:title" content={title} />
		<meta property="og:description" content={metaDescription} />
		<meta property="og:type" content="website" />
		<meta name="twitter:card" content="summary" />
		<meta name="twitter:creator" content={site.siteMetadata?.social?.twitter || ``} />
		<meta name="twitter:title" content={title} />
		<meta name="twitter:description" content={metaDescription} />
      {children}
    </>
  )
}
export default Seo

meta={[ 配列 ]} だったのが直書きになったので、

const Seo = ({ description = '', lang = 'ja', meta = [], title, image }) =>

↑から、↓になりました。

const Seo = ({ description, title, children }) =>

😨 これだとBlogPostが渡した「heroImage」の収まり場がない。と今更気づいたけど、その訂正はさておき

lang = 'jp',

<Helmet
      htmlAttributes={{
        lang,
      }}

の行き場。

<html lang="ja">

HTML <head> 要素ではないlang属性は、サイトのルートにgatsby-ssr.jsファイルを作成して、

gatsby-ssr.js
/**
 * Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
 *
 * See: https://www.gatsbyjs.com/docs/reference/config-files/gatsby-ssr/
 */

/**
 * @type {import('gatsby').GatsbySSR['onRenderBody']}
 */
exports.onRenderBody = ({ setHtmlAttributes }) => {
  setHtmlAttributes({ lang: `ja` })
}

と設定することで加筆できます。Gatsby Server Rendering APIs | Gatsby を見てね! と書いてあるので素直に開いて日本語表示をGoogleさんにお願いして、 「gatsby-starter-blog」から模倣した振る舞いの理屈を復習します。


いつもながら、Gatsbyのドキュメンテーションを見ると(なんでこんなに親切なの?!)と感心・・・をちょっと通り過ぎる、上回るものがあるんですが。

今日わかった、今気づいた。

この網羅感は MDN Web Docs を彷彿とする。

そしてグレートなGatsbyはどちらの意味の命名なんだろう。
文学界では、ディケンズ「二都物語」と並んで思い出すような「献身」男の代名詞・・・。

ともかくも、そこへ行けばどうにかなる! 絶対感が、(あぁMDNに似てたかぁ)とやっと自分の中で腑に落ちたところで。
この人は、おもむろにiTune Storeを開き、松任谷由実「DESTINY」を購入しましたよね(笑

ー「怖可愛いサンダルがどうとかいう曲」を歌ってくれヒロセちゃん!とカラオケボックスで宣っていた30代もわずかに成長はしている。たぶんおそらく。

閑話休題ー

Gatsby公式サイトの献身性については機会があったら書くとして、最後にいちばんユニークな部分。
サイトルート、トップページ、について書いて終わります。

index.js

まずすごく単純なところを見落として、最後に手間取ったindex.js 処理。

保存版にしているbranchで古いファイルを見ても、特にSeoコンポーネントを呼び出しもせず、
でもgatsby-config.jsの module.exports = { siteMetadata: { の記載内容はしっかり反映されていました。

そもそもReact Helmetプラグインをほぼ無意識に使っていただけなので、今更にサーバーレンダリングだ、Reactフックだと文字面だけ追って理解もなく。ただ何かうまいことやってくれてたんだろなと想像するのみ。

で、途中一度、暴挙に出て。
ただ単純に直に書くことにした。

index.js
export const Head = ({ data }) => {
  return (
    <>
      <title>{data.site.siteMetadata.title}</title>
      <meta name="description" content={data.site.siteMetadata.description} />
    </>
  )
}

他のページ群で使いまわしているSeoコンポーネントを通すには、title=を渡さないと
<title></title> タグを書いてもらえない。

空白文字列を渡せばパイプはじまりで書かれる。
素直に title="Home"と渡して <title>Home | SiteName</title> としたって間違いじゃないけど、懐かしのオーサリングソフトにでも書いてもらったか?!な味わい深い感じになる。

まぁそのように書いているわけですからね、Seoコンポーネントで。
むしろ今になって考えると、ブラウザのデベロッパーツールまで使って、いろいろやってみたこの筆者こそ変。


あれこれ書いちゃったあとで「gatsby-starter-blog」の方を再度、確認にいくと title= に"All posts"と渡してましたね・・・きゃー。オーサリングソフトノフカイアジワイとか失敬な!輩でごめんなさい。

index.js
export const Head = () => <Seo title="All posts" />

ま。スターターは学舎ですからね、卒業するまでは汎用性大事。

「gatsby-starter-blog」の通りでいくと、何をやってもパイプが残り、 直書きすると、Seoコンポーネントに書いた内容がルートページのHEADに載せられない😨本末転倒なことになるため、少し改変しました。

三項演算子を見直してなおす

<title>{defaultTitle ? `${title} | ${defaultTitle}` : title}</title>

条件 (三項) 演算子 - JavaScript | MDN を見ながら、 似て非なるものを見比べた。

titleTemplate={defaultTitle ? `%s | ${defaultTitle}` : null}
条件に続いて疑問符 (?)、そして条件が真値であった場合に実行する式、コロン (:) が続き、条件が偽値であった場合に実行する式が最後に来ます。

下の、React-helmet向けだった方は defaultTitleが真値であるため | パイプを前に置く整形をしてたわけで、 siteMetadata.titleを間違って消したりすれば偽値となって、nullが返る。

上は、defaultTitleが真値なら、{title} と {defaultTitle} の間に | パイプを挿んで整形してね
defaultTitleが偽値なら、titleを返せば良いよ。
これはもぅ、siteMetadata.titleを故意に消さないかぎり | パイプから逃れられない式なので、書き換え。

<title>{title ? `${title} | ${defaultTitle}` : defaultTitle}</title>

titleが真値なら、title|defaultTitle になり
titleが偽値なら、defaultTitle が返る。

つまりページタイトルを設定する気のないサイトルート、index.js は、defaultTitle=siteMetadata.title が表示される。

あー長かった。

それに合わせて、seo.js の冒頭title, を空でもいいよーに変更して終わり。

components/seo.js
const Seo = ({ description, title = '', children }) => {

あ。肝心なindex.jsはtitle以外の何かを渡さねばSeoコンポーネントを使えないので、姑息にdescriptionを渡しました。
components/seo.js
export const Head = ({ data }) => {
  return (
    <Seo
      description={data.site.siteMetadata.description}
    />
  )
}

Gatsby Head API いいですね!⭐️⭐️⭐️⭐️⭐️

先に書いちゃうと、Lighthouseスコアは、React-helmet も Gatsby Head API も 現状では同じでした。
特にSEO部分は注力してないままの数値、内容は前に同じでトレースしただけなのでこれは当然。

非推奨になったから、やむなく移行、しかも主要プラグインをアンインストールしてからの「大工事」みたいに想定してましたが、まーほんとヘルメット脱いでさっぱりしたわぁ、くらいの軽い作業で HTML<head> に直接アクセスできるようになったというのは、考えたら、たまらん恩賜公園なみの解放感。

同じReactでも、先日まで触ってたWordPressでは「functions.php」しかほぼ窓口がない。
そこしかないから、そこを読めばだいたい載ってる・・・てのも、慣れれば都、かもしれないが、再び棲みたいと思えないのもね、うーん。なんだかとても勿体無い。

「Gutenbergはとても魅力的な女の子。だけどあの娘の親がちょっとアレで、特にグランパのPHPが・・・」的な話は詮無いのでやめよう。(Gutenbergは元男の子だけどさ)


React-helmet、素晴らしいネーミングセンスだと思ってましたが、最短でのお別れになりました。