Next.js+Vercel+microCMSでブログを作る
どうも。
Next.jsとmicroCMSを使用してブログを作成した経験を共有したいと思います。
このプロジェクトで遭遇したさまざまなハマりポイントや、microCMSとNext.jsを実際に使って得た思いなどを記録していきたいと思います。
1.今回作ったもの
ヘッドレスCMSとしてのmicroCMSと、Next.jsのSSG(ISR)を組み合わせて、ブログを作成することにします。
ブログのホスティングには、Next.jsの開発元であるVercelを利用し、ホスティングとCI/CDの両方を一括して行います。
2.microCMSを使う
microCMSは日本製のヘッドレスCMSです。
詳細な情報は公式ドキュメントに記載されていますので、アカウントの作成やサービスの設定などは公式ドキュメントを参照しながら進めてください。
次にAPIの作成を行います。microCMSでは、ブログ記事の場合は「/blogs」、タグの場合は「/tags」といったようにエンドポイントを設定していきます。
そして、各エンドポイントに対して必要なフィールドを作成していきます。
これにより、APIから取得するデータの構造を定義することができます。
# api schema
{
id: *自動インサート;
title: テキストフィールド;
description: テキストボックス;
content: リッチエディタ;
category: セレクトボックス;
thumbnail: イメージ;
isSlide: トグルスイッチ;
publishedAt: デートピッカー;
}
フィールドの作成時には、フィールドのフォーマットを指定しながら作成を行います。
具体的には、必須項目の設定や、フィールドのデータ型など、細かな指定が可能です。
これにより、APIから取得するデータにおいて必要な項目やデータの形式を定義することができます。
3.Dynamic Routingとは?
複雑なアプリケーションでは、予め定義されたパスを使用してルートを定義するだけでは不十分な場合があります。
Next.jsでは、ページ名に角括弧(ブラケット)を使用することで、動的なルーティング(別名スラッグやプリティURLなど)を作成できます。
ただし、固定のルーティングだけで運用するのは少し難しいです。
たとえば、"/page/1"というルートが存在し、それに対応する"1.html"を作成してアップロードする方法では、運用コストが高くなってしまいます。
そこで、"1"というIDのブログを投稿して、"/page/1"にアクセスすれば自動で該当の投稿内容を取得し表示する機能が欲しいのです。
これを簡単に実現できるのがDynamic Routingです。
ディレクトリやファイル名にブラケットを使用することで、動的な値と一致させてルーティングすることができます。これはとても便利ですね。
今回のブログでは、以下のようにルーティングしたいので、ブラケットを使用して設定していきます。
pages/_app.tsx
pages/_document.tsx
pages/index.tsx // トップページ
pages/[id].tsx // 個別記事ページ
pages/page/[pages].tsx //一覧記事のページ1,ページ2といったページネーション付き一覧ページ
pages/category/[id]/index.tsx // 各カテゴリトップページ
pages/category/[id]/page/[pages].tsx // カテゴリ記事のページ1,ページ2といったページネーション付き一覧ページ
ここでひとつ注意があります。
# 最初こうしていたが無理だった
pages/category/[id]/index.tsx
pages/category/[id]/page/[id].tsx // こいつがエラーになります
このように、同じルートを持つDynamic Routingしたいルーティングに対しては、同じ値をブラケットに使用することはできませんので、注意が必要です。
では、個別記事を静的に生成するにはどのようにすればいいのでしょうか?
Next.jsでは、以下の3つの特殊なAPIが組み込まれています。
これらのメソッドはサーバー側で処理される特殊なAPIとなっており、クライアント側のwindowオブジェクトへのアクセスなどはできません。
なお、getStaticPropsを使用する場合、getStaticPathsも併せて使用する必要があります。
4.getServerSideProps
このメソッドは、SSR(サーバーサイドレンダリング)される際に呼び出されるライフサイクルメソッドです。
ユーザーがアクセスするタイミングでgetServerSideProps内の処理が呼び出され、都度SSRしてHTMLを返します。
ただし、next exportを使用して静的HTMLを生成する場合にはこのメソッドは利用できません。
5.getStaticPaths
このメソッドは、SSG(静的サイト生成)時にのみ呼び出されるライフサイクルメソッドであり、getStaticPropsよりも前に実行されます。
このメソッド内では、先ほど説明したようなDynamic Routingのブラケット内の[id].tsxや[pages].tsxのルーティングに対応するparams部分を生成します。
事前の処理として、このメソッド内でルーティング用のパスを生成し、その結果をgetStaticPropsにコンテキストとして渡すことができます。
6.getStaticProps
このメソッドは、SSG(静的サイト生成)時にのみ呼び出されるライフサイクルメソッドであり、getStaticPathsの後に実行されます。
このメソッドでは、外部からデータを取得し、そのデータをコンポーネントのPropsに流したり、getStaticPathsから返されたコンテキスト(Dynamic Routingのブラケット内のparams)を受け取り、ビルド時に動的にデータを取得することができます。
7.順番に作っていきます
まずは、TOPページから作成していきます。
TOPページでは、カテゴリなどに関係なく最新の記事を9件一覧表示したいだけなので、getStaticPropsメソッドだけを使用します。
# pages/index.tsx
//----------------------------------
// props
//----------------------------------
export const getStaticProps: GetStaticProps = async () => {
const notes = await getTopContents('id,publishedAt,title,introduction,body,thumbnail,category', 9);
return {
props: {
notes: notes,
}
};
};
データの取得処理は抽象化されており、microCMSから取得したいフィールドと件数を渡すことでデータを取得できるようになっています。
取得したデータはreturn文を使用して返されます。
次に個別記事ページを作成します。
こちらはDynamic Routingが必要なページですね。
# pages/[id].tsx
// データを取得する関数のreturnの型
type IPaths = {
fallback?: string | boolean;
paths: {
params: {
[key: string]: string;
};
}[];
};
// データを取得する関数
export const getIdPaths = async (limit: number): Promise<IPaths> => {
const results = await getIndex(limit); // microCMSからデータをfetchする関数
const paths = results.contents.map((content) => {
// type IPathsの型にもあるようにgetStaticPathsのreturnではpaths.params~のようなオブジェクトの型で返さないといけないので予め沿った形のオブジェクトにして返す
return {
params: { id: content.id.toString() } // 個別記事のidを入れる
};
});
return {
paths,
};
};
//----------------------------------
// paths
//----------------------------------
export const getStaticPaths = async (): Promise<IPaths> => {
const paths = await getIdPaths(100); // 全記事を取得できる値を入れる
return {
...paths,
fallback: 'blocking'
};
};
//----------------------------------
// props
//----------------------------------
export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => {
const notes = await getId(String(context?.params?.id)); // getStaticPathsでreturnしたpaths内のparamsがコンテキストから取得できる
return {
props: {
notes: notes,
}
};
};
microCMSからデータをfetchしてparamsに組み込む際に、注意点があります。
Dynamic Routingブラケットの値である[id]と、getStaticPathsでreturnする際のparams内のキー(今回はid)は一致している必要がありますので注意してください。
# pages/[id].tsx
params: {
id: content.id.toString() // data: content.id.toString() // ← 別のキーだとエラーになる }
}
同様に、Dynamic Routingを行いたい部分に対してgetStaticPathsとgetStaticPropsを使用して処理を追加していきます。
以下は想定されるルーティングの例です。
# pages/category/[id]/index.tsx // 各カテゴリトップページ
↓
pages/category/design/index.tsx // デザインカテゴリの一覧ページ
pages/category/tech/index.tsx // 技術カテゴリの一覧ページ
同様に、Dynamic Routingのブラケットに含めたい値(この場合は"category"と"design"という文字列)をgetStaticPaths内で用意し、paramsに格納してpathsをreturnするだけです。
returnされたコンテキストからカテゴリ名(["design", "tech"]など)を取得し、getStaticProps内でmicroCMSから各カテゴリのデータをfetchする処理を追加するだけです。
また、ページネーションのように1,2,3,4という値をDynamic Routingのブラケットに含めたい場合も同様の手順です。
8.ISRする
ISR(インクリメンタル静的再生成)は、通常のSSGとは異なり、ビルド時にデータをフェッチして静的なHTMLを生成するだけではなく、新しい記事の追加などの場合にもページの再ビルドを自動的に行ってくれる機能です。
これにより、新しい記事がページに反映されるまでの待ち時間をなくすことができます。
ISRを実現するためには、getStaticPropsのreturn文にrevalidate(再検証)を追加するだけで簡単に設定することができます。
revalidateには再生成までの間隔を指定することができます。
一定時間ごとにバックグラウンドでデータの再取得とページの再レンダリングが行われ、新しいHTMLが生成されます。
export const getStaticProps: GetStaticProps = async (context: GetStaticPropsContext) => {
const notes = await getId(String(context?.params?.id)); // getStaticPathsでreturnしたpaths内のparamsがコンテキストから取得できる
return {
props: { notes: notes },
revalidate: 60 // 60秒毎にという意味になる
};
};
revalidateを60秒に指定した場合、getStaticPropsを使用してSSGされたページでは60秒間は静的に生成されたHTMLを返します。
60秒が経過した後、ユーザーからのアクセスがあった場合にはバックグラウンドでデータの再取得と再レンダリングが行われ、ページが再生成されます。
その後のリクエストに対しては、再生成したページのキャッシュが返されます。
この機能により、SSGの利点である静的なパフォーマンスとキャッシュの使用と、動的なデータの柔軟な更新が組み合わさったハイブリッドな機能が提供されています。
これにより、ビルド後でもブログなどのコンテンツの更新を即座にページに反映することが可能になりました。
9.VercelにDeployする
VercelとGithubアカウントを連携し、リポジトリを同期すると、VercelがContinuous Deployment(CD)を自動的に構築してくれます。
mainブランチにMergeされ、ビルドが成功すると自動的にDeployが行われます。
Githubアカウントの連携については割愛します。
最後は駆け足でしたが、getStaticPathsやgetStaticPropsが具体的にいつ、どの順番で何をどこまで処理しているのかを理解すれば、それほど難しくはないと思います。
それでは。