Loading...
Loading...
Deploy Next.js to Cloudflare Workers with full App Router, Pages Router, ISR, and SSG support. Load when creating Next.js projects for Workers, migrating from Vercel/next-on-pages, configuring caching (R2/KV/D1), accessing Cloudflare bindings via getCloudflareContext, or fixing bundle size issues.
npx skill4agent add null-shot/cloudflare-skills cloudflare-opennext@opennextjs/cloudflarenpm create cloudflare@latest -- my-next-app --framework=next --platform=workers
cd my-next-app
npm run dev # Local development with Next.js
npm run preview # Preview in Workers runtime
npm run deploy # Deploy to Cloudflare# 1. Install dependencies
npm install @opennextjs/cloudflare@latest
npm install --save-dev wrangler@latest
# 2. Create wrangler.jsonc (see Configuration section)
# 3. Create open-next.config.ts
# 4. Update next.config.ts
# 5. Add scripts to package.json
# 6. Deploy
npm run deploy@opennextjs/cloudflarenext build.open-next/worker.js_next/staticpublic// ❌ Remove this - Edge runtime not supported
export const runtime = "edge";
// ✅ Default Node.js runtime - fully supported
// No export needed, this is the defaultnodejs_compat{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-nextjs-app",
"main": ".open-next/worker.js",
"compatibility_date": "2024-12-30",
"compatibility_flags": [
"nodejs_compat", // Required for Node.js APIs
"global_fetch_strictly_public" // Security: prevent local IP fetches
],
"assets": {
"directory": ".open-next/assets", // Static files
"binding": "ASSETS"
},
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "my-nextjs-app" // Must match "name" above
}
],
"images": {
"binding": "IMAGES" // Optional: Enable image optimization
}
}nodejs_compatcompatibility_date2024-09-23WORKER_SELF_REFERENCEmainassetsimport { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
});import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Your Next.js configuration
};
export default nextConfig;
// Enable bindings access during `next dev`
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
initOpenNextCloudflareForDev();# .dev.vars
NEXTJS_ENV=developmentNEXTJS_ENV.envdevelopment.env.developmentproduction.env.productiongetCloudflareContext()import { getCloudflareContext } from "@opennextjs/cloudflare";
// Route Handler (App Router)
export async function GET(request: Request) {
const { env, cf, ctx } = getCloudflareContext();
// Access KV
const value = await env.MY_KV.get("key");
// Access R2
const object = await env.MY_BUCKET.get("file.txt");
// Access D1
const result = await env.DB.prepare("SELECT * FROM users").all();
// Access Durable Objects
const stub = env.MY_DO.idFromName("instance-1");
const doResponse = await stub.fetch(request);
// Access request info
const country = cf?.country;
// Background tasks
ctx.waitUntil(logAnalytics());
return Response.json({ value });
}
// API Route (Pages Router)
export default async function handler(req, res) {
const { env } = getCloudflareContext();
const data = await env.MY_KV.get("key");
res.json({ data });
}
// Server Component
export default async function Page() {
const { env } = getCloudflareContext();
const data = await env.MY_KV.get("key");
return <div>{data}</div>;
}// In SSG route (generateStaticParams, etc.)
const { env } = await getCloudflareContext({ async: true });
const products = await env.DB.prepare("SELECT * FROM products").all();.dev.varsnpx wrangler types --env-interface CloudflareEnv cloudflare-env.d.tspackage.json{
"scripts": {
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
}
}wrangler.jsoncopennextjs-cloudflare# Build the Next.js app and transform for Workers
npx opennextjs-cloudflare build
# Build and preview locally with Wrangler
npm run preview
# or
npx opennextjs-cloudflare preview
# Build and deploy to Cloudflare
npm run deploy
# or
npx opennextjs-cloudflare deploy
# Build and upload as a version (doesn't deploy)
npm run upload
# or
npx opennextjs-cloudflare upload
# Populate cache (called automatically by preview/deploy/upload)
npx opennextjs-cloudflare populateCache local # Local bindings
npx opennextjs-cloudflare populateCache remote # Remote bindings{
"scripts": {
"dev": "next dev",
"build": "next build",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
}
}| Cache Type | Use Case | Storage Options |
|---|---|---|
| Incremental Cache | ISR/SSG page data | R2, KV, Static Assets |
| Queue | Time-based revalidation | Durable Objects, Memory |
| Tag Cache | On-demand revalidation | D1, Durable Objects |
// Static Site (SSG only)
import staticAssetsCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: staticAssetsCache,
enableCacheInterception: true,
});
// Small Site with ISR
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
queue: doQueue,
tagCache: d1NextTagCache,
});// wrangler.jsonc
{
"images": {
"binding": "IMAGES"
}
}<Image>minimumCacheTTLdangerouslyAllowLocalIP// ❌ WRONG - Global client causes I/O errors
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// ✅ CORRECT - Per-request client
import { cache } from "react";
import { Pool } from "pg";
export const getDb = cache(() => {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
maxUses: 1, // Don't reuse connections across requests
});
return drizzle({ client: pool, schema });
});
// Usage in route
export async function GET() {
const db = getDb();
const users = await db.select().from(usersTable);
return Response.json(users);
}export const runtime = "edge"cache()export const runtime = "edge"next buildnext build --turbo| Feature | Support | Notes |
|---|---|---|
| App Router | ✅ Full | All features supported |
| Pages Router | ✅ Full | Including API routes |
| Route Handlers | ✅ Full | GET, POST, etc. |
| Dynamic Routes | ✅ Full | |
| SSG | ✅ Full | Static Site Generation |
| SSR | ✅ Full | Server-Side Rendering |
| ISR | ✅ Full | Incremental Static Regeneration |
| PPR | ✅ Full | Partial Prerendering |
| Middleware | ✅ Partial | Standard middleware works, Node Middleware (15.2+) not supported |
| Image Optimization | ✅ Full | Via Cloudflare Images binding |
| Composable Caching | ✅ Full | |
| next/font | ✅ Full | Font optimization |
| after() | ✅ Full | Background tasks |
| Turbopack | ❌ No | Use standard build |
# Local development with Next.js dev server
npm run dev
# Preview in Workers runtime (faster than deploy)
npm run preview
# Deploy to production
npm run deploy
# Update TypeScript types after binding changes
npm run cf-typegennext devinitOpenNextCloudflareForDev()npm run preview@cloudflare/next-on-pages@cloudflare/next-on-pageseslint-plugin-next-on-pages@opennextjs/cloudflarenext.config.tssetupDevPlatform()initOpenNextCloudflareForDev()getRequestContext@cloudflare/next-on-pagesgetCloudflareContext@opennextjs/cloudflareexport const runtime = "edge"create-next-appmiddlewarevercel-blog-startercf-typegennpm run previewcache()observability