Next.js+microCMS+Tailwind+Github Pagesでブログを構築する

2021-04-01T14:00:00.000Z

はじめに

ブログ初投稿を何にするか悩んだのですが,そこそこ頑張って構築したこのブログについて書くことにしました。

これまでブログサイトはWordPress→Jekyllと移行してきました。
WordPressは確かに書きやすいですしプラグインも多いのですが,頻繁なアップデート等が必要です。面倒!!!
Jekyllはメンテナンス・カスタマイズは楽なのですが,ちょっと面倒。
そこで,「WordPressのように記事を書くことができる,静的なWebサイト」を求めて放浪していました。
その中で知ったのがJamstack構成です。私の希望にピッタリ!
というわけでNext.js+microCMS+Tailwind+Github Pages+Github Actionsで構築することにしました。

(注意)Next.jsやHeadless CMSをほぼ初めて触るので,記事の内容に誤りがある可能性があります。その場合,Twitter(@seigo2018)にご報告いただければ幸いです。

イメージ図

microCMSについて

国産HeadlessCMSです。シンプルで良いです。自分の必要な機能はあったのでこれにしました。


環境とか

  • Node.js 14.x
  • Next.js 10.0.9
  • microCMS Hobbyプラン
  • Github Pages


microCMSの設定

アカウント作成からサービス設定までは多くの記事がありますので省略し,エンドポイントの設定から説明します。
ブログの本文などを格納するブログコンテンツは以下の構造です。

blogエンドポイント

tagsエンドポイント

次に,タグについても以下のように設定します。 (この記事内ではタグ関連のページや処理にはあまり触れません。時間があったら別記事で書くかも)

Next.jsプロジェクト

(余談) 私は今回全てのNext.jsに関わる開発をdocker内で行いました。

Next.jsプロジェクトのベースとしてTailwind Nextjs Starter Blogを使用しました。
こちらのテンプレートは記事のコンテンツの管理にdataフォルダのマークダウンファイルを使用するので,その部分をmicroCMSからデータを取得する処理に書き換える必要があります。

プロジェクトの作成

npx create-next-app プロジェクト名
cd プロジェクト名


package.json

以下のコマンドを登録しています。(それぞれnpx next devのように実行できるので,記載しなくても問題ないです。)

  "scripts": {
    "dev""next dev",
    "build""next build",
    "start""next start",
    "export""next export"
  },


環境変数について

開発環境での環境変数は,プロジェクトルートに以下のような.envファイルを作成し,使用します。

API_KEY=取得したAPI-KEY

GitHub Actionsで呼び出す際には,Secretに追加し使用しています。(詳細はGitHub Actionsの項にて)

index.js

index.jsgetStaticProps内をmicroCMS用に書き換えつつ,データ構造に合わせて表示する部分も変更します。
公式ドキュメントとブログを参考にデータを取得します。URLは自分の設定したサービス名と,blogコンテンツの格納されているエンドポイントを指定し,headerにはAPI_KEYを付加しています。

  const key = {
    headers: {'X-API-KEY': process.env.API_KEY},
  };
  const res = await fetch('https://設定したサービス名.microcms.io/api/v1/blog', key)
  const data = await res.json()


変数名などは適宜変更していきます。
以下が全体のコードです。

import Link from '@/components/Link'
import { PageSeo } from '@/components/SEO'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'


const MAX_DISPLAY = 5
const postDateTemplate = { year'numeric'month'long'day'numeric' }


export const getStaticProps = async () => {
  const key = {
    headers: {'X-API-KEY': process.env.API_KEY},
  };
  const res = await fetch('https://設定したサービス名.microcms.io/api/v1/blog', key)
  const data = await res.json()
  return {
    props: {
      posts: data.contents,
    },
  };
};


export default function Home({ posts }{
  return (
    <>
      <PageSeo
        title={siteMetadata.title}
        description={siteMetadata.description}
        url={siteMetadata.siteUrl}
      />
      <div className="divide-y divide-gray-200 dark:divide-gray-700">
        <div className="pt-6 pb-8 space-y-2 md:space-y-5">
          <h1 className="text-2xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
            Latest
          </h1>
          <p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
            {siteMetadata.description}
          </p>
        </div>
        <ul className="divide-y divide-gray-200 dark:divide-gray-700">
          {!posts.length && 'No posts found.'}
          {posts.slice(0, MAX_DISPLAY).map((post) => {
            const id = post.id
            const title = post.title
            const date = post.date
            const tags = post.tags
            const description = post.description
            return (
              <div>
              <li key={id} className="py-12">
                <article>
                  <div className="space-y-2 xl:grid xl:grid-cols-4 xl:space-y-0 xl:items-baseline">
                    <dl>
                      <dt className="sr-only">Published on</dt>
                      <dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
                        <time dateTime={date}>
                          {new Date(date).toLocaleDateString(siteMetadata.locale, postDateTemplate)}
                        </time>
                      </dd>
                    </dl>
                    <div className="space-y-5 xl:col-span-3">
                      <div className="space-y-6">
                        <div>
                          <h2 className="text-2xl font-bold leading-8 tracking-tight">
                            <Link
                              href={`/blog/${id}`}
                              className="text-gray-900 dark:text-gray-100"
                            >
                              {title}
                            </Link>
                          </h2>
                          <div className="flex flex-wrap">
                            {tags.map((tag) => (
                              <Tag key={tag.name} text={tag.name} />
                            ))}
                          </div>
                        </div>
                        <div className="prose text-gray-500 max-w-none dark:text-gray-400">
                          {description}
                        </div>
                      </div>
                      <div className="text-base font-medium leading-6">
                        <Link
                          href={`/blog/${id}`}
                          className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
                          aria-label={`Read "${title}"`}
                        >
                          Read more &rarr;
                        </Link>
                      </div>
                    </div>
                  </div>
                </article>
              </li>
              </div>
            )
          })}
        </ul>
        
      </div>
      {posts.length > MAX_DISPLAY && (
        <div className="flex justify-end text-base font-medium leading-6">
          <Link
            href="/blog"
            className="text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
            aria-label="all posts"
          >
            All Posts &rarr;
          </Link>
        </div>
      )}
    </>
  )
}


blog.js

ブログ一覧を表示するところですが,基本的な処理はindex.jsと同じですので省略します。

blog/[id].js

ブログの個別画面を作成します。
まず,getStaticPathsでは,microCMSからデータを取得する処理は同一ですが,取得した後に静的生成のためのパスを指定します。

  const paths = data.contents.map(content => `/blog/${content.id}`);
  return {paths, fallbackfalse};


次にgetStaticPropsに関してはblog.jsindex.jsと同じですので以下略。

後は変数名を修正したのみです。

  const key = {
    headers: {'X-API-KEY': process.env.API_KEY},
  };
  const data = await fetch('https://seigo2016-blog.microcms.io/api/v1/blog', key)
    .then(res => res.json())
    .catch(() => null);
  const paths = data.contents.map(content => `/blog/${content.id}`);
  return {paths, fallbackfalse};

export const getStaticPaths = async () => {
  const key = {
    headers: {'X-API-KEY': process.env.API_KEY},
  };
  const data = await fetch('https://seigo2016-blog.microcms.io/api/v1/blog', key)
    .then(res => res.json())
    .catch(() => null);
  const paths = data.contents.map(content => `/blog/${content.id}`);
  return {paths, fallbackfalse};
};

export const getStaticProps = async context => {
    const id = context.params.id;
    const key = {
      headers: {'X-API-KEY': process.env.API_KEY},
    };
    const data = await fetch(
      'https://seigo2016-blog.microcms.io/api/v1/blog/' + id,
      key,
    )
      .then(res => res.json())
      .catch(() => null);
    return {
      props: {
        blog: data,
      },
    };
};

export default function Blog({ blog }{
  return (
    <main>
      <h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">{blog.title}</h1>
      <p>{blog.date}</p>
      <div className="divide-y"></div>
      <div
        dangerouslySetInnerHTML={{
          __html: `${blog.body}`,
        }}
      />
    </main>
  );
}


<Image>タグの修正

このtailwindテンプレートでは,next/imageが使用されています。
こちらは画像の最適化を行うモジュールですが,デフォルトの画像最適化loaderはvercelでのみ使用できるため,GitHub Actionなどではそのまま使用できません。
その場合ImgixやCloudinary,akamaiを使用する方法もありますが(参考),今回はnext/imageの代わりに<img>タグを使うことにしました。
このような部分を

        <Image
          alt={title}
          src={imgSrc}
          className="lg:h-48 md:h-36 object-cover object-center"
          width={544}
          height={306}
        />


こんな感じに

          <img
            alt={title}
            src={imgSrc}
            decoding="async"
            width={544}
            height={306}
            class="lg:h-48 md:h-36 object-cover object-center"
          />


テスト

ローカルでテストしてみましょう。
わかりやすいように,予め適当なテスト記事を公開しておくと良いと思います。
yarn devで実行し,localhost:3000(デフォルト)にアクセスすることでサイトが確認できるはずです。

GitHub Actionsの設定

GitHub Actionsでビルドして,GitHub Pagesにデプロイするようにします。
※node_modulesや.envはきちんと.gitignoreに追加しましょう
.github/workflows/gh-pages.ymlを作成し,編集します。

run on

Ubuntu 20.04

Use Action


流れとしては

  1. node環境をセット
  2. yarn.lockを元にyarnでインストール
  3. yarn build
  4. yarn export
  5. jekyllは使用しないのでnojekyllファイルを作成
  6. outディレクトリをGithub Pagesにデプロイ

となります。
5に関しては,nojekyllファイルのない状態ではjekyll前提でホストされるため,_で始まるファイル(今回は_nextディレクトリ配下)が無視されるため注意が必要です。

事前にSecretsにGITHUB_TOKENとmicroCMSのAPI_KEYを登録しておきましょう。
GITHUB_TOKENはGitHub Pagesにデプロイするために使用するため,権限はrepoのみでOKです。
actions-gh-pagesの詳しい使い方はドキュメントを参照してください。
今回私はサブドメインを割り当てているため,CNAMEの項目にblog.seigo2016.comを指定しています。

name: github pages

on:
  push:
    branches:
    - master

jobs:
  build-deploy:
    runs-on: ubuntu-20.04
    env: 
      API_KEY: ${{ secrets.API_KEY }}
    steps:
    - uses: actions/[email protected]


    - name: setup node
      uses: actions/[email protected]
      with:
        node-version: '14'


    - name: install
      run: yarn


    - name: build
      run: yarn build


    - nameexport files
      run: yarn export


    - name: add nojekyll
      run: touch ./out/.nojekyll


    - name: deploy pages
      uses: peaceiris/[email protected]
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./out
        cname: blog.seigo2016.com


これでmasterブランチにpushすると,Actionが実行されます。成功していれば,URLにアクセスするとページが表示されるはずです。

コンテンツの更新

このままでは,GitHubにソースコードをpushしたときにのみビルドが走るため,microCMSで記事を公開しても反映されません。
反映するためには,Webhookを使用して,microCMSで記事を更新した際にActionが実行されるようにします。
microCMS管理画面のブログコンテンツのAPI設定Webhookタブを開き,追加ボタンをクリックするとサービスを選択する画面になります。
そのサービスの一覧からGitHub Actionsを選択すると以下のような入力欄が表示されます。

GitHubトークンにはrepo権限のみ付与したトークンを,リポジトリのユーザーやリポジトリ名,トリガーイベントにも適切な名称を入力しておきます。
通知するタイミングはお好みで。
こうすることで記事を更新するとGitHub Actionsのdispatchイベントが発火するようになりました。
合わせてgh-pages.ymlも変更しておきます。

トリガーにrepository_dispatchを追加し,先程入力したイベントの名称を指定します。

name: github pages

on:
  repository_dispatch: 
    types: [page_build] //入力したトリガーイベントの名称
  push:
    branches:
    - master


これで記事を公開した場合にビルドが走り,反映されるようになったかと思います。

最後に

microCMS,全体的に使い勝手がいいですね。ごちゃごちゃしてないですし。

後,ブログサイト構築には関係ないですが,手順を文章化するのはなかなか難しいですね...
文章を書くいい機会にはなるのでこれからもアウトプットのためにブログを書いていこうかなと思います。