🤫

Vercel + Next.js + microcmsでOn-demand ISR

2023/08/24に公開

触る機会があったのでメモがてら記述します。

対象者

記事の内容は

  • next.js
  • vercel
  • microcms
    を触ったことがある方向けです。

ISR, On-demand ISRとは

ISRでは、ページアクセス時に指定したキャッシュ時間が過ぎていた場合、ページを再生成することで情報を更新する方法です。
この場合、特定の条件(キャッシュ時間前、自身がトリガーとなった場合)では、前回のビルドしたページが配信されてしまっていました。

ですが、On-demand ISRは、必要に応じてページを再生成することが出来ます。
例えば、今回のようにトリガーをCMSの更新にしておくことで、ユーザーがそのページにアクセスした際には、最新の情報が反映されたページを表示することが可能になります。

(ただし、どちらも、裏でビルドが終了する前にアクセスした場合には前回のビルドしたページが配信されます。)

環境

node-version: v18.8.0

"next": "13.4.4",
"react": "18.2.0",

※Pages Routerを使用しています。

実装

1. microCMSのwebhookの作成

CMSを更新したとき発火させるwebhookを作成します

webhookの種類は"カスタム通知"を選択してください。
以下の画面が表示されますので、同じ用に設定してください。

  • url
    https://test.jp 部分は
    自身のバーセルのドメインに合わせてください

  • シークレット値
    セキュリティのために追加しています。
    公式ではruby -rsecurerandom -e 'puts SecureRandom.hex(20)' を使用して推測されにくい値を作成しています。
    シークレット値を追加するとクエストのヘッダにX-MICROCMS-Signature: <COMPUTED_HASH>が付与されます。
    ※設定しなくてもAPIは使用できますが、不正利用を防ぐために基本的に設定したほうが良いです。

webhookの設定場所
https://document.microcms.io/manual/webhook-setting

  • カスタムリクエストヘッダー
    Apiを叩くときにヘッダーに付与されます。
    (処理内で、より詳細な分岐をしたりするときに便利です。)

2.next.js側でcms.tsの作成

pages/api/cms.tsを作成

cms.ts

export default async function Cms(req: any, res: any) {
}

リクエストがmicroCMSからであることの検証

  • X-MICROCMS-Signatureヘッダで受け取った値
  • ペイロード(request.body)と設定したシークレット値を元にした値
    を元に検証します。
export default async function Cms(req: any, res: any) {
	try {
		const crypto = require('crypto');
		const requestBody = typeof req.body === 'object' ? JSON.stringify(req.body) : req.body;
		const expectedSignature = crypto
			.createHmac('sha256', <シークレット>)
			.update(requestBody)
			.digest('hex');
		let signature = req.headers['X-MICROCMS-Signature'] || req.headers['x-microcms-signature'];

		if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
			return res.status(401).send('Invalid token');
		}
	} catch (err) {
		console.log(err);
		return res.status(401).send('Invalid token');
	}
}

<シークレット> のところを先程設定した、シークレット値と置き換えてください。


シークレット値がtestの場合

.createHmac('sha256', test)

とします。

情報を元に更新を行う。

export default async function Cms(req: any, res: any) {

	// micro cmsのカスタムリクエストヘッダーに登録した情報を取得
	let pageName = req.headers['X-PAGE-NAME'] || req.headers['x-page-name'];
	// 更新された情報 これらをもとにページを変更してください。
	const publishValue = req.body.contents.new.publishValue 
	
	// ex: 更新したいページが "/" の場合
	try {
		await res.revalidate(`/`);
		return res.status(200).send();
	} catch (err) {
		console.log(err);
		return res.status(500).send('Error revalidating');
	}
}
const body = req.body

には以下のような情報が含まれています。
必要な情報を取り出して使って下さい。

{
  service: 'webhook-test',
  api: 'news',
  id: 'x2xkcwog9521',
  type: 'edit',
  contents: {
    old: {
      id: 'x2xkcwog9521',
      status: ['DRAFT'],
      draftKey: 'Vyf_XTclTY',
      publishValue: null,
      draftValue: {
        id: 'x2xkcwog9521',
        createdAt: '2021-06-02T05:56:24.513Z',
        updatedAt: '2021-06-02T06:05:09.601Z',
        publishedAt: '2021-06-02T06:05:09.601Z',
        revisedAt: '2021-06-02T06:05:09.601Z',
        title: 'タイトルです',
      },
    },
    new: {
      id: 'x2xkcwog9521',
      status: ['PUBLISH'],
      draftKey: null,
      publishValue: {
        id: 'x2xkcwog9521',
        createdAt: '2021-06-02T05:56:24.513Z',
        updatedAt: '2021-06-02T06:05:09.601Z',
        publishedAt: '2021-06-02T06:05:09.601Z',
        revisedAt: '2021-06-02T06:05:09.601Z',
        title: 'タイトルです',
      },
      draftValue: null,
    },
  },
}

またこのタイミングで
micro cmsのカスタムリクエストヘッダーに登録した情報を取得して、処理を分岐させるのも良いと思います。

let pageName = req.headers['X-PAGE-NAME'] || req.headers['x-page-name'];

で取得できます。

let pageName = req.headers['X-PAGE-NAME'] || req.headers['x-page-name'];
if(pageName == "top"){
   await res.revalidate(`/`);
}else{
   await res.revalidate(`/works`);
}

3. 最終的なコード

cms.ts

export default async function Cms(req: any, res: any) {
	try {
		const crypto = require('crypto');
		const requestBody = typeof req.body === 'object' ? JSON.stringify(req.body) : req.body;
		const expectedSignature = crypto
			.createHmac('sha256', <シークレット>)
			.update(requestBody)
			.digest('hex');
		let signature = req.headers['X-MICROCMS-Signature'] || req.headers['x-microcms-signature'];

		if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
			return res.status(401).send('Invalid token');
		}
	} catch (err) {
		console.log(err);
		return res.status(401).send('Invalid token');
	}
	// micro cmsのカスタムリクエストヘッダーに登録した情報を取得
	let pageName = req.headers['X-PAGE-NAME'] || req.headers['x-page-name'];
	// 更新された情報 これらをもとに更新したいページを変更してください。
	const publishValue = req.body.contents.new.publishValue 
	
	// 更新したいページが "/" の場合
	try {
		await res.revalidate(`/`);
		return res.status(200).send();
	} catch (err) {
		console.log(err);
		return res.status(500).send('Error revalidating');
	}
}

4. vercelでビルド

完成したコードを含めた状態で、vercelでビルドを行ってください。

5. microcmsの更新

microcmsで、webhookを付与したコンテンツを更新してください。

6. vercelで更新処理が走っているか確認

vercelでプロジェクトを開きます。
メニューからLogsを選択したください。
Logsでは通常、30分前までのログを確認できます。(カスタム項目もあります。)

以下は私の環境で成功のログです。
Statusが200, 308と返却されています。

※特に、うまく更新されないとき、こちらでエラーが返却されている可能性が高いので必ず確認してください。

ちょっとしたハマりどころ

1.

let signature = req.headers['X-MICROCMS-Signature'] || req.headers['x-microcms-signature'];


ドキュメントの通り、大文字小文字どちらでくるか分からないので、大文字で取得出来なかった場合、小文字を試す実装にしています。

2.
参考サイトでよく

unstable_revalidate("/")

と記述されていますが、安定版では、

revalidate("/")

に関数名が変更されましたので、注意してください。

参考

https://blog.microcms.io/on-demand-isr/

CSR,SSR,SSG,ISRについての記事

最後に

思ったより簡単に出来たので便利かと思いました。
記事についてなにかありましたら、コメントいただけますと幸いです。

Discussion