All Articles

投稿月ごとに記事を表示するページ追加で苦労した話

はじめに

↓ こういう感じで投稿月ごとの記事数と、ある投稿月の記事を表示するページを追加しました。

image.png

「Category とか Tag とかと同じ理屈だし簡単やろ」と思ってたら全然簡単じゃなかったし、PR は以下の規模になってしまったし、そこそこ大規模な改修になっちゃったのでその時の作業メモを多少清書して書き残します。

image.png

Gatsby の理解

以下 gatsby-starter-lumen が前提

allMarkdownRemark クエリ

gatsby 内部では GraphQL によって内包するコンテンツにアクセスする。allMarkdownRemarkは gatsby で mx/mdx をビルドした後のデータを取り出すことができる Query で、記事タイトルなど markdown 内で記述した一通りのデータに加え、on-create-node.tsで定義した独自の要素をも取得することができるらしい。

allMarkdownRemarkクエリをうまく使えば記事の集計が必要な動的ページの生成が可能ということはとりあえず理解。

templates

動的なページ生成の要となるのが templates ディレクトリ以下の tsx ファイル群であり、ページを構成するブログ記事のデータの取得や React の component を使用したページの構成も行う様子。

ポイントは

import { graphql } from "gatsby";

で gatsby からimportしたgraphqlで、これはタグであり以下のようにタグ付きテンプレートリテラルとして使用する。

export const query = graphql`
  query PostedMonthTemplate(
    $limit: Int!
    $offset: Int!
    $dateUpperLimit: Float
    $dateLowerLimit: Float
  ) {
    site {
      siteMetadata {
        title
        subtitle
      }
    }
    allMarkdownRemark(
      limit: $limit
      skip: $offset
      filter: {
        frontmatter: {
          template: { eq: "post" }
          draft: { ne: true }
          date: { lte: $dateUpperLimit, gte: $dateLowerLimit }
        }
      }
      sort: { order: DESC, fields: [frontmatter___date] }
    ) {
      edges {
        node {
          fields {
            slug
            categorySlug
            postedMonthSlug
          }
          frontmatter {
            title
            date
            category
            description
            slug
          }
        }
      }
    }
  }
`;

gatsby はこのgraphqlタグが付与されてexportされたクエリを認識する。このgraphqlタグで定義したクエリの引数はpageContextとして渡されている辞書を参照することができる。つまり、graphqlタグのクエリとReact.FCの引数は同じものを参照していて、渡されたpageContextから好きなものを使用可能ということらしい。

もし GraphQL のクエリに引数が不要な場合は、gatsby が備えるuseStaticQueryを使うと良さそう。実際に hooks ディレクトリ以下にこれをラップした便利関数群が実装されていて、特に引数を必要としないカテゴリリストのページなどでgraphqlタグを使ったクエリの代わりに使われている。

create-pages.ts

internal/create-pages.ts がブログのページを構築する重要な役割を担っているらしい。

createPage関数にページのパス、templates ディレクトリ以下のページを定義する component、pageContextに渡される引数群の 3 つを渡すことでページが作られる。

gatsby-starter-lumen ではベースパスや component などが constants という形で外に切り出されている様子。

ページ定義の流れ

Category や Tag を踏襲して

  1. 投稿月と投稿月ごとの記事数を取得するuseStaticQueryをラップした新しい hook の実装
  2. 投稿月と投稿月ごとの記事数を表示したナビゲーション用のページ作成
  3. 投稿月で絞る GraphQL クエリの実装とそれを含む各投稿月ごとの記事表示用のページ作成
  4. internal/create-pages.ts でページ生成
  5. 投稿月のナビゲーションページを表示するメニュー項目を増やす

という感じでいけそう。

ハマりポイント

日付定義の形式変更

以下 gatsby の公式ドキュメントによれば、ltegteなどで日付を絞り込めそうに見えるが、実はこの段階のdateの値は ISO8601 に沿った string であり、大小により絞り込めない。regexがあるのでこれを使えば特定月の投稿のみ取得できそうだが、正規分布よりは単純な大小比較の方が計算量的に早そう。

最終的に ISO8601 を取りやめてdayjsで色々ラップしつつYYYYMMDDHHmmss形式の number 型で記事の投稿日を定義することにした。

markdown で記事を書く際に、これまで"2022-12-16T00:00:00.000Z"のように記述していた日付を20221216000000のように記述する。多少可読性が落ちている点が気になるところではある。markdown を解釈するプラグインにカスタムの処理を入れて、人が書く定義の方は変えず内側で取り扱うときの形式だけ変更すると良さそうであるが、面倒なのでまた今度。

元々 string だった部分を number に変えたために方々で問題が生じたのでちまちまつぶす必要あり。YYYYMMDDHHmmss形式の日付を受け取って dayjs オブジェクトを経由して必要な string に変換する補助関数を実装することで、これまでの実装との整合性を持たせる。

投稿月による記事絞り込みクエリ

GraphQL の filter だけでは投稿月ごとの集計が難しそう (元々カテゴリカルな値に基づく集計は用意な一方、数値などに基づく集計は少々厳しそう) なので、reduceを使用した以下補助関数を用いて GraphQL によって取得した結果を加工する方向で実装した。

const months = allMarkdownRemark?.group
  ? allMarkdownRemark.group.reduce(
      (acc: Array<Group>, cur: Group): Array<Group> => {
        const month = cur.fieldValue.substring(0, 6);
        if (!acc.length) {
          return [
            {
              fieldValue: month,
              totalCount: 1,
            },
          ];
        } else if (acc[acc.length - 1].fieldValue === month) {
          acc.splice(acc.length - 1, 1, {
            fieldValue: acc[acc.length - 1].fieldValue,
            totalCount: acc[acc.length - 1].totalCount + 1,
          });
          return acc;
        } else {
          acc.push({
            fieldValue: month,
            totalCount: 1,
          });
          return acc;
        }
      },
      [],
    )
  : [];

kebab case (おまけ)

日付に関する変更とは無関係に、create-page.ts など internal 周辺で使われている kebab case 変換補助関数と src 以下で使われている kebab case 変換補助関数の実装が異なることに気付く。

リンク先の相対パスが合わずエラーになるので正しそうな方に寄せる形で修正。

Published 2022/12/27

ShuntaIto による技術ブログ