Skip to content

Nextjs

Source: src/Cloudflare/Website/Nextjs.ts

A Cloudflare Worker deployed from a Next.js project.

Nextjs builds the app with the wrangler-free OpenNext pipeline from @distilled.cloud/nextjs: next build runs through @opennextjs/cloudflare, the resulting worker is bundled into a self-contained ES module set, and the static assets (including prerendered pages and the read-only incremental cache) deploy as Workers static assets. Input files are content-hashed so unchanged projects skip the build and deploy entirely.

Both @distilled.cloud/nextjs and its peer @opennextjs/cloudflare must be installed in the deploying project — the source provider is loaded with a dynamic import().

Local dev (alchemy dev) defaults to preview parity — the built worker served under workerd. Set nextjs: { devMode: "hmr" } for the real next dev (Turbopack HMR) with the Worker’s bindings proxied onto getCloudflareContext().

ISR comes in two flavors, chosen by the project’s open-next.config.ts: the zero-infra static-assets incremental cache (prerendered pages serve as built; revalidation writes are a no-op), or the fully writable KV-backed setup (revalidatePath/revalidateTag and time-based regeneration all work) — see the Writable ISR section below. OpenNext’s WORKER_SELF_REFERENCE self service binding is always wired on deploy.

Known limitations (upstream @opennextjs/cloudflare):

  • Edge-runtime routes/pages (export const runtime = "edge") are not supported — the build fails with the offending route list; remove the directive (the node runtime runs on Workers). Middleware is fine.
  • next/image optimization requires a zone with Cloudflare Images; on workers.dev, use unoptimized (images serve as raw assets).
  • Partial Prerendering / "use cache" (cacheComponents) and Pages-Router i18n config are untested/out of scope for now. App Router i18n via middleware works (middleware is fully supported).

A single call builds the app with OpenNext and deploys the worker plus its static assets. The project needs an open-next.config.ts — the read-only static-assets incremental cache is a good default:

open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: staticAssetsIncrementalCache,
});

Basic Next.js site

const site = yield* Cloudflare.Website.Nextjs("Site");

Explicit project root

const site = yield* Cloudflare.Website.Nextjs("Site", {
rootDir: "./apps/web",
});

Resources passed via env become Worker bindings, readable in route handlers and server components through OpenNext’s getCloudflareContext().

const bucket = yield* Cloudflare.R2.Bucket("Uploads");
const site = yield* Cloudflare.Website.Nextjs("Site", {
env: {
UPLOADS: bucket,
},
});
app/api/upload/route.ts
import { getCloudflareContext } from "@opennextjs/cloudflare";
export async function PUT(request: Request) {
const { env } = getCloudflareContext();
await env.UPLOADS.put("key", await request.text());
return Response.json({ ok: true });
}

With the KV incremental cache, ISR revalidation actually writes: revalidatePath / revalidateTag purge entries, and time-based revalidate windows regenerate pages in the background through the same-worker Durable Object queue. Configure OpenNext for it and bind the pieces — WORKER_SELF_REFERENCE is wired automatically:

open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
import kvNextTagCache from "@opennextjs/cloudflare/overrides/tag-cache/kv-next-tag-cache";
export default defineCloudflareConfig({
incrementalCache: kvIncrementalCache,
queue: doQueue,
tagCache: kvNextTagCache,
});
const incCache = yield* Cloudflare.KV.Namespace("NextIncCache");
const tagCache = yield* Cloudflare.KV.Namespace("NextTagCache");
const site = yield* Cloudflare.Website.Nextjs("Site", {
env: {
NEXT_INC_CACHE_KV: incCache,
NEXT_TAG_CACHE_KV: tagCache,
// The revalidation queue: a Durable Object class shipped in the
// OpenNext worker bundle itself.
NEXT_CACHE_DO_QUEUE: Cloudflare.DurableObject("NEXT_CACHE_DO_QUEUE", {
className: "DOQueueHandler",
}),
},
});

By default, every project file outside build outputs is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when the project has large directories that don’t affect the build output.

const site = yield* Cloudflare.Website.Nextjs("Site", {
memo: {
include: ["app/**", "public/**", "package.json", "next.config.mjs", "open-next.config.ts"],
},
});

The nextjs prop tunes the OpenNext pipeline: a custom build command, minification, or reusing an existing .next build.

const site = yield* Cloudflare.Website.Nextjs("Site", {
nextjs: {
buildCommand: "npx next build --no-lint",
minify: true,
},
});

Calling Nextjs with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Site extends Cloudflare.Website.Nextjs<Site>()("Site", {
rootDir: "./apps/web",
}) {}
const site = yield* Site;