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

Информирование поисковых систем о ваших локализованных страницах

Проблема

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

Решение

Используйте свойство alternates в функции generateMetadata в Next.js. Указав список всех доступных языков для данной страницы, 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'; // Ваш 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;

  // Создайте карту языковых альтернатив
  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}`,
      },
    },
  };
}

// Остальная часть вашего компонента layout
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 для текущей страницы и 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'; // Путь для этой страницы

  // Создание языковых альтернатив
  const alternatesMap = locales.reduce((acc, locale) => {
    acc[locale] = `${siteBaseUrl}/${locale}${path}`;
    return acc;
  }, {} as Record<string, string>);

  return {
    title: 'О нас', // Добавьте переведённый заголовок
    alternates: {
      canonical: `${siteBaseUrl}/${lang}${path}`,
      languages: {
        ...alternatesMap,
        'x-default': `${siteBaseUrl}/${defaultLocale}${path}`,
      },
    },
  };
}

export default function AboutPage() {
  return <div>Содержимое страницы "О нас"</div>;
}