hreflang صحيح في Next.js — مُولَّد لا مُصان
تنشر معظم مواقع Next.js متعدّدة اللغات عنقود hreflang معطوبًا. والخلل واحد دائمًا — الوسوم تُكتب باليد.
مواصفة hreflang قصيرة وقواعدها بسيطة. ومع ذلك تكاد كل عملية تنفيذ ندقّقها تُخطئ فيها، والسبب الجذري هو نفسه دائمًا تقريبًا: أحدهم يصون البدائل يدويًا، ثم تتحرّك الترجمات ولا تتحرّك الوسوم.
القواعد الثلاث التي تُنتهك
1. كل صفحة في العنقود يجب أن تُدرج كل صفحات العنقود، بما فيها نفسها. فـhreflang ثنائي الاتجاه. إن أشارت صفحتك الإنجليزية إلى الألمانية ولم تُشر الألمانية إليها في المقابل، تُهمل Google العلاقة. الإشارة الذاتية ليست اختيارية.
2. لا تُعلن عن ترجمة غير موجودة. توجيه hreflang="de" إلى صفحة إنجليزية
لأن النسخة الألمانية «قادمة قريبًا» أسوأ من حذفه. أنت تخبر الزاحف أن
للناطقين بالألمانية صفحةً بالألمانية، ثم تخيّب ظنّهم.
3. الوسم x-default للاحتياط لا للإنجليزية. يشير إلى الصفحة التي تُقدَّم
لمن لا تستهدف لغتهم — عادةً أداة اختيار اللغة، أو لغتك الافتراضية إن لم تكن
لديك أداة.
لماذا تتعفّن الوسوم المصانة يدويًا دائمًا
تأمّل دورة الحياة. تُطلق بالإنجليزية والألمانية، فيكتب أحدهم البيانات الوصفية:
// The version that will be wrong within a month
alternates: {
languages: {
en: 'https://example.com/pricing',
de: 'https://example.com/de/pricing',
},
}ثم تُطلق الإسبانية، لكن لصفحات التسويق فقط. ثم يُسحب مقال ألماني من النشر. ثم يُعيد أحدهم تسمية slug. كلٌّ من هذه طلب دمج منفصل، في جزء مختلف من قاعدة الشيفرة، ولا يمسّ أيٌّ منها هذا الكائن.
وبعد ستة أشهر تُبلّغ Search Console عن «لا توجد وسوم عائدة» في بضع مئات من الروابط، ولا أحد يستطيع إعادة بناء السبب.
ولّدها من المحتوى لا من قائمة
الحل أن تجعل البدائل قيمة مشتقّة. في قاعدة شيفرة هذا الموقع — وهي عامة — تجيب طبقة المحتوى عن سؤال واحد:
/**
* Which locales actually have this document. Translations are published as
* they are written, so a page's hreflang cluster must reflect reality.
*/
export function availableLocalesFor(
collection: Collection,
slug: string,
): Locale[] {
return locales.filter((locale) =>
fs.existsSync(path.join(collectionDir(collection, locale), `${slug}.mdx`)),
);
}ويستهلكها مساعد البيانات الوصفية:
const languages: Record<string, string> = {};
for (const l of availableLocales) {
languages[hreflangMap[l]] = localizedUrl(l, path);
}
// `x-default` only makes sense when the default locale really has the page.
if (availableLocales.includes(defaultLocale)) {
languages['x-default'] = localizedUrl(defaultLocale, path);
}
return {
alternates: { canonical: localizedUrl(locale, path), languages },
// ...
};احذف ملف ترجمة فيختفي الوسم في البناء التالي. أضِف واحدًا فيظهر. لا توجد قائمة تنسى تحديثها، لأنه لا توجد قائمة أصلًا.
وتأتي الإشارة الذاتية مجانًا: اللغة الحالية داخل availableLocales بحكم
التعريف، فتُصدَر دائمًا.
رموز اللغات، ومتى تُضاف المنطقة
استخدم رمز اللغة مجرّدًا ما لم تكن تقدّم فعلًا محتوى مختلفًا لمناطق مختلفة:
de— الناطقون بالألمانية في أي مكان.de-AT— فقط إذا كان الزوّار النمساويون يرون أسعارًا أو توافرًا أو نصوصًا قانونية مختلفة عن الألمان.
رمز منطقة لا تستطيع تبريره يُشتّت إشاراتك بين عنقودين بلا فائدة. أما
الاستثناءات المعتادة فحقيقية: en-GB مقابل en-US للإملاء والعملة،
وes-MX مقابل es-ES للمفردات والأسعار، وpt-BR مقابل pt-PT.
والمنطقة وحدها ليست آلية استهداف جغرافي أبدًا. فـhreflang="de" لا تعني
«اعرضها في ألمانيا»، بل تعني «هذه الصفحة للناطقين بالألمانية».
العربية وسائر لغات الاتجاه من اليمين إلى اليسار
لا تحتاج لغات اليمين‑إلى‑اليسار شيئًا خاصًا من hreflang — فـar رمز لغة
كغيره. ما تحتاجه فعلًا هو dir صحيح على المستند، وهذا يجب أن يكون لكل لغة على
حدة:
export default async function LocaleLayout({ children, params }) {
const { locale } = await params;
return (
<html lang={locale} dir={dirFor(locale)}>
{/* ... */}
</html>
);
}ثم اكتب تخطيطك بخصائص CSS المنطقية — padding-inline-start بدل
padding-left، وmargin-inline-end بدل margin-right، وtext-align: start
بدل left. وأدوات Tailwind ps-* وpe-* وms-* وme-* تقابلها مباشرةً.
افعل ذلك من البداية فينعكس التخطيط العربي من تلقاء نفسه؛ وأجّله فتقضي أسبوعًا
في البحث عن المواضع الاثني عشر التي لم تنعكس.
أما الاستثناءات التي يجب ألا تنعكس فهي: كتل الشيفرة، وعناوين البريد،
والروابط، وأسماء العلامات التجارية بالحروف اللاتينية. اضبط عليها
direction: ltr صراحةً.
كيف تتحقّق
ثلاثة فحوص، بترتيب تصاعدي في الموثوقية:
- اعرض مصدر الصفحة. لا المفتّش — فالمفتّش يعرض DOM بعد الترطيب. الأمر
curl -s https://example.com/de/pricing | grep alternateيُظهر ما يستلمه الزاحف. - Search Console ← الاستهداف الدولي. يُبلّغ عن أخطاء «لا توجد وسوم عائدة» و«رمز لغة غير معروف» على مستوى الموقع كلّه.
- ازحف عليه. يرسم Screaming Frog أو Sitebulb العنقود كاملًا ويخبرك أي الصفحات تنقصها وسوم متبادلة. وهذا يلتقط الحالات التي تكون فيها الوسوم صحيحة فرادى بينما الرسم البياني ناقص.
وخريطة الموقع يجب أن توافق
يمكن أن يسكن hreflang في ترويسة HTML أو في خريطة الموقع. وإن أصدرتهما معًا — وينبغي أن تفعل — فيجب أن يتطابقا تمامًا، لأن أي تعارض بينهما يُحسم على نحو لا يمكن التنبّؤ به.
والمبدأ نفسه ينطبق: ولّد خريطة الموقع من المساعد نفسه الذي تستخدمه الصفحات. نتناول ذلك، مع صحّة الروابط الأساسية، في روابط أساسية وخرائط موقع لا يمكن أن تنحرف.
