شغّل Next.js في أي مكان — حتى حيث لا يعمل Node.js. OpenNext هو محول مفتوح المصدر يقوم بترجمة تطبيق Next.js قياسي ليعمل على منصات serverless لا تتضمّن وقت تشغيل Node.js كاملًا، مثل Cloudflare Workers. مع المحول الرسمي @opennextjs/cloudflare تحافظ على تجربة المطور المعتادة في Next.js مع الحصول على بدء بارد بأقل من ميلي ثانية، وأكثر من 300 موقع حافة، وارتباطات مباشرة مع D1 وR2 وKV وDurable Objects.
ما الذي ستبنيه
في هذا الدرس، ستنشر تطبيق Next.js 15 جاهزًا للإنتاج على Cloudflare Workers باستخدام OpenNext. التطبيق هو لوحة تحكم بنمط SaaS صغيرة تستعرض جميع الميزات التي تعتمد عليها عادة:
- App Router مع تخطيطات متداخلة وReact Server Components
- Server Actions تكتب إلى قاعدة بيانات Cloudflare D1
- رفع الصور إلى R2 (تخزين كائنات متوافق مع S3)
- التوليد الثابت التزايدي (ISR) مدعوم بـ Cloudflare KV
- Edge middleware للمصادقة وi18n
- مسارات ديناميكية مع
generateStaticParamsوإعادة التحقق عند الطلب - التدفق وSuspense مع Server Components
في النهاية، سيكون لديك تطبيق Next.js موزع عالميًا — أرخص وأسرع وأبسط في التشغيل من نشر Node.js التقليدي — دون إعادة كتابة صفحة واحدة.
لماذا OpenNext؟
Vercel هي أسهل طريقة لشحن Next.js، لكنها ليست الوحيدة. الاستضافة الذاتية على Node.js تعمل، لكنها تتطلب خادمًا دائم التشغيل، وزمن استجابة إقليمي، وعبء تشغيل. أنشئ OpenNext لسد هذه الفجوة عبر تحويل ناتج بناء Next.js إلى حزم خاصة بالمنصة.
محول Cloudflare (@opennextjs/cloudflare) يأخذ الناتج المستقل من Next.js ويعيد كتابته ليعمل داخل وقت تشغيل Workers V8 isolate. مقارنة باستضافة Node.js التقليدية، تحصل على:
- بدء بارد بأقل من ميلي ثانية — تنطلق الـ V8 isolates فورًا تقريبًا
- أكثر من 300 نقطة تواجد — يعمل تطبيقك على الحافة في جميع أنحاء العالم
- ارتباطات منصة مباشرة — D1 وR2 وKV وDurable Objects وQueues وVectorize كلها متاحة ككائنات JavaScript دون عبء SDK
- تسعير يصل إلى الصفر — ادفع لكل طلب لا لكل مثيل تشغيل
- بدون Docker، بدون Kubernetes، بدون PM2 — أمر
wrangler deployويصبح تطبيقك مباشرًا
يدعم OpenNext كلًا من Next.js 14 وNext.js 15، بما في ذلك Server Actions وPartial Prerendering وميزات React 19 التي ظهرت في 2025.
المتطلبات المسبقة
قبل البدء، تأكد من توفر:
- Node.js الإصدار 20 أو أحدث (
node --version) - npm 10+ أو pnpm 9+ أو bun 1.2+
- حساب Cloudflare (الخطة المجانية كافية لمتابعة الدرس)
- تثبيت Wrangler CLI عالميًا:
npm install -g wrangler@latest - معرفة أساسية بـ Next.js App Router وTypeScript
- محرر شفرة (يفضّل VS Code)
استخدم Wrangler للمصادقة مع حسابك في Cloudflare لمرة واحدة:
wrangler loginسيفتح هذا نافذة متصفح ويخزن بيانات الاعتماد محليًا حتى تجري الأوامر التالية المصادقة تلقائيًا.
الخطوة 1: إنشاء مشروع Next.js جديد
ابدأ بمشروع Next.js 15 جديد. يحتفظ فريق OpenNext بقالب بدء، لكن للوضوح سننطلق من الصفر لتفهم كل قطعة متحركة.
npx create-next-app@latest noqta-edge-app \
--typescript \
--app \
--tailwind \
--eslint \
--src-dir \
--import-alias "@/*"
cd noqta-edge-appتأكد أن كل شيء يعمل محليًا:
npm run devافتح http://localhost:3000 ويجب أن ترى صفحة Next.js الافتراضية. أوقف خادم التطوير قبل المتابعة.
الخطوة 2: تثبيت محول OpenNext لـ Cloudflare
ثبّت المحول وتبعياته:
npm install --save-dev @opennextjs/cloudflare wrangler@latest
npm install @cloudflare/workers-typesتحتوي حزمة @opennextjs/cloudflare على محول البناء وأدوات للوصول إلى ارتباطات Cloudflare من داخل شفرة Next.js. توفر @cloudflare/workers-types أنواع TypeScript لـ D1 وR2 وKV وQueues وبقية واجهة Workers API.
أضف أنواع Workers إلى tsconfig.json ليعمل الإكمال التلقائي في المحرر:
{
"compilerOptions": {
"types": ["@cloudflare/workers-types"]
}
}الخطوة 3: إعداد wrangler.toml
أنشئ ملف wrangler.toml في جذر المشروع. يخبر هذا الملف Wrangler بما يجب نشره وأين، وأي موارد Cloudflare يجب ربطها.
name = "noqta-edge-app"
main = ".open-next/worker.js"
compatibility_date = "2026-05-01"
compatibility_flags = ["nodejs_compat"]
# مطلوب من OpenNext: تقديم الأصول الثابتة مباشرة
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
# تخزين مؤقت مدعوم بـ KV — يستخدمه ISR وfetch cache
[[kv_namespaces]]
binding = "NEXT_INC_CACHE_KV"
id = "REPLACE_WITH_KV_ID"
# قاعدة بيانات لـ Server Actions
[[d1_databases]]
binding = "DB"
database_name = "noqta-edge-db"
database_id = "REPLACE_WITH_D1_ID"
# تخزين كائنات للرفع
[[r2_buckets]]
binding = "UPLOADS"
bucket_name = "noqta-edge-uploads"السطر compatibility_flags = ["nodejs_compat"] بالغ الأهمية. إنه يفعّل واجهات Node.js المتعددة المنصات التي يعتمد عليها Next.js (Buffer وcrypto وutil وغيرها). بدونه سيعمل البناء، لكن العديد من الأجزاء الداخلية لـ Next.js ستفشل في وقت التشغيل.
الخطوة 4: إنشاء موارد Cloudflare
زوّد KV namespace وD1 database وR2 bucket من سطر الأوامر. كل أمر يطبع المعرّف الذي تحتاج لصقه في wrangler.toml.
# مساحة KV لـ ISR / cache
wrangler kv namespace create NEXT_INC_CACHE_KV
# قاعدة بيانات D1
wrangler d1 create noqta-edge-db
# سلة R2
wrangler r2 bucket create noqta-edge-uploadsحدّث wrangler.toml بالمعرفات التي يعيدها الأمران الأولان. تُشار إلى سلال R2 بالاسم، فلا حاجة للمعرف هناك.
الآن أنشئ مخطط قاعدة البيانات. احفظ الملف التالي باسم schema.sql:
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX IF NOT EXISTS idx_posts_created_at
ON posts (created_at DESC);طبّقه محليًا وعن بُعد:
# تطبيق على محاكي D1 المحلي
wrangler d1 execute noqta-edge-db --local --file=schema.sql
# تطبيق على قاعدة البيانات الإنتاجية
wrangler d1 execute noqta-edge-db --remote --file=schema.sqlالخطوة 5: إضافة إعدادات بناء OpenNext
يحتاج OpenNext إلى ملف إعدادات صغير ليعرف كيفية بناء المشروع. أنشئ open-next.config.ts في جذر المشروع:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: kvIncrementalCache,
});هذا الملف الواحد يربط ISR caching المبني على KV. يأتي المحول مع عدة وحدات تجاوز (R2 cache وregional cache وdurable object cache); KV هو الافتراضي ويعمل لمعظم التطبيقات.
حدّث package.json بسكربتات البناء والنشر:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"preview": "opennextjs-cloudflare build && wrangler dev",
"deploy": "opennextjs-cloudflare build && wrangler deploy"
}
}ينفذ build بناء Next.js القياسي. ينفذ deploy بناء OpenNext (الذي يحوّل ناتج Next.js إلى حزمة Worker) ثم يشحنها بـ Wrangler.
الخطوة 6: قراءة الـ Bindings من داخل Next.js
أنظف طريقة للوصول إلى ارتباطات Cloudflare من Server Components أو Route Handlers أو Server Actions هي عبر مساعد OpenNext getCloudflareContext(). أنشئ src/lib/cf.ts:
import { getCloudflareContext } from "@opennextjs/cloudflare";
import type { D1Database, R2Bucket, KVNamespace } from "@cloudflare/workers-types";
interface Env {
DB: D1Database;
UPLOADS: R2Bucket;
NEXT_INC_CACHE_KV: KVNamespace;
}
export async function getEnv(): Promise<Env> {
const ctx = await getCloudflareContext({ async: true });
return ctx.env as unknown as Env;
}الآن يمكن لأي Server Component سحب البيئة في سطر واحد:
const { DB } = await getEnv();آمن من حيث الأنواع، بدون قوالب نصية، ومُكتشف الأنواع كاملًا مع ارتباطات مشروعك.
الخطوة 7: بناء Server Action مع D1
Server Actions هي أنظف طريقة لتعديل البيانات في Next.js 15، وتعمل بسلاسة على OpenNext. أنشئ src/app/posts/actions.ts:
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { getEnv } from "@/lib/cf";
export async function createPost(formData: FormData) {
const title = String(formData.get("title") ?? "").trim();
const body = String(formData.get("body") ?? "").trim();
if (!title || !body) {
throw new Error("Title and body are required");
}
const { DB } = await getEnv();
await DB.prepare(
"INSERT INTO posts (title, body) VALUES (?, ?)"
)
.bind(title, body)
.run();
revalidatePath("/posts");
redirect("/posts");
}ثم Server Component يعرض ويُنشئ المنشورات في src/app/posts/page.tsx:
import { getEnv } from "@/lib/cf";
import { createPost } from "./actions";
export const revalidate = 60;
interface Post {
id: number;
title: string;
body: string;
created_at: number;
}
export default async function PostsPage() {
const { DB } = await getEnv();
const result = await DB.prepare(
"SELECT id, title, body, created_at FROM posts ORDER BY created_at DESC LIMIT 50"
).all<Post>();
const posts = result.results ?? [];
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="text-3xl font-bold">Posts</h1>
<form action={createPost} className="mt-6 space-y-3">
<input
name="title"
placeholder="Title"
className="w-full rounded border p-2"
required
/>
<textarea
name="body"
placeholder="What's on your mind?"
className="w-full rounded border p-2 h-28"
required
/>
<button
type="submit"
className="rounded bg-orange-500 px-4 py-2 font-medium text-white"
>
Publish
</button>
</form>
<ul className="mt-10 space-y-6">
{posts.map((p) => (
<li key={p.id} className="rounded border p-4">
<h2 className="text-xl font-semibold">{p.title}</h2>
<p className="mt-2 text-gray-700">{p.body}</p>
</li>
))}
</ul>
</main>
);
}التوجيه export const revalidate = 60 يحترمه OpenNext: تُخزَّن الصفحة المرسومة في KV وتُقدَّم من الـ cache لمدة 60 ثانية، مما يقلّل بشكل كبير من قراءات D1 تحت الحمل.
الخطوة 8: رفع الملفات إلى R2
R2 هو متجر كائنات Cloudflare المتوافق مع S3 بدون رسوم خروج. أضف Route Handler في src/app/api/upload/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { getEnv } from "@/lib/cf";
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
const { UPLOADS } = await getEnv();
const key = `uploads/${crypto.randomUUID()}-${file.name}`;
await UPLOADS.put(key, await file.arrayBuffer(), {
httpMetadata: { contentType: file.type },
});
return NextResponse.json({
key,
url: `/api/files/${encodeURIComponent(key)}`,
});
}ومعالج قراءة مطابق في src/app/api/files/[...key]/route.ts:
import { NextRequest } from "next/server";
import { getEnv } from "@/lib/cf";
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ key: string[] }> }
) {
const { key } = await params;
const fullKey = key.join("/");
const { UPLOADS } = await getEnv();
const obj = await UPLOADS.get(fullKey);
if (!obj) {
return new Response("Not found", { status: 404 });
}
return new Response(obj.body, {
headers: {
"content-type": obj.httpMetadata?.contentType ?? "application/octet-stream",
"cache-control": "public, max-age=31536000, immutable",
},
});
}يبثّ R2 الاستجابات مباشرة عبر الـ Worker — بدون buffering، بدون قفزة مزدوجة. يمكنك تقديم أصول أكبر من 100 ميجابايت دون أي عناء.
الخطوة 9: تشغيل التطبيق محليًا على Wrangler
خادم تطوير Next.js نفسه لا يستطيع كشف ارتباطات Cloudflare، لذا للاختبار من الطرف إلى الطرف محليًا استخدم Wrangler:
npm run previewيشغّل هذا أولًا opennextjs-cloudflare build، الذي ينتج مجلد .open-next يحتوي نقطة دخول الـ Worker والأصول الثابتة. ثم يقدم Wrangler الـ Worker على http://localhost:8787، مع الوصول الكامل إلى محاكيات قاعدة بيانات D1 المحلية وKV namespace وR2 bucket.
ستشاهد سجلات مثل:
Building Next.js (production)...
Building OpenNext Cloudflare bundle...
Bundling worker.js (4.2 MB minified, 1.1 MB gzipped)
Ready on http://localhost:8787
ادخل المسار /posts، أرسل النموذج، وتأكد أن الصفوف تظهر في مثيل D1 المحلي:
wrangler d1 execute noqta-edge-db --local --command="SELECT * FROM posts;"الخطوة 10: النشر إلى الإنتاج
عندما تكون راضيًا محليًا، اشحن إلى الإنتاج بأمر واحد:
npm run deployيرفع Wrangler الحزمة، يسجّل الـ Worker على رابط *.workers.dev، ويربط موارد KV وD1 وR2. خلال ثوانٍ قليلة يعمل تطبيق Next.js على كل مركز بيانات لـ Cloudflare على الكوكب.
اربط نطاقًا مخصصًا عبر لوحة تحكم Cloudflare (Workers and Pages ثم Custom Domains) أو عبر Wrangler:
wrangler deploy --routes "edge.noqta.tn/*"تزوّد Cloudflare وتجدّد شهادات TLS تلقائيًا — بدون سكربتات Let's Encrypt للصيانة.
التخزين المؤقت وISR وrevalidatePath
ينفّذ OpenNext عقد التخزين المؤقت الكامل لـ Next.js فوق Cloudflare KV:
- الصفحات الثابتة تُكتب مرة واحدة وقت البناء وتُدفع إلى KV
- صفحات ISR (
export const revalidate = N) يعاد التحقق منها في الخلفية عند أول طلب بعد انتهاء الصلاحية revalidatePath()وrevalidateTag()في Server Actions تنظف مدخلات KV ذات الصلة فورًا- fetch cache (
fetch(url, { next: { revalidate: 300 } })) يُخزَّن أيضًا في KV
لأن KV مكرر إلى كل موقع حافة، فإن أي إصابة cache في أي مكان من العالم تُقدَّم في أقل من 50 ميلي ثانية — عادة أقرب إلى 5 ميلي ثانية.
إذا احتجت محتوى ديناميكيًا منخفض الكمون (لكل طلب، بدون تخزين مؤقت)، فإن OpenNext يكشف أيضًا تجاوز regional cache وتجاوز Durable Object cache للإبطال المتسق بقوة. راجع وثائق OpenNext للمفاضلات.
Edge Middleware
يعمل middleware في Next.js على الحافة افتراضيًا — ومع OpenNext، يعني هذا داخل نفس الـ Worker. بوابة مصادقة بسيطة تبدو متطابقة مع نشر Vercel:
import { NextRequest, NextResponse } from "next/server";
export function middleware(req: NextRequest) {
const session = req.cookies.get("session");
if (!session && req.nextUrl.pathname.startsWith("/dashboard")) {
const loginUrl = new URL("/login", req.nextUrl);
loginUrl.searchParams.set("from", req.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};لا استيرادات خاصة، ولا أغلفة خاصة بالمنصة — نفس الشفرة التي تعمل على Vercel تعمل على Cloudflare Workers.
القابلية للملاحظة
تشحن Cloudflare قابلية ملاحظة من الطراز الأول لـ Workers:
- سجلات حية عبر
wrangler tail - Workers Analytics في لوحة التحكم (عدد الطلبات، معدل الأخطاء، p50/p99 latency)
- Tail Workers لإعادة توجيه السجلات إلى Datadog أو Logflare أو Honeycomb
- دعم OpenTelemetry عبر حزمة
otel-cf-workers
فعّل التسجيل المنظم باستخدام console.log() في Server Components — يلتقطها Wrangler مع طوابع زمنية بدقة الميلي ثانية ومعرّفات الطلبات.
اختبار التنفيذ
تحقق أن النشر يعمل من الطرف إلى الطرف:
- زر رابط الإنتاج وحمّل
/posts - أرسل النموذج بعنوان ومحتوى جديدين
- تأكد أن المنشور الجديد يظهر (Server Action كتب إلى D1)
- راقب
wrangler tailلسجل الطلبات الحي - شغّل
wrangler d1 execute noqta-edge-db --remote --command="SELECT COUNT(*) FROM posts;"وتحقق من عدد الصفوف - ارفع ملفًا عبر نقطة
/api/uploadثم اقرأه عبر/api/files/[key]
إن مرّت الخطوات الست، فلديك تطبيق Next.js يعمل بالكامل على Cloudflare Workers.
استكشاف الأخطاء
فشل البناء برسالة Cannot find module 'node:fs' — تأكد من ضبط compatibility_flags = ["nodejs_compat"] في wrangler.toml، وأن compatibility_date على أو بعد 2024-09-23.
getCloudflareContext() يفشل أثناء تطوير Next.js المحلي — أمر next dev لا يكشف الارتباطات. استخدم npm run preview (الذي يستدعي wrangler dev) لأي مسار شفرة يلامس D1 أو R2 أو KV أثناء التطوير.
حزمة Worker تتجاوز 10 ميجابايت — حد الخطة المجانية. استخدم إعداد unstable_streamingFetch في OpenNext لإزالة التبعيات الجانبية للخادم غير المستخدمة. الخطة المدفوعة ترفع الحد إلى 25 ميجابايت.
صفحات ISR لا يتم تحديثها — تأكد أن NEXT_INC_CACHE_KV مرتبطة وأنك لم تضبط dynamic = "force-static" بالخطأ. راقب الـ Worker وابحث عن مفتاح الـ cache في كل طلب.
الأصول الثابتة تعيد 404 — يجب أن يشير قسم [assets] في wrangler.toml إلى .open-next/assets. تحقق أن المجلد موجود بعد تشغيل البناء.
الخطوات التالية
- أضف المصادقة عبر Better Auth أو Auth.js v5 — كلاهما يعمل على Workers
- ابثّ إكمال الذكاء الاصطناعي عبر Workers AI أو OpenAI Realtime API
- انقل المهام الطويلة إلى Cloudflare Queues المُشغّلة من Server Actions
- أضف Vectorize للبحث الدلالي وRAG فوق بياناتك في D1
- أعدّ بيئات Wrangler (
--env stagingو--env production) لنشر آمن متعدد المراحل - أضف سير عمل GitHub Actions ينفّذ
npm run deployعند كل دفع إلىmain
الخلاصة
يجعل OpenNext من الممكن أخذ أي تطبيق Next.js 15 — App Router وServer Actions وISR وmiddleware والكثير — وتشغيله على Cloudflare Workers بملف إعدادات واحد وأمر نشر واحد. تحصل على توزيع عالمي وبدء بارد فوري ووصول مباشر إلى أساسيات المنصة من الطراز الأول مثل D1 وR2 وKV، مع الحفاظ على تجربة مطور Next.js القياسية التي يعرفها فريقك.
لـ SaaS صغير أو موقع محتوى عالي الحركة، هذا المكدس أرخص بشكل كبير من مضيف Node.js وأسرع بكثير من دالة Vercel أحادية المنطقة. لعبء عمل مؤسسي، تتسع نفس الأنماط لمليارات الطلبات دون لمس عنقود Kubernetes.
Next.js الذي تعرفه، منشور في كل مكان. هذا وعد OpenNext — وفي 2026 صار أخيرًا حقيقة إنتاجية.