thumbnail_gatsby_logo

lastmodタグを追加するためのgatsby-plugin-sitemapの使い方解説

公開日2021.08.10
更新日2021.08.26

gatsby-plugin-sitemapで作成したサイトマップに最終更新日を追加する方法です。 出力されるサイトマップのカスタマイズ方法について詳細な解説が見つけられなかったので、私が把握した設定方法の解説をまとめました。

目次目次
  • gatsby-plugin-sitemapの最終更新日
  • 動作に必要な最小構成
  • gatsby-plugin-sitemapのoptions解説
  • query
  • resolveSiteUrl
  • resolvePagePath
  • resolvePages
  • filterPages
  • serialize
  • 完成コード
  • query
  • resolveSiteUrl
  • resolvePages
  • serialize
  • まとめ

gatsby-plugin-sitemapの最終更新日

Gatsby でサイトマップを生成する公式プラグイン、gatsby-plugin-sitemap で出力される sitemap.xml には、初期状態では最終更新日が挿入されません。

最初は別に不満はなかったのですが、Google Search Console で操作している時に、どうせ渡すならサイトの正確な情報にしたいと思い、実装してみることにしました。

以下のリンク先の、現在のブログのサイトマップを見てもらうと、<lastmod>で最終更新日が出力されていることが分かると思います。

https://blog.abarabakuhatsu.com/sitemap/sitemap-0.xml

動作に必要な最小構成

今回は完成したコードがとても長く、いきなり見ても分かりづらいと思うので、解説用に gatsby-plugin-sitemap のデフォルトの設定を入れたものを作成してみました。 それぞれのオプションの用途が分かりやすいと思います。 「解説の存在など不要だ!」という方は目次のリンクから完成コードまで飛んでください。

最小構成というわけで、この内容で gatsby-config.ts に plugins: [`gatsby-plugin-sitemap`] とオプションなしの名前だけを書いた場合と全く同じサイトマップが生成されます。 これを元にして欲しい機能を追加すれば簡単にサイトマップを調整できると思います。 汎用性を考えて型情報は消しているのでjsでも動く、と思います。(未確認)

gatsby-config.ts
{
  resolve: 'gatsby-plugin-sitemap',
  options: {
    query: `{
      site {
        siteMetadata {
          siteUrl
        }
      }
      allSitePage {
        nodes {
          path
        }
      }
    }`,
    resolveSiteUrl: (data) => {
      return data.site.siteMetadata.siteUrl
    },
    resolvePagePath: (page) => {
      return page.path
    },
    resolvePages: (data) => {
      return data.allSitePage.nodes
    },
    serialize: (page, { resolvePagePath }) => {
      return {
        url: resolvePagePath(page),
        changefreq: 'daily',
        priority: 0.7,
      }
    },
  },
},

gatsby-plugin-sitemapのoptions解説

オプションの中から今回の設定変更に関わる以下のものについての解説です。

  • query
  • resolveSiteUrl
  • resolvePagePath
  • resolvePages
  • filterPages
  • serialize

他のオプションは名前のままの機能なので、公式を見るだけで解説の必要もないでしょう。

全体の処理流れが把握しやすいようにイメージ図を作ってみました。 正確な処理の流れを書いたものではないですが、それぞれのオプションの戻り値がどう関係しているのかというイメージは伝わるかと思います。

add last modified date information to sitemap 01

query

戻り値: object

query: `{
  site {
    siteMetadata {
      siteUrl
    }
  }
  allSitePage {
    nodes {
      path
    }
  }
}`,

サイトマップ生成に必要な情報を GraphQL で所得する起点です。 所得したデータは resolveSiteUrl と resolveSite の引数に data として渡されます。

デフォルトでは Gatsby で必ず生成されるノードだけを使ってサイトマップを生成するので、設定を書き換えないと最終更新日が所得できないわけです。

resolveSiteUrl

戻り値: string

resolveSiteUrl: (data) => {
  return data.site.siteMetadata.siteUrl
},

戻り値はhttps://foo.comのようなサイト URL の文字列です。 これと後で作成する各ページへのパスを合成してサイトマップが作成されます。

引数で上記の query の結果を data として受け取れます。 もちろん、GraphQL の結果が使えるので、ソースはsiteMetadata に限らず、設定すれば HeadlessCMS などから自由に情報を取ってくることが出来ます

この関数の戻り値の初期値はreturn data.site.siteMetadata.siteUrlとなっているで、gatsby-plugin-sitemapのマニュアル冒頭で gatsby-config ファイルに siteMetadata を設定するよう書かれているわけです。 これを query で所得できない場合、resolveSiteUrl で別の情報を渡してやらないとビルドが失敗します。

resolvePagePath

戻り値: string

resolvePagePath: (page: { path }) => {
  return page.path
},

filterPages と serialize の内部で利用されていて、resolvePages からの戻り値である page オブジェクトの中の path を取り出して返します。 例では内部のデフォルトの設定通りに serialize の中で使用しています。 文字通りreturn page.pathしかしていません。

自分でプラグインを設定する場合、中でこの関数をわざわざ呼び出すのは冗長なので、あまり使うことはなさそうですね。 これの設定をするとしたら、filterPages と serialize をデフォルト設定のまま使いたいけれど、resolvePages だけは変更したので、URL の格納先の名前が path から名称が変更されている場合とかでしょうか? その場合でも resolvePages で path に格納してから出力してやればいいので、私にはちょっと用途が思いつきませんWoozy Face Emoji

resolvePages

戻り値: Array

resolvePages: (data) => {
  return data.allSitePage.nodes
},

ここで所得したGraphQLのデータから必要なものを選別して配列に格納します。 最終更新日を追加するという、今回の課題では一番重要な箇所です。

これも resolveSiteUrl と同じく、引数で query 結果を data として受け取れます。 GraphQL から所得した data を map なりを使って加工して、最終的に path を持つオブジェクトの配列になるように出力してやればいいわけです。

resolvePagesの必須戻り値型
{ path: string }[]

この後の filterPages 内部で resolvePagePath が path が存在するかチェックするので、resolvePagePath の内容を変更しない場合は、戻り値となるオブジェクトの中には path が必須になります。 逆に path さえあればいいので、追加する分には問題ありません。

今回の場合は lastmod を加えて resolvePages の戻り値をこうするわけです。

完成予定型
{ 
  path: string
  lastmod: string
}[]

filterPages

内部的には親となる pageFilter 関数の中に defaultFilterPagesと、ユーザー設定の filterPages が存在する形になっています。 defaultFilterPages は excludes オプションで指定したページに該当するものを排除しています。

戻り値の説明に

Returns: Boolean - - true excludes the path, false keeps it.

//Google翻訳 //戻り値:ブール値--- trueはパスを除外し、falseはパスを保持します。 https://www.gatsbyjs.com/plugins/gatsby-plugin-sitemap/#filterPages

とあるように、この関数で true になる URL が除外されます。 pageFilter の filter メソッドの最後で return !(defaultFilterMatches || customFilterMatches)という処理になっており、通常の filter メソッドの false を除外する処理とは逆になるので、使う場合は注意が必要そうですね。 (ただ、私はこれを使っていないので細かい挙動については未検証です)

serialize

戻り値: object

serialize: (page, { resolvePagePath }) => {
  return {
    url: resolvePagePath(page),
    changefreq: `daily`,
    priority: 0.7,
  }
},

最終的なサイトマップのデータになるオブジェクトを生成する関数です。 resolvePages で作られて、filterPages を通過してきた配列が for of で回されて、この関数で処理されます。 map メソッドの中身のようなものなので、resolvePages とは違い、戻り値は配列ではなくサイトマップに記載される個々のページ情報の入ったオブジェクトとして返します。

デフォルトでは changefreq と priority を固定値で設定するのに使用されていますね。 これの戻り値がサイトマップのタグの中身になります。

また、例では挙動と合わせるために、第二引数で resolvePagePath を所得していますが、実際に使うときは特に必要ありません。

完成時に resolvePages で加えた lastmod を追加することで、sitemap に最終更新日が加わることになります。 TypeScript で言うとこんな感じになります。

戻り値の型
{
  url: string
  changefreq: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'
  priority: 0.0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1.0
  lastmod: string
}

完成コード

これがサイトで使用している gatsby-plugin-sitemap の完成形です。 最初はこれを記事の冒頭に掲載しようとしたのですが、いきなりこれを見ても目が滑りそうなのでやめました。

必要な記述なのですが、type 情報と複数の GraphQL のクエリがあるのでかなり長いです。

gatsby-config.ts
export const plugins = [
  {
    resolve: 'gatsby-plugin-sitemap',
    options: {
      query: `
      query PluginSitemap {
        contentfulSiteMetaData {
          site_url
        }
        pablish_sort_data: allContentfulBlogPost(
          sort: {fields: published_date, order: DESC}
          limit: 1
        ) {
          edges {
            node {
              published_date
            }
          }
        }
        allContentfulBlogPost {
          edges {
            node {
              slug
              category {
                slug
                master_category {
                  slug
                }
              }
              published_date
              modified_date
            }
          }
        }
      }
      `,
      resolveSiteUrl: ({
        contentfulSiteMetaData: { site_url },
      }: resolve_site_url_type): string => site_url,
      resolvePages: ({
        pablish_sort_data: { edges: top_pablish_date },
        allContentfulBlogPost: { edges: blog_posts },
      }: resolve_pages_type): resolve_pages_return_type => {
        const blog_posts_sitemap_data = blog_posts.map(
          ({ node }): page_information_type => {
            const path = `/${node.category.master_category.slug}/${node.category.slug}/${node.slug}`
            const lastmod = compare_dates_return_newer(
              node.published_date,
              node.modified_date
            )
            return { path, lastmod }
          }
        )
        const top_page_sitemap_data = {
          path: '/',
          lastmod: top_pablish_date[0].node.published_date,
        }
        return blog_posts_sitemap_data.concat(top_page_sitemap_data)
      },
      serialize: ({
        path,
        lastmod,
      }: page_information_type): sitemap_serialize_return_type => {
        return {
          url: path,
          changefreq: 'daily',
          priority: 0.7,
          lastmod,
        }
      },
    },
  },
]

// 以下はTypeScript用の型設定
type resolve_site_url_type = {
  contentfulSiteMetaData: {
    site_url: string
  }
}

type resolve_pages_type = {
  pablish_sort_data: pablish_sort_data_type
  allContentfulBlogPost: all_contentful_blog_post_type
}

type pablish_sort_data_type = {
  edges: {
    node: {
      published_date: string
    }
  }[]
}

type all_contentful_blog_post_type = {
  edges: {
    node: {
      slug: string
      category: {
        slug: string
        master_category: {
          slug: string
        }
      }
      published_date: string
      modified_date: string
    }
  }[]
}

type page_information_type = {
  path: string
  lastmod: string
}

type resolve_pages_return_type = page_information_type[]

type sitemap_serialize_return_type = {
  url: string
  changefreq:
    | 'always'
    | 'hourly'
    | 'daily'
    | 'weekly'
    | 'monthly'
    | 'yearly'
    | 'never'
  priority: 0.0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1.0
  lastmod: string
}

見てみると分かると思うのですが、GraphQLのクエリを加工するのでサイトの設計ごとの変化がとても大きいです。 始める前は公式の解説が薄いことが不満でしたが、確かにマニュアルでこれを全て解説をするのは難しいなと思いました。

ですか、この記事のオプションの解説を読んだなら、ここで何をやっているかは一目瞭然だと思います。 だから、最初にコードを掲載せずにオプションの解説から始める必要があったんですね。

query

どうせならサイト構築に必要な情報は全て集約してしまいたいので、サイトURL を gatsby-config.ts 内の siteMetadata ではなく Contentful から所得しています。

pablish_sort_data はサイトのトップページの更新日のために所得させています。 トップページにある新着記事が部分が更新されるので、記事の中で一番新しい公開日ですね。

allContentfulBlogPost では個々の記事のパスと更新日を一括で所得。 このブログでは URL を /master_category.slug/category.slug/slug として生成するので、3 つの slug が必要となっています。

resolveSiteUrl

resolveSiteUrl: ({
  contentfulSiteMetaData: { site_url },
}: resolve_site_url_type): string => site_url,

GraphQL で受け取ったデータを分割代入で中身を取り出しています。 resolveSiteUrl: (data) => data.contentfulSiteMetaData.site_urlとやってることは一緒です。 ここは、そのまんまサイト URL を返しているだけですね。

resolvePages

resolvePages: ({
  pablish_sort_data: { edges: top_pablish_date },
  allContentfulBlogPost: { edges: blog_posts },
}: resolve_pages_type): resolve_pages_return_type => {
  const blog_posts_sitemap_data = blog_posts.map(
    ({ node }): page_information_type => {
      const path = `/${node.category.master_category.slug}/${node.category.slug}/${node.slug}`
      const lastmod = compare_dates_return_newer(
        node.published_date,
        node.modified_date
      )
      return { path, lastmod }
    }
  )
  const top_page_sitemap_data = {
    path: '/',
    lastmod: top_pablish_date[0].node.published_date,
  }
  return blog_posts_sitemap_data.concat(top_page_sitemap_data)
},

blog_posts_sitemap_data には記事情報が格納された配列を map メソッドで回して、パスと更新日だけのセットにして代入しています。

lastmod の設定時に使用されている compare_dates_return_newer 関数は、二つの日付を比較して新しいものを返す自作関数です。 また、同時に modified_date が設定されていない場合は値が null になるので、これを除外しています。

top_page_sitemap_data には記事情報では作成されないトップページの情報を追加します。 このサイトはプラグインの gatsby-plugin-remove-trailing-slashes で URL の trailing slash を排除しているのに path を '/' としているのは、空だとエラーが出るからです。

そうして作った、blog_posts_sitemap_data と top_page_sitemap_data を組み合わせて、serialize で使用するための戻り値としているわけです。

serialize

serialize: ({
  path,
  lastmod,
}: page_information_type): sitemap_serialize_return_type => {
  return {
    url: path,
    changefreq: 'daily',
    priority: 0.7,
    lastmod,
  }
},

ここは戻り値のオブジェクトに固定値を追加しているだけですね。 変化としては、今までは path という key で渡ってきていた情報が url に格納されることぐらいでしょうか。 lastmod は変数名と設定するべき key 名が同じなのでショートハンドでそのまま渡しています。

まとめ

今回、いくらマニュアルを読んでも意味が分からず、初めて Gatsby の plugin のコードをちゃんと読みましたが、どんなにマニュアルを読むよりも正確に内容が分かりますね。 “Talk is cheap. Show me the code.”という有名な言葉は事実だと実感できました。

Top
Back to Top
トップへ戻る