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.12
ما ستتعلمه
- كيفية تكوين ربط حاوية 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-303 |
| 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 > Transformations5 |
ستحتاج أيضًا إلى curl وبعض صور JPEG كعينات (1024×768 أو أكبر لنتمكن من رؤية إعادة التحجيم تعمل فعليًا).
الخطوة 1 — إنشاء هيكل الـ Worker
قم بإنشاء المشروع باستخدام C3 (واجهة سطر أوامر create-cloudflare الرسمية). اختر TypeScript، وقالب Hello World، وبدون إطار عمل للواجهة الأمامية (front-end)، وتخطى تهيئة Git إذا كان لديك مستودع أب بالفعل:
npm create cloudflare@latest cf-image-cdn -- \
--type=hello-world \
--lang=ts \
--no-deploy \
--no-git
cd cf-image-cdn
قم بتثبيت إصدار Wrangler صراحةً حتى لا تؤدي إصدارات التصحيح المستقبلية إلى تعطل البناء الخاص بك:
npm install --save-dev wrangler@4.84.1
تحقق:
npx wrangler --version
# ⛅️ wrangler 4.84.1
node --version
# v22.x.x
إذا أظهر node --version الإصدار v20 أو أقدم، فقم بالترقية — سيرفض Wrangler 4 البدء.
الخطوة 2 — إنشاء حاوية R2 ورفع الصور الأصلية
أنشئ حاوية من واجهة سطر الأوامر (الاسم عالمي داخل حسابك، أحرف صغيرة، والشرطات مسموحة):
npx wrangler r2 bucket create cf-image-cdn-originals
ضع بعض الصور التجريبية فيها:
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
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:
npx wrangler types
سيقوم ذلك بكتابة worker-configuration.d.ts بناءً على روابطك وتاريخ التوافق — وهو البديل الحديث لإضافة @cloudflare/workers-types يدويًا.7
الخطوة 4 — قراءة الصور من R2
قم ببرمجة معالج جلب بسيط يقوم ببث البيانات الأصلية مرة أخرى. هذا يثبت أن الربط يعمل قبل إضافة التحويلات:
// 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 قم بتشغيله محليًا:
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
// 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 فقط.10metadata: "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):
// 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 });
أمران يجب استيعابهما:
caches.defaultنطاقه هو مركز البيانات (Data-center scoped). الطلب الذي يصل إلى مركز بيانات IAD لن يرى متغيراً مخزناً في CDG. هذا متوقع — فمقابل تحويل إضافي واحد لكل مركز بيانات، تحصل على أقل زمن انتقال ممكن للقراءة عند وجود النسخة في التخزين المؤقت.13- استخدم
ctx.waitUntil()للكتابة في التخزين المؤقت. يتيح ذلك لـ Worker إرجاع الاستجابة إلى المتصفح دون انتظار اكتمال عملية الكتابة في التخزين المؤقت.13
الخطوة 7 — توقيع روابط URL لمنع إساءة استخدام التحويلات
بدون التوقيع، يمكن لأي شخص طلب ?w=320، ?w=321، ?w=322، ...، ?w=10000 واستهلاك ميزانية التحويل الشهرية في دقائق. لدينا طبقتان من الدفاع:
- القائمة المسموح بها في
parseWidth(الخطوة 5) ترفض أيw=خارجALLOWED_WIDTHS. - توقيع HMAC-SHA-256 على URL يضمن قبول المفاتيح التي قمنا بإنشائها فقط.
قم بإنشاء مفتاح توقيع قوي مرة واحدة وقم بتخزينه كسر (secret):
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:
npx wrangler types
أضف فحص التوقيع (داخل fetch، قبل قراءة R2):
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، حتى لا يتمكن المهاجم من الحصول على استجابة مخزنة بتوقيع مزور:
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+):
// 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 — النشر والتحقق
npx wrangler deploy
سيقوم Wrangler بطباعة عنوان URL العام للـ worker (شيء مثل cf-image-cdn.<your-subdomain>.workers.dev) بالإضافة إلى أي مسارات مخصصة قمت بتكوينها. تحقق من المسار الناجح باستخدام curl -v:
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 ويتخطى محرك التحويل تماماً.
للطلبات الشرطية:
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.5env.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 — كلاهما مشروح في احتراف تطوير وظائف الحافة. - أضف تدفق رفع موقع (signed-upload flow) حتى يتمكن المستخدمون النهائيون من نشر الصور مباشرة إلى R2 دون المرور عبر خادمك الأصلي.
- بالنسبة للاستجابات غير الصورية حيث لا تزال ترغب في التخزين المؤقت على الحافة، تنطبق نفس أنماط Cache API من هذا البرنامج التعليمي، كما هو مغطى في نشر الحافة في عصر السحابة الأصلية.
Footnotes
-
أسعار 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 مليون مللي ثانية من وحدة المعالجة المركزية متضمنة). ↩
-
إصدار 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. ↩ -
أسعار Cloudflare Images — https://developers.cloudflare.com/images/pricing/ (5,000 تحويل فريد شهرياً مجاناً لكل حساب؛ 0.50 دولار لكل 1,000 تحويل إضافي؛ التخزين 5 دولارات لكل 100,000 صورة شهرياً؛ التوصيل 1 دولار لكل 100,000 — التخزين والتوصيل ينطبقان فقط عند استخدام Cloudflare Images bucket، وليس المصادر الخارجية مثل R2). ↩ ↩2
-
التحويل عبر Workers — https://developers.cloudflare.com/images/transform-images/transform-via-workers/ ("لتقديم التحويلات على نطاقك (zone)، يجب عليك أولاً تفعيل الميزة: Images ← Transformations ← اختر النطاق ← Enable for zone."). ↩ ↩2
-
مرجع إعدادات Wrangler — https://developers.cloudflare.com/workers/wrangler/configuration/ (مخطط
r2_bucketsالحالي معbinding،bucket_name، واختياريremote = true). ↩ -
كتابة Cloudflare Workers بلغة TypeScript — https://developers.cloudflare.com/workers/languages/TypeScript/ ("يُنصح بـ
wrangler typesبدلاً من@cloudflare/workers-typesلأنه ينشئ الأنواع (types) من روابطك وتاريخ التوافق الخاص بك"). ↩ -
مرجع R2 Workers API — https://developers.cloudflare.com/r2/API/workers/workers-API-reference/ (دلالات
R2ObjectBody.body: ReadableStream،httpEtag،writeHttpMetadata(Headers)،get/put/delete). ↩ ↩2 -
وثائق Miniflare للتطوير المحلي — https://developers.cloudflare.com/workers/testing/miniflare/ (محاكاة R2/KV/Durable Objects تحت
.wrangler/state/). ↩ -
رابط 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). ↩ ↩2 -
حدود منصة Workers — https://developers.cloudflare.com/workers/platform/limits/ (128 ميجابايت من الذاكرة لكل استدعاء؛ استخدم الـ streams للحمولات الكبيرة). ↩
-
روابط Cloudflare Images — https://developers.cloudflare.com/images/transform-images/bindings/ + ملاحظات أسعار Cloudflare Images (كل استدعاء لرابط
env.IMAGESيُحتسب كتحويل قابل للفوترة؛ لا ينطبق إلغاء التكرار المستند إلى URL). ↩ ↩2 -
واجهة Workers Cache API — https://developers.cloudflare.com/workers/runtime-apis/cache/ (
caches.default.put/match/delete، معالجة ETag/If-None-Match، نطاق مركز البيانات، يحترم HTTPCache-Control؛ استخدمctx.waitUntil()للكتابة في التخزين المؤقت دون حظر الاستجابة). ↩ ↩2 ↩3