- كل المقالات
- بناء التطبيقات على Cloudflare
- استضافة موقع على Cloudflare Workers: من التجربة المحلية إلى نطاقك الخاص
استضافة موقع على Cloudflare Workers: من التجربة المحلية إلى نطاقك الخاص
أضف صفحة إلى واجهتك البرمجية المبنية بـ Hono، وافهم كيف تقدّم Workers الملفات وتشغّل شيفرة الخادم، ثم انشر الموقع عبر Git واختبره واربطه بنطاقك.
بنينا في الجزء الثالث واجهة برمجية تقدّر مدة قراءة النصوص، واختبرناها من الطرفية. سنضيف هنا صفحة يفتحها القارئ ويلصق فيها نصه، فتظهر له النتيجة.
يمكن استضافة الصفحة والواجهة البرمجية معا على Cloudflare Workers. يحمّل المتصفح ملفات HTML وCSS وJavaScript، وعندما يرسل القارئ النموذج، يستقبل Hono الطلب ويحسب النتيجة. كل ذلك على اسم المضيف نفسه، من دون فصل الواجهة في خدمة استضافة أخرى.
سنختبرها محليا، ونراجع نسخة منشورة من فرع مستقل، ثم نتيحها على نطاقك. نريد أن نعرف ما يحدث في كل طلب: أي ملف أو دالة أعادت الاستجابة؟ وما الإعدادات المستخدمة؟ وهل يعرض المتصفح آخر نسخة نشرناها؟
للمتابعة، افتح مشروع reading-api من الجزء الثالث. يفترض أن يكون لديك الملفان src/index.ts وsrc/analyze.ts، مع تثبيت مكتبة Hono وأداة Wrangler. سنحتفظ بشيفرة الحساب والتحقق من المدخلات، ونضيف إليها ملفات HTML وJavaScript مباشرة. بهذه الطريقة يسهل تتبع الطلبات قبل إدخال إطار عمل في المشروع.
- ما الذي يطلبه المتصفح؟
- إضافة صفحة للأداة
- التعامل مع الصفحات غير الموجودة
- النشر وربط المستودع
- ربط نطاقك بالموقع
- تحديث الملفات واستخدام أطر العمل
يمكن حساب مدة القراءة بالكامل داخل المتصفح، بحيث لا يغادر النص جهاز القارئ. نستخدم الواجهة البرمجية هنا لنتعلم كيف نربط صفحة بتطبيق Worker؛ وستصبح الحاجة إلى الخادم أوضح عندما نضيف وظائف مثل حفظ المسودات.
ما الذي يطلبه المتصفح؟
عند فتح الأداة، يطلب المتصفح صفحة HTML أولا. يجد فيها رابطا لملف CSS وآخر لملف JavaScript، فيطلبهما أيضا. وعند إرسال النموذج، يرسل طلبا جديدا إلى /api/reading-time.
يمكن تجهيز الملفات الثلاثة قبل وصول أي زائر. أما نتيجة الحساب فتتوقف على النص المرسل في الطلب.
تتيح Workers Static Assets نشر الملفات مع تطبيق Worker نفسه. نحدد مجلدا للملفات المتاحة للزوار، ونحدد ملف الدخول الذي يحتوي على شيفرة الخادم. تتولى المنصة تقديم الملفات، فلا نحتاج إلى خادم منفصل يقرأها من القرص ويرسلها.
وصف الصفحة بأنها «ثابتة» لا يعني أنها تخلو من التفاعل. يمكن لصفحة HTML جاهزة أن تشغّل JavaScript، وتطلب بيانات من API، وتحدّث ما تعرضه. المقصود هو أن HTML جاهز مسبقا، ولا نولّده من جديد لكل طلب.
متى تُجهّز الصفحة؟
ثلاث طرق لعرض المحتوى
يمكن للموقع الواحد الجمع بين هذه الطرق بحسب حاجة كل صفحة.
- قبل الزيارةHTML مولّد مسبقا
يتحول المقال أثناء البناء إلى ملف HTML جاهز لتقديمه عند وصول الطلب.
- عند وصول الطلبتوليد HTML على الخادم
يولّد Worker الصفحة لهذا الطلب، وقد يستخدم بيانات الجلسة أو معلومات محدثة.
- بعد تحميل الصفحةتفاعل داخل المتصفح
تحدّث JavaScript ما تعرضه الصفحة. في أداتنا، تطلب النتيجة من API ثم تضيفها إلى الصفحة.
يمكن إضافة التفاعل سواء كان HTML جاهزا مسبقا أو مولّدا عند الطلب.
في أداتنا، يرى جميع الزوار النموذج نفسه. لذلك سنقدمه كملف جاهز، ونشغّل دالة الحساب عند إرسال النص فقط.
وماذا عن Cloudflare Pages؟
توصي Cloudflare حاليا باستخدام Workers للمشاريع الجديدة. فهي تدعم الملفات الثابتة وشيفرة الخادم والبناء من Git ومعاينة الفروع. أما Pages فما زالت لها آلية النشر الخاصة بها وPages Functions؛ لذا ستصادفها في المشاريع القائمة والشروحات الأقدم.
أما إذا كان موقعك يعمل على Pages، فراجع ما سيترتب على نقله أولا: محوّل إطار العمل (adapter)، ومسارات Functions، وقواعد إعادة التوجيه، وطريقة المعاينة. يوضح دليل الانتقال من Pages إلى Workers الفروق التي تحتاج إلى مراعاتها.
إضافة صفحة للأداة
إذا كانت لديك واجهة جاهزة، فاستخدم ملفاتها بدلا من النموذج أدناه، وتابع خطوات التوجيه والنشر. وإذا كان إطار العمل يولّد حزمة Worker، فاستخدم إعدادات محوّله كما سنوضح في قسم Astro.
أنشئ مجلد public داخل reading-api. بعد إضافة ملفات هذا الجزء، سيصبح ترتيب المشروع كالتالي:
reading-api/
├── public/
│ ├── index.html
│ ├── styles.css
│ ├── app.js
│ ├── 404.html
│ └── _headers
├── src/
│ ├── analyze.ts
│ └── index.ts
├── package.json
├── package-lock.json
└── wrangler.jsonc
ضع في public ما يجوز للزائر تنزيله فقط. أبقِ ملف .dev.vars وبيانات الاعتماد وشيفرة الخادم خارجه. مجلد الملفات المنشورة يجب أن يكون محددا بوضوح؛ لا تستخدم جذر المستودع لهذا الغرض.
تحديد الملفات في إعدادات Wrangler
أضف الخاصية التالية إلى المستوى الأعلى من wrangler.jsonc، مع إبقاء main وvars وبقية إعدادات الجزء الثالث:
{
"assets": {
"directory": "./public",
"binding": "ASSETS",
"not_found_handling": "404-page",
"run_worker_first": ["/api", "/api/*", "/health"]
}
}
يحدد directory مجلد الملفات، ويتيح الربط ASSETS طلب استجابة من خدمة الملفات داخل شيفرة Worker. أما 404-page فيختار صفحة HTML للعنوان غير الموجود. ويضمن run_worker_first أن تمر مسارات الواجهة البرمجية وفحص الحالة عبر Worker أولا.
أبقِ تاريخ التوافق الحالي أثناء هذه الخطوة؛ تغيير سلوك بيئة التشغيل يحتاج إلى اختبار مستقل. ثم أعد توليد الأنواع ليشمل CloudflareBindings الربط الجديد ASSETS:
npx wrangler types --env-interface CloudflareBindings
في الوضع الافتراضي، تستطيع المنصة إعادة الملف المطابق للمسار من دون تشغيل شيفرتنا. أما المسارات التي تُعالج في Worker أولا، فتضمن وصول طلبات API إلى Hono حتى لو وُجد ملف يطابق عنوان الطلب.
تتبّع الطلب
اسم مضيف واحد، وطريقتان للاستجابة
اختر طلبا لترى كيف تعالجه إعدادات المقال. هذا شرح توضيحي؛ لا يرسل طلبات فعلية.
- فتح الصفحة الرئيسيةGET /المتصفحخدمة الملفاتHTML بالحالة 200
تعيد خدمة الملفات public/index.html مباشرة، من دون تشغيل معالج Hono.
- تحميل JavaScriptGET /app.jsالمتصفحخدمة الملفاتJavaScript بالحالة 200
تعيد خدمة الملفات public/app.js، ثم يشغّله المتصفح.
- حساب مدة القراءةPOST /api/reading-timeالمتصفحWorker · HonoJSON بالحالة 200
يتحقق Hono من المدخلات ويستدعي analyzeReading(). إذا كانت صالحة، يعيد النتيجة لتعرضها الصفحة.
- فتح مسار API كصفحةGET /api/reading-timeالمتصفحWorker · HonoJSON بالحالة 405
تتضمن الاستجابة Allow: POST. تضمن قاعدة /api/* وصول الطلب إلى Hono بدلا من إرجاع صفحة HTML بديلة.
- فتح رابط غير موجودGET /missingانتقال من المتصفحخدمة الملفاتHTML بالحالة 404
تعيد خدمة الملفات public/404.html بالحالة 404. يبقى العنوان المطلوب في شريط المتصفح.
تمر مسارات API وفحص الحالة عبر Worker أولا. وقد يصل طلب ملف غير موجود إلى Hono أيضا إن لم يكن طلب انتقال؛ عندها يحيله إلى ASSETS.
كتابة الصفحة
نحتاج إلى حقل للنص له تسمية واضحة، وقائمة لاختيار اللغة، وحقل لسرعة القراءة، ومساحة لعرض النتيجة وإعلانها لقارئ الشاشة. تجمع الصفحة المدخلات وترسلها، ثم تعرض ما تعيده الواجهة البرمجية.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Reading time</title>
<meta
name="description"
content="Estimate reading time for English and Arabic text."
/>
<link rel="stylesheet" href="/styles.css" />
<script type="module" src="/app.js"></script>
</head>
<body>
<main>
<h1>How long will this take to read?</h1>
<p>Paste a paragraph and choose a pace to estimate its reading time.</p>
<form id="reading-form">
<label for="text">Text to read</label>
<textarea
id="text" name="text" rows="8" dir="auto"
maxlength="10000" required
></textarea>
<label for="language">Text language</label>
<select id="language" name="language">
<option value="en">English</option>
<option value="ar" lang="ar">العربية</option>
</select>
<label for="pace">Reading speed, in words per minute</label>
<input
id="pace" name="wordsPerMinute" type="number"
min="80" max="600" step="1" value="200" required
/>
<p class="hint">200 is a starting assumption. Adjust it to suit the text.</p>
<button type="submit" disabled>Estimate reading time</button>
<p class="hint">Submitting sends your text to this site's API.</p>
</form>
<p id="result" role="status" aria-atomic="true"></p>
<noscript>This tool needs JavaScript to submit text and show a result.</noscript>
</main>
</body>
</html>
تجعل dir="auto" اتجاه الكتابة في حقل النص يتبع محتواه. واجهة المثال إنجليزية وتقبل النصوص العربية والإنجليزية؛ تحديد اتجاه النص لا يغيّر لغة الأزرار والتسميات.
يبدأ زر الإرسال معطلا، وتفعّله JavaScript بعد تجهيز دالة الإرسال. إذا تعذر تحميل الشيفرة، لن يُرسل النموذج النص بالخطأ بالطريقة الافتراضية التي تنقل المتصفح إلى عنوان جديد.
تنسيق النموذج
يكفي هنا خط مقروء، وحقول تناسب شاشة الهاتف، ومؤشر واضح للعنصر المحدد عند التنقل بلوحة المفاتيح. لا نحتاج إلى مكتبة لتنسيق هذه الصفحة.
:root {
font-family: system-ui, sans-serif;
line-height: 1.6;
color: #24211d;
background: #f8f6f1;
}
* { box-sizing: border-box; }
body { margin: 0; }
main { max-width: 42rem; margin: auto; padding: 2rem 1.25rem; }
h1 { line-height: 1.2; text-wrap: balance; }
label { display: block; margin-block: 1rem 0.35rem; font-weight: 600; }
textarea, select, input, button {
width: 100%;
font: inherit;
padding: 0.75rem;
border: 1px solid #81786d;
border-radius: 0.3rem;
}
textarea, select, input { color: inherit; background: #fff; }
textarea { resize: vertical; }
button { margin-top: 1.25rem; color: #fff; background: #29499c; }
button:disabled { opacity: 0.65; cursor: wait; }
:focus-visible { outline: 3px solid #29499c; outline-offset: 3px; }
.hint { font-size: 0.9rem; color: #5e564b; }
#result { min-height: 3rem; font-weight: 600; }
إرسال النص إلى الواجهة البرمجية
أنشئ ملف public/app.js. نستخدم العنوان النسبي /api/reading-time حتى يذهب الطلب إلى أصل الصفحة نفسه، أي البروتوكول واسم المضيف والمنفذ ذاتها. سيعمل ذلك محليا، وعلى رابط المعاينة، وعلى نطاقك، من دون تعديل العنوان في الشيفرة.
const form = document.querySelector("#reading-form");
const button = form.querySelector('button[type="submit"]');
const result = document.querySelector("#result");
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (button.disabled) return;
const fields = new FormData(form);
button.disabled = true;
result.textContent = "Estimating…";
try {
const response = await fetch("/api/reading-time", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: fields.get("text"),
language: fields.get("language"),
wordsPerMinute: Number(fields.get("wordsPerMinute")),
}),
signal: AbortSignal.timeout(10_000),
});
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("application/json")) {
throw new Error("The server returned an unexpected response. Try again.");
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.error ?? "The estimate could not be calculated.");
}
const words = data.words === 1 ? "word" : "words";
const seconds = data.seconds === 1 ? "second" : "seconds";
result.textContent =
`${data.words} ${words} · about ${data.seconds} ${seconds}` +
` at ${data.wordsPerMinute} words per minute.`;
} catch (error) {
if (error instanceof DOMException && error.name === "TimeoutError") {
result.textContent = "The request took too long. Please try again.";
} else if (error instanceof TypeError) {
result.textContent = "Couldn't reach the server. Check your connection.";
} else {
result.textContent = error instanceof Error
? error.message
: "Something went wrong. Please try again.";
}
} finally {
button.disabled = false;
}
});
button.disabled = false;
يفحص المتصفح القيود الأساسية للنموذج قبل إرساله، لكن على الخادم التحقق من كل حقل أيضا؛ يمكن لأي عميل إرسال طلب مباشر من دون استخدام الصفحة. شيفرة الجزء الثالث تتحقق من المدخلات، وتضع حدا لحجم محتوى الطلب، وتعيد أخطاء بصيغة JSON.
لا ترمي fetch() استثناء لمجرد أن الخادم أعاد الحالة 400 أو 500، لذلك نفحص response.ok. ونفحص نوع المحتوى قبل قراءته كـ JSON: إذا أعادت إعدادات التوجيه صفحة HTML بالخطأ، تظهر رسالة مفهومة بدلا من خطأ تقني في تحليل JSON.
نستخدم textContent لعرض النتيجة كنص، من دون تفسيرها على أنها HTML.
ولأن الصفحة والواجهة البرمجية تشتركان في الأصل نفسه، لا نحتاج إلى إعداد CORS لهذا الاتصال. تظل الواجهة البرمجية متاحة للجميع؛ CORS لا يتحقق من هوية من يستدعيها.
إضافة ترويسات للملفات العامة
لإضافة ترويسات إلى استجابات الملفات، أنشئ _headers داخل مجلد public:
/*
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
تطبق هذه القواعد عند تقديم الملفات. لا تضيف الترويسات تلقائيا إلى JSON الذي يولّده Hono؛ اضبط ترويسات تلك الاستجابات داخل شيفرة الخادم عند الحاجة. كذلك، لن تفحص دالة وسيطة في Hono طلب الصفحة الرئيسية إذا أجابته خدمة الملفات مباشرة. إذا كانت الملفات خاصة، وجّه طلباتها عبر التحقق من الهوية قبل إعادتها للزائر.
التعامل مع الصفحات غير الموجودة
إذا فتح الزائر رابطا غير موجود، نريد أن نعرض له صفحة تساعده على العودة إلى الأداة. أما طلب مسار API غير موجود، فيجب أن يتلقى خطأ بصيغة JSON. سنفصل الحالتين في معالج المسارات غير المعروفة.
في src/index.ts، استبدل كتلة app.notFound(...) الحالية بما يلي:
app.notFound((c) => {
const path = c.req.path;
const isApi = path === "/api" || path.startsWith("/api/");
const isPageRequest = c.req.method === "GET" || c.req.method === "HEAD";
if (isApi || !isPageRequest) {
return c.json({ error: "Not found." }, 404);
}
return c.env.ASSETS.fetch(c.req.raw);
});
أبقِ المسارات والدوال الوسيطة وapp.onError وexport default app كما هي. يستدعي ASSETS.fetch() خدمة الملفات عبر الربط مباشرة؛ ولا يرسل طلبا عاما جديدا يعود إلى Hono.
أضف صفحة الخطأ:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Page not found · Reading time</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<main>
<h1>That page isn't here.</h1>
<p>The address may be wrong, or the page may have moved.</p>
<a href="/">Open the reading-time tool</a>
</main>
</body>
</html>
بهذا الإعداد، يعيد العنوان غير الموجود صفحة الخطأ مع رمز الحالة 404. لا نحوّل كل رابط مكسور إلى الصفحة الرئيسية؛ تحتاج محركات البحث وعملاء HTTP إلى رمز يصف النتيجة، إلى جانب الرسالة التي يقرأها الزائر.
هناك تفصيل في التوجيه يستحق الاختبار. عند استخدام 404-page مع تاريخ توافق يوافق 1 نيسان 2025 أو أحدث، كما في مشروعنا، تذهب طلبات الانتقال إلى صفحة مباشرة إلى خدمة الملفات، ما لم تنطبق قاعدة تشغّل Worker أولا. تحمل هذه الطلبات الترويسة Sec-Fetch-Mode: navigate، بينما لا يضيفها curl عادة.
لهذا حجزنا /health ومسارات API في run_worker_first: تصل إلى Hono سواء فُتحت من شريط العنوان أو طُلبت برمجيا. أما طلبات GET الأخرى لملفات غير موجودة، إن لم تكن طلبات انتقال، فقد تصل إلى app.notFound الذي يطلب صفحة الخطأ من ASSETS. يشرح دليل توجيه المواقع الثابتة هذا السلوك.
لا تفعّل single-page-application لمجرد أن الأداة تتكون من صفحة واحدة. يعيد هذا الخيار index.html باستجابة نجاح عند غياب الملف، لتتولى شيفرة التوجيه في المتصفح اختيار المحتوى. ليس لدينا مثل هذه الشيفرة، ولذلك يجب أن يظل العنوان غير الموجود خطأ 404.
التجربة المحلية وتتبع الطلبات
نفّذ من مجلد المشروع:
npx tsc --noEmit
npx wrangler dev --local
إذا أبلغك Wrangler أن تاريخ التوافق أحدث من بيئة التشغيل المحلية التي يضمها، حدّث حزمة المشروع بالأمر npm install --save-dev wrangler@latest، ثم أعد توليد الأنواع والفحص. احتفظ بالتعديل الناتج في ملف القفل.
افتح العنوان الذي يطبعه Wrangler، وهو عادة http://localhost:8787. جرّب نصا إنجليزيا قصيرا، ثم نصا عربيا، وغيّر سرعة القراءة. في هذا الإعداد، يصل النص إلى Worker الذي يعمل محليا على جهازك.
افتح تبويب Network في أدوات المطور قبل إعادة تحميل الصفحة. سترى طلبات HTML وCSS وJavaScript. أرسل النموذج وراقب طلب POST: يجب أن تكون استجابته JSON تحتوي على عدد الكلمات والمدة المقدرة، من دون إعادة تحميل الصفحة.
اختبر الحالات التالية، فكل منها يكشف نوعا مختلفا من الأخطاء:
| ما تختبره | النتيجة المتوقعة |
|---|---|
فتح / | النموذج بتنسيقه الصحيح وزر إرسال يعمل |
| إرسال نص صالح | استجابة JSON بالحالة 200 من POST /api/reading-time |
| إرسال نص فارغ مباشرة إلى API | استجابة JSON بالحالة 400 من الخادم |
فتح /api/reading-time من شريط العنوان | استجابة JSON بالحالة 405 والترويسة Allow: POST |
فتح /api/missing | استجابة JSON بالحالة 404 |
فتح /missing | صفحة الخطأ مع الحالة 404 |
فتح /health | استجابة JSON بالحالة 200، حتى عند فتحه كصفحة |
| قطع الاتصال ثم الإرسال | رسالة توضح تعذر الاتصال، ثم إتاحة زر الإرسال من جديد |
لإرسال طلب يحمل ترويسة الانتقال من الطرفية، ثم تجربة طلب حساب عادي:
curl -i http://localhost:8787/health \
-H 'Sec-Fetch-Mode: navigate'
curl -i http://localhost:8787/missing \
-H 'Sec-Fetch-Mode: navigate'
curl -i http://localhost:8787/api/reading-time \
-H 'Content-Type: application/json' \
--data '{"text":"Small tools deserve good interfaces.","language":"en"}'
يحتوي نص الطلب الأخير على خمس كلمات. وبسرعة القراءة الافتراضية، 200 كلمة في الدقيقة، تقرّب الواجهة المدة إلى الأعلى فتعيد ثانيتين. هذه نتيجة مبنية على سرعة نفترضها ويمكن تعديلها، وليست قياسا لسرعة قارئ بعينه.
نشر الموقع وربطه بـ Git
قبل النشر الأول، جرّب تجهيز الحزمة:
npx wrangler deploy --dry-run
يجهز الأمر ملفات النشر من دون نشرها فعليا. عندما تريد إتاحة المثال على الإنترنت، سجّل الدخول ثم انشره:
npx wrangler login
npx wrangler deploy
افتح الرابط الذي يظهر وأرسل النموذج منه. نجاح التجربة المحلية لا يختبر إعدادات النسخة المنشورة أو اسم مضيفها.
بعد وضع المشروع في مستودع على GitHub أو GitLab، اربطه بالتطبيق من Settings → Builds. راجع هذه الإعدادات قبل الاعتماد على النشر التلقائي:
| الإعداد | القيمة في مشروعنا |
|---|---|
| فرع الإنتاج | الفرع الذي تنشر منه، مثل main |
| المجلد الجذر | المجلد الذي يحتوي على package.json الخاص بالمشروع |
| أمر البناء | npx wrangler types --env-interface CloudflareBindings && npx tsc --noEmit |
| أمر النشر | npx wrangler deploy |
| اسم Worker | يطابق name في wrangler.jsonc |
يعيد أمر البناء توليد أنواع الموارد المرتبطة قبل فحص TypeScript. بذلك يعمل في نسخة جديدة من المستودع حتى لو لم تتضمن ملفات الأنواع المولّدة. يتولى Wrangler تجميع شيفرة Worker أثناء النشر، أما HTML الذي كتبناه فلا يحتاج إلى خطوة تحويل.
أضف ملف قفل الحزم إلى المستودع لتُثبّت الإصدارات المسجلة فيه. وإذا استخدمت Astro، فيجب توليد الموقع قبل نشره، عادة بالأمر npm run build. تفصل إعدادات Workers Builds بين أمر البناء وأمر النشر.
من جهازك إلى الموقع المنشور
اختبر التطبيق في كل مرحلة
تختلف الإعدادات والظروف بين التجربة المحلية والمعاينة والإنتاج.
- ١ · محلياالصفحة والواجهة البرمجية
شغّل Wrangler. جرّب مدخلات صالحة وأخرى غير صالحة، وروابط مفقودة، ورسائل الخطأ في المتصفح.
- ٢ · المعاينةإعدادات النسخة المنشورة
انشر الفرع، وافحص متغيراته والموارد المرتبطة به، ثم شارك الرابط للمراجعة.
- ٣ · الإنتاجما يراه القارئ فعليا
ادمج التعديل في فرع الإنتاج. بعد النشر، افتح النطاق الحقيقي وأرسل النموذج.
اختبر التفاعل كاملا على الرابط المنشور، بما في ذلك حالات الخطأ.
معاينة الفرع تعمل على Cloudflare
تتيح معاينة الفرع مراجعة التعديل على رابط منشور قبل وصوله إلى فرع الإنتاج. ويمكنك مشاركة الرابط مع من يراجع عملك. لهذه النسخة إعداداتها الخاصة التي لا تختبرها التجربة المحلية وحدها.
حتى تاريخ كتابة المقال، 6 تشرين الثاني 2026، تستخدم Worker Previews الأمر npx wrangler preview، وتتطلب Wrangler بالإصدار 4.135.0 أو أحدث. حدّث الحزمة المثبتة في المشروع عند الحاجة؛ تحديث نسخة Wrangler العامة على جهازك لا يغيّر النسخة التي يستخدمها المشروع.
تستخدم التطبيقات الجديدة المرتبطة بـ Builds آلية المعاينة الجديدة افتراضيا. وقد تبقى التطبيقات الأقدم على الآلية السابقة إلى أن تنقلها صراحة. فعّل بناء المعاينات من إعدادات الفروع، وافحص أمر المعاينة المسجل فيها؛ فقد يظل التطبيق القديم يستخدم الأمر السابق.
لإعداد الآلية الجديدة، أضف previews إلى المستوى الأعلى من wrangler.jsonc:
{
"previews": {
"vars": {
"SERVICE_NAME": "reading-api-preview",
"DEFAULT_WPM": 200
}
}
}
أبقِ assets في المستوى الأعلى أيضا؛ يرفع Wrangler ملفات الفرع عند تشغيل npx wrangler preview. يوضح دليل إعداد المعاينات موضع كل إعداد. متغيرات المستوى الأعلى لا تنتقل تلقائيا إلى المعاينة، ولذلك عرّفنا قيمها هنا.
بعد نشر المعاينة، افتح /health وتأكد من أن اسم الخدمة هو reading-api-preview، ثم جرّب النموذج. عندما نضيف التخزين، سنحدد قاعدة بيانات المعاينة أيضا؛ اختلاف الرابط لا يعني أن البيانات منفصلة.
إذا كان محتوى المعاينة خاصا، احمِ الوصول إليها. يستطيع أي شخص استخدام الرابط غير المحمي إذا وصل إليه.
ربط نطاقك بالموقع
يمكن استخدام التطبيق عبر رابط workers.dev. ولإتاحته باسم تختاره، اربطه بنطاقك.
إذا كان النطاق مفعّلا على Cloudflare، أضف Custom Domain إلى الإعدادات. استخدم اسم مضيف غير مستخدم ضمن نطاق تملكه:
{
"routes": [
{ "pattern": "reading.example.com", "custom_domain": true }
]
}
انشر من جديد. تنشئ Cloudflare سجل DNS وشهادة TLS الخاصة بـ Custom Domain. قد يمنع سجل موجود ومتعارض إتمام الإعداد؛ يوضح مقال DNS طريقة فحص السجلات.
لن نغيّر عنوان API في الصفحة. يظل الطلب النسبي موجها إلى /api/reading-time، لكنه سيصل الآن إلى reading.example.com. وعند فتح معاينة فرع، سيصل إلى اسم مضيف تلك المعاينة.
اختر عنوانا أساسيا واحدا للموقع العام. إذا أتحته على النطاق مباشرة وعلى www، فأعد توجيه العنوان الآخر إلى الأساسي مع الاحتفاظ بالمسار. أضف عنوانا ووصفا مناسبين لكل صفحة محتوى، واجعل روابط canonical تشير إلى النطاق العام، وأدرج المحتوى المنشور في خريطة الموقع. اختبر إعادة التوجيه على صفحة داخلية أيضا، لا على الرئيسية فقط.
التخزين المؤقت عند تحديث الملفات
تتولى خدمة الملفات في Cloudflare تقديم ما رفعناه. في استجابات الملفات المعتادة، تكون ترويسة التخزين المؤقت للمتصفح افتراضيا Cache-Control: public, max-age=0, must-revalidate، وترافقها ETag للتحقق مما إذا كان الملف قد تغير. يمكن للمتصفح الاحتفاظ بنسخة، لكنه يتحقق من صلاحيتها قبل إعادة استخدامها. التفاصيل في مرجع ترويسات الملفات.
هذا مناسب لأسماء ملفاتنا الثابتة. إذا عدّلنا app.js فسيظل عنوانه /app.js. ولو سمحنا للمتصفح باستخدام نسخته مدة سنة من دون تحقق، فقد لا يصل الإصلاح الجديد إلى من زار الموقع سابقا.
قد تولّد أداة البناء لاحقا اسما مثل app.a1b2c3.js، يتغير الجزء الأخير منه بتغير المحتوى. عندها يمكن الاحتفاظ بالملف مدة طويلة مع immutable؛ فصفحة HTML الجديدة ستطلب اسما جديدا. طبّق هذه السياسة على الملفات ذات الأسماء المرتبطة بمحتواها، لا على HTML أو جميع مسارات الموقع.
بعد نشر تعديل
هل سيحمّل المتصفح الشيفرة الجديدة؟
لنفترض أن المتصفح يحتفظ بالإصدار الأول، وأن التخزين المؤقت مفعّل، ثم فتحت الصفحة مجددا فطلبت الملف. قارن بين ثلاث سياسات.
- اسم ثابت مع التحقق قبل الاستخدام/app.js · max-age=0, must-revalidateالإصدار الأول محفوظالتحقق باستخدام ETagتنزيل الإصدار الثاني
يبقى العنوان نفسه. لأن الملف تغير، يعيد الخادم المحتوى الجديد. أما إذا لم يتغير، فتتيح استجابة 304 للمتصفح استخدام نسخته المحفوظة.
- اسم ثابت مع صلاحية سنة/app.js · max-age=31536000, immutableالإصدار الأول محفوظالنسخة ما زالت صالحةاستخدام الإصدار الأول
قد يستخدم المتصفح الملف القديم من دون سؤال الخادم. رفع محتوى جديد إلى العنوان نفسه لا يلغي صلاحية النسخة المحفوظة لديه.
- اسم يتغير بتغير المحتوى مع صلاحية سنة/app.b2.js · max-age=31536000, immutableالملف app.a1.js محفوظHTML الجديد يطلب app.b2.jsتنزيل الإصدار الثاني
العنوان الجديد غير موجود في ذاكرة هذا المتصفح. يتغير اسم الحزمة بتغير محتواها، ويجب تحديث HTML ليشير إلى الاسم الجديد.
في مثالنا، أبقِ التحقق قبل إعادة الاستخدام لأن أسماء الملفات ثابتة. خصص التخزين الطويل مع immutable للملفات التي تتغير عناوينها بتغير محتواها.
نشر إصدار جديد لا يمسح ذاكرة المتصفح المؤقتة، والصفحة المفتوحة تواصل تشغيل الشيفرة التي حمّلتها. يفيد التحقق من صلاحية الملف عند التحميل التالي، ويفيد الاسم الجديد عندما تصل إلى المتصفح صفحة HTML تشير إليه. يشرح دليل التخزين المؤقت من MDN الآليتين.
تحتفظ استجابات API بالترويسة Cache-Control: no-store التي أضفناها في الجزء الثالث. تعتمد النتيجة على النص المرسل، ولن نضيف في هذا المثال تخزينا مؤقتا لها.
ماذا يتغير عند استخدام Astro؟
لا تحتاج صفحة النموذج إلى كثير من القوالب. أما المدونة، ففيها تخطيطات مشتركة وقوائم تنقل وبيانات للمقالات ومجموعة من ملفات المحتوى. يساعد إطار العمل على إدارة هذا التكرار.
هذا الموقع مبني باستخدام Astro. تُولّد المقالات مسبقا كملفات HTML، بينما تُولّد الصفحات التي تحتاج إلى الطلب الوارد، مثل صفحة التواصل، عند الطلب. ننشر النوعين باستخدام محوّل Cloudflare: ملفات جاهزة للقراءة، وشيفرة خادم لمعالجة التفاعل الذي يحتاج إليها.
عند نشر مشروع يستخدم إطار عمل، حدد مخرجات البناء بوصفها الملفات المنشورة. ملف .astro أو .tsx الذي تعدّله ليس صفحة جاهزة للمتصفح. اتبع تعليمات المحوّل؛ فقد يتولى هو أو إضافة Cloudflare لـ Vite توليد ملف دخول Worker وإعدادات الملفات. لا تستبدل الإعدادات المولّدة بقيم main وassets.directory من مثالنا اليدوي.
ماذا ينتج عن البناء؟
مشروع واحد ينتج ملفات وشيفرة خادم
إذا جمع مشروع Astro بين صفحات مولّدة مسبقا ومسارات تعمل عند الطلب، فسيحتاج إلى النوعين. ينزّل المتصفح الملفات العامة فقط.
- الملفات العامةيطلبها المتصفح وينزّلهاHTML وCSS وJavaScript للمتصفحخدمة الملفات الثابتة
تتحول المقالات المولّدة مسبقا وموارد الواجهة إلى ملفات متاحة للزوار. كل ما تضعه في JavaScript المرسلة للمتصفح يمكن للزائر قراءته.
- حزمة الخادميشغّلها Workerمسارات عند الطلب وشيفرة خادمبيئة تشغيل Worker
يعالج Worker طلبات هذه المسارات ويعيد استجاباتها. لا تُقدّم الحزمة كملف قابل للتنزيل؛ وتقرأ الشيفرة القيم السرية من إعدادات Worker عبر env.
انشر المخرجات التي يجهزها المحوّل. اختيار جذر المستودع كمجلد للملفات المنشورة قد يكشف الشيفرة المصدرية بدلا من تقديم الموقع المبني.
نفّذ البناء وافحص الناتج: أي الصفحات أصبحت ملفات HTML؟ وأي المسارات تحتاج إلى Worker؟ وما ملفات JavaScript التي سينزّلها الزائر؟
تشخيص مشكلات النشر
ابدأ بالمرحلة التي فشلت. إذا تعذر تثبيت الحزم، فلم يصل المشروع إلى البناء أصلا. وإذا نجح البناء لكن النموذج لا يصل إلى API، فافحص استجابة HTTP الفعلية.
| المشكلة | ما تفحصه أولا |
|---|---|
| الصفحة تفتح لكن زر الإرسال يبقى معطلا | هل حُمّل /app.js؟ وهل تظهر أخطاء في وحدة تحكم المتصفح؟ |
| النموذج يتلقى HTML بدلا من JSON | عنوان الطلب، وإعداد الصفحة البديلة، وقواعد run_worker_first |
فتح العنوان في المتصفح يختلف عن طلبه بـ curl | الترويسة Sec-Fetch-Mode وقواعد التوجيه |
| HTML جديد يحمّل JavaScript قديما | ترويسات التخزين المؤقت، وهل تتغير أسماء الملفات بتغير محتواها؟ |
| API تعمل محليا وتفشل في المعاينة | متغيرات المعاينة وقيمها السرية والموارد المرتبطة بها |
| البناء يتوقف قبل تجميع الشيفرة | سجلات تثبيت الحزم، وملف القفل، وذاكرة البناء المؤقتة |
حدثت المشكلة الأخيرة عند نشر المقال السابق على هذا الموقع. أظهرت سجلات Cloudflare ملفا مؤقتا تالفا لحزمة esbuild، ثم الخطأ ETXTBSY أثناء التثبيت. كان الفشل قبل وصول Astro إلى تجميع المقال. في مثل هذه الحالة، تعطيك السجلات سببا لـ مسح ذاكرة البناء المؤقتة وإعادة المحاولة على نسخة الشيفرة نفسها، قبل تغيير شيفرة التطبيق.
إذا تسبب تعديل منشور في تعطيل التطبيق، فاتبع خطوات العودة إلى إصدار سابق، ثم اختبر الصفحة والواجهة البرمجية معا. العودة إلى إصدار سابق لا تمسح نسخ المتصفحات المحفوظة، ولا تتراجع عن البيانات المكتوبة في قاعدة البيانات. لذلك يهم اختيار أسماء الملفات وسياسة تخزينها قبل منحها صلاحية طويلة.
كم تكلف طلبات الموقع؟
وقت كتابة المقال، تدرج Cloudflare طلبات الملفات الثابتة المباشرة ضمن الاستخدام المجاني غير المحدود، من دون رسوم إضافية لتخزين تلك الملفات. أما الطلبات التي تشغّل شيفرة Worker فتخضع لتسعير Workers. يوضح مرجع التكلفة والحدود الفرق.
في مثالنا، لا يتطلب تقديم HTML وCSS وJavaScript تشغيل دالة التحليل. إرسال النص هو ما يستدعيها. وإذا ضبطت Worker ليعمل قبل كل طلب ملف، فستُحتسب تلك الاستدعاءات أيضا، حتى لو كانت الاستجابة النهائية ملفا جاهزا.
لا يشمل ذلك استخدام خدمة البناء أو تسجيل النطاق أو الخدمات التي تضيفها لاحقا. عند تقدير التكلفة، افصل طلبات الملفات عن استدعاءات شيفرة Worker.
جرّب نشر تعديل صغير
غيّر عنوان الصفحة في فرع مستقل، ثم ارفع التعديل. تأكد من ظهوره في معاينة الفرع، ومن بقاء صفحة الإنتاج كما هي. بعد المراجعة والدمج، افتح النطاق الفعلي وأرسل النموذج من جديد. بهذه التجربة تختبر خطوات النشر التي أعددتها كاملة.
يمكنك استخدام البنية نفسها لنموذج ملاحظات أو لوحة تحكم صغيرة: ملفات عامة للواجهة، ودوال في Worker للعمليات التي تحتاج إلى الخادم، ومعاينة تختبر فيها عملهما معا.
قد تكون الخطوة التالية حفظ المسودة والعودة إليها لاحقا. وهذا يفتح موضوع التخزين الدائم: كيف نحفظ البيانات ونستعلم عنها، وكيف نمنع تجارب المعاينة من تعديل بيانات الإنتاج؟ سنعود إلى هذا التطبيق عند الحديث عن D1.
إذا كنت تعرف من يستعد لنشر موقعه الأول على Cloudflare، شاركه المقال. وأي جزء أتعبك أكثر في تجاربك: التوجيه، أم المعاينة، أم التخزين المؤقت؟ اكتب لي عما واجهته، أو شارك تجربتك مع رابط المقال على X أو LinkedIn.