대체 언어 버전 연결하기 (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. generateMetadataalternates 추가하기

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>;
}