대체 언어 버전 연결하기 (hreflang)
검색 엔진에 로컬라이즈된 페이지 알리기
문제
애플리케이션에 /en/page와 /fr/page에서 동일한 콘텐츠를 제공하고 있습니다. 검색 엔진은 이를 두 개의 별도 경쟁 페이지로 인식합니다. 이들을 연결하는 메커니즘이 없으면 검색 순위가 분산되고, 프랑스 사용자들에게 프랑스어 페이지 대신 영어 페이지가 검색 결과에 표시될 수 있습니다.
해결책
Next.js의 generateMetadata 함수 내에서 alternates 속성을 사용하세요. 특정 페이지에 사용 가능한 모든 언어 목록을 제공하면, Next.js는 자동으로 문서 <head>에 <link rel="alternate" hreflang="..." /> 태그를 생성하여 검색 엔진에 이러한 페이지 간의 관계를 알립니다.
단계
1. 사이트의 기본 URL 정의하기
hreflang 태그는 상대 URL이 아닌 절대 URL이 필요합니다. 사이트의 표준 기본 URL을 구성 파일에 저장하세요.
// i18n-config.ts
export const locales = ['en', 'es', 'fr'];
export const defaultLocale = 'en';
export const siteBaseUrl = 'https://www.example.com'; // 프로덕션 URL
2. generateMetadata에 alternates 추가하기
app/[lang]/layout.tsx(모든 페이지에 적용) 또는 특정 페이지 파일에서 generateMetadata 함수를 내보내세요.
// app/[lang]/layout.tsx
import { locales, siteBaseUrl } from '@/i18n-config';
import type { Metadata } from 'next';
type Props = {
params: { lang: string };
children: React.ReactNode;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang } = params;
// 언어 대체 항목 생성
const alternatesMap = locales.reduce((acc, locale) => {
acc[locale] = `${siteBaseUrl}/${locale}`;
return acc;
}, {} as Record<string, string>);
return {
alternates: {
canonical: `${siteBaseUrl}/${lang}`,
languages: {
...alternatesMap,
'x-default': `${siteBaseUrl}/${defaultLocale}`,
},
},
};
}
// 레이아웃 컴포넌트의 나머지 부분
export default function RootLayout({ children, params }: Props) {
return (
<html lang={params.lang}>
<body>{children}</body>
</html>
);
}
이 코드는 각 로케일을 해당 절대 기본 URL(예: en: 'https://www.example.com/en')에 매핑하는 languages 객체를 생성합니다. 또한 현재 페이지의 canonical URL과 지정되지 않은 언어의 사용자에게 어떤 버전을 표시할지 검색 엔진에 알려주는 x-default URL을 설정합니다.
3. 중첩된 페이지에서 alternates 처리하기
'/about'과 같은 중첩된 페이지의 경우, 메타데이터 함수에 전체 경로를 포함해야 합니다.
// app/[lang]/about/page.tsx
import { locales, siteBaseUrl, defaultLocale } from '@/i18n-config';
import type { Metadata } from 'next';
type Props = {
params: { lang: string };
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang } = params;
const path = '/about'; // 이 페이지의 경로
// 언어 대체 항목 생성
const alternatesMap = locales.reduce((acc, locale) => {
acc[locale] = `${siteBaseUrl}/${locale}${path}`;
return acc;
}, {} as Record<string, string>);
return {
title: 'About Us', // 번역된 제목 추가
alternates: {
canonical: `${siteBaseUrl}/${lang}${path}`,
languages: {
...alternatesMap,
'x-default': `${siteBaseUrl}/${defaultLocale}${path}`,
},
},
};
}
export default function AboutPage() {
return <div>About page content</div>;
}