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.js
のgetStaticProps
内を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 →
</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 →
</Link>
</div>
)}
</>
)
}
blog.js
ブログ一覧を表示するところですが,基本的な処理はindex.jsと同じですので省略します。
blog/[id].js
ブログの個別画面を作成します。
まず,getStaticPaths
では,microCMSからデータを取得する処理は同一ですが,取得した後に静的生成のためのパスを指定します。
const paths = data.contents.map(content => `/blog/${content.id}`);
return {paths, fallback: false};
次にgetStaticProps
に関してはblog.js
とindex.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, fallback: false};
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, fallback: false};
};
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
- actions/checkout@v2
- actions/setup-node@v2
- peaceiris/actions-gh-pages@v3
流れとしては
- node環境をセット
- yarn.lockを元にyarnでインストール
- yarn build
- yarn export
- jekyllは使用しないのでnojekyllファイルを作成
- 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/checkout@v2
- name: setup node
uses: actions/setup-node@v2
with:
node-version: '14'
- name: install
run: yarn
- name: build
run: yarn build
- name: export files
run: yarn export
- name: add nojekyll
run: touch ./out/.nojekyll
- name: deploy pages
uses: peaceiris/actions-gh-pages@v3
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,全体的に使い勝手がいいですね。ごちゃごちゃしてないですし。
後,ブログサイト構築には関係ないですが,手順を文章化するのはなかなか難しいですね...
文章を書くいい機会にはなるのでこれからもアウトプットのためにブログを書いていこうかなと思います。