قد تكون كل صفحة في موقعك على Next.js تشارك رابط الصفحة الرئيسية
يدمج Next بيانات الصفحة الوصفية في بيانات التخطيط، وopenGraph هو موضع الخلل. ثلاثة أعطال في البيانات الوصفية لا تراها من داخل موقعك، والمساعد الواحد الذي يغلقها جميعاً.
ظل هذا غير مرئي من داخل الموقع أسابيع. كل صفحة تُعرض بشكل صحيح، وكل عنوان صحيح في تبويب المتصفح، ولا شيء في البناء يشتكي. ولم يظهر العطل إلا حين لصق أحدهم رابطاً في Slack.
الدمج يجري لكل حقل، وopenGraph حقل
يدمج Next بيانات المسار الوصفية في بيانات تخطيطه الأب. وكلمة دمج تؤدي عملاً صامتاً كثيراً، لأن الدمج يقع عند المستوى الأعلى من الكائن لا داخله. فالصفحة التي تصدّر عنواناً ووصفاً فقط ترث كامل كائن openGraph من التخطيط كما كُتب، بما فيه og:url.
وعلى موقعنا عنى ذلك أن كل صفحة خدمة وكل دراسة حالة تنشر og:url يشير إلى الصفحة الرئيسية، مع og:title وog:description الخاصين بها. وعند المشاركة على LinkedIn أو Slack، كانت دراسة الحالة تعرض صورتها فوق عنوان الرئيسية ورابط الرئيسية. الصورة كانت صحيحة، ولهذا تحديداً لم ينظر أحد مرتين. فتلك الصور تُولَّد وقت البناء، فكانت الجزء الوحيد الذي لم يشك فيه أحد.
الإصلاح الذي يزيد الطين بلة
الرد البديهي هو تجاوز openGraph في الصفحة وضبط العنوان الصحيح. وهذا ما فعلته مقالات مدونتنا، فأدخل عطلاً ثانياً: التجاوز يستبدل الكائن كله لا يدمج فيه، فأصلحت تلك الصفحات og:title وأسقطت og:url بالكامل.
// layout.tsx
export const metadata = {
openGraph: {
url: "https://example.com/en",
title: "Homepage title",
description: "Homepage description",
images: ["/en/opengraph-image"],
},
};
// page.tsx — inherits the layout's ENTIRE openGraph object.
// og:url is the homepage. So is og:title.
export const metadata = {
title: "Case study",
description: "What we built",
};
// page.tsx — the "fix". Replaces the object outright:
// og:url and og:images are now gone.
export const metadata = {
openGraph: { title: "Case study" },
};اضبط الأربعة معاً، في موضع واحد
النسخة الوحيدة التي تبقى مُصلَحة هي مساعد واحد يستدعيه كل مسار، ويصدر دائماً العنوان والوصف والرابط المعياري وكائن openGraph كاملاً معاً. ليس لأن المساعد أنظف، بل لأن نمط الفشل هو كائن جزئي، والمساعد وحده يجعل الكائن الجزئي غير قابل للتمثيل.
وما دمت هناك: كفّ عن كتابة الأوصاف مرتين
العطل القريب هو سلسلة واحدة تؤدي وظيفتين. فقرة خدمة كُتبت لتُقرأ على الصفحة بلغت 231 محرفاً كوصف وصفي، فقطعتها Google في منتصف الجملة. والحل ليس تقصير الفقرة، بل اشتقاق المقتطف منها: أكبر عدد من الجمل الكاملة التي تتسع، فينتهي حيث انتهى الكاتب لا عند عدّ محارف.
const MAX_DESC = 158;
export function metaDescription(text: string, max = MAX_DESC): string {
const clean = text.replace(/\s+/g, " ").trim();
if (clean.length <= max) return clean;
// Keep the ender attached to the sentence it closes. The Arabic full
// stop and question mark are in the class because the same helper runs
// on all three locales, and a Latin-only split silently returns the
// whole paragraph on an Arabic page.
const sentences = clean.match(/[^.!?۔؟]+[.!?۔؟]*\s*/g) ?? [clean];
let out = "";
for (const s of sentences) {
if (out && (out + s).trim().length > max) break;
out += s;
}
out = out.trim();
if (out) return out;
// One sentence longer than the limit: trim at the last whole word.
const cut = clean.slice(0, max - 1);
return cut.slice(0, cut.lastIndexOf(" ")).trim() + "…";
}والعناوين تريد المعاملة نفسها لسبب مختلف. فإلحاق لاحقة العلامة بلا تمييز أعطانا About | The team behind Growna | Growna في اثنتي عشرة صفحة، لأن عدة عناوين كانت تحمل العلامة أصلاً. وحين لا تتسع اللاحقة داخل الستين محرفاً التي تعرضها Google تقريباً، فإسقاطها أفضل من بترها: اسم علامة مقطوع في آخر النتيجة يُقرأ أسوأ من غياب الاسم، والنطاق معروض بجواره في الحالتين.
كيف تتحقق فعلاً
لا شيء من هذه مرئي في المتصفح، وهذا سبب بقائها كله. اجلب HTML الخام واقرأ الوسوم. وافعل ذلك لمسار فرعي لا للصفحة الرئيسية، لأن الرئيسية هي الصفحة الوحيدة التي يصادف أن وراثة openGraph من التخطيط فيها صحيحة.
# Read what you actually ship, from a child route.
curl -s https://example.com/en/work/some-case-study \
| grep -oE '<meta property="og:[^"]*" content="[^"]*"|<link rel="canonical"[^>]*>'
# What you want to see:
# og:url the CHILD url, not the homepage
# og:title the child's own title
# og:image the child's own image
# canonical self-referencing, and matching og:url
