Как связать альтернативные языковые версии в React Router v7

Связывайте языковые альтернативы для поисковых систем

Проблема

Когда сайт предоставляет один и тот же контент на нескольких языках, поисковые системы сталкиваются с трудностью. Без явных сигналов они воспринимают каждую языковую версию как отдельную, не связанную страницу. Например, франкоязычный пользователь, ищущий на французском, может увидеть англоязычную версию выше в выдаче, даже если французская версия тоже есть. Аналогично, англоязычный пользователь может попасть на немецкую страницу. Это происходит потому, что поисковые системы не могут автоматически определить, что /en/about и /fr/about — это переводы друг друга, а не конкурирующие страницы с дублирующимся контентом.

Из-за этой путаницы авторитет страницы делится между языковыми версиями, а пользовательский опыт ухудшается. Поисковым системам нужны явные метаданные, чтобы понимать, какие страницы являются языковыми альтернативами одного и того же контента, чтобы показывать подходящую версию в зависимости от языка и местоположения пользователя.

Решение

Добавьте на каждую страницу теги hreflang со списком всех доступных языковых версий этого контента. Эти теги используют атрибут rel="alternate", чтобы указать, что связанные страницы — это переводы, а не дубликаты. Каждый тег указывает языковой код и ведёт на URL соответствующей языковой версии.

Объявляя такие связи в метаданных страницы, вы помогаете поисковым системам понять структуру сайта и показывать каждому пользователю нужную языковую версию. Это повышает релевантность результатов поиска и предотвращает санкции за дублирующийся контент.

Шаги

1. Создайте вспомогательную функцию для генерации ссылок с hreflang

Атрибут hreflang использует языковые коды ISO 639-1, к которым при необходимости добавляется региональный код ISO 3166-1 Alpha 2. Создайте утилиту, которая будет генерировать описания ссылок для всех языковых версий страницы.

type HreflangLink = {
  tagName: "link";
  rel: "alternate";
  hrefLang: string;
  href: string;
};

export function buildHreflangLinks(
  pathname: string,
  locales: string[],
  baseUrl: string,
): HreflangLink[] {
  return locales.map((locale) => ({
    tagName: "link",
    rel: "alternate",
    hrefLang: locale,
    href: `${baseUrl}/${locale}${pathname}`,
  }));
}

Эта функция принимает текущий путь, список поддерживаемых локалей и базовый URL вашего сайта, а затем возвращает массив дескрипторов ссылок, которые функция meta может отобразить.

2. Добавьте ссылку x-default для резервного варианта

Значение hreflang x-default указывает на страницу по умолчанию, если ни одна другая страница не подходит лучше, и не нацелено на конкретный язык или локаль. Добавьте это, чтобы направлять пользователей, чей язык не поддерживается.

export function buildHreflangLinks(
  pathname: string,
  locales: string[],
  baseUrl: string,
  defaultLocale: string,
): HreflangLink[] {
  const links = locales.map((locale) => ({
    tagName: "link",
    rel: "alternate",
    hrefLang: locale,
    href: `${baseUrl}/${locale}${pathname}`,
  }));

  links.push({
    tagName: "link",
    rel: "alternate",
    hrefLang: "x-default",
    href: `${baseUrl}/${defaultLocale}${pathname}`,
  });

  return links;
}

Ссылка x-default обычно ведёт на основную языковую версию вашего сайта и служит резервом для пользователей, чьи языковые предпочтения не совпадают ни с одной из ваших конкретных версий.

3. Экспортируйте ссылки hreflang из вашей meta-функции

Функция meta может задавать теги link на основе данных. Используйте её, чтобы возвращать ссылки hreflang для каждого маршрута.

import type { Route } from "./+types/about";
import { buildHreflangLinks } from "~/utils/hreflang";

const SUPPORTED_LOCALES = ["en", "fr", "de", "es"];
const BASE_URL = "https://example.com";
const DEFAULT_LOCALE = "en";

export function meta({ location }: Route.MetaArgs) {
  const hreflangLinks = buildHreflangLinks(
    location.pathname,
    SUPPORTED_LOCALES,
    BASE_URL,
    DEFAULT_LOCALE,
  );

  return [
    { title: "About Us" },
    { name: "description", content: "Learn about our company" },
    ...hreflangLinks,
  ];
}

Функция meta возвращает массив дескрипторов, которые могут включать объекты с tagName, установленным в "link". React Router отображает их как элементы link в заголовке документа.

4. Убедитесь, что компонент Meta находится в корневом макете

Компонент Meta рендерит все мета-теги, созданные экспортом meta из модуля маршрута, и должен находиться внутри head вашего документа. Проверьте, что он включён в корневой макет.

import { Links, Meta, Outlet, Scripts } from "react-router";

export default function Root() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}

Компонент Meta собирает и отображает все мета-дескрипторы из совпавшего маршрута, включая теги ссылок hreflang, которые вы определили на шаге 3.

5. Адаптируйте путь для маршрутов с префиксом локали

Если ваши маршруты содержат локаль в пути (например, /en/about), уберите её перед созданием ссылок hreflang, чтобы все языковые версии указывали на одну и ту же логическую страницу.

export function meta({ location }: Route.MetaArgs) {
  const pathWithoutLocale = location.pathname.replace(/^\/[a-z]{2}(\/|$)/, "/");

  const hreflangLinks = buildHreflangLinks(
    pathWithoutLocale,
    SUPPORTED_LOCALES,
    BASE_URL,
    DEFAULT_LOCALE,
  );

  return [{ title: "About Us" }, ...hreflangLinks];
}

Это гарантирует, что /en/about, /fr/about и /de/about все создают hreflang-ссылки, указывающие на правильные языковые URL для одного и того же контента.