Связывание альтернативных языковых версий (hreflang)

Как сообщить поисковикам о локализованных страницах

Проблема

У приложения есть идентичный контент по адресам /en/page и /fr/page. Поисковые системы воспринимают их как две отдельные, конкурирующие страницы. Без механизма связывания их позиции в поиске разделяются, и пользователи из Франции могут увидеть английскую страницу в результатах поиска вместо французской.

Решение

Используйте свойство alternates внутри функции Next.js generateMetadata. Указав список всех доступных языков для страницы, Next.js автоматически сгенерирует теги <link rel="alternate" hreflang="..." /> в <head> документа, что даст понять поисковикам связь между этими страницами.

Шаги

1. Укажите базовый URL сайта

Теги hreflang требуют абсолютные, а не относительные 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. Добавьте alternates в generateMetadata

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

Этот код создает объект languages, который сопоставляет каждую локаль с её абсолютным базовым URL (например, en: 'https://www.example.com/en'). Также задается URL canonical для текущей страницы и URL x-default, который указывает поисковикам, какую версию показывать пользователям с неуказанным языком.

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