Toolz פועל על נתב האפליקציות Next.js בתשע שפות, והגדרת hreflang עברה שלוש גרסאות שגויות לפני שהיא הלכה כמו שצריך. הראשון שם מודול-סקופ alternates אובייקט בפריסה, שאינו יכול להשתנות לפי מקום, אז כל דף מתורגם הכריז על כתובת האתר באנגלית כקנונית שלו. זה הבאג שמוחק תרגומים: הוא אומר לגוגל /es/pricing הוא שכפול של /pricing ואסור להוסיף לאינדקס, מה שמבטל בשקט את עבודת התרגום בזמן שכל עמוד עדיין מציג נכון.
הגרסה השנייה תיקנה את הקנוני ופרסמה כל מקום מוגדר כחלופה, כולל מקומות שתרגומם עדיין לא נוצר. אז האתר פרסם אשכול hreflang ששם דפים שהיו טקסט באנגלית תחת כתובת URL ספרדית. הגרסה השלישית, זו שפועלת כעת, שואבת את הסט החלופי מכיסוי התרגום בפועל, לכל עמוד.
מדריך זה עובר במערך העבודה: תצורת המקום, ה generateMetadata יישום, גרסת מפת האתר, ושלוש הטעויות למעלה כדי שתוכל לדלג עליהן. זה יושב מתחת ל מדריך hreflang שלם לצד ה גרסת וורדפרס.
TL;DR: בנתב האפליקציות, hreflang מגיע מ
alternates.languagesהוחזר עיgenerateMetadata, והקנוני חייב להיבנות מהמקום הפעיל ולא להכריז פעם אחת בהיקף המודול. בנה את שניהם ממפת מקום אחת, פלט את הסט ההדדי המלא בכל עמוד בקבוצה, וכלולx-default. פרסם רק אזורים שהתוכן שלהם קיים באמת, או שאתה מפרסם אשכול המצביע על דפים לא מתורגמים. ה גנרטור hreflang שימושי לבדיקת הפלט המעובד מול ערכת התייחסות מאומתת.
מה Next.js נותן לך מהקופסה?
ה-API של Metadata תומך ישירות ב-hreflang. חוזר alternates.languages מ generateMetadata מייצר את <link rel="alternate" hreflang> תגיות בראש:
export async function generateMetadata({ params }) {
const { locale } = await params
return {
alternates: {
canonical: 'https://example.com/de/preise',
languages: {
'en-US': 'https://example.com/pricing',
'de-DE': 'https://example.com/de/preise',
'x-default': 'https://example.com/pricing',
},
},
}
}
Next.js הופך אותם לראש ומטפל ב x-default מפתח ללא מעטפת מיוחדת, כמו שלו הפניה ל-Metadata API מסמכים. מה שהיא לא עושה זה להחליט אילו מקומות שייכים לסט, לשמור את הקנוני מסונכרן, או לעצור אותך לשים את כל האובייקט בהיקף מודול שבו הוא לא יכול להשתנות. אלה החלקים שאתה צריך לתקן, והם החלקים שנשברים.
שים לב גם ש metadataBase משפיע על כתובות URL יחסיות כאן. Hreflang דורש כתובות URL מוחלטות, אז כל אחד מהקבוצות metadataBase ולהשתמש בנתיבים, או לבנות כתובות URL מוחלטות בעצמך אני מעדיף לבנות אותם במפורש ממקור מוגדר, כי פריסת תצוגה מקדימה שפולטת כתובות URL של ייצור היא קטגוריית הבעיה שלה.
שלב 1: מפת מקום אחת, ורק אחת
הכל במורד הזרם קורא מזה. מקום צריך שלוש עובדות: קטע המסלול, תג BCP 47 עבור hreflang ו <html lang>, והאם זה נשלח כרגע.
// i18n/locales.ts
export const DEFAULT_LOCALE = 'en'
export const LOCALES = [
{ code: 'en', hreflang: 'en-US', label: 'English' },
{ code: 'de', hreflang: 'de-DE', label: 'Deutsch' },
{ code: 'fr', hreflang: 'fr-FR', label: 'Français' },
{ code: 'ja', hreflang: 'ja-JP', label: '日本語' },
]
/** '/pricing' -> '/de/pricing', and '/pricing' for the default locale. */
export function localize(path: string, locale: string): string {
return locale === DEFAULT_LOCALE ? path : `/${locale}${path}`
}
ה code וה hreflang ערך הם שדות נפרדים בכוונה קטע המסלול הוא de כי אף אחד לא רוצה /de-DE/pricing בכתובות ה-URL שלהם, וערך hreflang הוא de-DE כי זה מה שאתה רוצה להכריז. Conflating אותם פירושו או כתובות URL מכוערות או חשוף de בביאור, וברגע שתוסיף pt-BR לצד pt-PT הקונפלציה מפסיקה לעבוד בכלל.
שלב 2: עוזר שבונה את הסט
פונקציה אחת, המשמשת כל עמוד, כך שהצורה לא יכולה להיסחף בין מסלולים:
export function hreflangAlternates(path: string, locales: string[], base = SITE_URL) {
const eligible = LOCALES.filter((l) => locales.includes(l.code))
// A single-entry cluster is not a cluster: emit nothing rather than
// advertise a one-sided relationship.
if (eligible.length < 2) return []
const alts = eligible.map((l) => ({
hreflang: l.hreflang,
href: base + localize(path, l.code),
}))
alts.push({ hreflang: 'x-default', href: base + localize(path, DEFAULT_LOCALE) })
return alts
}
שתי החלטות שם בפנים שוות גניבה.
החזרה המוקדמת על פחות משני אזורים. דף שקיים רק באנגלית לא אמור לפלוט hreflang כלל ביאור הפניה עצמית יחיד אינו שגוי בדיוק, אבל זה רעש, והגרסה של קוד זה שפלטה אותו גרמה לכל עמוד באנגלית בלבד להיראות כמו אשכול שבור בדוחות סריקה.
x-default מצורף על ידי העוזר, לא על ידי המתקשר. כל מימוש שראיתי שמשאיר את החזרה למתקשר מסתיים בתבנית אחת ששכחה אותו. מקפלים אותו לתוך הדבר שבונה את הסט ואי אפשר לשכוח אותו.
שלב 3: חוט אותו ל-generateMetadata
// app/[locale]/pricing/page.tsx
import { localize, hreflangAlternates, SHIPPING_LOCALES } from '@/i18n/locales'
import { SITE_URL } from '@/lib/site'
export async function generateMetadata({ params }) {
const { locale } = await params
const path = '/pricing'
const alternates = hreflangAlternates(path, SHIPPING_LOCALES)
return {
alternates: {
// Built from the ACTIVE locale. This is the line that matters.
canonical: `${SITE_URL}${localize(path, locale)}`,
...(alternates.length
? { languages: Object.fromEntries(alternates.map((a) => [a.hreflang, a.href])) }
: {}),
},
}
}
הקנוני הוא הקו לבהות בו זה חייב להיות פונקציה של locale, מה שאומר שהוא לא יכול לחיות בקבוע של מודול-סקופ, לא יכול לחיות בפריסת השורש, ולא ניתן לשתף אותו על פני קטע המקום. כל עמוד בקבוצה מסתיים בדף זהה languages מפה וקנוני ייחודי לעצמו, שזו בדיוק הצורה hreflang וקנוני לדרוש.
מכיוון שהמפה זהה בכל הקבוצה, ההדדיות מסופקת מבחינה מבנית ולא על ידי משמעת. הדף הגרמני מפרט גרמנית, אנגלית, צרפתית, יפנית ו-x-default; כך גם הדף באנגלית. אין היגיון לכל עמוד שיכול להשמיט ערך.
שלב 4: לפרסם רק את מה שקיים
זהו השלב שרוב המדריכים מדלגים עליו, והוא זה שיצר את הגרסה הגרועה ביותר של ההגדרה שלנו.
אם התרגומים שלך נוצרים באופן אסינכרוני, או שמקום מאוכלס למחצה, רשימת המקומות המוגדרים והתוכן המתורגם הם קבוצות שונות. פרסום הרשימה המוגדרת פירושו פרסום hreflang עבור דפים שהם טקסט באנגלית תחת כתובת URL מקומית. גוגל עוקבת אחר ההערה, מוצאת אנגלית במקום שבו הובטחה גרמנית, וייצרת תוכן כפול בתשעה אזורים בו-זמנית.
התיקון הוא להעביר כיסוי אמיתי ולא את התצורה:
// Coverage is per-page, not global: this page might be translated
// into three languages while the one next to it has none.
const locales = await localesForPage(path)
const alternates = hreflangAlternates(path, locales)
חבר את זה עם robots: { index: false, follow: true } בדפים שקיימים במקום אך אין להם עדיין עותק מתורגם. הם נשארים נגישים לכל מי שמגיע מהניווט שלך, והם נשארים מחוץ לאינדקס עד שהעותק נוחת. שניהם יורדים אוטומטית ברגע שהכיסוי מתמלא, ללא פריסת מעקב.
שלב 5: גרסת מפת האתר
אם אתה מעדיף לשמור על hreflang מחוץ לראשי הדפים, אותו עוזר מזין מסלול מפת אתר. אחד <loc> לכל פיסת תוכן עם מחליפים כילדים, ולא אחד <loc> לכל מקום:
// app/sitemap-pages.xml/route.ts
const entries = PAGES.map((path) => {
const alts = hreflangAlternates(path, SHIPPING_LOCALES)
const links = alts
.map((a) => `\n <xhtml:link rel="alternate" hreflang="${a.hreflang}" href="${a.href}"/>`)
.join('')
return ` <url>\n <loc>${SITE_URL}${path}</loc>${links}\n </url>`
})
const xml =
`<?xml version="1.0" encoding="UTF-8"?>\n` +
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` +
`xmlns:xhtml="http://www.w3.org/1999/xhtml">\n${entries.join('\n')}\n</urlset>`
ה xmlns:xhtml הצהרה על <urlset> נדרש וקל לשכוח; בלעדיו מתעלמים מהערכים החלופיים. שימו לב למבנה: רישום אחד <loc> לכל מקום מכפיל את הקובץ בספירת המקום ומכניס כל תרגום לתחרות עם הקנוני שלו. אותו כיסוי גילוי, חלק מהערכים.
בחר שיטה אחת אם אתה פולט גם תגי ראש וגם ערכי מפת אתר עבור אותן כתובות אתרים, יש לך שני מקורות שייסחפו. שלנו הם תגי ראש לדפים ומפת אתר מתחלפים לקטלוג הכלים, שהם סטים מפורקים.
האם מקום ברירת המחדל מקבל קידומת כתובת URL?
זוהי החלטת ניתוב שמשנה כל כתובת URL בערכת ה-hreflang שלך, אז בצע אותה לפני שאתה כותב את העוזר ולא אחריו.
שתי אסטרטגיות נפוצות. קידומת לפי הצורך משרת את מיקום ברירת המחדל ללא קידומת ב /pricing וכל השאר ב /de/pricing. תמיד קידומת משרת כל מקום תחת קטע, כולל /en/pricing, עם /pricing ניתוב מחדש. תוכנת הביניים Next.js תומכת בשניהם, ו-next-intl חושפת אותה כ-a localePrefix הגדרה.
לפי הצורך (/pricing, /de/pricing) |
תמיד (/en/pricing, /de/pricing) |
|
|---|---|---|
| כתובות URL קיימות באנגלית | נשמר | כל אחד הופך להפניה מחדש |
| hreflang עבור מקום ברירת המחדל | נקודות בכתובת האתר הלא קבועה | נקודות ב /en/... |
| יעד x-default | השורש הלא מקודם, שהוא גם ה en כתובת אתר |
חייב לבחור מקום אחד עם קידומת |
| סימטריה בקוד | localize() צריך סניף ברירת מחדל |
אין סניף; כל מקום אחיד |
אני משתמש בקידומת לפי הצורך באתר קיים, מכיוון ששכתוב כל כתובת URL באנגלית באינדקס כדי להשיג סימטריה היא עלות גדולה עבור זכייה קטנה בסדר, והפניות מחדש בדפים בעלי התנועה הגבוהה ביותר שלך אינן בחינם. על מבנה ירוק, תמיד קידומת נקייה יותר: ה localize עוזר מפסיד במקרה המיוחד שלו, ואף פעם אין שאלה אם /pricing ו /en/pricing הם אותו עמוד.
מה שחשוב עבור hreflang בכל מקרה הוא עקביות. כתובת האתר בביאור, כתובת האתר בקנוניקל וכתובת האתר שמשרתת 200 ללא הפניה מחדש חייבות להיות אותה מחרוזת. השילוב היחיד שנשבר בצורה מהימנה הוא תמיד קידומת עם hreflang שעדיין מצביע על כתובות האתרים הלא קבועות: כל ערך עבור אזור ברירת המחדל שם הפניה מחדש, ותג ההחזרה חי ביעד ולא בכתובת האתר שציינת.
באיזו ספריה כדאי להשתמש?
רוב הגדרות App Router i18n מסתיימות next-intl או next-i18next, ואף אחד מהם לא מייצר עבורך hreflang. הם פותרים ניתוב וטעינת הודעות; ההערות עדיין שלך לפלוט. זה בסדר, כי העוזר למעלה הוא עשרים שורות ואתה רוצה את זה בשליטה שלך בכל מקרה.
| דאגה | מטופל על ידי הספרייה | שלך |
|---|---|---|
| ניתוב מקומי ותוכנות ביניים | כן | |
| קטלוגים של הודעות ו-fallback | כן | |
<html lang> |
בדרך כלל | ודא שהוא תואם לערך hreflang |
alternates.canonical לכל מקום |
לא | בנה מהמקום הפעיל |
alternates.languages |
לא | בנה ממפת המקום |
x-default |
לא | תצרף בעוזר |
הדבר היחיד הספציפי לספרייה שצריך לבדוק הוא <html lang>. נתונים מובנים ו-hreflang הסותרים את שפת המסמכים גרועים מאף אחד, ופריסה המקודדת lang="en" בזמן הגשת גרמנית היא שארית נפוצה באופן מפתיע.
איך מאמתים את זה?
בנה וקרא את הפלט בפועל, כי התבנית אינה הראיה:
next build && next start
curl -s http://localhost:3000/de/preise | grep -E 'rel="(canonical|alternate)"'
שלוש בדיקות על הפלט הזה הקנוני הוא כתובת האתר שהבאת יש בדיוק ערך אחד לכל מקום פלוס אחד x-default. כֹּל href הוא מוחלט ותואם את כתובת האתר שבה מוגש הדף, כולל קו נטוי נגרר.
ואז תביא אח ותבדל את השניים languages בלוקים. הם צריכים להיות זהים בתים; רק הקנוני שונה. אם הם לא זהים, משהו בדף שלך בונה את הסט מהמקום הנוכחי ולא מהקבוצה, שהוא כשל ההדדיות בתחפושת הנפוצה ביותר שלו.
לבסוף, הדבק את הסט לתוך מחולל תג hreflang כדי לאמת את הקודים עצמם. הוא בודק כל ערך מול צורת ה-ISO, מסמן תגי משנה לא מזוהים, תופס כפילויות ומזהיר כאשר לסט אין נפילה. הוא פועל בצד הלקוח, כך שמבנה כתובת URL של בימוי נשאר פרטי. ה מדריך כלים עובר בזרימת העבודה, ו 12 שגיאות hreflang נפוצות מכסה מה לחפש ברגע שהסימון פעיל.
שאלות נפוצות
כיצד אוכל להוסיף תגי hreflang בנתב האפליקציות של Next.js?
להחזיר an alternates.languages חפץ מ generateMetadata, מיפוי כל תג BCP 47 לכתובת האתר המוחלטת שלו, והגדר alternates.canonical מהמקום הפעיל. Next.js מציג את שניהם לתוך הראש.
מדוע יש לבנות את הקנוני בתוך generateMetadata?
כי זה צריך להשתנות לפי מקום. canonical של מודול-היקף לא יכול, ולכן כל דף מתורגם יכריז על כתובת ה-URL המקומית כברירת מחדל כקנונית, מה שמסיר את התרגום מהאינדקס.
האם next-intl מייצר תגי hreflang באופן אוטומטי?
מס 'next-intl מטפל ניתוב וטעינת הודעות. hreflang ההערות ואת per-locale קנוני הם עדיין שלך לפלוט מ generateMetadata.
כיצד אוכל להוסיף x-default ב-Next.js?
הוסף an 'x-default' מפתח ל alternates.languages אובייקט מצביע על כתובת האתר שלך fallback.Next.js מעביר את המפתח ללא טיפול מיוחד. הוסף אותו בתוך העוזר שבונה את הסט כך שאף תבנית לא תוכל לשכוח אותו.
האם hreflang צריך להיכנס לראש או למפת האתר ב-Next.js?
כל אחד מהם עובד, אבל לא שניהם עבור אותן כתובות URL.תגיות ראש באמצעות generateMetadata קל יותר לנפות באגים מסלול מפת אתר מתאים לקטלוגים גדולים ושומר על ראשי עמודים רזים; זכור את xmlns:xhtml הצהרה על <urlset>.
מה אם מקום מתורגם רק בחלקו?
פרסם רק את המקומות שתוכנם קיים באמת עבור אותו עמוד, וסמן את הדפים הלא מתורגמים noindex, follow. פרסום רשימת המקומות המוגדרת מפרסם אשכול המצביע על דפים לא מתורגמים.
האם כתובות URL של hreflang ב-Next.js צריכות להיות מוחלטות?
כן. Hreflang דורש כתובות URL מוסמכות לחלוטין. כל אחד מהם מוגדר metadataBase או לבנות את כתובות האתרים ממקור מוגדר כך פריסות תצוגה מקדימה לא פולטות כתובות URL ייצור.
כיצד אוכל לבדוק את פלט ה-hreflang המעובד?
הפעל בניית הפקה, הבא שני דפי אחים והשווה. ה languages בלוקים צריכים להיות זהים בתים בכל הקבוצה בעוד שכל אחד מהם מצביע בכתובת האתר שלו.



