Gatsbyでサイトマップを生成する

gatsby-plugin-sitemap(v5.3.0)対応

Soudai Sasada

Soudai Sasada

このブログは Gatsby で作成していますがバージョンが3.0.0となっており他の依存も古くなっていたので一気に更新することにしました。プラグインでは gatsby-image が gatsby-plugin-image となっていましたがマイグレーションガイドがあり記事のとおりに問題なくマイグレーションできました。他にも preact を使っていたのですが一度削除し新たに gatsby-plugin-preact のドキュメントにあるとおりに入れ直しビルド時に webpack に関するエラーが出ていたためエラー内容どおりに webpack をインストールしたところ解消しこれも特に問題なく移行ができました。一方で gatsby-plugin-sitemap は一気にメジャーバージョンを3系から5系に変えたということもありプラグインのAPIインターフェースが大きく変わっていたためにオプション設定周りで少々苦戦しました。最終的にはソースコードを読み込んで理解できましたがせっかくなので記事としてまとめようと思います。

インストール

まずはインストールします。

$ yarn add gatsby-plugin-sitemap
# npm install gatsby-plugin-sitemap

次に他のプラグインと同様にルートディレクトリに配置している gatsby-config.jsplugins で宣言します。

gatsby-config.js

siteMetadata: {
  siteUrl: `https://www.example.com`,
},
plugins: [
  {
    resolve: `gatsby-plugin-sitemap`
  }
]

デフォルトの設定では siteMetadata.siteUrl に依存しているためオプションをオーバーライドせずそのまま使いたい場合には siteMeatadata.siteUrl だけ設定すれば動きます。

生成先のディレクトリを指定する

デフォルトだと生成先がルートではなく /sitemap というディレクトリの下になります。が、Googleのガイドラインを見るとルートディレクトリに配置すべきとの記述があります。わざわざライブラリがデフォルトで特定のディレクトリを作る構成になっているので問題ないとは思いますが以下の文面を読むと本当に問題ないのか気になります。

サイトの任意の場所にサイトマップを配置できますが、サイトマップは親ディレクトリの子孫にのみ影響します。そのため、サイトのルート上に配置されたサイトマップは、サイト上のすべてのファイルに影響を及ぼす可能性があります。サイトマップはサイトのルート上に配置することをおすすめします。

幸いにも生成先は output というオプションを指定すれば変更できるのでもしルートディレクトリやそれ以外にも自身が指定した先にサイトマップを配置したい方は指定しましょう。

gatsby-config.js

siteMetadata: {
  siteUrl: `https://www.example.com`,
},
plugins: [
  {
    resolve: `gatsby-plugin-sitemap`,
    options: {
      output: '/' // 生成先をルートディレクトリに変更
    }
  }
]

特定のサイトを除外する

インデックス対象から除外しているページをサイトマップからも除外するには excludes オプションが使えます。 excludes オプションには文字列の配列を指定でき、指定した文字列と各ページのURLを minimatch で一致しているか検証、一致している場合には除外されるという仕組みとなっています。

具体的にどのような処理をしているかというとdefaultFilterPagesという関数をページをフィルタリングするためのループの中で呼び出しておりそこで真を返したページを除外する、という処理が行われています。これは後述の filterPages というカスタムフィルターの場合にも使われる処理ですが filterPages になにも設定されていない場合はデフォルト値として defaultFilterPages が指定されておりそれにより呼び出される、という仕組みとなっています。

例えば筆者の場合は以下のように設定しています。

gatsby-config.js

siteMetadata: {
  siteUrl: `https://www.example.com`,
},
plugins: [
  {
    resolve: `gatsby-plugin-sitemap`,
    options: {
      output: '/',
      excludes: ['/404?(.*)', '/contact', '/thanks', '/**/privacy']
    }
  }
]

一つ目の /404?(.*)/404 の他に /404.html にもマッチします。また、最後の /**/privacy/hoge/privacy/fuga/privacy にマッチしこれらのマッチしたページは除外することができます。 minimatch の詳しい使い方は他に色々と詳しく書いている方がいるので調べてみると良いかと思います。

なお、デフォルトのフィルターの処理ではなく filterPages というオプションに真偽値を返す関数を渡すことで自身でフィルターを設定することもできます。ただこちらも仕組みとしては上述のデフォルトのフィルター関数と同様にページをフィルタリングするためのループ内でページの各URLに対し excludes で設定された文字列の配列に対し Array.prototype.some() 関数の引数として呼び出すため、 minimatch では表現しづらいような場合には有効なのかもしれませんがほぼ使う機会はないのではないかという気がします。このあたりは実際にドキュメントを読んだだけだと意図が読み取れず( excludes を指定しないと filterPages が呼び出されないなど)最初はバグかと思いました。

まとめ

最終的に筆者の環境ではこのような設定になりました(実際には他の設定もありますが gatsby-plugin-sitemap に関係しないため関係するもののみを抜粋しています)。

gatsby-config.js
module.exports = {
  siteMetadata: {
    siteUrl: process.env.SITE_URL,
  },
  plugins: [
    {
      resolve: `gatsby-plugin-sitemap`,
      options: {
        query: `
          {
            site {
              siteMetadata {
                siteUrl
              }
            }
            allSitePage {
              nodes {
                path
              }
            }
            allContentfulBlogPost(
              filter: { node_locale: { eq: "ja-JP" }, published: { eq: true } }
            ) {
              edges {
                node {
                  slug
                  updatedAt
                }
              }
            }
            allSitePage {
              nodes {
                path
              }
            }
          }
        `,
        output: '/',
        resolvePages: ({
          allSitePage: { nodes: allPages },
          allContentfulBlogPost: { edges: posts }
        }) => {
          const blogPosts = posts.reduce((acc, edge) => {
            const { slug, updatedAt } = edge.node
            acc[`/${slug}`] = { updatedAt }
            return acc
          }, {})
          return allPages.map(page => {
            const { path } = page
            const base = { path }
            return { ...base, ...blogPosts[path] }
          })
        },
        excludes: ['/404?(.*)', '/contact', '/thanks', '/**/privacy'],
        serialize: ({ path, updatedAt }) =>  {
          const site = {
            url: path,
            changefreq: `daily`,
            priority: updatedAt ? 0.7 : 0.5,
          }
          if (!updatedAt) return site

          const lastmod = { lastmod: updatedAt }
          return { ...site, ...lastmod }
        },
      }
    },
    // and more plugins...
  ],
  // and more settings...
}

サイトマップが生成されているか確認しもしサイトマップのパスが変更された場合にはサーチコンソールから新しいサイトマップのインデックスファイルを送信しておきましょう。

sitemap-submission-in-search-console

このサイトはこれまでバージョンが3系でサイトマップのインデックスファイルを生成されておらず単一のサイトマップで十分でしたがバージョンが5系となりインデックスファイルが自動生成されるようになったため構成そのものが変わりました。デフォルトではページの生成時に head 要素内の link 要素でサイトマップのリンクが設置されるようになっておりサイトマップはそちらからも辿れるためいずれ新しいサイトマップが参照されるとは思いますがクローラーがいつ新しいサイトマップを参照するかは特に保証されているわけではないので心配な方は送信しておくと良いでしょう。

感想

queryresolvePagesserialize といった他のオプションはバージョンアップ前とそこまで大きく変わっていなかったのでドキュメントを確認するだけで元々の設定ほぼそのままで動き特につまずくことはなかったのですが、フィルタリング周りは個人的に非常に分かりづらく、最初はバグかと思ったのでコントリビュートしようとリポジトリを clone し実際にいくつかコミットを積んでいく過程で気が付きました。

バグだと思った原因は確認する限り他に以下の2つの問題も生じたためです。

  • トップページはトレイリングスラッシュを取り除いたURLにできない
  • lastmod をどのように指定しても Date.prototype.toISOString() で変換される

前者、後者ともに gatsby-plugin-sitemap が直接依存している sitemap (以下 sitemap.js )というライブラリに関係しており、前者は要望があったものの sitemap.js で非対応が明言されており、後者は gatsby-plugin-sitemap が sitemap.js の simpleSitemapAndIndex という関数に依存しておりこの関数が仕様で日付のみの形式をサポートしていないためでした。これは sitemap.js に問題があるわけではなく gatsby-plugin-sitemap が依存している関数が対応していないだけで sitemap.js では他の関数で yyyy-MM-dd のフォーマットに対応しています。これはサイトマップの仕様として定義されている W3C-DTF 形式に則っておりなんらかの理由で時間まで指し示すことができないような場合には適しています。ドキュメントを読む限り Google は lastmod の値と loc が指し示すページの実際の更新日時の一貫性を見ているため時間まで指し示すことができない場合は時間が0時0分0秒へ変換されてしまうよりも yyyy-MM-dd としておきたいユースケースを想定しているのではないかと推測していますが gatsby-plugin-sitemap としては必要ないということかもしれません。

フィルタリングについては仕様の誤認でしたので横着をせずにドキュメントを読み込めばそこまで至らずとも解決できたのかな?と思いましたが、仕組みを知るうえで実際に手を動かして修正しようと試みたのは本格的なモノレポ構成に触れるという観点で結果的に良い経験になりました。

実際に Pull Request を出すまでやってみましたが Gatsby のように巨大なエコシステムを持つライブラリでモノレポ構成だと Issues や Pull Requests から類似のチケットや修正が出されていないかを探すのが難しい点やリポジトリのクローンに時間が掛かる点、コミット履歴に自身が関心を持っている内容とは関係ないものが多いため雑に探せない点、CIに時間が掛かる点に課題を感じました。今回はあまり時間を取れずちゃんと読んでいないのですが How to Contribute にあるとおり hubgatsby-dev-cliLerna あたりのツールを抑えておくと良さそうです。

この記事がお役に立てたら一杯のコーヒーを恵んで頂けると励みになります 😊

Buy Me A Coffee
© 2020, Soudai Sasada