0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js App RouterとSanity CMSを連携したISRオンデマンド再検証(On-demand Revalidation)実装手順

0
Posted at

Next.js App RouterとSanity CMSを連携したISRオンデマンド再検証(On-demand Revalidation)実装手順

Feature(202).png

はじめに

こんにちは、NKKTech Global 技術チームです。

コーポレートサイト、メディア、ECサイトなどを構築する際、SEOとユーザー体験を最大化するためには「静的HTML(SSG)の爆速なTTFB(Time to First Byte)」が不可欠です。しかし、運用担当者からは「CMSで公開したら1秒でも早く本番画面に反映してほしい」という要求が突きつけられます。

  • SSR (cache: 'no-store'): 常に最新だが、リクエストごとにCMSへ問い合わせるため表示が遅く、サーバー費用が跳ね上がる。
  • 時間ベースISR (next: { revalidate: 60 }): 表示は速いが、更新反映まで最大60秒のタイムラグが発生し、不要な再生成コストがかかる。
  • オンデマンド再検証(On-demand Revalidation): 普段は静的キャッシュからミリ秒で配信し、CMSで「Publish / Delete」が実行された瞬間にだけWebhook経由でキャッシュを即座にパージする。

本記事では、Next.js 14 / 15 のApp RouterとSanity CMSを組み合わせ、最高速の表示と完全なリアルタイム更新を両立させる実装手順をコード付きで解説します。


1. オンデマンド再検証のアーキテクチャ

SanityのWebhookイベントをNext.jsのRoute Handler(API)で受け取り、暗号署名を検証した上でタグベースのキャッシュ破棄(revalidateTag)を実行します。


2. 必要なパッケージのインストール

Next.jsプロジェクトに必要なSanityクライアントとWebhook検証ライブラリを導入します。

npm install next-sanity @sanity/webhook

3. タグ付きデータフェッチ関数の実装 (src/sanity/lib/fetch.ts)

Next.js App Routerでは、fetch リクエストに tags を付与することで、特定のデータキャッシュをグループ化して識別できます。

import { createClient, type QueryParams } from 'next-sanity';

export const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: '2026-10-01',
  useCdn: false, // オンデマンド再検証を行う場合はCDNキャッシュを無効化
});

export async function sanityFetch<QueryResponse>({
  query,
  params = {},
  tags = [],
}: {
  query: string;
  params?: QueryParams;
  tags?: string[];
}): Promise<QueryResponse> {
  return client.fetch<QueryResponse>(query, params, {
    next: {
      // キャッシュタグを付与 (例: ['post', 'post:hello-world'])
      tags,
    },
  });
}

4. ページコンポーネントの実装 (src/app/posts/[slug]/page.tsx)

記事詳細ページで sanityFetch を呼び出します。記事の種類を表す post タグと、個別記事を識別する post:${slug} タグを両方付与します。

import { notFound } from 'next/navigation';
import { sanityFetch } from '@/sanity/lib/fetch';

interface Props {
  params: Promise<{ slug: string }>;
}

interface Post {
  title: string;
  slug: string;
  content: string;
}

export default async function PostPage({ params }: Props) {
  const { slug } = await params;

  const query = `*[_type == "post" && slug.current == $slug][0]{
    title,
    "slug": slug.current,
    content
  }`;

  const post = await sanityFetch<Post | null>({
    query,
    params: { slug },
    // 記事全体用タグと、該当記事専用タグを両方指定
    tags: ['post', `post:${slug}`],
  });

  if (!post) {
    notFound();
  }

  return (
    <article className="max-w-2xl mx-auto py-10">
      <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
      <div className="prose">{post.content}</div>
    </article>
  );
}

5. Webhook受信APIの実装 (src/app/api/revalidate/route.ts)

SanityからのWebhookリクエストを受け取り、改ざん防止のHMAC署名検証を行ってからキャッシュを削除します。

import { revalidateTag } from 'next/cache';
import { type NextRequest, NextResponse } from 'next/server';
import { parseBody } from 'next-sanity/webhook';

interface SanityWebhookPayload {
  _type: string;
  slug?: {
    current?: string;
  };
}

export async function POST(req: NextRequest) {
  try {
    // 1. 署名検証とボディのパース
    const { isValidSignature, body } = await parseBody<SanityWebhookPayload>(
      req,
      process.env.SANITY_REVALIDATE_SECRET
    );

    // 2. 署名が不正な場合は401を返してブロック
    if (!isValidSignature) {
      return new NextResponse('Invalid signature', { status: 401 });
    }

    if (!body?._type) {
      return new NextResponse('Bad Request: Missing _type', { status: 400 });
    }

    // 3. ドキュメントタイプに応じた再検証
    if (body._type === 'post') {
      // 記事一覧や共通コンポーネントを再検証
      revalidateTag('post');
      
      // 個別記事のタグが存在すれば特定ページのみ再検証
      if (body.slug?.current) {
        revalidateTag(`post:${body.slug.current}`);
      }

      return NextResponse.json({
        revalidated: true,
        now: Date.now(),
        type: body._type,
        slug: body.slug?.current,
      });
    }

    return NextResponse.json({
      revalidated: false,
      message: 'No matching tag for this document type',
    });
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    return new NextResponse(`Error revalidating: ${message}`, { status: 500 });
  }
}

6. Sanity Studio側のWebhook設定

Sanityの管理画面(manage.sanity.io)でWebhookを登録します。

  1. プロジェクトを選択し、API > Webhooks > Create Webhook をクリック。
  2. 以下の項目を設定:
    • Name: Next.js On-Demand Revalidation
    • URL: https://your-domain.com/api/revalidate
    • Dataset: production
    • Trigger on: Create, Update, Delete
    • Filter: _type == "post" (監視対象のスキーマを指定)
    • Projection: { _type, slug } (必要最小限のペイロードのみ送信)
    • Secret: ランダムな長い文字列を生成し、Next.js側の環境変数 SANITY_REVALIDATE_SECRET と一致させる。

7. 本番運用のセキュリティとTips

  1. 下書き(Drafts)の除外:
    Sanityでは編集中に drafts.xxxx というIDで自動保存が走ります。公開時のみ再検証させたい場合は、WebhookのFilterに !(_id in path('drafts.**')) を追加してください。
  2. CDNクライアントの useCdn: false:
    Sanityのクライアント設定で useCdn: true にしていると、Webhookを受け取ってNext.jsが再フェッチした際にSanityのエッジCDNキャッシュ(古いデータ)を掴んでしまうことがあります。オンデマンド再検証運用の場合は useCdn: false を指定するのが定石です。
  3. タグ設計の粒度:
    サイト規模が巨大な場合、revalidateTag('post') だけで全記事をパージするとサーバー負荷が跳ね上がります。必ず post:${slug} や category:${categorySlug} のように階層化したタグを持たせ、影響範囲を局所化してください。

まとめ:Jamstackは「真のリアルタイム」へ

Next.js App Routerの revalidateTag とSanity Webhookを組み合わせることで、以下の3大メリットを完全に享受できます。

  1. 最高峰のWeb Vitals: 全ユーザーがエッジ上の静的キャッシュ(TTFB数十ms)を閲覧可能。
  2. 公開直後の即時反映: 運用者が「更新」を押した瞬間にキャッシュが切り替わり、リロード待ちのストレスを解消。
  3. インフラコストの最小化: 閲覧時のサーバーサイドレンダリング計算とCMSへのAPIリクエストを極限まで削減。

NKKTech Globalでは、モダンフロントエンド(Next.js / Nuxt)とヘッドレスCMS(Sanity, microCMS, Strapi)を組み合わせた、エンタープライズ規模の高速Webサイト・EC基盤構築を多数手掛けています。


お問い合わせ先

Next.js移行、ヘッドレスCMSの導入設計、パフォーマンスチューニングに関するご相談は、下記よりお気軽にお問い合わせください。

著者:NKKTech Global 技術チーム
私たちは、最新のフロントエンド技術とクラウドアーキテクチャで、ビジネスの成長を加速させるWeb体験を創出します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?