Gridsomeで月別アーカイブ機能を作成する。

6 min読了の目安(約5400字TECH技術記事

もともと自分のブログに投稿する予定だったのですが、ZennにGridsomeの投稿が1つもなくて絶望したのでZennに書くことにしました。
(Gridsomeについて限りなく雑に説明すると、静的サイトジェネレーターGatsbyのVue版です。自分のブログもGridsomeで作っています。)
ちなみに私はVueもReactもほとんど触ったことがないフロントエンド初心者です。

実現したいこと

Gridsomeで月別アーカイブ機能を作成します。
ブログによくあるこういうやつです。

アーカイブのURLは
https://retrorocket.biz/archives/date/2020/10
のように/archives/date/{Year}/{Month}で閲覧できるようにします。

動作確認環境

$ gridsome info
  System:
    OS: Linux 4.19 Ubuntu 20.04.1 LTS (Focal Fossa)
  Binaries:
    Node: 12.18.3 - /usr/local/bin/node
    Yarn: 1.22.5 - /usr/local/bin/yarn
    npm: 6.14.8 - /usr/local/bin/npm
  npmPackages:
    gridsome: ^0.7.21 => 0.7.21 
  npmGlobalPackages:
    @gridsome/cli: 0.3.4

GraphQLのクエリにallBlogPostを使用する前提で話を進めていますが、allWordpressPost等を使う場合は適宜読み替えてください。

やること

  1. 月別アーカイブを/archives/date/{Year}/{Month}で閲覧するために、Pages APIを使って個別ページを作る
  2. サイドバーから月別アーカイブの一覧を表示するために、componentを作成する

月別アーカイブ用の個別ページ作成

Pages APIcreatePagesを使用すると、特定のパスで使用するデータとテンプレートを自分で編集できます。

今回は以下の流れで個別ページを作りました。

  1. GraphQLのallBlogPostで全投稿から投稿年と投稿月だけ取得し、createPagesで対応する年月のページを作成
  2. template内でGraphQLのfilterを使用し、前の工程で取得した年と月に該当する投稿を一覧表示する
    実装にあたり、Gatsby.jsで年ごと、月ごとで記事一覧を表示したい - Qiitaを大変参考にいたしました。ありがとうございます。

gridsome.server.js

// gridsome.server.js
module.exports = api => {
  api.createPages(async ({ graphql, createPage }) => {
    // GraphQLで全投稿を検索
    // 投稿年と投稿月だけ取得する
    const { data } = await graphql(`{
      allBlogPost {
        edges {
          node {
            year: date(format: "YYYY")
            month: date(format: "YYYY,MM")
          }
        }
      }
    }`)

    const years = new Set();
    const yearMonths = new Set();

    // 全投稿から取得した投稿年と投稿月の重複を削除
    data.allBlogPost.edges.forEach(({ node }) => {
      years.add(node.year);
      yearMonths.add(node.month);
    });

    // 年ページの作成
    years.forEach(year => {
      createPage({
        path: `/archives/date/${year}`,
        component: "./src/templates/Years.vue",
        context: {
          displayYear: year,
	  // template内で投稿年の1/1から12/31までの記事一覧を取得するために、年末の日時を呼び出せるようにする
          periodStartDate: `${year}-01-01T00:00:00.000Z`,
          periodEndDate: `${year}-12-31T23:59:59.999Z`
        }
      });
    });

    // 月ページの作成
    yearMonths.forEach(yearMonthStr => {
      const yearMonth = yearMonthStr.split(",");
      // 指定した月の末日を取得
      const date = new Date(yearMonth[0], yearMonth[1], 0);
      const year = date.getFullYear();
      const month = ("00" + (date.getMonth() + 1)).slice(-2);
      const day = ("00" + date.getDate()).slice(-2);
      createPage({
        path: `/archives/date/${yearMonth[0]}/${yearMonth[1]}`,
        component: "./src/templates/Years.vue",
        context: {
          displayYear: `${yearMonth[0]}/${yearMonth[1]}`,
	  // template内で投稿月の1日から月末までの記事一覧を取得するために、月末の日時を呼び出せるようにする
          periodStartDate: `${year}-${month}-01T00:00:00.000Z`,
          periodEndDate: `${year}-${month}-${day}T23:59:59.999Z`
        }
      });
    });

  })
}

template

api.createPagesで設定した)periodStartDateperiodEndDateの範囲内にある記事を一覧で出力します。

<!-- ./src/templates/Years.vue -->
<template>
  <Layout>
    <div>
      <h1 class="entry-title" itemprop="headline">
        {{ $context.displayYear }}
      </h1>

      <ul>
        <li v-for="{ node } in $page.years.edges" :key="node.id">
          <g-link :to="node.path">
            <span v-html="node.title" />
          </g-link>
          {{ node.date }}
        </li>
      </ul>
    </div>
  </Layout>
</template>

<!-- periodStartDateとperiodEndDateの範囲内にある記事を検索する -->
<page-query>
query PostsByDate($periodStartDate: Date, $periodEndDate: Date) {
  years: allBlogPost(filter: {date: {between: [$periodStartDate, $periodEndDate]} }) {
    edges {
      node {
        id
        title
        path
        date(format: "YYYY/MM/DD")
      }
    }
  }
}
</page-query>

これで、/archives/date/{Year}/{Month}にアクセスすると、該当する記事の一覧が表示できるようになりました。

月別アーカイブ一覧用のcomponentを作成

かなりインチキくさい実装ですが、以下の流れでコンポーネントを作りました。

  1. allBlogPostで全投稿から投稿年と投稿月だけ取得する
  2. 投稿年を表示するために、v-for内でSetを使用し、重複した投稿年を削除してループを回す
  3. 投稿月を表示するために、投稿年に対応する月をfilter (GraphQLのfilterではなく Array.prototype.filter()のほう)で絞り込んで表示する
<template>
  <div>
    <div
      v-for="(year, yindex) in new Set(
        $static.years.edges.map((e) => e.node.year)
      )"
      :key="`y-${yindex}`"
    >
      <h6>
        » {{ year }}
      </h6>
      <div>
        <g-link
          v-for="(month, mindex) in new Set(
            $static.years.edges
              .map((e) => e.node.month)
              .filter((e) => e.indexOf(year) === 0)
              .reverse()
          )"
          :key="`m-${mindex}`"
          :to="`/archives/date/${month}`"
          >{{ month.slice(-2) }}</g-link
        >
        <g-link
          :to="`/archives/date/${year}`"
          >all</g-link
        >
      </div>
    </div>
  </div>
</template>

<static-query>
query {
  years: allBlogPost(sortBy: "published_at", order: ASC) {
    edges { 
      node { 
        year: date(format: "YYYY")
        month: date(format: "YYYY/MM")
      }
    }
  }
}
</static-query>

これでサイドバーに月別アーカイブ一覧が表示できるようになりました。
もともと自分のブログで受けた問い合わせの回答として書いた記事なのですが、誤りがあれば指摘いただけると幸いです。