لو كل ما تكتب مقال بتضطر تفتح Figma وتصمم صورة Open Graph من الصفر، إنت بتحرق 12–15 دقيقة في كل مقال. في 200 مقال، ده يساوي 40+ ساعة شغل ضايعة في بكسلات. المقال ده بيوريك إزاي تبني pipeline يولّد صورة OG احترافية لكل مقال بصفر تدخّل يدوي، مستخدمًا Satori من Vercel.
أتمتة صور Open Graph بـ Satori: الفكرة الكاملة
الحل هنا مش إضافة، ده تغيير كامل لطريقة الشغل. بدل ما تصمم الصورة، إنت بتبرمج قالب مرة واحدة — وبعدها كل مقال بياخد صورته منه تلقائيًا.
المشكلة باختصار
صورة Open Graph هي اللي بتظهر لمّا حد يشارك رابط مقالك على تويتر أو لينكدإن أو واتساب. لو ماكانتش موجودة، الرابط بيظهر أصلع وبيقل معدل النقر بنسبة 35% تقريبًا بناءً على أرقام Backlinko. لو كانت موجودة وحلوة، الـ CTR بيعلى. الطرق الشائعة لتوليدها كلها فيها مشكلة:
- Figma يدوي: بيشتغل لـ 5 مقالات في الأسبوع، بيقع لو بتنشر يوميًا.
- Canva templates: أسرع من Figma بس لسه بيحتاج فتح متصفح + تعديل + تصدير + رفع.
- headless Chrome (Puppeteer): بطيء (3–5 ثواني لكل صورة) ومكلف في الـ CI (يحتاج 1GB RAM).
إيه هو Satori؟ مثال بسيط الأول
تخيّل إنك عندك آلة صناعة بطاقات معايدة. الآلة دي بتاخد منك قالب ورقي متقطّع (زي استنسل)، وبتختم عليه الاسم والتاريخ والرسمة — وطلعت بطاقة كاملة في ثانيتين. إنت ما بترسمش البطاقة، إنت بس بتقولها "الاسم محمد" و"العيد الفطر" وهي بتلصق الباقي. Satori بيشتغل بالظبط كده، بس بدل البطاقة الورقية، قالبك هو كود JSX، والمدخل بتاعك هو بيانات المقال (العنوان + الاسم + التاريخ).
علميًا: Satori مكتبة JavaScript من فريق Vercel، بتاخد كود JSX + CSS وبتحوّله لـ SVG مباشرة بدون ما تفتح متصفح. بعد كده ممكن تحوّل الـ SVG لـ PNG بمكتبة @resvg/resvg-js. الفرق الأهم عن Puppeteer: مفيش browser engine، فالعملية بتاخد ~80 مللي ثانية بدل 3 ثواني.
التثبيت وإعداد التيمبليت
الحزم المطلوبة اتنين بس. ثبّتهم:
npm install satori @resvg/resvg-js
# optional: لو عايز خطوط عربية جميلة
npm install @fontsource/cairoدلوقتي اكتب القالب. افرض عندك مقال عنوانه "أتمتة تنظيف Docker" ومكتوب في 2026. القالب هيستقبل العنوان والتاريخ كـ props ويرجّع JSX:
// og-template.tsx
export const OGTemplate = ({ title, date, author }) => ({
type: 'div',
props: {
style: {
width: 1200,
height: 630,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '80px',
background: 'linear-gradient(135deg, #0f172a, #1e40af)',
color: 'white',
fontFamily: 'Cairo',
},
children: [
{
type: 'div',
props: {
style: { fontSize: 28, opacity: 0.7 },
children: 'ahmedhaies.com',
},
},
{
type: 'div',
props: {
style: { fontSize: 64, fontWeight: 700, lineHeight: 1.2 },
children: title,
},
},
{
type: 'div',
props: {
style: { fontSize: 26, opacity: 0.6 },
children: `${author} · ${date}`,
},
},
],
},
});عندنا هنا تصميم بسيط: خلفية متدرّجة، اسم الموقع فوق، العنوان الضخم في النص، التاريخ تحت. كل ده يتعدّل من مكان واحد.
سكربت التوليد
ده السكربت اللي بياخد بيانات المقال ويطلع PNG:
// generate-og.mjs
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import fs from 'node:fs';
import { OGTemplate } from './og-template.js';
const cairoRegular = fs.readFileSync('./fonts/Cairo-Regular.ttf');
const cairoBold = fs.readFileSync('./fonts/Cairo-Bold.ttf');
export async function generateOG({ title, date, author, slug }) {
const svg = await satori(
OGTemplate({ title, date, author }),
{
width: 1200,
height: 630,
fonts: [
{ name: 'Cairo', data: cairoRegular, weight: 400 },
{ name: 'Cairo', data: cairoBold, weight: 700 },
],
}
);
const png = new Resvg(svg).render().asPng();
fs.writeFileSync(`./public/og/${slug}.png`, png);
console.log(`✓ ${slug}.png (${(png.length / 1024).toFixed(1)} KB)`);
}الصورة النهائية بتطلع ~180 كيلوبايت في المتوسط، بمقاس 1200×630 (المقاس الرسمي لـ Facebook و Twitter Cards).
التكامل مع GitHub Actions
الفكرة: كل مرة تـ push مقال جديد، الـ workflow يكتشف الملف الجديد، يشغّل السكربت، ويعمل commit بالصورة. ده ملف .github/workflows/og.yml:
name: Generate OG Images
on:
push:
paths: ['content/posts/**.md']
jobs:
og:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: node scripts/build-all-og.mjs
- name: Commit generated images
run: |
git config user.name "og-bot"
git config user.email "og-bot@users.noreply.github.com"
git add public/og/
git diff --staged --quiet || git commit -m "chore: regenerate OG images"
git pushالسكربت build-all-og.mjs بيقرأ الـ frontmatter من كل ملف Markdown، بيستدعي generateOG لكل واحد، ويخطّي اللي عنده الصورة متولّدة فعلًا (بمقارنة hash للعنوان).
أرقام حقيقية من تجربة فعلية
- زمن التوليد: 80 مللي ثانية لكل صورة على GitHub runner عادي.
- توليد 180 صورة دفعة واحدة: 14 ثانية (معظمها npm install).
- حجم الصورة: 160–220 كيلوبايت (أقل من 1/3 ما كانت بتطلعه Figma الأصلي عندي بـ 650KB).
- وقت الكتّاب المحرّر: من 15 دقيقة/مقال إلى صفر. في 200 مقال = 50 ساعة.
- زمن الـ CI: 42 ثانية لكل push (بدل 3–4 دقائق لو كنت بتستخدم Puppeteer).
Trade-offs وما يجب الانتباه له
الحل ده مش مجاني الكلفة. الأمانة تقتضي الأرقام دي:
- CSS المدعوم محدود: Satori بيدعم subset من CSS بس. نسيت
float،position: absoluteبتصرفات معينة، و backdrop-filter. اعتمد على flexbox خالص. - مفيش JavaScript runtime: ممنوع
useStateأوuseEffect. القوالب لازم تكون pure functions بترجّع JSX. - الخطوط بتتحمّل في الذاكرة: كل خط بيزوّد استهلاك الـ RAM بـ 5–15 ميجا. لو عندك 4 أوزان، احسبها.
- الافتراض إن: الإعداد ده مبني على أن عندك أقل من 500 مقال وتحدّث أقل من 10 ملفات Markdown في الـ push الواحد. فوق كده ابدأ تفكّر في on-demand generation بدل batch.
متى لا تستخدم Satori
الطريقة دي بتفشل في الحالات دي:
- تصاميم بصرية معقّدة جدًا: لو قالبك محتاج تأثيرات Figma متقدّمة (glassmorphism حقيقي، 3D transforms). افتح Figma وصدّر يدويًا.
- محتوى RTL مختلط مع LTR في نفس السطر: الدعم لسه مش كامل في Satori 0.10.x. اختبر قبل ما تعتمد.
- لو الصورة بتتغيّر بناءً على بيانات real-time: ساعتها استخدم Vercel OG API مباشرة (edge function) بدل الـ batch build.
المصادر
- مستودع Satori الرسمي على GitHub — المرجع الأساسي للمكتبة وملف README الشامل.
- وثائق Vercel لتوليد OG Images — تفاصيل الـ API والقيود.
- مكتبة resvg-js — المسؤولة عن تحويل SVG إلى PNG.
- دراسة Backlinko عن تأثير Open Graph على CTR — مصدر رقم الـ 35%.
- Facebook Sharing Best Practices — المقاس الرسمي 1200×630.
الخطوة التالية
افتح المقال اللي نشرته آخر مرة، شوف إيه صورة Open Graph بتاعته (جرّبها على opengraph.xyz). لو مفيش صورة أو الصورة مش متّسقة، ابدأ بتيمبليت Satori واحد فقط لنوع المحتوى الأكثر نشرًا عندك. بعد ما يشتغل لـ 10 مقالات، زوّد المتغيّرات. متحاولش تبني 5 قوالب من أول يوم — هتسيب المشروع.