🙌

Next.js On-demand Revalidationをブログ機能に実装してみて理解する

2023/08/09に公開

イントロダクション

Next.jsは、Reactのフレームワークです。
2023年5月4日、v13.4となり、App Routerが安定版となりました。

https://nextjs.org/blog/next-13-4

ルーティング手法をはじめとした、さまざまな変更点があります。(挙げ出したらキリがない)
今回は、Cache周り、特にOn-demand Revalidationについて、
ブログ機能でどのように使用するのが有効なのかを考えましたので、共有させていただきます。

On-demand Revalidation

Next.js Cache

そもそもCacheが意味わかんねえよ!って方は、公式ドキュメントと下記の記事を参考にしてみてください。とてもわかりやすいです。

https://nextjs.org/docs/app/building-your-application/caching

https://zenn.dev/sumiren/articles/664c86a28ec573

要点としては、fetchで取得したデータを

  • 静的に表示し続ける(デフォルト)
  • 再検証を特定の条件で行い、新しいデータを取得する(Revalidation)
  • アクセスのたびに新しいデータを取得する(Opting-out)

この3つのどれかから、fetchリクエスト単位で制御することができるということです。
おそらく文章だけ見ても初めは意味不明だと思うので、実装して検証されることをお勧めします。

そもそも、Revalidationとは

まずは公式ドキュメント

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#revalidating-data

要するに、キャッシュされたデータを再検証することです。

On-demand Revalidationでできること

データの再検証を revalidateTag revalidatePath が実行されたタイミングで行うことができます。
この2つの関数が実行できる場所は、現在のところRoute HandlerとServer Actionsに限られています。

revalidateTag

Tagと名前がついている通り、fetchリクエストに対してタグ付けし、
そのタグを指定するとデータの再検証が行われます。

タグ付け↓

fetch('/api/blog', {next: tags:['blog']})

blogとタグ付けされているfetchリクエストを再検証↓🔥

revalidateTag('blog')

revalidatePath

もうお察しのいいみなさんならわかるでしょう。特定のパス以下のfetchリクエストを再検証します。

/blogページ以下を再検証↓🔥

revalidatePath('/blog')

どのように使う?

公式ドキュメントの引用↓

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).

要約すると、ヘッドレスCMSからの更新されたコンテンツなど、最新の情報をできるだけ早く表示したい場合に使ってねとのことです。

ブログ機能でOn-demand Revalidationを活用する

記事を更新したときに再検証する

ブログでは一般的にCMSを使用しコンテンツを管理しますが、コンテンツは、変更があったタイミングでWebサイト上に反映されるべきです。

これをApp RouterでのNext.jsで実現するには、On-demand Revalidationが有効です。

再デプロイでは解決できない

Pages RouterでSSGを行っていたときは、コンテンツの変更をおこなったとき、
Vercelの場合、Deploy Hooksを使用していた方が多いと思います。(簡単にいうとデプロイを発火できるWebhook)

https://vercel.com/docs/concepts/deployments/deploy-hooks

ただ、App Routerでは、キャッシュがデプロイにまたがって永続化されるため、再デプロイしても、コンテンツは更新されないのです。

公式ドキュメントからの引用↓

Next.js has a built-in Data Cache that persists the result of data fetches across incoming server requests and deployments. This is possible because Next.js extends the native fetch API to allow each request on the server to set its own persistent caching semantics.

実装してみる

おしらせと、サービスをCMS化してるので是非見てみてください↓(ちゃっかり宣伝すみません😅)

https://www.ynot.jp

環境

  • Next.js : v13.4.9
  • ホスティング : Vercel
  • CMS : microCMS

実装したいこと

  1. コンテンツをmicroCMSから取得して、表示する
  2. microCMSでコンテンツの変更を行ったとき、Next.jsにWebhookでおしらせする
  3. おしらせが来たら、パラメータに仕込んだタグを読み取って、そのタグがついているリクエストを再検証
  4. 最新のコンテンツを表示

実装例

  1. コンテンツをmicroCMSから取得して、表示する

microCMSのダッシュボード側の設定は割愛させてください。
だれでも直感的に操作できますので、触ってみたい方は以下から↓

https://microcms.io/

ここでは、お知らせ一覧の部分を実装してみます。

まずは、microCMS JS SDKのセットアップ

import { createClient } from "microcms-js-sdk"

const MICROCMS_SERVICE_DOMAIN = process.env.MICROCMS_SERVICE_DOMAIN!

const MICROCMS_API_KEY = process.env.MICROCMS_API_KEY!

export const cmsClient = createClient({
  serviceDomain: MICROCMS_SERVICE_DOMAIN,
  apiKey: MICROCMS_API_KEY,
})

お知らせ一覧(NewsFeedコンポーネント)を実装します。

import { cmsClient } from "@/libs/cms"
// レスポンスの型定義
import { NewsCMSResponse } from "@/types/news"
// ニュースをそれぞれ表示するためのコンポーネント、アニメーションを実装するためにframer-motionを使用しているのでファイル分割してます
import { NewsItem } from "./NewsItem"

interface Props {
  limit?: number
}

export const NewsFeed = async ({ limit }: Props) => {
  const response: NewsCMSResponse = await cmsClient.get({
    customRequestInit: {
      next: {
        tags: ["news"],
      },
    },
    endpoint: "news",
    queries: { limit },
  })
  const news = response.contents

  return (
    <div className="flex flex-col gap-8 overflow-x-hidden">
      {news.map((data) => (
        <NewsItem news={data} key={data.id} />
      ))}
    </div>
  )
}

実装ポイントの解説に移ります。
まずは、データ取得の処理ですね。

  const response: NewsCMSResponse = await cmsClient.get({
    customRequestInit: {
      next: {
        tags: ["news"],
      },
    },
    endpoint: "news",
    queries: { limit },
  })
  const news = response.contents

App Router以降、コンポーネント内にuseEffect、SWR等を用いなくても、
非同期処理を書くことができるようになりました。

そこはこの記事を読んでいる皆さんならわかるとして、
目新しいものでいうと、get関数の中にcustomRequestInitというプロパティがあることだと思います。

microCMS JS SDKのv2.5.0のアップデートで、Next.jsのfetchオプションがサポートされ、
SDKからでもNext.jsのcache戦略が使用できるようになったというわけなんです。(神)

https://github.com/microcmsio/microcms-js-sdk/releases/tag/v2.5.0

今回はタグで実装するので、

tags: ["news"]

を設定しています。

  1. microCMSでコンテンツの変更を行ったとき、Next.jsにWebhookでおしらせする
  2. おしらせが来たら、パラメータに仕込んだタグを読み取って、そのタグがついているリクエストを再検証
  3. 最新のコンテンツを表示

ここはいっきに実装していきます。
microCMSにはWebhookの機能が実装されているので、受け皿となるRoute Handlerを実装します。

import { secureCompare } from "@/utils/auth"
import { revalidateTag } from "next/cache"
import { NextRequest, NextResponse } from "next/server"

export async function POST(request: NextRequest) {
  const apiKey = request.headers.get("X-WEBHOOK-API-KEY")

  if (!secureCompare(apiKey, process.env.WEBHOOK_API_KEY)) {
    return NextResponse.json({ message: "Unauthorized" }, { status: 401 })
  }

  const tag = request.nextUrl.searchParams.get("tag")

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

  revalidateTag(tag)

  return NextResponse.json({ revalidated: true, now: Date.now() })
}

Route Handlerについての基本的な学習に関しては公式ドキュメントを見てください。

https://nextjs.org/docs/app/building-your-application/routing/route-handlers

実装ポイントとしては、

APIキーでとても簡易的な認証をかけています。
目的としてはこの関数が不必要に叩かれないようにしているのですが、気にならないのであれば不必要かもしれません。

  const apiKey = request.headers.get("X-WEBHOOK-API-KEY")

  if (!secureCompare(apiKey, process.env.WEBHOOK_API_KEY)) {
    return NextResponse.json({ message: "Unauthorized" }, { status: 401 })
  }

次にクエリパラメーターからタグを受け取ります。

  const tag = request.nextUrl.searchParams.get("tag")

最後に再検証🔥

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

  revalidateTag(tag)

  return NextResponse.json({ revalidated: true, now: Date.now() })

これでCMSが更新されたタイミングでNext.js側のCacheも削除、再検証されるわけですね🎉

まとめ

  • Next.js App RouterのCache戦略は主に3つ
    • 静的に表示し続ける(デフォルト)
    • 再検証を特定の条件で行い、新しいデータを取得する(Revalidating)
    • アクセスのたびに新しいデータを取得する(Opting-out)
  • On-demand Revalidationを実行するには、2つの選択肢がある
    • revalidateTag
    • revalidatePath
  • On-demand Revalidationをブログ機能で使用するときは、CMSとNext.jsをWebhookで連携する
  • (地味に重要なこと) キャッシュはデプロイにまたがって永続化される

今後のNext.jsは、cacheの仕様がもっと複雑化していくと思われるので、
cacheについての理解、キャッチアップがマストになってくるのではないかなーと思います。

zennは初投稿で、そこまでTech記事を書くことってなかったので、構成がわかりづらいところ等あるかと思いますので、ご指摘いただけますと嬉しいです🙌

Discussion