**TL;DR.** Hreflang сообщает поисковым системам, какой вариант локали показывать каждому пользователю. Убедитесь, что все сделано правильно: (1) полные взаимные кластеры с самоссылками, (2) правильные коды языков ISO, (3) резервный x-default, (4) фильтрация по сущностям, чтобы не рекламировать несуществующие переводы. Все остальное — детали реализации.

## Что делает hreflang

Hreflang — это HTML-атрибут (и эквивалентная аннотация в карте сайта), который сигнализирует Google, на какой язык и регион нацелена страница. Правильно реализованный hreflang:

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

Hreflang не повышает рейтинги. Он контролирует, какой вариант локали занимает какое место.

## Шаг 1: Решите, где использовать hreflang

Три варианта:

| Метод                 | Плюсы                                         | Минусы                                       |
| --------------------- | --------------------------------------------- | -------------------------------------------- |
| HTML head             | Легко отлаживать; один источник на страницу   | Добавляет байты к каждому HTML-ответу       |
| HTTP `Link` заголовок | Работает для не-HTML ресурсов (PDF и т.д.)    | Трудно отлаживать; редко требуется          |
| XML карта сайта       | Наиболее компактно для больших каталогов      | Менее заметно для людей; требует дисциплины в картах сайта |

По умолчанию для большинства сайтов электронной коммерции в 2026 году — это **HTML head**. Переключитесь на только карту сайта при более чем 50,000 URL.

## Шаг 2: Создайте полный кластер

Для каждого переведенного URL выводите hreflang для него самого и для каждого другого варианта.

Кластер из 4 локалей (en, de, fr, es) для продукта:

```html
<link rel="alternate" hreflang="en" href="https://example.com/en/products/leather-bag" />
<link rel="alternate" hreflang="de" href="https://example.com/de/products/leather-bag" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/products/leather-bag" />
<link rel="alternate" hreflang="es" href="https://example.com/es/products/leather-bag" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/products/leather-bag" />
```

Этот кластер появляется в заголовке **каждого** варианта локали этого продукта. Немецкий URL выводит тот же кластер, французский URL выводит тот же кластер и т.д.

Кластер должен быть взаимным — A указывает на B, B указывает на A. Невзаимные кластеры игнорируются Google.

## Шаг 3: x-default

`x-default` — это резервный вариант для пользователей, чей язык браузера не соответствует ни одному из ваших вариантов. Обычно это ваш английский (или URL по умолчанию).

```html
<link rel="alternate" hreflang="x-default" href="https://example.com/en/products/leather-bag" />
```

Один и тот же URL может быть как `hreflang="en"`, так и `hreflang="x-default"`. Только один URL в кластере получает `x-default`.

## Шаг 4: Фильтрация по фактически переведенным сущностям

Самая большая практическая ошибка: выводить кластер из 9 локалей для продукта, который переведен только на 3 локали. Рекламируемые URL возвращают 404; Google видит сломанный кластер и может игнорировать весь hreflang сайта.

Исправление: фильтрация по сущности `availableLocales`.

```ts
// Псевдокод
interface ProductForSeo {
  slug: string;
  availableLocales: string[];  // например, ["en", "de", "fr"]
}

interface StoreForSeo {
  supportedLocales: string[];  // например, ["en", "de", "fr", "es", "it"]
  defaultLocale: string;
}

function buildHreflangCluster(product: ProductForSeo, store: StoreForSeo) {
  const effective = product.availableLocales.filter((l) =>
    store.supportedLocales.includes(l)
  );
  return effective.map((locale) => ({
    hreflang: locale,
    href: `https://example.com/${locale}/products/${product.slug}`,
  }));
}
```

`src/lib/seo/storefront.ts` Ordiko реализует это точное пересечение.

## Шаг 5: Правильные коды языков

Используйте коды языков **ISO 639-1**:

| Язык                     | Код      |
| ------------------------ | --------- |
| Английский               | `en`      |
| Испанский                | `es`      |
| Французский              | `fr`      |
| Немецкий                 | `de`      |
| Португальский            | `pt`      |
| Итальянский              | `it`      |
| Русский                  | `ru`      |
| Украинский               | `uk`      |
| Китайский (упрощенный)   | `zh-Hans` |
| Китайский (традиционный)  | `zh-Hant` |
| Японский                 | `ja`      |
| Корейский                | `ko`      |
| Арабский                 | `ar`      |

Для языка + региона (когда вы обслуживаете вариант локали-региона):

| Вариант                  | Код      |
| ------------------------ | --------- |
| Американский английский  | `en-US`   |
| Британский английский    | `en-GB`   |
| Канадский английский     | `en-CA`   |
| Мексиканский испанский   | `es-MX`   |
| Испанский (Испания)      | `es-ES`   |
| Французский (Франция)    | `fr-FR`   |
| Французский (Канада)      | `fr-CA`   |
| Португальский (Бразилия) | `pt-BR`   |
| Португальский (Португалия)| `pt-PT`   |

Не используйте:

- `en-EN` (недопустимо)
- `gb` (используйте `en-GB`)
- `cn` (используйте `zh-Hans`)
- `kr` (используйте `ko`)

## Шаг 6: Проверка

Три проверки:

1. **Самоссылка**: каждый URL должен включать себя в своем собственном кластере.
2. **Взаимность**: каждый вариант в кластере должен включать каждый другой вариант.
3. **Доступность**: каждый URL в кластере должен возвращать 200.

Инструменты:

| Инструмент                       | Что проверяет                               |
| -------------------------------- | ------------------------------------------- |
| Google Search Console            | Ошибки hreflang в реальном времени         |
| [hreflang.org/validator](https://hreflang.org) | Корректность кластера                   |
| Sitebulb / Screaming Frog        | Взаимность и самоссылка                   |
| Ahrefs / Semrush                 | Отчеты hreflang в аудите сайта            |
| Ручной `curl`                    | Доступность по каждому URL                 |

Шаблон теста Vitest для CI:

```ts
import { describe, it, expect } from 'vitest';

describe('hreflang cluster', () => {
  it('every variant URL returns 200', async () => {
    const cluster = [
      'https://example.com/en/products/x',
      'https://example.com/de/products/x',
      'https://example.com/fr/products/x',
    ];
    for (const url of cluster) {
      const res = await fetch(url);
      expect(res.status).toBe(200);
    }
  });

  it('every variant emits self-referencing hreflang', async () => {
    for (const url of cluster) {
      const html = await fetch(url).then((r) => r.text());
      expect(html).toContain(`hreflang="${getLocale(url)}" href="${url}"`);
    }
  });
});
```

## Особые случаи

**Одностраничные приложения**: выводите hreflang на стороне сервера в заголовке документа. Не полагайтесь на инъекцию на стороне клиента — краулер Google может не выполнить ваш JS.

**ccTLDs**: hreflang все еще применяется. `example.de` может указать `hreflang="de-DE"` для себя и ссылаться на `example.com/en/...` для `en`.

**Регион без языкового различия**: если вы обслуживаете `en-US` и `en-GB` с идентичным контентом, hreflang различает их. Не делите URL — дайте каждому свой собственный URL, даже если контент идентичен.

## Общие ошибки

| Ошибка                                   | Эффект                                           |
| ---------------------------------------- | ------------------------------------------------ |
| Невзаимный кластер                       | Google полностью игнорирует hreflang             |
| Вариант URL возвращает 404               | Google игнорирует URL; может понизить кластер   |
| Неправильный код языка                   | Вариант рассматривается как нецелевой           |
| Несколько URL с x-default                | Один игнорируется произвольно                    |
| Hreflang только на каноническом URL     | Подстраницы не имеют сигнала hreflang           |
| Смешивание абсолютных и относительных URL | Всегда используйте абсолютные URL                 |
| Разные домены для локали без согласованного hreflang | Кластер ломается                       |

## Как Ordiko использует hreflang

`src/lib/seo/storefront.ts` вычисляет `alternates.languages` для каждого объекта метаданных страницы Next.js. Функция принимает:

- `store.supportedLocales`
- `entity.availableLocales` (необязательно, по умолчанию — `store.supportedLocales`)
- `store.defaultLocale` (используется для x-default)

И выводит кластер пересечения в заголовке HTML. Самоссылка, взаимность и x-default гарантированы реализацией. Фильтрация по сущности является опциональной через поле `availableLocales` в загрузчике сущностей.

## Часто задаваемые вопросы

**HTML head или карта сайта — что лучше?**
HTML head легче отлаживать, потому что вы можете проверить с помощью "Просмотр кода". Карта сайта более компактна для магазинов с 50k+ URL, потому что hreflang выводится один раз для каждой пары URL, а не в заголовке каждой страницы. Выбирайте HTML для магазинов с менее чем 50k URL; карту сайта выше этого. Ordiko по умолчанию выводит HTML head.

**Какой код языка использовать для испанского Латинской Америки и испанского Испании?**
es-MX для Мексики, es-AR для Аргентины, es-CO для Колумбии, es-ES для Испании. Комбинация язык-регион поддерживается. Для одной латинской американской локали, обслуживающей несколько стран, es-419 (код ООН для Латинской Америки и Карибского бассейна) действителен, но менее широко поддерживается.

**Могу ли я иметь несколько URL с x-default?**
Нет. Один x-default на кластер. Если ваш бизнес обслуживает английский по всему миру, .com (или ваш URL по умолчанию) является x-default. Один и тот же URL может быть как hreflang='en', так и hreflang='x-default'.

**Как Ordiko обрабатывает корректность hreflang?**
Библиотека SEO витрины Ordiko (src/lib/seo/storefront.ts) пересекает доступные локали каждой сущности с поддерживаемыми локалями магазина при выводе alternates.languages. Продукт, переведенный только на en + de, будет рекламировать только эти две локали, независимо от более широкого набора локалей магазина.