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

٨ مايو ٢٠٢٦

Cloudflare Workers + R2 Image CDN: 2026 Tutorial

لتقديم صور مُعاد تحجيمها من 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.js22.0.0 أو أحدثأسقط Wrangler 4 دعم Node 20 بعد انتهاء الدعم (EOL) في 2026-04-303
Wrangler CLI4.84.1مثبت على إصدار عمره ≥14 يومًا؛ الأحدث هو 4.90.0 ولكن قم بتثبيت ملف القفل (lockfile) على أي إصدار تقوم بـ npm install له
TypeScript5.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 فقط.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):

// 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):

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.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

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


Footnotes

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

  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). 2

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

  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). 2

  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). 2

  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). 2

  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() للكتابة في التخزين المؤقت دون حظر الاستجابة). 2 3


نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.