---
title: "استضافة موقع على Cloudflare Workers: من التجربة المحلية إلى نطاقك الخاص"
description: "أضف صفحة إلى واجهتك البرمجية المبنية بـ Hono، وافهم كيف تقدّم Workers الملفات وتشغّل شيفرة الخادم، ثم انشر الموقع عبر Git واختبره واربطه بنطاقك."
author: "Omar Albeik"
date: 2026-11-06
type: tutorial
topics: [infrastructure]
language: ar
reading_time_minutes: 23
canonical_url: https://omaralbeik.com/ar/blog/series/cloudflare/hosting-a-website-on-cloudflare-workers
translation_url: https://omaralbeik.com/en/blog/series/cloudflare/hosting-a-website-on-cloudflare-workers
source_url: https://omaralbeik.com/ar/blog/series/cloudflare/hosting-a-website-on-cloudflare-workers.md
---

# استضافة موقع على Cloudflare Workers: من التجربة المحلية إلى نطاقك الخاص

بنينا في الجزء الثالث واجهة برمجية تقدّر مدة قراءة النصوص، واختبرناها من الطرفية. سنضيف هنا صفحة يفتحها القارئ ويلصق فيها نصه، فتظهر له النتيجة.

يمكن استضافة الصفحة والواجهة البرمجية معا على Cloudflare Workers. يحمّل المتصفح ملفات HTML وCSS وJavaScript، وعندما يرسل القارئ النموذج، يستقبل Hono الطلب ويحسب النتيجة. كل ذلك على اسم المضيف نفسه، من دون فصل الواجهة في خدمة استضافة أخرى.

سنختبرها محليا، ونراجع نسخة منشورة من فرع مستقل، ثم نتيحها على نطاقك. نريد أن نعرف ما يحدث في كل طلب: أي ملف أو دالة أعادت الاستجابة؟ وما الإعدادات المستخدمة؟ وهل يعرض المتصفح آخر نسخة نشرناها؟

للمتابعة، افتح مشروع `reading-api` من [الجزء الثالث](/ar/blog/series/cloudflare/understanding-cloudflare-workers). يفترض أن يكون لديك الملفان `src/index.ts` و`src/analyze.ts`، مع تثبيت مكتبة Hono وأداة Wrangler. سنحتفظ بشيفرة الحساب والتحقق من المدخلات، ونضيف إليها ملفات HTML وJavaScript مباشرة. بهذه الطريقة يسهل تتبع الطلبات قبل إدخال إطار عمل في المشروع.

- [ما الذي يطلبه المتصفح؟](#ما-الذي-يطلبه-المتصفح)
- [إضافة صفحة للأداة](#إضافة-صفحة-للأداة)
- [التعامل مع الصفحات غير الموجودة](#التعامل-مع-الصفحات-غير-الموجودة)
- [النشر وربط المستودع](#نشر-الموقع-وربطه-بـ-git)
- [ربط نطاقك بالموقع](#ربط-نطاقك-بالموقع)
- [تحديث الملفات واستخدام أطر العمل](#التخزين-المؤقت-عند-تحديث-الملفات)

يمكن حساب مدة القراءة بالكامل داخل المتصفح، بحيث لا يغادر النص جهاز القارئ. نستخدم الواجهة البرمجية هنا لنتعلم كيف نربط صفحة بتطبيق Worker؛ وستصبح الحاجة إلى الخادم أوضح عندما نضيف وظائف مثل حفظ المسودات.

## ما الذي يطلبه المتصفح؟

عند فتح الأداة، يطلب المتصفح صفحة HTML أولا. يجد فيها رابطا لملف CSS وآخر لملف JavaScript، فيطلبهما أيضا. وعند إرسال النموذج، يرسل طلبا جديدا إلى `/api/reading-time`.

يمكن تجهيز الملفات الثلاثة قبل وصول أي زائر. أما نتيجة الحساب فتتوقف على النص المرسل في الطلب.

تتيح [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/) نشر الملفات مع تطبيق Worker نفسه. نحدد مجلدا للملفات المتاحة للزوار، ونحدد ملف الدخول الذي يحتوي على شيفرة الخادم. تتولى المنصة تقديم الملفات، فلا نحتاج إلى خادم منفصل يقرأها من القرص ويرسلها.

وصف الصفحة بأنها «ثابتة» لا يعني أنها تخلو من التفاعل. يمكن لصفحة HTML جاهزة أن تشغّل JavaScript، وتطلب بيانات من API، وتحدّث ما تعرضه. المقصود هو أن HTML جاهز مسبقا، ولا نولّده من جديد لكل طلب.

**ثلاث طرق لعرض المحتوى**

يمكن للموقع الواحد الجمع بين هذه الطرق بحسب حاجة كل صفحة.

1. **قبل الزيارة — HTML مولّد مسبقا**: يتحول المقال أثناء البناء إلى ملف HTML جاهز لتقديمه عند وصول الطلب.
2. **عند وصول الطلب — توليد HTML على الخادم**: يولّد Worker الصفحة لهذا الطلب، وقد يستخدم بيانات الجلسة أو معلومات محدثة.
3. **بعد تحميل الصفحة — تفاعل داخل المتصفح**: تحدّث JavaScript ما تعرضه الصفحة. في أداتنا، تطلب النتيجة من API ثم تضيفها إلى الصفحة.

يمكن إضافة التفاعل سواء كان HTML جاهزا مسبقا أو مولّدا عند الطلب.

*اختر وقت توليد HTML بحسب البيانات التي تحتاجها الصفحة. وجود أزرار وتفاعل لا يفرض توليدها على الخادم عند كل زيارة.*

في أداتنا، يرى جميع الزوار النموذج نفسه. لذلك سنقدمه كملف جاهز، ونشغّل دالة الحساب عند إرسال النص فقط.

## وماذا عن Cloudflare Pages؟

توصي Cloudflare حاليا [باستخدام Workers للمشاريع الجديدة](https://developers.cloudflare.com/pages/). فهي تدعم الملفات الثابتة وشيفرة الخادم والبناء من Git ومعاينة الفروع. أما Pages فما زالت لها آلية النشر الخاصة بها وPages Functions؛ لذا ستصادفها في المشاريع القائمة والشروحات الأقدم.

أما إذا كان موقعك يعمل على Pages، فراجع ما سيترتب على نقله أولا: محوّل إطار العمل (adapter)، ومسارات Functions، وقواعد إعادة التوجيه، وطريقة المعاينة. يوضح [دليل الانتقال من Pages إلى Workers](https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/) الفروق التي تحتاج إلى مراعاتها.

## إضافة صفحة للأداة

إذا كانت لديك واجهة جاهزة، فاستخدم ملفاتها بدلا من النموذج أدناه، وتابع خطوات التوجيه والنشر. وإذا كان إطار العمل يولّد حزمة Worker، فاستخدم إعدادات محوّله كما سنوضح في [قسم Astro](#ماذا-يتغير-عند-استخدام-astro).

أنشئ مجلد `public` داخل `reading-api`. بعد إضافة ملفات هذا الجزء، سيصبح ترتيب المشروع كالتالي:

```text
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` وبقية إعدادات الجزء الثالث:

```jsonc title="wrangler.jsonc · إضافة إلى الإعدادات الحالية"
{
  "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`:

```bash
npx wrangler types --env-interface CloudflareBindings
```

في الوضع الافتراضي، تستطيع المنصة إعادة الملف المطابق للمسار من دون تشغيل شيفرتنا. أما [المسارات التي تُعالج في Worker أولا](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/)، فتضمن وصول طلبات API إلى Hono حتى لو وُجد ملف يطابق عنوان الطلب.

**اسم مضيف واحد، وطريقتان للاستجابة**

اختر طلبا لترى كيف تعالجه إعدادات المقال. هذا شرح توضيحي؛ لا يرسل طلبات فعلية.

1. **فتح الصفحة الرئيسية — GET /**: تعيد خدمة الملفات public/index.html مباشرة، من دون تشغيل معالج Hono.
   المسار: المتصفح ← خدمة الملفات ← HTML بالحالة 200
2. **تحميل JavaScript — GET /app.js**: تعيد خدمة الملفات public/app.js، ثم يشغّله المتصفح.
   المسار: المتصفح ← خدمة الملفات ← JavaScript بالحالة 200
3. **حساب مدة القراءة — POST /api/reading-time**: يتحقق Hono من المدخلات ويستدعي analyzeReading(). إذا كانت صالحة، يعيد النتيجة لتعرضها الصفحة.
   المسار: المتصفح ← Worker · Hono ← JSON بالحالة 200
4. **فتح مسار API كصفحة — GET /api/reading-time**: تتضمن الاستجابة Allow: POST. تضمن قاعدة /api/* وصول الطلب إلى Hono بدلا من إرجاع صفحة HTML بديلة.
   المسار: المتصفح ← Worker · Hono ← JSON بالحالة 405
5. **فتح رابط غير موجود — GET /missing**: تعيد خدمة الملفات public/404.html بالحالة 404. يبقى العنوان المطلوب في شريط المتصفح.
   المسار: انتقال من المتصفح ← خدمة الملفات ← HTML بالحالة 404

تمر مسارات API وفحص الحالة عبر Worker أولا. وقد يصل طلب ملف غير موجود إلى Hono أيضا إن لم يكن طلب انتقال؛ عندها يحيله إلى ASSETS.

*تذهب طلبات الصفحة والنموذج إلى اسم المضيف نفسه، لكن طلب الحساب وحده يحتاج إلى دالة تحليل النص.*

### كتابة الصفحة

نحتاج إلى حقل للنص له تسمية واضحة، وقائمة لاختيار اللغة، وحقل لسرعة القراءة، ومساحة لعرض النتيجة وإعلانها لقارئ الشاشة. تجمع الصفحة المدخلات وترسلها، ثم تعرض ما تعيده الواجهة البرمجية.

```html title="public/index.html"
<!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 بعد تجهيز دالة الإرسال. إذا تعذر تحميل الشيفرة، لن يُرسل النموذج النص بالخطأ بالطريقة الافتراضية التي تنقل المتصفح إلى عنوان جديد.

### تنسيق النموذج

يكفي هنا خط مقروء، وحقول تناسب شاشة الهاتف، ومؤشر واضح للعنصر المحدد عند التنقل بلوحة المفاتيح. لا نحتاج إلى مكتبة لتنسيق هذه الصفحة.

```css title="public/styles.css"
: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` حتى يذهب الطلب إلى أصل الصفحة نفسه، أي البروتوكول واسم المضيف والمنفذ ذاتها. سيعمل ذلك محليا، وعلى رابط المعاينة، وعلى نطاقك، من دون تعديل العنوان في الشيفرة.

```js title="public/app.js"
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`:

```text title="public/_headers"
/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
```

تطبق هذه القواعد عند تقديم الملفات. لا تضيف الترويسات تلقائيا إلى JSON الذي يولّده Hono؛ اضبط ترويسات تلك الاستجابات داخل شيفرة الخادم عند الحاجة. كذلك، لن تفحص دالة وسيطة في Hono طلب الصفحة الرئيسية إذا أجابته خدمة الملفات مباشرة. إذا كانت الملفات خاصة، وجّه طلباتها عبر التحقق من الهوية قبل إعادتها للزائر.

## التعامل مع الصفحات غير الموجودة

إذا فتح الزائر رابطا غير موجود، نريد أن نعرض له صفحة تساعده على العودة إلى الأداة. أما طلب مسار API غير موجود، فيجب أن يتلقى خطأ بصيغة JSON. سنفصل الحالتين في معالج المسارات غير المعروفة.

في `src/index.ts`، استبدل كتلة `app.notFound(...)` الحالية بما يلي:

```ts title="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.

أضف صفحة الخطأ:

```html title="public/404.html"
<!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`. يشرح [دليل توجيه المواقع الثابتة](https://developers.cloudflare.com/workers/static-assets/routing/static-site-generation/) هذا السلوك.

لا تفعّل `single-page-application` لمجرد أن الأداة تتكون من صفحة واحدة. يعيد هذا الخيار `index.html` باستجابة نجاح عند غياب الملف، لتتولى شيفرة التوجيه في المتصفح اختيار المحتوى. ليس لدينا مثل هذه الشيفرة، ولذلك يجب أن يظل العنوان غير الموجود خطأ 404.

## التجربة المحلية وتتبع الطلبات

نفّذ من مجلد المشروع:

```bash
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`، حتى عند فتحه كصفحة |
| قطع الاتصال ثم الإرسال | رسالة توضح تعذر الاتصال، ثم إتاحة زر الإرسال من جديد |

لإرسال طلب يحمل ترويسة الانتقال من الطرفية، ثم تجربة طلب حساب عادي:

```bash
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

قبل النشر الأول، جرّب تجهيز الحزمة:

```bash
npx wrangler deploy --dry-run
```

يجهز الأمر ملفات النشر من دون نشرها فعليا. عندما تريد إتاحة المثال على الإنترنت، سجّل الدخول ثم انشره:

```bash
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](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/) بين أمر البناء وأمر النشر.

**اختبر التطبيق في كل مرحلة**

تختلف الإعدادات والظروف بين التجربة المحلية والمعاينة والإنتاج.

1. **١ · محليا — الصفحة والواجهة البرمجية**: شغّل Wrangler. جرّب مدخلات صالحة وأخرى غير صالحة، وروابط مفقودة، ورسائل الخطأ في المتصفح.
2. **٢ · المعاينة — إعدادات النسخة المنشورة**: انشر الفرع، وافحص متغيراته والموارد المرتبطة به، ثم شارك الرابط للمراجعة.
3. **٣ · الإنتاج — ما يراه القارئ فعليا**: ادمج التعديل في فرع الإنتاج. بعد النشر، افتح النطاق الحقيقي وأرسل النموذج.

اختبر التفاعل كاملا على الرابط المنشور، بما في ذلك حالات الخطأ.

*راجع الفرع على رابط معاينة قبل دمجه، ثم اختبر النطاق الفعلي بعد النشر. لكل مرحلة إعدادات ومشكلات لا تكشفها المرحلة السابقة.*

### معاينة الفرع تعمل على Cloudflare

تتيح معاينة الفرع مراجعة التعديل على رابط منشور قبل وصوله إلى فرع الإنتاج. ويمكنك مشاركة الرابط مع من يراجع عملك. لهذه النسخة إعداداتها الخاصة التي لا تختبرها التجربة المحلية وحدها.

حتى تاريخ كتابة المقال، 6 تشرين الثاني 2026، تستخدم [Worker Previews](https://developers.cloudflare.com/workers/previews/) الأمر `npx wrangler preview`، وتتطلب Wrangler بالإصدار 4.135.0 أو أحدث. حدّث الحزمة المثبتة في المشروع عند الحاجة؛ تحديث نسخة Wrangler العامة على جهازك لا يغيّر النسخة التي يستخدمها المشروع.

تستخدم التطبيقات الجديدة المرتبطة بـ Builds آلية المعاينة الجديدة افتراضيا. وقد تبقى التطبيقات الأقدم على الآلية السابقة إلى أن تنقلها صراحة. فعّل بناء المعاينات من [إعدادات الفروع](https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/)، وافحص أمر المعاينة المسجل فيها؛ فقد يظل التطبيق القديم يستخدم الأمر السابق.

لإعداد الآلية الجديدة، أضف `previews` إلى المستوى الأعلى من `wrangler.jsonc`:

```jsonc title="wrangler.jsonc · إضافة إلى الإعدادات الحالية"
{
  "previews": {
    "vars": {
      "SERVICE_NAME": "reading-api-preview",
      "DEFAULT_WPM": 200
    }
  }
}
```

أبقِ `assets` في المستوى الأعلى أيضا؛ يرفع Wrangler ملفات الفرع عند تشغيل `npx wrangler preview`. يوضح [دليل إعداد المعاينات](https://developers.cloudflare.com/workers/previews/configuration/) موضع كل إعداد. متغيرات المستوى الأعلى لا تنتقل تلقائيا إلى المعاينة، ولذلك عرّفنا قيمها هنا.

بعد نشر المعاينة، افتح `/health` وتأكد من أن اسم الخدمة هو `reading-api-preview`، ثم جرّب النموذج. عندما نضيف التخزين، سنحدد قاعدة بيانات المعاينة أيضا؛ اختلاف الرابط لا يعني أن البيانات منفصلة.

إذا كان محتوى المعاينة خاصا، احمِ الوصول إليها. يستطيع أي شخص استخدام الرابط غير المحمي إذا وصل إليه.

## ربط نطاقك بالموقع

يمكن استخدام التطبيق عبر رابط `workers.dev`. ولإتاحته باسم تختاره، اربطه بنطاقك.

إذا كان النطاق مفعّلا على Cloudflare، أضف Custom Domain إلى الإعدادات. استخدم اسم مضيف غير مستخدم ضمن نطاق تملكه:

```jsonc title="wrangler.jsonc · إضافة إلى الإعدادات الحالية"
{
  "routes": [
    { "pattern": "reading.example.com", "custom_domain": true }
  ]
}
```

انشر من جديد. تنشئ Cloudflare سجل DNS وشهادة TLS الخاصة بـ [Custom Domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/). قد يمنع سجل موجود ومتعارض إتمام الإعداد؛ يوضح [مقال DNS](/ar/blog/series/cloudflare/understanding-cloudflare-dns) طريقة فحص السجلات.

لن نغيّر عنوان API في الصفحة. يظل الطلب النسبي موجها إلى `/api/reading-time`، لكنه سيصل الآن إلى `reading.example.com`. وعند فتح معاينة فرع، سيصل إلى اسم مضيف تلك المعاينة.

اختر عنوانا أساسيا واحدا للموقع العام. إذا أتحته على النطاق مباشرة وعلى `www`، فأعد توجيه العنوان الآخر إلى الأساسي مع الاحتفاظ بالمسار. أضف عنوانا ووصفا مناسبين لكل صفحة محتوى، واجعل روابط `canonical` تشير إلى النطاق العام، وأدرج المحتوى المنشور في خريطة الموقع. اختبر إعادة التوجيه على صفحة داخلية أيضا، لا على الرئيسية فقط.

## التخزين المؤقت عند تحديث الملفات

تتولى خدمة الملفات في Cloudflare تقديم ما رفعناه. في استجابات الملفات المعتادة، تكون ترويسة التخزين المؤقت للمتصفح افتراضيا `Cache-Control: public, max-age=0, must-revalidate`، وترافقها `ETag` للتحقق مما إذا كان الملف قد تغير. يمكن للمتصفح الاحتفاظ بنسخة، لكنه يتحقق من صلاحيتها قبل إعادة استخدامها. التفاصيل في [مرجع ترويسات الملفات](https://developers.cloudflare.com/workers/static-assets/headers/).

هذا مناسب لأسماء ملفاتنا الثابتة. إذا عدّلنا `app.js` فسيظل عنوانه `/app.js`. ولو سمحنا للمتصفح باستخدام نسخته مدة سنة من دون تحقق، فقد لا يصل الإصلاح الجديد إلى من زار الموقع سابقا.

قد تولّد أداة البناء لاحقا اسما مثل `app.a1b2c3.js`، يتغير الجزء الأخير منه بتغير المحتوى. عندها يمكن الاحتفاظ بالملف مدة طويلة مع `immutable`؛ فصفحة HTML الجديدة ستطلب اسما جديدا. طبّق هذه السياسة على الملفات ذات الأسماء المرتبطة بمحتواها، لا على HTML أو جميع مسارات الموقع.

**هل سيحمّل المتصفح الشيفرة الجديدة؟**

لنفترض أن المتصفح يحتفظ بالإصدار الأول، وأن التخزين المؤقت مفعّل، ثم فتحت الصفحة مجددا فطلبت الملف. قارن بين ثلاث سياسات.

1. **اسم ثابت مع التحقق قبل الاستخدام — /app.js · max-age=0, must-revalidate**: يبقى العنوان نفسه. لأن الملف تغير، يعيد الخادم المحتوى الجديد. أما إذا لم يتغير، فتتيح استجابة 304 للمتصفح استخدام نسخته المحفوظة.
   المسار: الإصدار الأول محفوظ ← التحقق باستخدام ETag ← تنزيل الإصدار الثاني
2. **اسم ثابت مع صلاحية سنة — /app.js · max-age=31536000, immutable**: قد يستخدم المتصفح الملف القديم من دون سؤال الخادم. رفع محتوى جديد إلى العنوان نفسه لا يلغي صلاحية النسخة المحفوظة لديه.
   المسار: الإصدار الأول محفوظ ← النسخة ما زالت صالحة ← استخدام الإصدار الأول
3. **اسم يتغير بتغير المحتوى مع صلاحية سنة — /app.b2.js · max-age=31536000, immutable**: العنوان الجديد غير موجود في ذاكرة هذا المتصفح. يتغير اسم الحزمة بتغير محتواها، ويجب تحديث HTML ليشير إلى الاسم الجديد.
   المسار: الملف app.a1.js محفوظ ← HTML الجديد يطلب app.b2.js ← تنزيل الإصدار الثاني

في مثالنا، أبقِ التحقق قبل إعادة الاستخدام لأن أسماء الملفات ثابتة. خصص التخزين الطويل مع immutable للملفات التي تتغير عناوينها بتغير محتواها.

*ماذا يحصل في الزيارة التالية بعد تعديل JavaScript ونشره؟ المقارنة هنا تخص النسخة المحفوظة في متصفح القارئ، لا ذاكرة Cloudflare المؤقتة.*

نشر إصدار جديد لا يمسح ذاكرة المتصفح المؤقتة، والصفحة المفتوحة تواصل تشغيل الشيفرة التي حمّلتها. يفيد التحقق من صلاحية الملف عند التحميل التالي، ويفيد الاسم الجديد عندما تصل إلى المتصفح صفحة HTML تشير إليه. يشرح [دليل التخزين المؤقت من MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching) الآليتين.

تحتفظ استجابات API بالترويسة `Cache-Control: no-store` التي أضفناها في الجزء الثالث. تعتمد النتيجة على النص المرسل، ولن نضيف في هذا المثال تخزينا مؤقتا لها.

## ماذا يتغير عند استخدام Astro؟

لا تحتاج صفحة النموذج إلى كثير من القوالب. أما المدونة، ففيها تخطيطات مشتركة وقوائم تنقل وبيانات للمقالات ومجموعة من ملفات المحتوى. يساعد إطار العمل على إدارة هذا التكرار.

هذا الموقع مبني باستخدام Astro. تُولّد المقالات مسبقا كملفات HTML، بينما تُولّد الصفحات التي تحتاج إلى الطلب الوارد، مثل صفحة التواصل، عند الطلب. ننشر النوعين باستخدام محوّل Cloudflare: ملفات جاهزة للقراءة، وشيفرة خادم لمعالجة التفاعل الذي يحتاج إليها.

عند نشر مشروع يستخدم إطار عمل، حدد **مخرجات البناء** بوصفها الملفات المنشورة. ملف `.astro` أو `.tsx` الذي تعدّله ليس صفحة جاهزة للمتصفح. اتبع [تعليمات المحوّل](https://docs.astro.build/en/guides/integrations-guide/cloudflare/)؛ فقد يتولى هو أو إضافة Cloudflare لـ Vite توليد ملف دخول Worker وإعدادات الملفات. لا تستبدل الإعدادات المولّدة بقيم `main` و`assets.directory` من مثالنا اليدوي.

**مشروع واحد ينتج ملفات وشيفرة خادم**

إذا جمع مشروع Astro بين صفحات مولّدة مسبقا ومسارات تعمل عند الطلب، فسيحتاج إلى النوعين. ينزّل المتصفح الملفات العامة فقط.

صفحات ومكونات وأنماط ← Astro مع محوّل Cloudflare

1. **الملفات العامة — يطلبها المتصفح وينزّلها**: تتحول المقالات المولّدة مسبقا وموارد الواجهة إلى ملفات متاحة للزوار. كل ما تضعه في JavaScript المرسلة للمتصفح يمكن للزائر قراءته.
   المسار: HTML وCSS وJavaScript للمتصفح ← خدمة الملفات الثابتة
2. **حزمة الخادم — يشغّلها Worker**: يعالج Worker طلبات هذه المسارات ويعيد استجاباتها. لا تُقدّم الحزمة كملف قابل للتنزيل؛ وتقرأ الشيفرة القيم السرية من إعدادات Worker عبر env.
   المسار: مسارات عند الطلب وشيفرة خادم ← بيئة تشغيل Worker

انشر المخرجات التي يجهزها المحوّل. اختيار جذر المستودع كمجلد للملفات المنشورة قد يكشف الشيفرة المصدرية بدلا من تقديم الموقع المبني.

*قد ينتج البناء ملفات ينزّلها المتصفح وحزمة يشغّلها Worker. يحدد المحوّل ترتيب المجلدات الفعلي؛ الأسماء في الشكل تصف وظيفة المخرجات.*

نفّذ البناء وافحص الناتج: أي الصفحات أصبحت ملفات HTML؟ وأي المسارات تحتاج إلى Worker؟ وما ملفات JavaScript التي سينزّلها الزائر؟

## تشخيص مشكلات النشر

ابدأ بالمرحلة التي فشلت. إذا تعذر تثبيت الحزم، فلم يصل المشروع إلى البناء أصلا. وإذا نجح البناء لكن النموذج لا يصل إلى API، فافحص استجابة HTTP الفعلية.

| المشكلة | ما تفحصه أولا |
| --- | --- |
| الصفحة تفتح لكن زر الإرسال يبقى معطلا | هل حُمّل `/app.js`؟ وهل تظهر أخطاء في وحدة تحكم المتصفح؟ |
| النموذج يتلقى HTML بدلا من JSON | عنوان الطلب، وإعداد الصفحة البديلة، وقواعد `run_worker_first` |
| فتح العنوان في المتصفح يختلف عن طلبه بـ `curl` | الترويسة `Sec-Fetch-Mode` وقواعد التوجيه |
| HTML جديد يحمّل JavaScript قديما | ترويسات التخزين المؤقت، وهل تتغير أسماء الملفات بتغير محتواها؟ |
| API تعمل محليا وتفشل في المعاينة | متغيرات المعاينة وقيمها السرية والموارد المرتبطة بها |
| البناء يتوقف قبل تجميع الشيفرة | سجلات تثبيت الحزم، وملف القفل، وذاكرة البناء المؤقتة |

حدثت المشكلة الأخيرة عند نشر المقال السابق على هذا الموقع. أظهرت سجلات Cloudflare ملفا مؤقتا تالفا لحزمة esbuild، ثم الخطأ `ETXTBSY` أثناء التثبيت. كان الفشل قبل وصول Astro إلى تجميع المقال. في مثل هذه الحالة، تعطيك السجلات سببا لـ [مسح ذاكرة البناء المؤقتة](https://developers.cloudflare.com/workers/ci-cd/builds/build-caching/) وإعادة المحاولة على نسخة الشيفرة نفسها، قبل تغيير شيفرة التطبيق.

إذا تسبب تعديل منشور في تعطيل التطبيق، فاتبع [خطوات العودة إلى إصدار سابق](/ar/blog/series/cloudflare/understanding-cloudflare-workers#العودة-إلى-إصدار-سابق)، ثم اختبر الصفحة والواجهة البرمجية معا. العودة إلى إصدار سابق لا تمسح نسخ المتصفحات المحفوظة، ولا تتراجع عن البيانات المكتوبة في قاعدة البيانات. لذلك يهم اختيار أسماء الملفات وسياسة تخزينها قبل منحها صلاحية طويلة.

## كم تكلف طلبات الموقع؟

وقت كتابة المقال، تدرج Cloudflare طلبات الملفات الثابتة المباشرة ضمن الاستخدام المجاني غير المحدود، من دون رسوم إضافية لتخزين تلك الملفات. أما الطلبات التي تشغّل شيفرة Worker فتخضع لتسعير Workers. يوضح [مرجع التكلفة والحدود](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/) الفرق.

في مثالنا، لا يتطلب تقديم HTML وCSS وJavaScript تشغيل دالة التحليل. إرسال النص هو ما يستدعيها. وإذا ضبطت Worker ليعمل قبل كل طلب ملف، فستُحتسب تلك الاستدعاءات أيضا، حتى لو كانت الاستجابة النهائية ملفا جاهزا.

لا يشمل ذلك استخدام خدمة البناء أو تسجيل النطاق أو الخدمات التي تضيفها لاحقا. عند تقدير التكلفة، افصل طلبات الملفات عن استدعاءات شيفرة Worker.

## جرّب نشر تعديل صغير

غيّر عنوان الصفحة في فرع مستقل، ثم ارفع التعديل. تأكد من ظهوره في معاينة الفرع، ومن بقاء صفحة الإنتاج كما هي. بعد المراجعة والدمج، افتح النطاق الفعلي وأرسل النموذج من جديد. بهذه التجربة تختبر خطوات النشر التي أعددتها كاملة.

يمكنك استخدام البنية نفسها لنموذج ملاحظات أو لوحة تحكم صغيرة: ملفات عامة للواجهة، ودوال في Worker للعمليات التي تحتاج إلى الخادم، ومعاينة تختبر فيها عملهما معا.

قد تكون الخطوة التالية حفظ المسودة والعودة إليها لاحقا. وهذا يفتح موضوع التخزين الدائم: كيف نحفظ البيانات ونستعلم عنها، وكيف نمنع تجارب المعاينة من تعديل بيانات الإنتاج؟ سنعود إلى هذا التطبيق عند الحديث عن D1.

إذا كنت تعرف من يستعد لنشر موقعه الأول على Cloudflare، شاركه المقال. وأي جزء أتعبك أكثر في تجاربك: التوجيه، أم المعاينة، أم التخزين المؤقت؟ [اكتب لي عما واجهته](/ar/contact)، أو شارك تجربتك مع رابط المقال على X أو LinkedIn.
