---
title: 'Cloudflare Workers + R2 Image CDN: دليل تعليمي'
description: 'دليل عملي لعام 2026: بناء CDN للصور جاهزة للإنتاج باستخدام Cloudflare Workers + R2 — ربط IMAGES، و edge caching، وروابط موقعة (signed URLs)، والتفاوض على تنسيقات AVIF/WebP.'
url: https://nerdleveltech.com/ar/cloudflare-workers-r2-image-cdn-tutorial
language: ar
date: '2026-05-08'
category: cloud-devops
tags:
  - cloudflare
  - cloudflare-workers
  - r2
  - image-cdn
  - image-resizing
  - edge-computing
  - typescript
  - cdn
  - performance
markdown_url: https://nerdleveltech.com/ar/cloudflare-workers-r2-image-cdn-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/cloudflare-workers-r2-image-cdn-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# Cloudflare Workers + R2 Image CDN: دليل تعليمي

لتقديم صور مُعاد تحجيمها من R2 باستخدام Cloudflare Worker، قم بتخزين النسخ الأصلية في حاوية R2 (bucket) خاصة، واقرأها من خلال ربط الحاوية (bucket binding)، ثم قم بتمرير البيانات (bytes) عبر `env.IMAGES.input(...).transform(...).output(...)` لإعادة التحجيم الفوري. قم بتخزين كل نسخة في ذاكرة التخزين المؤقت (Cache) عند الحافة (edge) باستخدام Workers Cache API ليصبح الـ Worker هو شبكة توصيل المحتوى (CDN) الخاصة بصورك — بدون Sharp، وبدون Lambda، وبدون مصدر إضافي.

## ملخص

- الصور الأصلية موجودة في حاوية R2 خاصة؛ والـ Worker هو المسار العام الوحيد.
- يقوم ربط `IMAGES` الخاص بـ Workers بإعادة التحجيم، وإعادة الترميز، والتفاوض على المحتوى (content-negotiation) لـ AVIF/WebP/JPEG بناءً على رأس `Accept`.
- يقوم الـ Cache API بتخزين كل نسخة لمدة 30 يومًا عند حافة مركز البيانات، لذا فإن الطلب الثاني لا يصل أبدًا إلى R2 أو محرك التحويل.
- توقيع HMAC-SHA-256 على عنوان URL يمنع المهاجمين من إنشاء ملايين النسخ الفريدة واستهلاك ميزانية التحويل الخاصة بك.
- التكلفة الإجمالية على الخطة المجانية لحوالي 5,000 نسخة فريدة شهريًا: **$0**.[^1][^2]

## ما ستتعلمه

- كيفية تكوين ربط حاوية R2 وربط الصور في `wrangler.toml` لـ Wrangler 4
- كيفية تطبيق التحويلات الفورية من معالج جلب (fetch handler) الـ Worker
- كيفية التفاوض على AVIF/WebP/JPEG باستخدام رأس `Accept` و `Vary`
- كيفية تخزين النسخ مؤقتًا عند الحافة باستخدام Workers Cache API
- كيفية توقيع عناوين URL للتحويل باستخدام WebCrypto HMAC-SHA-256 لمنع إساءة استخدام المعاملات
- كيفية إرجاع استجابات `304 Not Modified` الشرطية باستخدام ETag

## المتطلبات الأساسية

| العنصر                 | الإصدار المثبت            | ملاحظات                                                                                                                   |
| ---------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Node.js                | 22.0.0 أو أحدث            | أسقط Wrangler 4 دعم Node 20 بعد انتهاء الدعم (EOL) في 2026-04-30[^3]                                                      |
| Wrangler CLI           | 4.84.1                    | مثبت على إصدار عمره ≥14 يومًا؛ الأحدث هو 4.90.0 ولكن قم بتثبيت ملف القفل (lockfile) على أي إصدار تقوم بـ `npm install` له |
| TypeScript             | 5.5.x                     | مرفق مع `npm create cloudflare@latest`                                                                                    |
| حساب Cloudflare        | مجاني أو أعلى             | تحويلات الصور متاحة في كل خطة، بما في ذلك المجانية، مع 5,000 تحويل فريد شهريًا متضمنة في الحساب[^4]                       |
| نطاق Cloudflare (zone) | مفعل (السحابة البرتقالية) | مطلوب حتى تتمكن لوحة التحكم من تفعيل "Enable for zone" تحت Images > Transformations[^5]                                   |

ستحتاج أيضًا إلى `curl` وبعض صور JPEG كعينات (1024×768 أو أكبر لنتمكن من رؤية إعادة التحجيم تعمل فعليًا).

## الخطوة 1 — إنشاء هيكل الـ Worker

قم بإنشاء المشروع باستخدام C3 (واجهة سطر أوامر `create-cloudflare` الرسمية). اختر TypeScript، وقالب Hello World، وبدون إطار عمل للواجهة الأمامية (front-end)، وتخطى تهيئة Git إذا كان لديك مستودع أب بالفعل:

```bash
npm create cloudflare@latest cf-image-cdn -- \
  --type=hello-world \
  --lang=ts \
  --no-deploy \
  --no-git
cd cf-image-cdn
```

قم بتثبيت إصدار Wrangler صراحةً حتى لا تؤدي إصدارات التصحيح المستقبلية إلى تعطل البناء الخاص بك:

```bash
npm install --save-dev wrangler@4.84.1
```

تحقق:

```bash
npx wrangler --version
# ⛅️ wrangler 4.84.1
node --version
# v22.x.x
```

إذا أظهر `node --version` الإصدار v20 أو أقدم، فقم بالترقية — سيرفض Wrangler 4 البدء.

## الخطوة 2 — إنشاء حاوية R2 ورفع الصور الأصلية

أنشئ حاوية من واجهة سطر الأوامر (الاسم عالمي داخل حسابك، أحرف صغيرة، والشرطات مسموحة):

```bash
npx wrangler r2 bucket create cf-image-cdn-originals
```

ضع بعض الصور التجريبية فيها:

```bash
npx wrangler r2 object put cf-image-cdn-originals/sunset.jpg \
  --file=./samples/sunset.jpg
npx wrangler r2 object put cf-image-cdn-originals/portrait.jpg \
  --file=./samples/portrait.jpg
```

تغطي الطبقة المجانية من R2 مساحة تخزين تبلغ 10 جيجابايت، ومليون عملية من الفئة A (كتابة)، و10 ملايين عملية من الفئة B (قراءة) شهريًا، مع عدم وجود رسوم خروج (egress) عبر جميع الطبقات — لذا فإن هذا البرنامج التعليمي بالكامل يعمل بتكلفة 0 دولار ما لم تتجاوز تلك الحدود.[^1]

## الخطوة 3 — ربط الحاوية في wrangler.toml

استبدل محتويات `wrangler.toml` (أو `wrangler.jsonc` إذا قام C3 بإنشاء JSON — كلاهما يعمل). يستخدم ملف TOML أدناه مخطط `r2_buckets` الحالي الذي تم التحقق منه مقابل وثائق تكوين Wrangler اليوم:[^6]

```toml
name = "cf-image-cdn"
main = "src/index.ts"
compatibility_date = "2026-05-06"

# R2 bucket holding the original images
[[r2_buckets]]
binding = "ORIGINALS"
bucket_name = "cf-image-cdn-originals"

# Cloudflare Images binding (no key required; billed against your account)
[images]
binding = "IMAGES"

# HMAC signing secret for transform URLs.
# Set with: npx wrangler secret put SIGNING_KEY
# (use a 32-byte random value generated with `openssl rand -hex 32`)

[vars]
ALLOWED_WIDTHS = "320,640,960,1280,1920"
DEFAULT_QUALITY = "82"
MAX_AGE_BROWSER = "604800"     # 7 days
MAX_AGE_EDGE    = "2592000"    # 30 days
```

قم بإنشاء أنواع وقت التشغيل (runtime types) حتى يتعرف TypeScript على `env.ORIGINALS`:

```bash
npx wrangler types
```

سيقوم ذلك بكتابة `worker-configuration.d.ts` بناءً على روابطك وتاريخ التوافق — وهو البديل الحديث لإضافة `@cloudflare/workers-types` يدويًا.[^7]

## الخطوة 4 — قراءة الصور من R2

قم ببرمجة معالج جلب بسيط يقوم ببث البيانات الأصلية مرة أخرى. هذا يثبت أن الربط يعمل قبل إضافة التحويلات:

```ts
// src/index.ts
export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const url = new URL(req.url);
    const key = url.pathname.replace(/^\/+/, "");
    if (!key) return new Response("Missing key", { status: 400 });

    const obj = await env.ORIGINALS.get(key);
    if (!obj) return new Response("Not found", { status: 404 });

    const headers = new Headers();
    obj.writeHttpMetadata(headers);
    headers.set("etag", obj.httpEtag);
    return new Response(obj.body, { headers });
  },
} satisfies ExportedHandler<Env>;
```

تقوم `R2ObjectBody.writeHttpMetadata()` بنسخ `Content-Type` و `Content-Encoding` وما شابهها من الكائن المخزن إلى رؤوس الاستجابة الخاصة بك، ويحتوي `obj.httpEtag` بالفعل على علامات الاقتباس المحيطة التي تتطلبها مواصفات HTTP.[^8] قم بتشغيله محليًا:

```bash
npx wrangler dev
# Then in another shell:
curl -sI http://127.0.0.1:8787/sunset.jpg | head -5
```

يجب أن ترى `200`، و `Content-Type` الأصلي، و `etag`. يقوم محاكي R2 المحلي الخاص بـ Wrangler (Miniflare) بتقديم الملفات من `.wrangler/state/` حتى تتمكن من التجربة دون اتصال بالإنترنت قبل النشر.[^9]

## الخطوة 5 — إضافة تحويلات الصور عبر ربط IMAGES

هذا هو جوهر البرنامج التعليمي. يأخذ ربط `IMAGES` الخاص بـ Workers دفقًا من البيانات، ويطبق تحويلات العرض/الارتفاع/التنسيق داخل محرك صور Cloudflare، ويسلمك دفقًا محولًا — لا حاجة لحاوية R2 عامة أو Worker مصدر منفصل:[^10]

```ts
// src/index.ts (replace the previous handler)
type Format = "image/avif" | "image/webp" | "image/jpeg";

function pickFormat(accept: string | null): Format {
  if (accept?.includes("image/avif")) return "image/avif";
  if (accept?.includes("image/webp")) return "image/webp";
  return "image/jpeg";
}

function parseWidth(url: URL, env: Env): number {
  const allowed = env.ALLOWED_WIDTHS.split(",").map(Number);
  const width = Number(url.searchParams.get("w") ?? "");
  if (!allowed.includes(width)) {
    throw new Response(`width must be one of ${env.ALLOWED_WIDTHS}`, { status: 400 });
  }
  return width;
}

export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(req.url);
    const key = url.pathname.replace(/^\/+/, "");
    if (!key) return new Response("Missing key", { status: 400 });

    let width: number;
    try {
      width = parseWidth(url, env);
    } catch (e) {
      if (e instanceof Response) return e;
      throw e;
    }

    const obj = await env.ORIGINALS.get(key);
    if (!obj) return new Response("Not found", { status: 404 });

    const format = pickFormat(req.headers.get("accept"));
    const result = (
      await env.IMAGES
        .input(obj.body)
        .transform({ width, fit: "scale-down", metadata: "none" })
        .output({ format, quality: Number(env.DEFAULT_QUALITY) })
    ).response();

    const headers = new Headers();
    headers.set("content-type", format);
    headers.set("vary", "Accept");                    // per-format browser caches
    headers.set("etag", `W/"${obj.httpEtag.replace(/"/g, ""}-${width}-${format}"`);
    return new Response(result.body, { status: 200, headers });
  },
} satisfies ExportedHandler<Env>;
```

ثلاثة تفاصيل يجب استيعابها:

- **أوضاع `fit`:** `scale-down`، `contain`، `cover`، `crop`، `pad`. يعد `scale-down` هو الافتراضي الأكثر أمانًا — فهو لا يقوم أبدًا بتكبير النسخ الأصلية الأصغر حجمًا.
- **`format`** يجب أن يكون واحدًا من `image/avif`، أو `image/webp`، أو `image/jpeg`، أو `image/png`. على عكس مسار التحويل القائم على URL، لا يحتوي الربط على اختصار `auto` — بل تقوم بالتفاوض على المحتوى في الكود. لهذا السبب نقرأ رأس `Accept` للطلب في `pickFormat()` ونصدر رأس `Vary: Accept` في الاستجابة: بدون `Vary`، ستقوم المتصفحات وذاكرات التخزين المؤقت الوسيطة بتقديم بيانات AVIF لعميل يدعم JPEG فقط.[^10]
- **`metadata: "none"`** يزيل بيانات EXIF و XMP من المخرجات، وهو ما تريده عادةً لشبكة CDN عامة.

ينطبق حد ذاكرة الـ Worker البالغ 128 ميجابايت — من خلال تمرير `obj.body` (وهو `ReadableStream`) مباشرةً إلى `env.IMAGES.input(...)`، فإننا لا نقوم أبدًا بتخزين النسخة الأصلية أو المخرجات في الذاكرة بشكل مؤقت.[^11]

## الخطوة 6 — تخزين متغيرات الصور مؤقتاً على الحافة (Edge)

هل تحويلات صور Cloudflare مجانية؟ **نعم — لأول 5,000 تحويل فريد شهرياً لكل حساب.** بعد ذلك، التكلفة هي 0.50 دولار لكل 1,000 تحويل.[^4] هناك تنبيه هام لـ Workers: كل استدعاء لربط `IMAGES` يُحتسب كتحويل مدفوع — حيث يتم تجاوز ميزة إلغاء التكرار الشهري التي تنطبق على التحويلات المستندة إلى URL عند استخدام الربط البرمجي.[^12] الـ Cache API هو ما يمنع تلك الفاتورة من التصاعد بشكل جنوني.

يعمل الـ Cache API كمخزن قيم-مفاتيح (key-value) مؤقت يعتمد على URL الطلب كمفتاح.[^13] قم بتغليف استدعاء التحويل بفحص للتخزين المؤقت حتى لا نستدعي الربط إلا في حالة عدم وجود النسخة مخزنة (misses):

```ts
// Add inside fetch(), AFTER parsing the width and BEFORE reading R2.
// Build a cache key that includes the negotiated format so AVIF and WebP don't collide.
const fmt = pickFormat(req.headers.get("accept"));
const keyURL = new URL(url.toString());
keyURL.searchParams.set("_fmt", fmt);
const cacheKey = new Request(keyURL.toString(), { method: "GET" });
const cache = caches.default;

const cached = await cache.match(cacheKey);
if (cached) {
  // Short-circuit conditional requests with a 304.
  const inm = req.headers.get("if-none-match");
  if (inm && cached.headers.get("etag") === inm) {
    return new Response(null, { status: 304, headers: cached.headers });
  }
  return cached;
}

// ... read R2 + invoke env.IMAGES as in Step 5 ...

// Build the response with caching headers and tee the stream so we
// can both return it AND store it in the cache.
const response = new Response(result.body, {
  status: 200,
  headers: {
    "content-type": format,
    "etag": `W/"${obj.httpEtag.replace(/"/g, "")}-${width}-${format}"`,
    "vary": "Accept",
    "cache-control":
      `public, max-age=${env.MAX_AGE_BROWSER}, s-maxage=${env.MAX_AGE_EDGE}, immutable`,
  },
});

const [browserStream, cacheStream] = response.body!.tee();
ctx.waitUntil(
  cache.put(cacheKey, new Response(cacheStream, { headers: response.headers })),
);
return new Response(browserStream, { headers: response.headers });
```

أمران يجب استيعابهما:

1. **`caches.default` نطاقه هو مركز البيانات (Data-center scoped).** الطلب الذي يصل إلى مركز بيانات IAD لن يرى متغيراً مخزناً في CDG. هذا متوقع — فمقابل تحويل إضافي واحد لكل مركز بيانات، تحصل على أقل زمن انتقال ممكن للقراءة عند وجود النسخة في التخزين المؤقت.[^13]
2. **استخدم `ctx.waitUntil()` للكتابة في التخزين المؤقت.** يتيح ذلك لـ Worker إرجاع الاستجابة إلى المتصفح دون انتظار اكتمال عملية الكتابة في التخزين المؤقت.[^13]

## الخطوة 7 — توقيع روابط URL لمنع إساءة استخدام التحويلات

بدون التوقيع، يمكن لأي شخص طلب `?w=320`، `?w=321`، `?w=322`، ...، `?w=10000` واستهلاك ميزانية التحويل الشهرية في دقائق. لدينا طبقتان من الدفاع:

1. القائمة المسموح بها في `parseWidth` (الخطوة 5) ترفض أي `w=` خارج `ALLOWED_WIDTHS`.
2. توقيع HMAC-SHA-256 على URL يضمن قبول المفاتيح التي قمنا بإنشائها فقط.

قم بإنشاء مفتاح توقيع قوي مرة واحدة وقم بتخزينه كسر (secret):

```bash
node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))'
# paste the output into the next command
npx wrangler secret put SIGNING_KEY
```

قم بتحديث الأنواع حتى يتم التعرف على `env.SIGNING_KEY`:

```bash
npx wrangler types
```

أضف فحص التوقيع (داخل `fetch`، قبل قراءة R2):

```ts
async function verifyHmac(env: Env, key: string, params: string, sig: string): Promise<boolean> {
  const enc = new TextEncoder();
  const cryptoKey = await crypto.subtle.importKey(
    "raw",
    enc.encode(env.SIGNING_KEY),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["verify"],
  );
  const sigBytes = Uint8Array.from(
    sig.match(/.{1,2}/g)!.map((b) => parseInt(b, 16)),
  );
  return crypto.subtle.verify(
    "HMAC",
    cryptoKey,
    sigBytes,
    enc.encode(`${key}?${params}`),
  );
}
```

ثم في المعالج — **قبل البحث في التخزين المؤقت أو أي قراءة من R2**، حتى لا يتمكن المهاجم من الحصول على استجابة مخزنة بتوقيع مزور:

```ts
const sig = url.searchParams.get("sig") ?? "";
const params = `w=${url.searchParams.get("w")}`;
if (!sig || !(await verifyHmac(env, key, params, sig))) {
  return new Response("bad signature", { status: 403 });
}
```

لإنشاء رابط URL من نظام إدارة المحتوى (CMS) الخاص بك أو خط أنابيب البناء (Node 22+):

```js
// scripts/sign-url.mjs
import { createHmac } from "node:crypto";
const SIGNING_KEY = process.env.SIGNING_KEY;
const key = "sunset.jpg";
const params = "w=640";
const sig = createHmac("sha256", SIGNING_KEY)
  .update(`${key}?${params}`)
  .digest("hex");
console.log(`https://images.example.com/${key}?${params}&sig=${sig}`);
```

سيقبل الـ Worker فقط الروابط التي أنشأها خط أنابيب البناء الخاص بك، لذا فإن مساحة مفاتيح التخزين المؤقت ستكون محدودة بـ `(image × allowed widths × formats)` بدلاً من أن تكون لانهائية.

## الخطوة 8 — النشر والتحقق

```bash
npx wrangler deploy
```

سيقوم Wrangler بطباعة عنوان URL العام للـ worker (شيء مثل `cf-image-cdn.<your-subdomain>.workers.dev`) بالإضافة إلى أي مسارات مخصصة قمت بتكوينها. تحقق من المسار الناجح باستخدام `curl -v`:

```bash
SIG=$(node scripts/sign-url.mjs | sed 's/.*sig=//')
curl -sI \
  -H 'accept: image/avif,image/webp,image/*'  \
  "https://cf-image-cdn.<sub>.workers.dev/sunset.jpg?w=640&sig=${SIG}" \
  | grep -E '^(HTTP|content-type|content-length|cf-cache-status|vary|etag)'
```

الطلب الأول سيطبع `cf-cache-status: MISS`، ويعيد `content-type: image/avif` (بافتراض وجود ترويسة `Accept` مكافئة لمتصفح حديث)، ويبلغ عن `content-length` أصغر بكثير من ملف JPEG الأصلي — عادةً ما يكون أصغر بنسبة 30-50% لـ AVIF و20-35% لـ WebP بنفس الجودة الإدراكية. الطلب الثاني المتطابق سيطبع `cf-cache-status: HIT` ويتخطى محرك التحويل تماماً.

للطلبات الشرطية:

```bash
ETAG=$(curl -sI ... | awk '/^etag/ {print $2}' | tr -d '\r')
curl -sI -H "If-None-Match: ${ETAG}" "https://cf-image-cdn.<sub>.workers.dev/..."
# HTTP/2 304
```

## الأخطاء الشائعة

- **`Transformations are not enabled for this zone`** — لم يتم تفعيل المفتاح في لوحة التحكم. اذهب إلى **Images ← Transformations** في لوحة تحكم Cloudflare، اختر النطاق (zone)، وانقر على *Enable for zone*.[^5]
- **`env.IMAGES is undefined`** — لقد نسيت كتلة ربط `[images]` في ملف `wrangler.toml`، أو لم تقم بإعادة تشغيل `npx wrangler types` بعد إضافتها. أعد التشغيل، ثم أعد تشغيل `wrangler dev`.
- **ارتفاع التكاليف بشكل غير متوقع** — كل استدعاء لـ `env.IMAGES.transform()` يتم محاسبتك عليه؛ يتم تجاوز ميزة إلغاء التكرار المستندة إلى URL التي تنطبق على مسارات Cloudflare Images الأخرى عند استخدام الربط.[^12] تحقق من معدل نجاح التخزين المؤقت (Cache API hit rate) عن طريق فحص `cf-cache-status` في ترويسات الاستجابة، وقم بزيادة `MAX_AGE_EDGE` إذا كانت الإصابات نادرة.
- **AVIF في متصفحات WebP (أو العكس)** — لقد نسيت إضافة `Vary: Accept` في الاستجابة، لذا قام تخزين مؤقت وسيط بتقديم التنسيق الخاطئ للعميل الخاطئ. أضف الترويسة قبل أي عملية `cache.put()`.
- **`R2 GetObject 404` على مفتاح موجود** — المفاتيح حساسة لحالة الأحرف والشرطات المائلة تحتسب. `Sunset.JPG` هو كائن مختلف عن `sunset.jpg`. تعرض قائمة الكائنات في لوحة تحكم R2 المفتاح الدقيق.[^8]

## الخطوات التالية

- اربط الـ Worker باسم مضيف مخصص مثل `images.example.com` باستخدام Workers Route أو Pages Function — كلاهما مشروح في [احتراف تطوير وظائف الحافة](https://nerdleveltech.com/mastering-edge-function-development-a-complete-practical-guide).
- أضف [تدفق رفع موقع (signed-upload flow)](https://nerdleveltech.com/serverless-at-the-edge-deep-dive-into-aws-lambdaedge) حتى يتمكن المستخدمون النهائيون من نشر الصور مباشرة إلى R2 دون المرور عبر خادمك الأصلي.
- بالنسبة للاستجابات غير الصورية حيث لا تزال ترغب في التخزين المؤقت على الحافة، تنطبق نفس أنماط Cache API من هذا البرنامج التعليمي، كما هو مغطى في [نشر الحافة في عصر السحابة الأصلية](https://nerdleveltech.com/edge-deployment-in-the-cloud-native-era-speed-scale-and-smarts).

---

[^1]: أسعار Cloudflare R2 — <https://developers.cloudflare.com/r2/pricing/> (المستوى المجاني: 10 جيجابايت تخزين، 1 مليون عملية من الفئة A، 10 مليون عملية من الفئة B، 0 دولار للخروج؛ التخزين القياسي 0.015 دولار/جيجابايت-شهر).

[^2]: أسعار Cloudflare Workers — <https://developers.cloudflare.com/workers/platform/pricing/> (الخطة المجانية: 100,000 طلب/يوم، 10 مللي ثانية من وحدة المعالجة المركزية لكل استدعاء؛ الخطة المدفوعة تبدأ من 5 دولارات شهرياً مع 10 مليون طلب + 30 مليون مللي ثانية من وحدة المعالجة المركزية متضمنة).

[^3]: إصدار Wrangler 4.90.0 على npm (2026-05-07) — حقل `engines` في الحزمة يعلن عن `node >= 22.0.0`. وصل Node.js 20.x إلى نهاية العمر الافتراضي في 2026-04-30. المصادر: <https://www.npmjs.com/package/wrangler> و <https://GitHub.com/cloudflare/workers-sdk/releases>.

[^4]: أسعار Cloudflare Images — <https://developers.cloudflare.com/images/pricing/> (5,000 تحويل فريد شهرياً مجاناً لكل حساب؛ 0.50 دولار لكل 1,000 تحويل إضافي؛ التخزين 5 دولارات لكل 100,000 صورة شهرياً؛ التوصيل 1 دولار لكل 100,000 — التخزين والتوصيل ينطبقان فقط عند استخدام Cloudflare Images bucket، وليس المصادر الخارجية مثل R2).

[^5]: التحويل عبر Workers — <https://developers.cloudflare.com/images/transform-images/transform-via-workers/> ("لتقديم التحويلات على نطاقك (zone)، يجب عليك أولاً تفعيل الميزة: Images ← Transformations ← اختر النطاق ← Enable for zone.").

[^6]: مرجع إعدادات Wrangler — <https://developers.cloudflare.com/workers/wrangler/configuration/> (مخطط `r2_buckets` الحالي مع `binding`، `bucket_name`، واختياري `remote = true`).

[^7]: كتابة Cloudflare Workers بلغة TypeScript — <https://developers.cloudflare.com/workers/languages/TypeScript/> ("يُنصح بـ `wrangler types` بدلاً من `@cloudflare/workers-types` لأنه ينشئ الأنواع (types) من روابطك وتاريخ التوافق الخاص بك").

[^8]: مرجع R2 Workers API — <https://developers.cloudflare.com/r2/API/workers/workers-API-reference/> (دلالات `R2ObjectBody.body: ReadableStream`، `httpEtag`، `writeHttpMetadata(Headers)`، `get/put/delete`).

[^9]: وثائق Miniflare للتطوير المحلي — <https://developers.cloudflare.com/workers/testing/miniflare/> (محاكاة R2/KV/Durable Objects تحت `.wrangler/state/`).

[^10]: رابط Cloudflare Images Workers — <https://developers.cloudflare.com/images/transform-images/bindings/> (`env.IMAGES.input(stream).transform({width, height, fit, metadata}).output({format, quality}).response()`؛ `fit` يقبل `scale-down`، `contain`، `cover`، `crop`، `pad`؛ `format` يقبل `image/avif`، `image/webp`، `image/jpeg`، `image/png`).

[^11]: حدود منصة Workers — <https://developers.cloudflare.com/workers/platform/limits/> (128 ميجابايت من الذاكرة لكل استدعاء؛ استخدم الـ streams للحمولات الكبيرة).

[^12]: روابط Cloudflare Images — <https://developers.cloudflare.com/images/transform-images/bindings/> + ملاحظات أسعار Cloudflare Images (كل استدعاء لرابط `env.IMAGES` يُحتسب كتحويل قابل للفوترة؛ لا ينطبق إلغاء التكرار المستند إلى URL).

[^13]: واجهة Workers Cache API — <https://developers.cloudflare.com/workers/runtime-apis/cache/> (`caches.default.put/match/delete`، معالجة ETag/If-None-Match، نطاق مركز البيانات، يحترم HTTP `Cache-Control`؛ استخدم `ctx.waitUntil()` للكتابة في التخزين المؤقت دون حظر الاستجابة).
