↓ こういう感じで投稿月ごとの記事数と、ある投稿月の記事を表示するページを追加しました。
「Category とか Tag とかと同じ理屈だし簡単やろ」と思ってたら全然簡単じゃなかったし、PR は以下の規模になってしまったし、そこそこ大規模な改修になっちゃったのでその時の作業メモを多少清書して書き残します。
以下 gatsby-starter-lumen が前提
gatsby 内部では GraphQL によって内包するコンテンツにアクセスする。allMarkdownRemark
は gatsby で mx/mdx をビルドした後のデータを取り出すことができる Query で、記事タイトルなど markdown 内で記述した一通りのデータに加え、on-create-node.ts
で定義した独自の要素をも取得することができるらしい。
allMarkdownRemark
クエリをうまく使えば記事の集計が必要な動的ページの生成が可能ということはとりあえず理解。
動的なページ生成の要となるのが 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
タグを使ったクエリの代わりに使われている。
internal/create-pages.ts がブログのページを構築する重要な役割を担っているらしい。
createPage
関数にページのパス、templates ディレクトリ以下のページを定義する component、pageContext
に渡される引数群の 3 つを渡すことでページが作られる。
gatsby-starter-lumen ではベースパスや component などが constants という形で外に切り出されている様子。
Category や Tag を踏襲して
useStaticQuery
をラップした新しい hook の実装という感じでいけそう。
以下 gatsby の公式ドキュメントによれば、lte
やgte
などで日付を絞り込めそうに見えるが、実はこの段階の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;
}
},
[],
)
: [];
日付に関する変更とは無関係に、create-page.ts など internal 周辺で使われている kebab case 変換補助関数と src 以下で使われている kebab case 変換補助関数の実装が異なることに気付く。
リンク先の相対パスが合わずエラーになるので正しそうな方に寄せる形で修正。