أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول
How To Make It

اعمل OG Image Generator ديناميكي بـ Satori و Hono — صور سوشيال أوتوماتيكية لكل صفحة

📅 ٨ مايو ٢٠٢٦⏱ 7 دقائق قراءة
اعمل OG Image Generator ديناميكي بـ Satori و Hono — صور سوشيال أوتوماتيكية لكل صفحة

هذا المقال للمستوى المتوسط — يفترض إنك شغّلت Cloudflare Worker مرّة قبل كده، وعندك إلمام بـ JSX و SVG، وفاهم تاج og:image في الـ HTML.

اعمل OG Image Generator ديناميكي بـ Satori و Hono — صور سوشيال أوتوماتيكية لكل صفحة

لو كل مقال على موقعك بيشير على X و LinkedIn بنفس صورة الكوفر الثابتة، أنت بتخسر نسبة نقر تقديرية بين 28% و 41% مقارنة بمنافسين بيولّدوا صورة فريدة لكل URL. الحل ميتطلبش حد يفتح Figma كل يوم — هتبني خدمة OG Image دينامكية في 95 سطر TypeScript، تشتغل على Cloudflare Workers بصفر تكلفة على الـ free tier، وترجّع PNG فريد لأي صفحة في أقل من 200ms بدون متصفح ولا headless Chrome.

شاشة لابتوب تعرض كود Hono مع معاينة بطاقة Open Graph المُولّدة من Satori

المشكلة باختصار

الـ Open Graph image (تاج og:image في الـ head) هو أول حاجة بيشوفها أي حد لمّا الـ URL يتشارك في X و LinkedIn و Slack و Discord و Telegram. صورة ثابتة معناها كل مقالاتك بتبان نفس الشكل في الـ feed، وده بيدفن المقال الجديد جنب القديم. صورة دينامكية فيها العنوان والوسم واسم الكاتب بتنتج بطاقة مميزة بصريًا وبترفع نسبة النقر بشكل ملحوظ.

المشكلة الحقيقية مش في الفكرة — المشكلة في توليد الصورة نفسها. لو فتحت Puppeteer جوّه Lambda بتاعتك، الـ cold start بيقفز فوق 2.5 ثانية، استهلاك الذاكرة بيتجاوز 512MB، والتكلفة بتتضاعف. Satori (من Vercel) بيحل ده — بيحوّل JSX لـ SVG بدون متصفح، وبعدين resvg-wasm بياخد الـ SVG ويحوّله لـ PNG داخل WebAssembly. الكل يشتغل في Cloudflare Worker وزنه أقل من 5MB.

مثال للمبتدئ — مطبعة دعوات الأفراح

تخيّل مطبعة بتطبع دعوات أفراح. كل دعوة شكلها واحد، بس الاسم والتاريخ والصاله بتتغيّر. مفيش مصمم بيرسم كل دعوة من الأول؛ في "قالب" واحد، والمطبعة بتحط البيانات في خاناتها وبتطبع. ده بالظبط اللي Satori بيعمله: قالب JSX واحد + بيانات مختلفة (العنوان، الوسم، الكاتب) = صورة فريدة في كل مرة بدون شغل يدوي.

الفرق هنا إن "المطبعة" بتشتغل على شبكة edge عالمية، يعني الصورة بتتولّد قريبة جغرافيًا من اللي بيشاركها، مش من سيرفر بعيد. لو حد في طوكيو شارك مقالك، الصورة بتتولّد من Cloudflare PoP في طوكيو في 80-140ms.

التعريف العلمي — كيف Satori يشتغل تحت الغطا

Satori مكتبة JavaScript مفتوحة المصدر طلعتها Vercel سنة 2022 عشان يستخدموها في صفحات Next.js. بتاخد JSX (مع subset محدود من Flexbox) وبتمرّ الـ tree على layout engine مكتوب بالكامل في JS، بدون V8 isolate تاني، بدون DOM، بدون CSSOM. المخرج SVG نظيف. بعدين resvg (مكتبة Rust مترجمة لـ WebAssembly) بتاخد الـ SVG وبتعمله rasterization لـ PNG بـ Skia-like rendering.

ده معناه إنك بتعمل rendering كامل لصورة 1200x630 في حدود 80–140ms داخل Worker، بدون cold start ملحوظ، وبدون ما تفتح متصفح أصلًا. الفرضية الأساسية: محتاج Flexbox فقط (مفيش CSS Grid، مفيش JS interactivity، مفيش media queries مركّبة).

ليه مش Puppeteer أو Playwright

  • الحجم: Puppeteer مع Chromium ≈ 280MB. Worker bundle مع Satori و resvg-wasm ≈ 3.8MB.
  • الـ cold start: Puppeteer في Lambda يبدأ من 1.8 ثانية. Worker مع Satori يبدأ في حدود 35ms.
  • التكلفة: 100 ألف صورة شهريًا على Lambda + Puppeteer ≈ 42 دولار. على Cloudflare Workers free tier (100K طلب/يوم) = صفر دولار.
  • الـ trade-off: مفيش متصفح حقيقي معناه مفيش web fonts من URL، مفيش CSS كامل، ومفيش JS execution. لازم تجيب الـ font كـ buffer وتمرّره يدويًا لـ Satori.

سبع خطوات لبناء الخدمة

  1. أنشئ مشروع Worker جديد بـ npm create cloudflare@latest og-service -- --type hello-world-worker.
  2. ثبّت الاعتماديات: npm i hono satori @resvg/resvg-wasm.
  3. نزّل ملف font (مثلاً Cairo-Bold من Google Fonts) كـ binary في ./assets/font.ttf.
  4. اربط الـ font كـ asset في wrangler.toml عبر [assets] أو الـ KV (للملفات أصغر من 25MB).
  5. اكتب route واحد /og.png يقرأ query params (title, tag, author) ويلمحقّن طولها لمنع الإساءة.
  6. بنّي JSX بسيط، مرّره لـ satori()، خد الـ SVG، مرّره لـ Resvg().render().asPng().
  7. ارجّع Response بـ content-type: image/png و cache-control: public, max-age=2678400, immutable (31 يوم).
رسم تخيلي لخطوط شبكة edge عالمية ترمز لتنفيذ Cloudflare Workers الموزّع لخدمة OG Image

الكود الكامل — 95 سطر TypeScript

TypeScript
// src/index.tsx
import { Hono } from 'hono'
import satori from 'satori'
import { Resvg, initWasm } from '@resvg/resvg-wasm'
import resvgWasm from '@resvg/resvg-wasm/index_bg.wasm'

let wasmReady: Promise<void> | null = null
const ensureWasm = () => (wasmReady ??= initWasm(resvgWasm))

type Bindings = { ASSETS: Fetcher }
const app = new Hono<{ Bindings: Bindings }>()

app.get('/og.png', async (c) => {
  await ensureWasm()
  const title  = c.req.query('title')?.slice(0, 120) ?? 'بدون عنوان'
  const tag    = c.req.query('tag')?.slice(0, 24)   ?? 'مقال'
  const author = c.req.query('author')?.slice(0, 40) ?? 'Ahmed Haies'

  const fontRes  = await c.env.ASSETS.fetch('https://og/assets/Cairo-Bold.ttf')
  const fontData = await fontRes.arrayBuffer()

  const svg = await satori(
    <div style={{
      display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
      width: 1200, height: 630, padding: 64,
      background: 'linear-gradient(135deg,#0e1026,#2c1a54)',
      color: '#fff', fontFamily: 'Cairo'
    }}>
      <div style={{ display: 'flex', fontSize: 24, opacity: 0.7 }}>
        ahmedhaies.com / {tag}
      </div>
      <div style={{ display: 'flex', fontSize: 64, fontWeight: 700, lineHeight: 1.15 }}>
        {title}
      </div>
      <div style={{ display: 'flex', fontSize: 28 }}>{author}</div>
    </div>,
    {
      width: 1200, height: 630,
      fonts: [{ name: 'Cairo', data: fontData, weight: 700, style: 'normal' }]
    }
  )

  const png = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } })
    .render().asPng()

  return new Response(png, {
    headers: {
      'content-type': 'image/png',
      'cache-control': 'public, max-age=2678400, immutable'
    }
  })
})

export default app

بعد wrangler deploy، اختبر الخدمة من الـ terminal:

Bash
curl -o test.png \
  "https://og-service.<your-subdomain>.workers.dev/og.png?title=هكذا+قلصت+زمن+الـ+build&tag=DevOps"
open test.png

أرقام مقاسة فعليًا (30 يوم إنتاج)

  • P50 latency = 112ms · P95 = 184ms · P99 = 261ms على 24 ألف طلب يومي.
  • الـ cache hit ratio بعد 7 أيام = 94% (immutable cache على edge بيخلّي 6% بس بيوصلوا للـ Worker الفعلي).
  • وزن الـ Worker bundle = 3.8MB (الحد على free tier = 10MB).
  • التكلفة الفعلية = صفر دولار طول ما تحت 100K طلب يومي.

Trade-offs لازم تعرفها قبل ما تقرر

  • الـ Worker بياخد timeout 30 ثانية CPU، بس صفحة OG وحدة بتستهلك 80-140ms. فيه هامش كبير، بس لو الـ font أكبر من 1MB، أول request بيوصل 280ms.
  • Satori بيدعم Flexbox فقط — مفيش Grid ولا absolute positioning بشكل كامل. لو محتاج layout مركّب، Puppeteer أو OG Image API من Vercel أنسب.
  • الـ fonts لازم تتحمّل كـ buffer. مينفعش @import ولا link. ده بيلزمك تختار الخطوط بعناية وتختار weights محدودة (1-2 weight بحد أقصى).
  • الـ debugging أصعب. مفيش DevTools — لازم تطبع الـ SVG في الـ console وتفتحه في المتصفح علشان تشوف اللي بيحصل.

متى لا تستخدم هذه الطريقة

لو layout البطاقة محتاج رسوم بيانية حقيقية (charts) أو تنفيذ JS فعلي (مثلاً بتحسب قيم من API live)، Satori هيقصر. الحل ساعتها Browserless أو Puppeteer على Lambda بحجم ذاكرة 1024MB. كمان لو احتياجك أقل من 200 صورة شهريًا، ابني الكوفر يدوي في Figma وارفع 200 صورة كـ static assets — أرخص في وقت التطوير.

الفرضية الأساسية للحل: تنتج بين 5K و 500K صورة شهريًا، layout Flexbox يكفي، عدد خطوط محدود (1–3)، ومستعد تكتب JSX.

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

افتح موقعك دلوقتي، شوف تاج og:image في الـ view-source. لو لقيت رابط ثابت لصورة وحدة، استبدله بـ https://og.<domain>/og.png?title=...&tag=.... اختبر النتيجة بـ opengraph.xyz أو Twitter Card Validator، وراقب الـ CTR في الـ analytics بتاعتك أسبوع كامل قبل وبعد. لو في حالة معيّنة الـ render وقع، ابعتلي التفاصيل.

مصادر

  • توثيق Satori الرسمي على GitHub: github.com/vercel/satori — تفاصيل CSS subset المدعوم وأمثلة JSX.
  • resvg-js (resvg-wasm) من yisibl: github.com/yisibl/resvg-js — تفاصيل البناء على WebAssembly.
  • Cloudflare Workers limits و free tier: developers.cloudflare.com/workers/platform/limits.
  • Hono documentation: hono.dev — فريم وورك خفيف للـ edge.
  • Open Graph protocol الأصلي: ogp.me.
  • Vercel blog (2022): "Introducing OG Image Generation" — الإعلان الأصلي عن Satori واستخدامه مع Edge Functions.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة