microCMS

Next.jsで月別アーカイブを実装してみよう

ひまらつ

この記事は公開後、1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

WordPressなどのCMSではよくみられる「月別アーカイブ」。
Jamstackなサイトではどのように実装するのが良いでしょうか?

当記事ではNext.jsでの実装を例に、月別アーカイブを実装する手順を紹介します。

月別アーカイブを実装する

前提として、「microCMS + Next.jsでJamstackブログを作ってみよう」のチュートリアルで作成されるブログを題材にし、ここに「月別アーカイブ」機能を追加しく形で進めていきます。

アウトプットはこのようなイメージです。


先に実装の流れを確認しておきましょう。
以下のようなステップになると思います。

  1. ブログ記事をすべて取得する
  2. 月ごとに記事をグルーピングする
  3. 「2022年2月(13)」のような表示を作る


なお、今回題材とするブログはSSGで作っています。そのため、月別アーカイブはビルド時に構築する想定です。
それでは実装していきましょう。

1. ブログ記事をすべて取得する

月別アーカイブを作成するタイミングはNext.jsの getStaticProps が良いでしょう。
まず、ブログ記事をすべて取得するには以下のように記述します。

// pages/index.js

export const getStaticProps = async () => {
  // "blog" のコンテンツを全件取得
  const data = await client.get({
    endpoint: "blog",
    queries: { fields: "publishedAt", limit: 3000 },
  });

  return {
    props: {
      contents: data.contents,
    },
  };
};


limitを指定しない場合、コンテンツは10件のみ取得されます。ここでは全件取得したいので大きめの値を指定しています。
また、ここでは記事の公開日付の情報だけがあれば良いので、fields クエリを使って取得するフィールドを絞り、通信量を抑えています。(詳細

2. 月ごとに記事をグルーピングする

microCMSで配信するコンテンツには publishedAt という、コンテンツを公開した日付の情報が付与されます。

日付はISO 8601形式のUTCで、たとえば以下のような形式の値です。

'2022-04-06T05:03:27.510Z'


今回は「月」でグルーピングしたいので、日にちや時間の情報はカットして以下の形式に変換したいと思います。

'2022_04'


この日付の変換を実装していきます。
まず、日付を扱うのに便利な dayjs というライブラリを準備します。

$ npm i -S dayjs


次に、日付をフォーマット変換する実装を追加します。

// libs/util.js
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";

dayjs.extend(utc);
dayjs.extend(timezone);

// UTC -> "2022_04" のフォーマットに変換
export const formatDate = (date) => {
  const formattedDate = dayjs.utc(date).tz("Asia/Tokyo").format("YYYY_MM");
  return formattedDate;
};


この関数はmicroCMSで取得したコンテンツと組み合わせて以下のように利用します。

console.log(data.contents[0].publishedAt)  //  -> '2022-04-06T05:03:27.510Z'

console.log(formatDate(data.contents[0].publishedAt))  // -> '2022_04'


記事が公開された月を表現できるようになりました。この値を使って記事をグルーピングしましょう。
先ほど定義した formatDate を使い、以下のように実装できます。

// libs/util.js

export const groupBy = function (contents) {
  return contents.reduce(function (group, x) {
    const yearMonthString = formatDate(new Date(x["publishedAt"]));
    (group[yearMonthString] = group[yearMonthString] || []).push(x);
    return group;
  }, {});
};


microCMSで取得したコンテンツを渡し、グルーピングしてみましょう。

// APIで取得
const data = await client.get({ endpoint: "blog", queries: { fields: "publishedAt", limit: 3000 } });

// 月別にグルーピング
const monthlyIndex = groupBy(data.contents);
console.log(monthlyIndex);


この出力は以下のようになります。(わかりやすくするため id や title を追記して表現しています)


keyに「2022_04」のような月を表現する文字列を、
valueにその月に書かれた記事の内容を持つ連想配列になっています。

これで月ごとにブログ記事をまとめられました。

「2022年2月(13)」のような表示を作る

ここまでの実装から、getStaticProps は次のようになっています。

// pages/index.js

export const getStaticProps = async () => {
  const data = await client.get({ endpoint: "blog", queries: { limit: 3000 } });
  const monthlyIndex = groupBy(data.contents);

  return {
    props: {
      blog: data.contents,
      monthlyIndex: monthlyIndex,
    },
  };
};


これらの情報を使い、表示部分は以下のように実装します。

// pages/index.js

export default function Home({ blog, monthlyIndex }) {
  return (
    <div>
      <div>
        <h3>月別アーカイブ</h3>
        <ul>
          {Object.keys(monthlyIndex).map((index) => (
            <li>
              <Link href={`archive/${index}`}>
                {index.split("_")[0] + "年" + index.split("_")[1] + "月"}
              </Link>
              ({monthlyIndex[index].length})
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}


うまく動作していれば以下のように表示されているはずです。

これで月別アーカイブの実装ができました。
最後に、このアーカイブのリンクをクリックしたときの遷移先ページを実装しましょう。

月ごとの記事一覧ページを作る

/archive/2022_04 のようなパスを追加し、対象の月に公開された記事の一覧を表示するよう実装してみます。

↓こういうイメージです

実装は以下のようになります。

// pages/archive/[date].js

import { client } from "../../libs/client";
import Link from "next/link";
import { groupBy } from "../../libs/util";

export default function BlogId({ title, blog }) {
  return (
    <div>
      <h2>{title}</h2>
      <ul>
        {blog.map((blog) => (
          <li key={blog.id}>
            <Link href={`/blog/${blog.id}`}>
              <a>{blog.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

// 1. パスを生成
export const getStaticPaths = async () => {
  const data = await client.get({ endpoint: "blog" });
  const monthlyIndex = groupBy(data.contents, "publishedAt");

  const paths = Object.keys(monthlyIndex).map((index) => `/archive/${index}`);
  return { paths, fallback: false };
};

// 2. 該当月のブログ記事を取得
export const getStaticProps = async (context) => {
  const date = context.params.date;
  const year = date.split("_")[0];
  const month = date.split("_")[1];

  // microCMSのfiltersクエリは >= を表現できないので開始時刻は1ミリ秒引いておく
  const startOfMonthTmp = new Date(year, month - 1, 1);
  const startOfMonth = new Date(startOfMonthTmp.getTime() - 1);

  const endOfMonth = new Date(year, month, 0);

  // filtersクエリで該当月の記事のみを取得
  const filters = `publishedAt[greater_than]${startOfMonth.toISOString()}[and]publishedAt[less_than]${endOfMonth.toISOString()}`;

  const data = await client.get({
    endpoint: "blog",
    queries: {
      filters: filters,
    },
  });

  return {
    props: {
      title: `${year}${month}月の記事一覧`,
      blog: data.contents,
    },
  };
};

少しだけ補足します。

1. getStaticPaths(パスを生成)

先ほどと同様で、記事を全件取得→グルーピングして、記事が存在する月のリストを作成しています。
/archive/2022_03/archive/2022_04のような月の数だけパスが作られるイメージです。

2. getStaticProps(該当月のブログ記事を取得)

該当月の記事を取得するために、microCMSの filters クエリを使って日付範囲を指定しています。
「publishedAt が4/1から4/30のもの」を表現するには greater_than と less_than を使います。
>= が表現できないため、月の開始日から1ミリ秒引いて工夫をしています)

これらを計算してfiltersパラメータを構築し、クエリパラメータに指定してコンテンツを取得します。


ページにアクセスするとこのようになっているはずです。


これで月ごとの記事一覧も完成です!

最後に

この記事ではJamstackなサイトで月別アーカイブを作成する手順を紹介させていただきました。
コンテンツの多いサイトでは役に立つ機能かと思います。ご参考になれば幸いです。

-----

microCMSは日々改善を進めています。
ご意見・ご要望は管理画面右下のチャット、公式Twitterお問い合わせからお気軽にご連絡ください!
引き続きmicroCMSをよろしくお願いいたします!

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • X
  • Discord
  • github

ABOUT ME

ひまらつ
SwiftやPythonやスプラトゥーンを楽しんでます