Next.js×microCMSなブログデータをJSONにしてContextで使い回す

前提

このブログを作り直そうと思い、ナウいNext.jsとmicroCMSの組み合わせで行こうとなりました。
全体の作りはmicroCMSの神記事「microCMS + Next.jsでJamstackブログを作ってみよう」を元にしています。
で作る間にその他にもブログを作っている方の記事を読んだのですが、記事取得を各ページのgetStaticPropsでやっていることが多かったです。

一旦マネして作ったのですが、めんどくね?ということが多々あり、

  • 違うページで同じ情報を何度も取得している
  • componentsではgetStaticPropsできないので渡してあげなくてはいけない
  • microCMSのGET APIでは全件を一気に取得できないので、計算してループする必要があるけど各ファイルに書きたくない

などなど。

そもそもNext.jsの経験もほぼないので何が正解か分からないのですが、ブログデータを一括で取得し使い回せるようにしてみました。

実装

JSON生成

さっそくでスミマセンが、全データ取得&JSON生成はやってる方がいらっしゃいました。
NextJSのinitialize pages( _app.tsx )でgetStaticPropsが使えなかったのでgetInitialPropsを使おうと思ったけどやめた - Qiita

next実行前にnodejsでjsonを作っておき、_app.tsxでcontextに登録することで好きな箇所で使うことができます。
もう私のブログを読む必要はありません。


はい

で私は全ブログデータと全カテゴリデータ、全タグデータを持つ3つのJSONを生成することにしました。

ブログデータでは、サムネイルが投稿されていないとき、カテゴリごとにデフォルト画像を設定したいので処理を追加しました。

~省略~

// サムネがない場合セット
const setThumb = (contents) => {
  contents.forEach((blog) => {
    if(!blog.thumbnail) {
      blog.thumbnail = {url: `/images/blog/thumb_${blog.category.id}.jpg`}
    }
  });
}

const createCmsJson = async() => {
  const contents = await getMicroCMSdata();
  setThumb(contents);
  const jsonData = JSON.stringify(contents, null, 2);
  fs.writeFileSync('data/allBlogData.json', jsonData);
}

createCmsJson();


のような感じです。
データの加工をどこでやるべきかは分かりませんが、何度も処理を書かなきゃいけないならこの段階でやるっていうふうに分けてます。

日付の形式処理をここでやっても良いかなと思ったのですが、既に日付パース用のコンポーネントを作成していたので見送りました。

カテゴリごとのブログ件数

そのカテゴリやタグに記事が何件登録されているか知りたいことがあると思います。
とはいえmicroCMSの管理画面では被参照件数を確認できるので、APIにすぐ来るような気もします。
(今はないと思います・・たぶん)

まず前述のような全ブログデータJSONがあるものとします。
ブログのJSONはこのような感じ

[
  {
    "id": "hogefugapiyo",
    "createdAt": "2022-03-05T11:26:24.554Z",
    "updatedAt": "2022-03-05T12:06:21.940Z",
    "publishedAt": "2021-06-16T11:26:00.000Z",
    "revisedAt": "2022-03-05T12:06:21.940Z",
    "title": "ブログタイトル",
    "body": "<p>ブログ本文</p>",
    "category": {
      "id": "diary",
      "createdAt": "2022-01-12T09:57:09.687Z",
      "updatedAt": "2022-01-12T10:08:42.729Z",
      "publishedAt": "2022-01-12T09:57:09.687Z",
      "revisedAt": "2022-01-12T10:08:42.729Z",
      "name": "日記"
    }
  },
  {
    "id": "foobarbaz",
    "createdAt": "2022-03-05T11:21:05.278Z",
    "updatedAt": "2022-03-05T12:29:05.980Z",
    "publishedAt": "2021-05-25T11:21:00.000Z",
    "revisedAt": "2022-03-05T12:29:05.980Z",
    "title": "ブログタイトル",
    "body": "<p>ブログ本文</p>",
    "category": {
      "id": "tech",
      "createdAt": "2022-03-05T09:34:08.858Z",
      "updatedAt": "2022-03-05T09:34:08.858Z",
      "publishedAt": "2022-03-05T09:34:08.858Z",
      "revisedAt": "2022-03-05T09:34:08.858Z",
      "name": "技術"
    }
  },
~略~
]


で各カテゴリ(「日記」とか「技術」)とかに何件あるんですかってのを、全カテゴリデータのjsonに記録しときます。

全カテゴリデータJSON現状このような感じ

[
  {
    "id": "tech",
    "createdAt": "2022-03-26T09:52:14.259Z",
    "updatedAt": "2022-03-26T09:52:14.259Z",
    "publishedAt": "2022-03-26T09:52:14.259Z",
    "revisedAt": "2022-03-26T09:52:14.259Z",
    "name": "技術"
  },
  {
    "id": "diary",
    "createdAt": "2022-01-12T09:57:09.687Z",
    "updatedAt": "2022-01-12T10:08:42.729Z",
    "publishedAt": "2022-01-12T09:57:09.687Z",
    "revisedAt": "2022-01-12T10:08:42.729Z",
    "name": "日記"
  },
~略~
]


全カテゴリデータを取得するコードは、最初に紹介した全ブログデータを取得するコードと同様です。
エンドポイントが変わるのみです。

そこにブログ件数を調べる処理を追加します。
生成済みの全ブログデータJSON(ここではallBlogData.json)を使用します。
なので生成処理は全ブログデータの後に実行する必要があります。並列じゃなくて直列にしておけば問題ないかと思います。

~略~

// 各カテゴリのブログ件数を、カテゴリオブジェクトのプロパティに追加
const countBlog = (contents) => {
  // 生成済みの全ブログデータJSONを使用します
  const blogJson = fs.readFileSync('data/allBlogData.json', 'utf-8');
  const blogObj = JSON.parse(blogJson);
  
  contents.forEach((category) => {
    // ループ中のカテゴリIDとブログのカテゴリIDが一致した数
    const count = blogObj.filter(blog => blog.category.id === category.id).length;

    // プロパティ追加
    category.count = count;
  });
}

const createCmsJson = async() => {
  const contents = await getMicroCMSdata();

  // 追加
  countBlog(contents);

  const jsonData = JSON.stringify(contents, null, 2);
  fs.writeFileSync('data/allCategoryData.json', jsonData);
}

createCmsJson();


このような感じにして実行すると

[
  {
    "id": "tech",
    "createdAt": "2022-03-26T09:52:14.259Z",
    "updatedAt": "2022-03-26T09:52:14.259Z",
    "publishedAt": "2022-03-26T09:52:14.259Z",
    "revisedAt": "2022-03-26T09:52:14.259Z",
    "name": "技術",
    "count": 5
  },
  {
    "id": "diary",
    "createdAt": "2022-01-12T09:57:09.687Z",
    "updatedAt": "2022-01-12T10:08:42.729Z",
    "publishedAt": "2022-01-12T09:57:09.687Z",
    "revisedAt": "2022-01-12T10:08:42.729Z",
    "name": "日記",
    "count": 15
  }
]


のようにcountプロパティが追加されます。
よくある、カテゴリ名の横に件数が表示されるやつとかがラクラク実装できるようになりましたわね。

次はコンテキストに登録したデータを実際使うのはどんなもんよってのをご紹介します。


pagesで使用

このブログのトップページで、記事一覧を表示するときの記述は以下のようになっています。
pages/index.tsx

import { useContext } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { microCMS } from '@/libs/context';
import Layout from '@/components/Layout';

export default function Home() {
  const allData = useContext(microCMS);
  const allBlogData = allData.allBlogData;
  allBlogData.splice(20); // 1ページに20件表示する

  return (
    <Layout home>
      <ul>
        {allBlogData.map((blog) => (
          <li key={blog.id}>
            <Link href={`/blog/${blog.id}`}>
              <a>
                <Image
                  alt=''
                  src={blog.thumbnail.url}
                  width={1200}
                  height={630}
                />
                <div>
                  <h1>{blog.title}</h1>
                  <span>{blog.publishedAt}</span>
                  <span>{blog.category.name}</span>
                </div>
              </a>
            </Link>
          </li>
        ))}
      </ul>
    </Layout>
  );
}


Componentsで使用

全ページのサイドバーにカテゴリ一覧やタグ一覧を表示している箇所は以下のようになっています。
components/Layout.tsx

import { useContext } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { microCMS } from '@/libs/context';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';

export default function Layout({ children, home = false } :{
  children: Object,
  home?: boolean
}) {
  // カテゴリ・タグをcontextから取得
  const allData = useContext(microCMS);
  const allCategoryData = allData.allCategoryData;
  const allTagData = allData.allTagData;

  // カテゴリー一覧
  function createCategoryNav() {
    const items: Object[] = [];
    if(allCategoryData.length >= 1) {
      allCategoryData.forEach((category) => (
        items.push(
          <li key={category.id}>
            <Link href={`/category/${category.id}/page/1/`}>
              <a>{category.name}</a>
            </Link>
          </li>
        )
      ))
    }
    return items;
  };

  // タグ一覧
  function createTagNav() {
    const items: Object[] = [];
    if(allTagData.length >= 1) {
      allTagData.forEach((tag) => (
        items.push(
          <li key={tag.id}>
            <Link href={`/tag/${tag.id}/page/1/`}>
              <a>{tag.name}</a>
            </Link>
          </li>
        )
      ))
    }
    return items;
  };
  return (
    <>
      <div>
        <div>
          <div>
            <Header home={home} />
            <div>
              <main>{children}</main>
              <nav>
                <Link href="/">
                  <a>トップ</a>
                </Link>
                <dl>
                  <dt>カテゴリー</dt>
                  <dd>
                    <ul>
                      {createCategoryNav()}
                    </ul>
                  </dd>
                </dl>
                <dl>
                  <dt>タグ</dt>
                  <dd>
                    <ul>
                      {createTagNav()}
                    </ul>
                  </dd>
                </dl>
              </nav>
            </div>
          </div>
          <Footer />
        </div>
      </div>
    </>
  )
};


useContextができない場合はJSONファイルをimportする

ちょっと記憶が定かじゃないのですが、getStaticPathsgetStaticPropsではuseContextできなかったと思うので、その場合は単純にJSONを使います。

記事詳細ページ
pages/blog/[id].tsx

import path from 'path';
import fs from 'fs';
import { useEffect } from 'react'
import { useRouter } from "next/router"
import Image from 'next/image';
import Layout from '@/components/Layout';
import { blog } from '@/interfaces/index';
import Link from 'next/link';
import allBlogData from '@/data/allBlogData.json';

export default function BlogId({ blog }: {
  blog: blog,
}){
  return (
    <Layout>
      <h1>{blog.title}</h1>
      <Image 
        alt=''
        src={blog.thumbnail.url}
        width={1200}
        height={630}
      />
      <div>
        <span>{blog.publishedAt}</span>
        <Link href={`/category/${blog.category.id}/page/1/`}>
          <a>{blog.category && `${blog.category.name}`}</a>
        </Link>
      </div>
      <ul>
        {blog.tag.map((tag) => (
          <li key={tag.id}>
            <Link href={`/tag/${tag.id}/page/1/`}>
              <a>{tag.name}</a>
            </Link>
          </li>
        ))}
      </ul>
      <div 
        dangerouslySetInnerHTML={{
          __html: `${blog.body}`,
        }}
      />
    </Layout>
  );
}

// 静的生成のパス指定
export const getStaticPaths = async () => {
  // allBlogDataはjsonから取得したデータ
  const paths = allBlogData.map((content: {id:string}) => `/blog/${content.id}/`);
  return { paths, fallback: false};
};

// データをテンプレートに渡す
export const getStaticProps = async (context: {
  params: { id: string },
}) => {
  // /dataのjsonファイルを参照
  const id = context.params.id;
  const filePath = path.join(process.cwd(),'data', 'allBlogData.json');
  const blogJson = fs.readFileSync(filePath, 'utf8');
  const blogData = JSON.parse(blogJson) as blog[];
  const currentBlog = blogData.find((blog) => blog.id === id) || {body:''};

  return {
    props: {
      blog: currentBlog,
    },
  };
};

こんな感じです。

おわりに

最初はよく分からないあまりReduxに片足突っ込んでオアアアッとなっていたのですが、いや別にデータをnext内でいじりたいわけじゃない・・もっと単純な話なんだよな・・と悟り、やり直して今に至ります。

もっとnextと仲良くなれるようガンバリマス。

© 2021 whike