تخطّى إلى المحتوى الرئيسي
صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط

صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط

November 13, 2026
هندسة الواجهات
8 دقيقة قراءة

الخلاصة: في Next.js 16، يكفي ملف opengraph-image.tsx داخل مقطع المسار (route segment) لتوليد صورة المشاركة لذلك المسار باستخدام ImageResponse من next/og، وتُكتب وسوم og:image تلقائيًا. المحرّك المستخدم هو نسخة Satori المضمّنة داخل Next.js وليس أحدث إصدار من Satori، لذا فإن حدود CSS والخطوط المتاحة يحدّدها إصدار Next.js المثبّت في المشروع.

معاينة الرابط هي أول ما يراه معظم الناس من أي مقال: البطاقة التي تظهر في Slack أو LinkedIn أو WhatsApp أو X. كان فريقنا في Devya يصدّر صورة PNG لكل مقال من أداة تصميم. توقّف ذلك عن العمل بكفاءة عندما صار المحتوى يُنشر يوميًا وبلغتين، فنقلنا بطاقات المشاركة إلى الكود عبر اصطلاح الملفات opengraph-image. الإعداد الأساسي كان سريعًا. هذه الملاحظات تغطي ما استغرق وقتًا أطول: خط بصيغة WOFF2 لم يستطع المحرّك المضمّن قراءته، وملف README يصف محرّكًا أحدث من الذي نشغّله، وصفحة ظلّت تعرض صورة الـ layout بدل صورتها.

أهم النقاط

• ملف opengraph-image.tsx هو Route Handler خاص: يقدّم Next.js الصورة التي يُرجعها ويضيف og:image وog:image:width وog:image:height وog:image:type وog:image:alt إلى الـ head الخاص بالمسار.

• صور OG المولّدة محسّنة استاتيكيًا افتراضيًا: يرسمها Next.js وقت البناء ما لم تستخدم دالة الصورة واجهة برمجية وقت الطلب (request-time API) أو بيانات غير مخزّنة مؤقتًا.

• تضمّن next/og نسختها الخاصة من Satori. يأتي Next.js 16.2.9 مع ‎@vercel/og 0.11.1 وSatori 0.25.0، الذي يعتمد على Yoga flexbox في التخطيط ويقرأ خطوط TTF وOTF وWOFF، وليس WOFF2.

• حقل openGraph.images الصريح في الصفحة يتغلّب على أي ملف opengraph-image، بما في ذلك الملفات الموروثة من المقاطع الأب.

• اضبط metadataBase في الـ layout الجذري. بدونه يحلّ Next.js روابط صور المشاركة على أصل (origin) احتياطي وقد يطبع تحذيرًا.

ماذا يولّد ملف opengraph-image.tsx فعليًا؟

ملف opengraph-image.tsx هو اصطلاح ملفات للبيانات الوصفية في Next.js: دالة مُصدَّرة افتراضيًا داخل مقطع مسار تُرجع Response يحمل صورة، فيقدّمها Next.js ويربطها من وسوم og:image لذلك المقطع. القيمة المُرجعة المعتادة هي ImageResponse من next/og، التي تحوّل JSX والأنماط المضمّنة (inline styles) إلى صورة PNG. ويؤدي الملف الشقيق twitter-image.tsx المهمة نفسها لوسوم twitter:image.

ثلاثة تصديرات اختيارية تتحوّل إلى وسوم meta: يصبح alt هو og:image:alt، ويصبح size هو og:image:width وog:image:height، ويصبح contentType هو og:image:type. في مقطع ديناميكي مثل app/blog/[slug] تستقبل دالة الصورة params على شكل Promise، تمامًا كالصفحات في Next.js 15 و16. بطاقتنا لكل مقال تنتظر params، وتحمّل المقال حسب الـ slug، وتُرجع ImageResponse بمقاس 1200 في 630 يحتوي عمودًا مرنًا (flex column) فيه العنوان واسم الموقع.

تفصيلتان في ذلك الملف جاءتا من أخطاء سابقة. نقرأ الخط مرة واحدة على مستوى الوحدة (module scope) باستخدام top-level await وreadFile من مسار تحت process.cwd()، وهو النمط المستخدم في توثيق Next.js، فلا يعود الرسم إلى القرص في كل مرة. كما نمرّر كل عنوان عبر دالة مساعدة صغيرة اسمها fitTitle() تعدّ نقاط الترميز (code points)، وتقصّ العنوان بعد 90 حرفًا، وتخفّض حجم الخط من 84px إلى 68px ثم 56px كلما طال العنوان، لأن Satori لا يحتوي أي آلية تصغّر النص ليناسب مساحته.

تصدير alt قيمة ثابتة، لذلك تشترك كل المقالات في نص بديل واحد. للحصول على نص بديل لكل مقال، تُرجع generateImageMetadata قائمة من واصفات الصور، لكل منها id وقيم alt وsize وcontentType خاصة بها، ثم تستقبل دالة الصورة ذلك الـ id على شكل Promise.

أي إصدار من Satori تشغّله next/og فعليًا؟

تشغّل next/og نسخة Satori المضمّنة داخل حزمة next المثبّتة، وليس أحدث إصدار من Satori. ‏Satori هي مكتبة من Vercel تحوّل JSX ومجموعة جزئية من CSS إلى SVG، وتغلّفها ‎@vercel/og مع Resvg، وهو محرّك SVG مكتوب بلغة Rust، لإنتاج صورة PNG. يترجم Next.js الحزمة ‎@vercel/og إلى next/dist/compiled، لذلك فإن إضافة إصدار أحدث من satori إلى package.json لا يغيّر شيئًا في ImageResponse.

اكتشفنا ذلك عند قراءة ملف README الخاص بـ Satori على GitHub. الفرع الرئيسي فيه يصف Taffy لتخطيط flexbox وgrid وblock، ودعم خطوط WOFF2، وتشكيل HarfBuzz للنصوص المركّبة مثل العربية. مشروعنا يعمل على Next.js 16.2.9، وأظهر أمرا grep على المجلد node_modules/next/dist/compiled/@vercel/og ما هو مضمّن فعلًا: حقل version في package.json ونص الإصدار satori@ داخل index.node.js.

كانت النتيجة ‎@vercel/og 0.11.1 مع Satori 0.25.0، الذي يأتي مع yoga.wasm للتخطيط وما زال يرمي الخطأ "Expected div to have explicit display: flex, display: contents, or display: none if it has more than one child node." ومرجع ImageResponse في توثيق Next.js يطابق النسخة المضمّنة: flexbox ومجموعة جزئية من CSS، بلا display: grid، وخطوط TTF أو OTF أو WOFF فقط. صرنا نعامل توثيق Next.js على أنه العقد الملزِم، وREADME الخاص بـ Satori على أنه معاينة لإصدار لاحق.

في مواقعنا ثنائية اللغة يهمّ هذا الأمر العربية أكثر من غيرها. يوثّق README الأحدث لـ Satori تخطيط الأسطر من اليمين إلى اليسار، لكننا لا نعتمد عليه عبر next/og قبل أن نرسم عنوانًا عربيًا بالنسخة المضمّنة ونفحص صورة PNG بأعيننا.

متى نستدعي Satori مباشرة بدل next/og؟

استدعاء satori مع ‎@resvg/resvg-js من Route Handler منطقي عندما يحتاج المشروع محرّكًا أحدث مما يضمّنه Next.js. هذه المفاضلات التي نزنها:

• إصدار Satori: تستخدم next/og ما يضمّنه Next.js المثبّت (0.25.0 في 16.2.9)، بينما التثبيت المباشر يُثبَّت ويُحدَّث في package.json الخاص بالمشروع.

• وسوم meta: تولّدها next/og من اصطلاح الملفات وتصديراته، بينما الإعداد المباشر يضيف openGraph.images يدويًا في generateMetadata.

• الخطوط: تقرأ next/og خطوط TTF وOTF وWOFF وتضمّن Geist Regular خطًا افتراضيًا، بينما الإعداد المباشر يقرأ ما يدعمه ذلك الإصدار من Satori.

• التخزين المؤقت: صور next/og محسّنة استاتيكيًا افتراضيًا، بينما الـ Route Handler المباشر يضبط Cache-Control بنفسه.

• الترقيات: تتحرك next/og مع ترقيات Next.js، بينما الإعداد المباشر يُرقّى بشكل مستقل.

لماذا تعرض الصفحة صورة og:image خاطئة؟

تعرض الصفحة صورة مشاركة خاطئة عندما تتغلّب البيانات الوصفية على مستوى الصفحة على ملف الصورة. يحدّد توثيق Next.js الترتيب: حقل openGraph.images أو twitter.images الصريح في الصفحة، سواء من metadata أو generateMetadata، يتغلّب على ملف opengraph-image أو twitter-image المقابل، سواء كان الملف في المقطع نفسه أو موروثًا من مقطع أب. ويمكن لملف الصورة المجاور للصفحة أن يستبدل الصور المضبوطة في الـ layout، لكن فقط عندما لا تحدّد الصفحة الحقل images.

الفخ الثاني هو الدمج. يدمج Next.js البيانات الوصفية من الـ layouts والصفحات دمجًا سطحيًا (shallow)، لذلك فإن صفحة تضبط openGraph بعنوان فقط تستبدل كائن openGraph الخاص بالـ layout بالكامل، بما فيه description وsiteName. يحتفظ فريقنا بحقول Open Graph المشتركة في ثابت واحد مُصدَّر ويفرده (spread) في كل صفحة، وهو الحل نفسه الذي يعرضه التوثيق.

الفخ الثالث هو الأصل (origin). تحتاج برامج الزحف إلى رابط og:image مطلق، ويبنيه Next.js من metadataBase. بدون metadataBase تُحلّ صور المشاركة على أصل احتياطي، وهو خارج Vercel يساوي http://localhost:3000، ويطبع Next.js تحذيرًا يسهل تجاهله. نضبط metadataBase مرة واحدة في الـ layout الجذري من متغير بيئة اسمه SITE_URL، ونضبط هناك أيضًا twitter.card على summary_large_image. تقرأ منصة X وسوم Open Graph عند غياب نظيراتها twitter:، لذلك يكفي ملف opengraph-image واحد للمنصتين، لكننا نفضّل التصريح ببطاقة الصورة الكبيرة بدل افتراضها.

كيف نفحص معاينات الروابط قبل مشاركة أي رابط؟

الفحص الموثوق هو جلب HTML الصفحة، وقراءة og:image منها، ثم جلب ذلك الرابط. أولًا نقرأ مخرجات next build، حيث يُعلَّم كل مسار بالرمز ○ إن كان استاتيكيًا، أو ● إن كان مُسبق الرسم عبر generateStaticParams، أو ƒ إن كان يُرسم عند الطلب. مسار opengraph-image المعلَّم بالرمز ƒ يُرسم مع كل زيارة من برنامج زحف، وهذا يعني غالبًا أن دالة الصورة استخدمت واجهة وقت الطلب أو بيانات غير مخزّنة مؤقتًا.

بعد ذلك يعمل سكربت قصير اسمه check-og.mts على الخادم بعد تشغيله. لكل مسار يجلب HTML، ويطابق وسم meta الخاص بـ og:image، ويحوّل & إلى &، ويجلب رابط الصورة، ويفشل ما لم تكن الاستجابة ناجحة ونوع المحتوى يبدأ بـ image/. على Node.js 24 يعمل السكربت بالأمر node scripts/check-og.mts دون خطوة بناء، لأن Node يزيل أنواع TypeScript بشكل أصلي.

خاصية في هذا الفحص يسهل إغفالها: رابط og:image مطلق ومبني من metadataBase، لذلك فإن فحص نشر تجريبي (preview deployment) يجلب صورة الإنتاج ما لم يشِر SITE_URL إلى النشر التجريبي.

تخزّن منصات التواصل المعاينات مؤقتًا، لذلك فإن إصلاح الصورة لا يصلح الروابط التي شوركت سابقًا. تحتوي أداة Sharing Debugger من Facebook على زر Scrape Again، وتعيد أداة Post Inspector من LinkedIn جلب الرابط عند الطلب، أما التطبيقات الأخرى فتحدّث المعاينة وفق جدولها الخاص. كما أن صور Open Graph لا تغيّر ترتيب نتائج البحث، بل تغيّر شكل الرابط عند مشاركته، وهذا سبب كافٍ لاختبارها مثل أي مخرجات أخرى.

الأسئلة الشائعة

س: هل نحتاج ملف twitter-image منفصلًا في Next.js؟

ج: غالبًا لا. تعود منصة X إلى وسوم Open Graph عند غياب وسوم twitter:، لذلك يكفي ملف opengraph-image واحد للمنصتين. أضف twitter-image فقط عندما تحتاج X تصميمًا مختلفًا، واضبط twitter.card على summary_large_image للحصول على البطاقة الكبيرة.

س: هل يمكن استخدام display: grid في صورة Open Graph في Next.js؟

ج: ليس عبر next/og في Next.js 16.2.9. نسخة Satori 0.25.0 المضمّنة تعتمد على Yoga flexbox في التخطيط، لذلك يجب إعادة بناء تخطيطات grid بحاويات flex متداخلة، أو رسمها باستدعاء إصدار أحدث من حزمة satori مباشرة.

س: لماذا لا يُحمَّل الخط المخصّص في ImageResponse؟

ج: السبب المعتاد هو ملف بصيغة WOFF2، وهي صيغة لا تستطيع نسخة Satori المضمّنة في next/og ضمن Next.js 16.2.9 قراءتها. حوّل الخط إلى TTF أو OTF، واقرأه باستخدام readFile من مسار تحت process.cwd()، ومرّر إدخالًا واحدًا في fonts لكل وزن.

س: هل تُولَّد ملفات opengraph-image وقت البناء أم مع كل طلب؟

ج: وقت البناء افتراضيًا. يحسّن Next.js الصور المولّدة استاتيكيًا ما لم تستخدم دالة الصورة واجهة وقت الطلب أو بيانات غير مخزّنة مؤقتًا، وعندها يُرسم المسار عند الطلب ويعلّمه next build بالرمز ƒ.

س: هل تساعد صورة Open Graph في تحسين محركات البحث (SEO)؟

ج: ليس في الترتيب. يتحكّم وسم og:image في بطاقة المعاينة التي تعرضها Slack وLinkedIn وX وتطبيقات المراسلة للرابط المُشارَك، وهذا يؤثر في قرار الناس بالنقر عليه.