Toolz 는 Next.js App Router 에서 9 개 언어로 실행되며,hreflang 설정은 제대로 진행되기 전에 세 가지 잘못된 버전을 거쳤습니다. 첫 번째는 모듈 범위를 넣었습니다 alternates 레이아웃의 객체,로케일에 따라 달라질 수 없으므로 모든 번역된 페이지는 영어 URL 을 표준으로 발표했습니다. That is the bug that deletes translations: it tells Google /es/pricing 의 중복입니다 /pricing 모든 페이지가 여전히 올바르게 렌더링되는 동안 번역 작업을 조용히 취소하는 색인을 생성해서는 안 됩니다.
두 번째 버전은 정경을 수정하고 아직 번역이 생성되지 않은 로케일을 포함하여 구성된 모든 로케일을 대체 로케일로 광고했습니다. 그래서 사이트는 스페인어 URL 아래에 영어 텍스트인 hreflang 클러스터 명명 페이지를 게시했습니다. 현재 실행 중인 세 번째 버전은 페이지당 실제 번역 범위에서 대체 세트를 파생합니다.
이 가이드는 작업 설정을 안내합니다: 로케일 구성, the generateMetadata 구현, 사이트맵 변형, 그리고 위의 세 가지 실수를 건너뛸 수 있습니다. 아래에 있습니다 완전한 흐레플랑 가이드 옆에 워드프레스 버전.
TL;DR: 앱 라우터에서 hreflang은 다음에서 나옵니다
alternates.languages에 의해 반환됨generateMetadata그리고 canonical 은 모듈 스코프에서 한번 선언하는 것이 아니라 active locale 로부터 빌드되어야 한다. build both from one locale map,emit the full recipractal set on every page in the group,and includex-default. 콘텐츠가 진정으로 존재하는 로케일만 광고하거나 번역되지 않은 페이지를 가리키는 클러스터를 게시합니다. The 흐레플랑 발전기 검증된 참조 세트에 대해 렌더링된 출력을 확인하는 데 유용합니다.
Next.js 는 상자에서 무엇을 제공합니까?
Metadata API 는 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 특별한 케이스가 없는 열쇠입니다 메타데이터 API 참조 문서. 그것이 하지 않는 것은 어떤 로케일이 집합에 속하는지 결정하거나,정규를 동기화하거나,변화할 수 없는 모듈 범위에 전체 객체를 넣는 것을 중지하는 것입니다. 그것들은 여러분이 바로 잡아야 하는 부분이고,깨지는 부분입니다.
또한 그 점에 유의하십시오 metadataBase 여기서 상대 URL 에 영향을 줍니다. Hreflang 은 절대 URL 이 필요하므로 둘 중 하나를 설정합니다 metadataBase 그리고 경로를 사용하거나 절대 URL 을 직접 빌드합니다. 구성된 원본에서 명시적으로 빌드하는 것을 선호합니다. 왜냐하면 프로덕션 URL 을 내보내는 미리보기 배포는 자체 문제 범주이기 때문입니다.
1단계: 하나의 로케일 맵, 단 하나
모든 다운스트림은 이것으로부터 읽습니다. 로케일은 세 가지 사실이 필요합니다: 경로 세그먼트,hreflang 에 대한 BCP 47 태그 및 <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}`
}
X-1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 10 code 그리고 the hreflang 값은 의도적으로 분리된 필드입니다. 경로 세그먼트는 de 아무도 원하지 않기 때문입니다 /de-DE/pricing URL과 hreflang 값은 다음과 같습니다 de-DE 왜냐하면 그것이 여러분이 발표하고 싶은 것이기 때문입니다. 그것들을 혼동하는 것은 추악한 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
}
거기에 있는 두 가지 결정은 훔칠 가치가 있습니다.
2개 미만의 지역에서 조기 복귀합니다. 영어로만 존재하는 페이지는 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 지도와 그 자체로 고유한 표준, 즉 정확히 모양입니다 흐레플랑과 정식 요구하다.
그룹 전체에서 지도가 동일하기 때문에,상호성은 규율에 의해서가 아니라 구조적으로 만족된다. 독일어 페이지에는 독일어,영어, 프랑스어,일본어, x-default; 가 나열된다. 영어 페이지도 마찬가지이다. 항목을 생략할 수 있는 페이지당 논리는 없다.
4단계: 존재하는 것만 광고하세요
이것은 대부분의 가이드가 건너뛰는 단계이며 최악의 설정 버전을 생성한 단계입니다.
번역이 비동기적으로 생성되거나 로케일이 절반으로 채워진 경우 구성된 로케일 목록과 번역된 콘텐츠가 다른 집합입니다. 구성된 목록을 광고한다는 것은 현지화된 URL 아래에 영어 텍스트인 페이지에 대해 hreflang 을 게시하는 것을 의미합니다. Google 은 주석을 따르고 독일어가 약속된 영어를 찾으며 한 번에 9 개의 로케일에 걸쳐 중복 콘텐츠를 제조했습니다.
수정 사항은 구성이 아닌 실제 적용 범위를 전달하는 것입니다:
// 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 을 페이지 헤드에서 벗어나게 하고 싶다면,같은 도우미가 사이트맵 경로를 피드한다. 1 <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>`
X-1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 10 xmlns:xhtml 선언 <urlset> 는 필수이며 잊기 쉽습니다; 그것 없이는 대체 항목이 무시됩니다. 구조에 유의하십시오: listing one <loc> 로케일당 파일에 로케일 수를 곱하고 모든 번역을 자체 표준과 경쟁하게 합니다. 동일한 검색 범위, 항목의 일부.
한 가지 방법을 선택하십시오. 동일한 URL 에 대해 헤드 태그와 사이트 맵 항목을 모두 내보내는 경우 표류하는 두 개의 소스가 있습니다. 우리의 것은 페이지의 헤드 태그이고 도구 카탈로그의 사이트 맵은 서로소 세트입니다.
기본 로케일에 URL 접두사가 있나요?
이것은 hreflang 세트의 모든 URL을 변경하는 라우팅 결정이므로 이후가 아닌 도우미를 작성하기 전에 만드십시오.
두 가지 전략이 일반적입니다. 필요에 따라 접두사 접두사가 붙지 않은 기본 로케일을 제공합니다 /pricing 그리고 다른 모든 것 /de/pricing. 항상 접두사 다음을 포함하여 세그먼트 아래의 모든 로케일을 제공합니다 /en/pricing, 와 함께 /pricing 리디렉션하기. Next.js 미들웨어는 이 두 가지를 모두 지원하며, next-intl은 이를 a로 노출합니다 localePrefix 설정.
필요에 따라 (/pricing, /de/pricing) |
항상 (/en/pricing, /de/pricing) |
|
|---|---|---|
| 기존 영어 URL | 보존된 | 모든 것이 리디렉션이 됩니다 |
| 기본 로케일에 대한 Hreflang | 접두사가 없는 URL의 포인트입니다 | 포인트 /en/... |
| x-기본 대상 | 접두사가 없는 어근이기도 합니다 en URL |
접두사 로케일을 하나 선택해야 합니다 |
| 코드의 대칭성 | localize() 기본 로케일 브랜치가 필요합니다 |
가지가 없습니다; 모든 지역은 균일합니다 |
나는 기존 사이트에서 필요에 따라 접두사를 사용합니다. 왜냐하면 대칭을 얻기 위해 모든 인덱스된 영어 URL을 다시 작성하는 것은 작은 깔끔함 승리에 큰 비용이 들고 트래픽이 가장 높은 페이지의 리디렉션은 무료가 아니기 때문입니다. 그린필드 빌드에서는 항상 접두사가 더 깨끗합니다: the localize 도우미는 특별한 경우에 패하고, 여부에 대한 질문은 없습니다 /pricing 그리고 /en/pricing 는 같은 페이지입니다.
어느 쪽이든 hreflang 에 중요한 것은 일관성입니다. 주석의 URL,정규의 URL,리디렉션 없이 200 을 제공하는 URL 은 동일한 문자열이어야 합니다. 안정적으로 끊어지는 한 가지 조합은 항상 - 접두사가 붙고 hreflang 은 여전히 접두사가 붙지 않은 URL 을 가리킵니다: 기본 로케일의 모든 항목은 리디렉션 이름을 지정하고 반환 태그는 사용자가 지정한 URL 이 아닌 대상에 있습니다.
어떤 라이브러리를 사용해야 할까요?
대부분의 앱 라우터 i18n 설정이 켜집니다 next-intl 또는 next-i18next그리고 둘 다 당신을 위해 hreflang을 생성하지 않습니다. 그들은 라우팅과 메시지 로딩을 해결합니다; 주석은 여전히 당신이 방출할 것입니다. 위의 도우미는 20줄이고 어쨌든 당신은 그것을 자신의 통제하에 두기를 원하기 때문에 괜찮습니다.
| 우려 | 도서관에서 취급하는 | 당신의 |
|---|---|---|
| 로케일 라우팅 및 미들웨어 | 예 | |
| 메시지 카탈로그 및 대체 | 예 | |
<html lang> |
보통 | hreflang 값과 일치하는지 확인합니다 |
alternates.canonical 로케일당 |
아니요 | 활성 로케일에서 빌드합니다 |
alternates.languages |
아니요 | 로케일 맵에서 빌드합니다 |
x-default |
아니요 | 도우미에 추가 |
도서관별로 확인해야 할 한 가지는 다음과 같습니다 <html lang>. 문서 언어와 모순되는 구조화된 데이터와 흐레플랑은 없는 것보다 나쁩니다. 그리고 하드코딩되는 레이아웃입니다 lang="en" 독일어를 섬기는 동안에는 놀라울 정도로 흔한 남은 음식이 있습니다.
어떻게 검증하는가?
실제 출력을 빌드하고 읽으십시오, 템플릿이 증거가 아니기 때문에:
next build && next start
curl -s http://localhost:3000/de/preise | grep -E 'rel="(canonical|alternate)"'
그 출력에 세 가지 검사. canonical 는 당신이 가져온 URL 입니다. 로케일 플러스 하나 당 정확히 하나의 항목이 있습니다 x-default. 모든 href 절대적이며 페이지가 제공되는 URL과 일치하며 후행 슬래시가 포함됩니다.
그런 다음 형제자매를 데려와 두 사람을 구별합니다 languages 블록. 그들은 바이트 동일해야; 만 canonical 다릅니다. 동일하지 않은 경우,페이지의 무언가가 그룹에서가 아니라 현재 로케일에서 세트를 구축하고있다,이는 가장 일반적인 변장의 상호 실패입니다.
마지막으로 세트를 에 붙여넣습니다 hreflang 태그 생성기 코드 자체의 유효성을 검사하기 위해. ISO 모양에 대해 각 값을 검사하고 인식할 수 없는 하위 태그를 플래그 지정하고 중복을 포착하고 집합에 대체 기능이 없을 때 경고합니다. 클라이언트 측을 실행하므로 스테이징 URL 구조는 비공개로 유지됩니다. The 도구 가이드 워크플로를 걷고, 그리고 12 일반적인 hreflang 오류 마크업이 실행되면 찾아야 할 사항을 다룹니다.
자주 묻는 질문
Next.js App Router 에서 hreflang 태그를 어떻게 추가합니까?
반환 alternates.languages 오브젝트 from generateMetadata, 각 BCP 47 태그를 절대 URL에 매핑하고 설정합니다 alternates.canonical 활성 로케일에서. Next.js 는 둘 다 헤드로 렌더링합니다.
왜 generateMetadata 내부에 canonical 을 빌드해야 하는가?
로케일에 따라 달라져야 하기 때문입니다. 모듈 범위 canonical 은 할 수 없으므로,모든 번역된 페이지는 기본-로케일 URL 을 canonical 로 선언하고,이는 인덱스에서 번역을 제거합니다.
next-intl은 hreflang 태그를 자동으로 생성합니까?
No. next-intl 은 라우팅과 메시지 로딩을 처리합니다. hreflang 주석과 per-locale canonical 은 여전히 여러분이 방출할 수 있는 것입니다 generateMetadata.
Next.js 에서 x-default 를 어떻게 추가하나요?
추가 'x-default' 열쇠 alternates.languages 폴백 URL 을 가리키는 개체. Next.js 는 특별한 처리 없이 키를 통과합니다. 어떤 템플릿도 잊어버릴 수 없도록 세트를 빌드하는 도우미 안에 추가합니다.
Hreflang은 Next.js의 머리 또는 사이트 맵에 들어가야합니까?
둘 중 하나는 작동하지만 동일한 URL 에 대해 둘 다 작동하지 않습니다. Head tags via generateMetadata 디버그하기가 더 쉽습니다. 사이트맵 경로는 큰 카탈로그에 적합하고 페이지 헤드를 기울게 유지합니다; 기억하세요 xmlns:xhtml 선언 <urlset>.
로케일이 부분적으로만 번역되면 어떻게 되나요?
해당 페이지에 대한 콘텐츠가 실제로 존재하는 로케일만 광고하고 번역되지 않은 페이지를 표시하십시오 noindex, follow. 구성된 로케일 목록을 광고하면 번역되지 않은 페이지를 가리키는 클러스터가 게시됩니다.
Next.js 의 hreflang URL 은 절대적이어야 합니까?
예. Hreflang 은 완전한 자격을 갖춘 URL 이 필요합니다. 어느 쪽이든 설정됩니다 metadataBase 또는 구성된 원본에서 URL을 빌드하여 미리보기 배포가 프로덕션 URL을 내보내지 않도록 합니다.
렌더링된 hreflang 출력을 어떻게 확인하나요?
프로덕션 빌드를 실행하고 두 형제 페이지를 가져와 비교합니다. The languages 블록은 그룹 전체에서 바이트 동일해야 하며 각 표준은 자체 URL을 가리킵니다.



