🔖

Next.jsのData Cacheが原因でCMS更新が反映されない問題

に公開

技術スタック

以下の技術スタックでブログサイトを構築。

  • Next.js(App Router)
  • Vercel
  • microCMS

記事の更新は microCMS 上で行い、Webhook を利用して Vercel に再ビルドを通知する構成を採用。

問題

microCMS 上で記事を更新すると、Webhook 経由で Vercel による再ビルドは正しく実行される。
しかし、ビルド後のサイトに変更が反映されないという問題が発生。。。

原因:Next.js の Data Cache

Next.js(App Router)では、サーバー側で fetch のレスポンスを永続的に保持する「Data Cache」が存在。

このキャッシュの特徴。

  • 明示的に 再検証(Revalidate) されない限り、サーバーリクエストやデプロイをまたいで保持され続ける。
    つまり、再ビルドされたとしても、キャッシュが削除されていなければ古いデータが表示され続けるという仕組み。

参考ドキュメント:
https://nextjs.org/docs/app/deep-dive/caching#data-cache

Next.js has a built-in Data Cache that persists the result of data fetches across incoming server requests and deployments.

The Data Cache is persistent across incoming requests and deployments unless you revalidate or opt-out.

解決策:On-demand Revalidation の導入

Data Cache を明示的に無効化する手段として、On-demand Revalidation を採用。
これは、CMS 側でコンテンツが更新されたタイミングで、Next.js の API 経由でキャッシュに付けたタグを指定し、該当キャッシュを削除する仕組み。

導入方針

  • fetchtags: ['blog'] を指定し、該当データに「blog」というキャッシュタグを付与する
  • CMS 側の Webhook から /api/revalidate?tag=blogPOST リクエストを送信する
  • API 側で revalidateTag('blog') を呼び出して、該当キャッシュを削除する

参考ドキュメント:
https://nextjs.org/docs/app/deep-dive/caching#data-cache

On-demand Revalidation: Revalidate data based on an event (e.g. form submission). On-demand revalidation can use a tag-based or path-based approach to revalidate groups of data at once. This is useful when you want to ensure the latest data is shown as soon as possible (e.g. when content from your headless CMS is updated).

実装例

データ取得関数

src/app/libs/microcms.ts

export const getBlogData = async (queries) => {
  return await client.getAllContents({
    endpoint: 'blog',
    queries: {
      fields: '',
      ...queries,
    },
    customRequestInit: {
      next: {
        tags: ['blog'],
      },
    },
  });
};
  • tags: ['blog'] を指定することで、この fetch のレスポンスには「blog」タグが付与され、Data Cache に保存される
  • revalidateTag('blog') を呼び出すことで、このタグに紐づいたキャッシュを削除できる

再検証用 API

src/app/api/revalidate/route.ts

import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request) {
  const tag = request.nextUrl.searchParams.get('tag');

  if (!tag) {
    return NextResponse.json({ message: 'No tag provided' }, { status: 400 });
  }

  try {
    revalidateTag(tag);
    return NextResponse.json({ revalidated: true, now: Date.now() });
  } catch (err) {
    return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
  }
}
  • CMS 側の Webhook をこのエンドポイントに向けて設定することで、更新のたびに自動で該当キャッシュを削除できる
  • キャッシュ削除後、次回のアクセス時には最新データが取得され、表示内容も更新される

実装の参考にした記事:
https://zenn.dev/ynot/articles/dc27182e5cc263

Discussion