メインコンテンツへスキップ

OpenNextとCloudflare Workersのビルド順序とキャッシュ実装

Next.jsをOpenNextでCloudflare Workersへデプロイする構成について、Wrangler設定、ビルド順序、読み取り専用SSGキャッシュの実装と検証を解説します。

(更新日:)7分で読めます
OpenNextとCloudflare Workersのビルド順序とキャッシュ実装

Workers Cache再導入前、このサイトには実際のリクエスト経路から参照されないworker-cache.jsと、対応していないWranglerの[cache]設定が同居していました。

設定の存在を確認するtestまであったのですが、activeなentrypointにはつながっていません。

設定ファイルだけを見て、cacheを実装したつもりになっていました。

2026年7月21日、entrypointをworker-cache.jsへ切り替え、.open-next/worker.jsへ委譲する構成で本番試験を始めました。7月30日のversion境界試験では旧versionのresponseが新versionから再利用されたため、いったんWorkers Cacheを無効化しました。

翌31日、CloudflareのWorkers Caching purge仕様を確認し直し、原因を切り分けました。zone cacheのpurgeではWorkers Cacheは消えません。そこでcache自体は有効へ戻し、deploy後にzone、Workers Cache、zoneの順でpurgeする構成へ改めました。最後にzoneをもう一度消すのは、最初のzone purgeとWorkers Cache purgeの間に旧responseが入り直す余地を閉じるためです。

そこで、SSG-firstの設計記事で触れた失敗を整理し、entrypointからビルド成果物まで実際につながる経路を検証しました。

Workers Cache導入前はOpenNext Workerを直接entrypointにした

OpenNext Workerを直接entrypointにしていたこの記事の公開時点では、wrangler.tomlのentrypointをOpenNextが生成する.open-next/worker.jsへ直接向けていました。間に独自wrapperは挟んでいませんでした。以下は当時の設定例です。

toml
main = ".open-next/worker.js"
workers_dev = false

[assets]
directory = ".open-next/assets"
binding = "ASSETS"

この時点ではrun_worker_firstを指定せず、既定値はfalseでした。OpenNextのStatic Assetsガイドが説明するとおり、public由来の静的assetはリクエストごとにWorkerを通さずWorkers Static Assetsから配信できます。asset binding自体の挙動はCloudflare公式でも確認できます。

ここで確認できたのは、当時のwrangler.tomlのentrypointとasset bindingです。Next.jsの各pageがrequest時にどの経路を通ったかまで、この設定だけから証明することはできません。

現在のentrypointはworker-cache.js

現在のwrangler.tomlは、worker-cache.jsをactive entrypointにします。通常requestはOpenNextが生成したWorkerへ委譲し、cache対象外responseの保護とdeploy後のpurgeだけをwrapperが受け持ちます。

toml
main = "worker-cache.js"
workers_dev = false

[cache]
enabled = true
cross_version_cache = false

[assets]
directory = ".open-next/assets"
binding = "ASSETS"

本番試験中はcross_version_cache = falseを設定していました。しかし、zone cacheをpurgeして新versionへ切り替えた後も、旧versionで付けたmarkerがcache対象responseに残りました。同じNRT PoPのno-store responseは新版markerを返していたため、単なるdeploy伝播ではありませんでした。

ここで見落としていたのがpurgeの境界です。Cloudflare公式の説明では、Dashboardやzone APIのpurgeはWorkers Cachingのcontentへ影響しないと明記されています。7月30日に実行したzone purgeだけでは、Workers Cacheを削除したことになりません。

cross_version_cache = falseは防御として残しました。その上でdeploy後、専用endpointからzone cacheをpurgeしてtokenの権限を確かめ、ctx.cache.purge({purgeEverything:true})を呼び、最後にzone cacheをもう一度purgeします。どこか1つでも失敗すればdeploy処理を成功扱いにしません。

cacheを切ったまま正しさを守るのではなく、正しい削除手段をdeploy経路へ入れた上でWorkers Cacheを使う。今回の是正はそこです。

更新をdeploy単位にしたからread-onlyを選んだ

このサイトは、記事の更新をビルドとデプロイの単位で扱います。リクエスト時にcacheを書き換える要件がないため、SSGルートのincremental cacheにはOpenNextが説明しているread-onlyのStatic Assets実装を選びました。

ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";

export default defineCloudflareConfig({
  incrementalCache: staticAssetsIncrementalCache,
});

OpenNext buildはcacheを.open-next/cache/<buildId>へ出します。しかし、asset bindingが配信するのは.open-next/assetsです。出力しただけでは届きません。

scripts/copy-cache-to-assets.mjsで、cacheを.open-next/assets/cdn-cgi/_next_cache/<buildId>へコピーします。

見るべきだったのはcache設定の有無ではなく、ビルド後の生成物がasset bindingの配下へ届いたかどうかでした。

staticAssetsIncrementalCacheはread-onlyで、revalidationには対応しません。時間ベースのrevalidationが必要になればR2 incremental cacheとQueue、on-demand revalidationが必要になればTag Cacheを含め、要件に応じた書き込み可能な構成へ切り替えます。

Pagefindがcache assetsと同じbuildに入る順序

Pagefindは静的HTMLを入力にするため、next buildの後にしか実行できません。一方、OpenNextが配信用assetを梱包した後にPagefindを作っても、検索indexは.open-next/assetsへ入りません。

つまり、Pagefindを実行できる場所はnext buildの後かつOpenNext buildの前です。

現在の順序は次の通りです。

MermaidOpenNext production build pipelineprebuildからWrangler bundle gateまでの直列build工程

production buildは生成、Next.js build、artifact検証、PagefindとAEO生成、OpenNext変換の順で進みます。最後にcache assetsのコピーとWrangler dry-runを通すことで、配信bundleへ進む前に不整合を止めます。

  1. prebuildでllms、OGP font、静的OGPを生成してからnext buildを実行します。
  2. HTML、CSS、SEO artifactsを検証し、Pagefind indexとAEO artifactsを生成します。
  3. OpenNext buildとcache assets copyの後にWrangler dry-runとbundle gateを実行します。

package.jsonでは、build:allがこの責任を直列にしています。

json
{
  "build": "next build",
  "postbuild": "... && pagefind --site .next ... && ...",
  "build:all": "pnpm build && opennextjs-cloudflare build --skipNextBuild && node scripts/copy-cache-to-assets.mjs",
  "build:check-size": "pnpm build:all && node scripts/check-bundle-size.js"
}

--skipNextBuildでNext.jsのビルドをやり直さないのも、このためです。検証とPagefindまで終えた同じNext.js成果物を、そのままOpenNextへ渡します。

testで本番レスポンスまで証明しない

ソースコードで検査するのは、17 routeのdynamicParams = falseworker-cache.jsがactive entrypointであること、Workers Cacheの[cache]設定、purge endpoint、そしてdeploy後にpurgeとcache検証が並ぶ順序です。ここでは、コードに書かれた契約までを見ます。

ビルド成果物では、同じ成果物にPagefind・静的OGP・font subset・SSG cache assetsが入ったかを確認し、Wrangler dry-runまで通します。Worker bundleとStatic Assetsは同じ容量に丸めません。Cloudflare Workersの制限とプロジェクト側の運用閾値を分け、bundleと配信assetを別々に計測します。SSG-firstで増えるasset側には、上限ではなく構成を見直すための閾値を置いています。

ここで確認できるのは、デプロイ前のソースコードとビルド成果物までです。本番の外部レスポンスを確認できたことにはなりません。

デプロイ後のworkers.devには外部からHTTPリクエストを送り、duplicate hostとしてindexableな本番HTMLを返していないかを確認します。probeの結果はsafe・blocker・inconclusiveに分け、Accessやerror responseで判断できない場合はinconclusiveです。応答が返っただけでsafeにはしません。

設定ではなく経路を残す

この構成を選べるのは、検索を静的indexとbrowserで完結させ、request-time処理を少数のAPIへ隔離し、更新をdeployまで待てるサイトだからです。ユーザーごとに内容が変わる、request時のDB queryや認証後の個別画面が主要機能になる、価格や在庫の更新をdeployまで待てないといった要件には合いません。

大量コンテンツでfull buildとcache assetsが増え続ける場合も、そのままでよいとは思っていません。ISRやon-demand revalidationが要件になればSSGを併用しつつ、read-only cacheからR2 incremental cache、Queue、Tag Cacheを要件に応じて組み合わせる構成へ切り替えます。

今回の失敗で、設定とtestがあるだけでは実行経路の証明にならないと分かりました。

それ以来、entrypoint・ビルド成果物・asset binding・デプロイ後の外部レスポンスを分け、どこまで確認できたかを残しています。現在のentrypointはworker-cache.jsで、Workers Cacheは有効です。deploy後の全削除とMISSからHITへの再充填確認までを、公開の条件にしています。

ここまで追えて、やっと実装したと言えると思っています。

Written by

柿添貴士(TakashiKakizoe) profile photo

柿添貴士(TakashiKakizoe)

Webエンジニア/テックリード

Fukuoka, Japan

  • PHP
  • Laravel
  • Next.js
  • AWS
  • Cloudflare
  • AI Agent