대체 언어 버전 연결하기 (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'; // Your production 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;

  // Create language alternates
  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}`,
      },
    },
  };
}

// Rest of your layout component
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'; // The path for this page

  // Create language alternates
  const alternatesMap = locales.reduce((acc, locale) => {
    acc[locale] = `${siteBaseUrl}/${locale}${path}`;
    return acc;
  }, {} as Record<string, string>);

  return {
    title: 'About Us', // Add your translated title
    alternates: {
      canonical: `${siteBaseUrl}/${lang}${path}`,
      languages: {
        ...alternatesMap,
        'x-default': `${siteBaseUrl}/${defaultLocale}${path}`,
      },
    },
  };
}

export default function AboutPage() {
  return <div>About page content</div>;
}