diff --git a/pages/cloudflare/bindings.mdx b/pages/cloudflare/bindings.mdx
index 5fd42db..0729384 100644
--- a/pages/cloudflare/bindings.mdx
+++ b/pages/cloudflare/bindings.mdx
@@ -35,6 +35,26 @@ export async function GET(request) {
+
+ Calling `getCloudflareContext()` in sync mode in a route that Next.js statically prerenders (i.e. a page or layout that does not use any Next.js Dynamic API) makes `next build` fail. In async mode, the build succeeds but the route is generated with the values available at build time, not the ones of the incoming request.
+
+If the route needs the context of the incoming request, make it render dynamically, i.e. by awaiting one of the Next.js Dynamic APIs such as [`headers()`](https://nextjs.org/docs/app/api-reference/functions/headers) before calling `getCloudflareContext()`:
+
+```js filename="app/page.js"
+import { headers } from "next/headers";
+import { getCloudflareContext } from "@opennextjs/cloudflare";
+
+export default async function Page() {
+ await headers(); // forces dynamic rendering
+ const myKv = getCloudflareContext().env.MY_KV_NAMESPACE;
+ // ...
+}
+```
+
+Alternatively, add [`export const dynamic = "force-dynamic";`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config) to the page or layout.
+
+
+
#### How to add bindings to your Worker
Add bindings to your Worker by adding them to your [wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/).
diff --git a/pages/cloudflare/caching.mdx b/pages/cloudflare/caching.mdx
index 06191da..a2ef251 100644
--- a/pages/cloudflare/caching.mdx
+++ b/pages/cloudflare/caching.mdx
@@ -692,7 +692,12 @@ You can only enable cache purge functionality on a zone (e.g., when using a cust
The cache purge component automatically clears the cache when a page is revalidated. It is only necessary if you use On-Demand revalidation along with one of the cache components that leverage the Cache API.
-This component can either call the Cache API's purge function directly or route the purge request through an intermediate durable object. Using a durable object helps buffer requests and avoid reaching [API rate limits](https://developers.cloudflare.com/cache/how-to/purge-cache/#hostname-tag-prefix-url-and-purge-everything-limits).
+This component can either call the Cache API's purge function directly (`purgeCache({ type: "direct" })`, the default) or route the purge request through an intermediate durable object (`purgeCache({ type: "durableObject" })`). Using a durable object helps buffer requests and avoid reaching [API rate limits](https://developers.cloudflare.com/cache/how-to/purge-cache/#hostname-tag-prefix-url-and-purge-everything-limits).
+
+Since `@opennextjs/cloudflare` 1.20.15, the `type` is honored for every purge:
+
+- With `durableObject`, the `NEXT_CACHE_DO_PURGE` binding is required. When it is missing, an error is logged and the cache is not purged. The adapter no longer falls back to direct purges, which are not buffered and count against the account-wide rate limit.
+- When `CACHE_PURGE_API_TOKEN` or `CACHE_PURGE_ZONE_ID` is missing, the tag cache is still updated but the purge is reported as failed (`Cloudflare cache purge failed: missing-credentials`). Set both variables, or remove `cachePurge` from your config if you don't need to purge the cache.
Cache purge are only called when you call `revalidateTag`, `revalidatePath` or `res.revalidate` in the pages router. It is not called for ISR revalidation.
@@ -746,6 +751,8 @@ If you want to use the durable object option, you will need to add the following
You can customize the duration of the cache purge buffering with the `NEXT_CACHE_DO_PURGE_BUFFER_TIME_IN_SECONDS` environment variable. The default is 5 seconds. It works by buffering the purge requests for a given amount of time and then sending them all at once. This is useful to avoid hitting the API rate limits.
+If you need a different behavior (i.e. a different storage schema or purging several zones), you can implement your own durable object by extending `BucketCachePurgeBase` from `@opennextjs/cloudflare/durable-objects/bucket-cache-purge` (available since 1.20.15). It keeps the alarm scheduling and the queue draining and lets you override the storage, batching, execution and settlement hooks. See the [`BucketCachePurge` implementation](https://github.com/opennextjs/opennextjs-cloudflare/blob/main/packages/cloudflare/src/api/durable-objects/bucket-cache-purge.ts) for an example.
+
### Debugging
You can add `NEXT_PRIVATE_DEBUG_CACHE=1` to your app `.env` file to debug any cache issue.
diff --git a/pages/cloudflare/cli.mdx b/pages/cloudflare/cli.mdx
index 22b7301..c08d329 100644
--- a/pages/cloudflare/cli.mdx
+++ b/pages/cloudflare/cli.mdx
@@ -26,15 +26,35 @@ It populates the configured [Open Next cache components](/cloudflare/caching) so
From version 1.13.0 of the Cloudflare adapter, R2 batch uploads are supported out of the box for preview and deploy without any additional setup.
-Before version 1.13.0 of the Cloudflare adapter, the `populateCache` command was supporting R2 batch uploads via `rclone` which required the following additional setup:
+
-The `populateCache` command supports R2 batching to speed up the upload of large number of files. To enable R2 batching, you need to create an R2 Account API token as described [in the docs](https://developers.cloudflare.com/r2/api/tokens/) and provide the following environment variables:
+#### Populating R2 with `rclone`
-- `R2_ACCESS_KEY_ID`: The access key ID of the R2 API token
-- `R2_SECRET_ACCESS_KEY`: The secret access key of the R2 API token
-- `CLOUDFLARE_ACCOUNT_ID`: The [account ID](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/#copy-your-account-id) where the R2 bucket is located
+From version 1.20.0 of the Cloudflare adapter, you can opt in to upload the R2 incremental cache with [`rclone`](https://rclone.org/) instead of the default upload, which can be faster for a large number of files, by passing `--rclone` to `populateCache remote`, `deploy` or `upload`. The option is only used when populating a remote R2 cache.
-
+It requires some additional setup:
+
+1. Install the optional `rclone.js` peer dependency in your project. pnpm users must also allow its install script:
+
+ ```sh
+ pnpm add rclone.js
+ pnpm approve-builds # select rclone.js
+ pnpm rebuild rclone.js
+ ```
+
+2. Create an R2 Account API token as described [in the docs](https://developers.cloudflare.com/r2/api/tokens/) and provide the following environment variables (i.e. in a `.env` file or in your CI environment):
+
+ - `R2_ACCESS_KEY_ID`: The access key ID of the R2 API token
+ - `R2_SECRET_ACCESS_KEY`: The secret access key of the R2 API token
+ - `CF_ACCOUNT_ID`: The [account ID](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/#copy-your-account-id) where the R2 bucket is located
+
+3. Pass the `--rclone` option:
+
+ ```sh
+ opennextjs-cloudflare deploy --rclone
+ ```
+
+`rclone` may not be supported on all platforms.
#### Populating remote bindings when Workers are protected by Cloudflare Access
diff --git a/pages/cloudflare/howtos/assets.mdx b/pages/cloudflare/howtos/assets.mdx
index 4fd0151..815bb69 100644
--- a/pages/cloudflare/howtos/assets.mdx
+++ b/pages/cloudflare/howtos/assets.mdx
@@ -42,6 +42,10 @@ When `run_worker_first` is set to `true`, all the requests will reach the Worker
"directory": ".open-next/assets",
"binding": "ASSETS",
"run_worker_first": true,
+ // Serve public files at their exact path only, like Next.js
+ "html_handling": "none",
+ // "404-page" is also allowed
+ "not_found_handling": "none",
},
// ...
}
@@ -49,6 +53,8 @@ When `run_worker_first` is set to `true`, all the requests will reach the Worker
The Open Next asset resolver will be used to retrieve the assets from the Worker.
+Since `@opennextjs/cloudflare` 1.20.16, public files are only served at their exact path when the Worker runs first, like Next.js. The build fails when `run_worker_first` is enabled (`true` or a list with at least one non-negated pattern) unless `html_handling` is set to `"none"` and `not_found_handling` is set to `"none"` or `"404-page"`. Other values would serve the files at other paths (i.e. `/about` for `public/about.html`), which a middleware matching the exact path of the file would not cover.
+
When `run_worker_first=true`, assets are served behind the middleware and Next rewrites and headers from [the Next config](https://nextjs.org/docs/app/api-reference/config/next-config-js). The [headers](https://developers.cloudflare.com/workers/static-assets/headers/) and [redirects](https://developers.cloudflare.com/workers/static-assets/redirects/) configured for the Worker Static Assets do not apply in this case.
`run_worker_first=true` should be used if you plan to use skew protection.
diff --git a/pages/cloudflare/howtos/skew.mdx b/pages/cloudflare/howtos/skew.mdx
index 36c5f16..c83553b 100644
--- a/pages/cloudflare/howtos/skew.mdx
+++ b/pages/cloudflare/howtos/skew.mdx
@@ -35,7 +35,7 @@ export default {
**Wrangler configuration**
-The Worker needs to serve the correct version of the app assets. For that it need be be executed before incoming requests are matched against the assets. Set [`run_worker_first`](https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first) to `true` in your wrangler configuration to enable this behavior:
+The Worker needs to serve the correct version of the app assets. For that it need be be executed before incoming requests are matched against the assets. Set [`run_worker_first`](https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first) to `true` in your wrangler configuration to enable this behavior. `html_handling` and `not_found_handling` must then be set as below (see [Workers Static Assets](/cloudflare/howtos/assets#run_worker_firsttrue)):
```jsonc filename="wrangler.jsonc"
{
@@ -45,6 +45,10 @@ The Worker needs to serve the correct version of the app assets. For that it nee
"directory": ".open-next/assets",
"binding": "ASSETS",
"run_worker_first": true,
+ // Serve public files at their exact path only, like Next.js
+ "html_handling": "none",
+ // "404-page" is also allowed
+ "not_found_handling": "none",
},
// ...
}
@@ -80,6 +84,99 @@ const nextConfig: NextConfig = {
};
```
+### Using Durable Objects with skew protection
+
+Cloudflare does not allow a worker with `preview_urls: true` to export Durable Object classes. The solution is to move all DO classes into a separate DO worker and reference them from the main worker using `script_name` bindings. The main worker continues to use `opennextjs-cloudflare deploy` and skew protection as normal.
+
+**DO worker**
+
+Create a separate entry point that re-exports the DO classes from the OpenNext build output:
+
+```js filename="do-worker.js"
+export { DOQueueHandler } from "./.open-next/.build/durable-objects/queue.js";
+export { DOShardedTagCache } from "./.open-next/.build/durable-objects/sharded-tag-cache.js";
+export { BucketCachePurge } from "./.open-next/.build/durable-objects/bucket-cache-purge.js";
+
+export default {
+ async fetch() {
+ return new Response(null, { status: 404 });
+ },
+};
+```
+
+**Wrangler configurations**
+
+The main worker references the DO classes via `script_name`. The DO worker holds the implementations and calls back to the main worker via `WORKER_SELF_REFERENCE` for ISR revalidation:
+
+```jsonc filename="wrangler.jsonc"
+{
+ "name": "my-app",
+ "preview_urls": true,
+ "services": [{ "binding": "WORKER_SELF_REFERENCE", "service": "my-app" }],
+ "durable_objects": {
+ "bindings": [
+ { "name": "NEXT_TAG_CACHE_DO_SHARDED", "class_name": "DOShardedTagCache", "script_name": "my-app-do" },
+ { "name": "NEXT_CACHE_DO_QUEUE", "class_name": "DOQueueHandler", "script_name": "my-app-do" },
+ { "name": "NEXT_CACHE_DO_PURGE", "class_name": "BucketCachePurge", "script_name": "my-app-do" },
+ ],
+ },
+ // ...
+}
+```
+
+```jsonc filename="wrangler.do.jsonc"
+{
+ "main": "do-worker.js",
+ "name": "my-app-do",
+ // The DO worker is only accessed through bindings
+ "workers_dev": false,
+ "services": [{ "binding": "WORKER_SELF_REFERENCE", "service": "my-app" }],
+ "durable_objects": {
+ "bindings": [
+ { "name": "NEXT_TAG_CACHE_DO_SHARDED", "class_name": "DOShardedTagCache" },
+ { "name": "NEXT_CACHE_DO_QUEUE", "class_name": "DOQueueHandler" },
+ { "name": "NEXT_CACHE_DO_PURGE", "class_name": "BucketCachePurge" },
+ ],
+ },
+ "migrations": [
+ { "tag": "v1", "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "BucketCachePurge"] },
+ ],
+}
+```
+
+**First deployment: circular dependency**
+
+On a new environment neither worker exists yet. Cloudflare validates `script_name` references and service bindings at deploy time, so deploying either worker first will fail. A bootstrap config for the DO worker without `WORKER_SELF_REFERENCE` breaks the deadlock:
+
+```jsonc filename="wrangler.do.bootstrap.jsonc"
+{
+ "main": "do-worker.js",
+ "name": "my-app-do",
+ // The DO worker is only accessed through bindings
+ "workers_dev": false,
+ // no services section
+ "durable_objects": {
+ "bindings": [
+ { "name": "NEXT_TAG_CACHE_DO_SHARDED", "class_name": "DOShardedTagCache" },
+ { "name": "NEXT_CACHE_DO_QUEUE", "class_name": "DOQueueHandler" },
+ { "name": "NEXT_CACHE_DO_PURGE", "class_name": "BucketCachePurge" },
+ ],
+ },
+ "migrations": [
+ { "tag": "v1", "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "BucketCachePurge"] },
+ ],
+}
+```
+
+Deploy sequence for a new environment:
+
+1. `opennextjs-cloudflare build` - builds the app, including the DO classes imported by `do-worker.js`
+2. `wrangler deploy --config wrangler.do.bootstrap.jsonc` - deploys the DO worker without `WORKER_SELF_REFERENCE`
+3. `opennextjs-cloudflare deploy` - deploys the main worker (DO worker now exists, `script_name` resolves)
+4. `wrangler deploy --config wrangler.do.jsonc` - redeploys the DO worker with `WORKER_SELF_REFERENCE`
+
+`DOQueueHandler` handles a missing `WORKER_SELF_REFERENCE` gracefully, so ISR revalidation is only briefly unavailable between steps 3 and 4. Subsequent deployments to an existing environment can skip step 2. The DO worker should be deployed after each build so that it uses the DO classes of the same version of the adapter.
+
### What you should know
- Because the Worker is configured to run in front of the assets Worker (`run_worker_first`), requesting an asset will count as a request to your Worker
diff --git a/pages/cloudflare/howtos/workerd.mdx b/pages/cloudflare/howtos/workerd.mdx
index 35aeb15..ad3b130 100644
--- a/pages/cloudflare/howtos/workerd.mdx
+++ b/pages/cloudflare/howtos/workerd.mdx
@@ -20,9 +20,10 @@ there are still minor differences. Some packages publish code for different runt
With such exports, Node.js applications use either `src/index.js` or `cjs/src/index.js` depending if the app use ESM or CJS.
However we want to use the `workerd` specific entrypoint when using the Cloudflare adapter.
-For that, you need to instruct Next.js not to bundle packages as it would use the node conditions by default.
-To do that, add those packages in the `serverExternalPackages` key of your `next.config.ts`:
+The adapter uses the `workerd` build of the packages that Next.js does not bundle. Since `@opennextjs/cloudflare` 1.20.15, this applies to all of them (and to their dependencies): the packages listed in `serverExternalPackages` and the packages that Next.js [keeps external by default](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages) (i.e. `pg` or `@prisma/client`). Before 1.20.15, only the packages listed in `serverExternalPackages` were using their `workerd` build.
+
+Next.js bundles the other packages with the Node.js conditions. For those, you need to instruct Next.js not to bundle them by adding them to the `serverExternalPackages` key of your `next.config.ts`:
```ts filename="next.config.ts"
import type { NextConfig } from "next";
diff --git a/pages/cloudflare/index.mdx b/pages/cloudflare/index.mdx
index 3c93a3a..ba4331f 100644
--- a/pages/cloudflare/index.mdx
+++ b/pages/cloudflare/index.mdx
@@ -27,8 +27,11 @@ This is an important difference from `@cloudflare/next-on-pages`, which only sup
### Supported Next.js versions
-All minor and patch versions of Next.js 16 and the latest minors of Next.js 14 and 15 are supported.
-Next.js 14 support will be dropped Q1 2026. It is [no more supported](https://nextjs.org/support-policy#supported-versions) by the Next.js team.
+All minor and patch versions of Next.js 16 and the latest minor of Next.js 15 are supported.
+
+The minimum versions are raised when Next.js releases security fixes. Check the `next` peer dependency of `@opennextjs/cloudflare` for the exact range supported by your version of the adapter.
+
+Next.js 14 is no longer supported, it has reached its [end of life](https://nextjs.org/support-policy#supported-versions).
To help improve compatibility, we encourage you to [report bugs](https://github.com/opennextjs/opennextjs-cloudflare/issues) and contribute code!
@@ -40,7 +43,7 @@ To help improve compatibility, we encourage you to [report bugs](https://github.
- [x] [Static Site Generation (SSG)](https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default)
- [x] [Server-Side Rendering (SSR)](https://nextjs.org/docs/app/building-your-application/rendering/server-components)
- [x] [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware)
-- [ ] [Node Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware#runtime) introduced in 15.2 are not yet supported
+ - [x] [Node Middleware](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) (i.e. `proxy.ts` in Next.js 16) - experimental support since `@opennextjs/cloudflare` 1.20.3
- [x] [Image optimization](https://nextjs.org/docs/app/building-your-application/optimizing/images) (See [this guide](/cloudflare/howtos/image) to configure [Cloudflare Images](https://developers.cloudflare.com/images/))
- [x] [Partial Prerendering (PPR)](https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering)
- [x] [Pages Router](https://nextjs.org/docs/pages)
diff --git a/pages/cloudflare/perf.mdx b/pages/cloudflare/perf.mdx
index 89ec921..7b481da 100644
--- a/pages/cloudflare/perf.mdx
+++ b/pages/cloudflare/perf.mdx
@@ -2,7 +2,7 @@ import { Callout } from "nextra/components";
## Performance tips
-They are a lot of features and knobs you can tune to improve the performance of your app. This page lists some of the recommend settings.
+There are a lot of features and knobs you can tune to improve the performance of your app. This page lists some of the recommend settings.
### General