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

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

Canonical URL: https://labs.eastbraver.com/blog/opennext-build-cache-pipeline
Published: 2026-07-17T21:52:00+09:00
Updated: 2026-07-31T12:32:00+09:00
Category: infrastructure
Tags: cloudflare, software-engineering

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仕様](https://developers.cloudflare.com/workers/cache/purge/)を確認し直し、原因を切り分けました。zone cacheのpurgeではWorkers Cacheは消えません。そこでcache自体は有効へ戻し、deploy後にzone、Workers Cache、zoneの順でpurgeする構成へ改めました。最後にzoneをもう一度消すのは、最初のzone purgeとWorkers Cache purgeの間に旧responseが入り直す余地を閉じるためです。

そこで、[SSG-firstの設計記事](/blog/nextjs-cloudflare-workers-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ガイド](https://opennext.js.org/cloudflare/howtos/assets)が説明するとおり、`public`由来の静的assetはリクエストごとにWorkerを通さずWorkers Static Assetsから配信できます。asset binding自体の挙動は[Cloudflare公式](https://developers.cloudflare.com/workers/static-assets/binding/)でも確認できます。

ここで確認できたのは、当時の`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公式の説明](https://developers.cloudflare.com/workers/cache/purge/)では、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実装](https://opennext.js.org/cloudflare/caching)を選びました。

```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の前です。

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

### OpenNext production build pipeline

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

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

*prebuildからWrangler bundle gateまでの直列build工程*

`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 = false`、`worker-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の制限](https://developers.cloudflare.com/workers/platform/limits/)とプロジェクト側の運用閾値を分け、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`への再充填確認までを、公開の条件にしています。

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