لو كل ما تكتب مقال بتضطر تفتح 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}`,
},
},
],
},
});