
# Ошибка в canonical, которая прячет ваши переводы

agentready.md выходит на десяти языках с префиксом в пути. Каждая страница существует как `/about`, `/es/about`, `/ja/about` и ещё семь адресов. Месяцами все девять переводов отдавали в `<head>` одну и ту же строку:

```html
<link rel="canonical" href="https://agentready.md/about">
```

`/es/about` сообщал поисковикам: я дубль английской страницы, индексируйте её. То же самое говорил `/ja/about`. И остальные семь. Десять языков, из них индексируется один.

## Что canonical заявляет на самом деле

`rel="canonical"` — это не подсказка о том, какой адрес выглядит опрятнее. Это утверждение: эта страница и та, которую я называю, — одна и та же, и в индекс должна попасть названная. Ссылки, сигналы ранжирования и сама запись в индексе достаются цели. Эта страница выбывает из борьбы.

Для `/product?color=blue`, указывающего на `/product`, поведение верное. Для перевода — прямо противоположное тому, что нужно. Испанская страница не дубль английской: это другая страница для другого читателя, и ранжироваться по испанским запросам она должна сама.

## Почему баг прожил так долго

Наш роутер срезает языковой префикс до маршрутизации, поэтому запрос к `/es/about` доходит до обработчика как `/about`, а язык едет отдельно. Решение разумное: маршруты объявляются один раз и работают на всех языках. Но `request.url` остаётся без префикса, а шаблон `<head>` собирал canonical именно из этого голого пути.

На английском баг даёт правильный результат, потому что у английского префикса нет. Все страницы, которые вы смотрите, разрабатывая на своём языке, выглядят верно. Ломается только в тех девяти, которые вы не читаете.

Исправление — один вызов, возвращающий префикс перед выводом адреса:

```ejs
<%# request.url уже потерял префикс /es/ — localizedUrl возвращает его. %>
<% const canonicalHref = localizedUrl(canonicalPath); %>
<link rel="canonical" href="<%= baseUrl %><%= canonicalHref %>">
```

## Правило: каждая языковая версия канонична сама себе

Самоссылающийся canonical, всегда. `/es/about` объявляет `/es/about`. `/ja/about` объявляет `/ja/about`. Для переводов исключений нет. Если две версии почти совпадают — та же карточка товара с тремя изменёнными словами, — это ничего не меняет: язык не дублирование.

## hreflang: каждая версия перечисляет все версии

Canonical говорит, какую страницу индексировать. `hreflang` говорит, какую из проиндексированных показать какому читателю. Нужны оба, и у `hreflang` есть два правила, которые нарушают чаще всего.

**Каждая версия перечисляет все версии, включая себя.** Десять языков — это десять ссылок `alternate` на всех десяти страницах, вместе с самоссылкой. Пропущенная самоссылка — самый частый дефект в остальном корректного набора.

**Ссылки должны быть взаимными.** Если английская страница называет испанскую, а испанская не называет английскую в ответ, поисковик отбрасывает связь, а нередко и весь набор, потому что проверить её нечем. Объявить себя чьим-то переводом может кто угодно. Подтвердить это может только вторая страница.

Генерируйте их из одного списка, а не поддерживайте десять:

```ejs
<% ['en','es','fr','de','pt','it','ja','zh','ko','ru'].forEach(l => { %>
<link rel="alternate" hreflang="<%= l %>" href="<%= baseUrl %><%= localizedUrl(canonicalPath, l) %>">
<% }) %>
<link rel="alternate" hreflang="x-default" href="<%= baseUrl %><%= canonicalPath %>">
```

Берите чистый код языка (`es`), когда одна версия обслуживает всех говорящих на нём. Регион (`es-ES`, `pt-BR`) добавляйте, только если вы действительно публикуете отдельные версии по рынкам: выдуманные регионы дробят набор без всякой пользы.

## x-default — не ещё один перевод

`x-default` указывает версию для читателя, чей язык вы не публикуете. Кто-то ищет по-нидерландски, нидерландского у вас нет — он получает то, на что указывает `x-default`.

Это не «самый важный язык» и не ещё одна запись в общем списке. У нас `x-default` остаётся на английском адресе без префикса, к которому весь сайт и так откатывается. Направьте его на страницу с конкретным языком, вроде `/es/about`, — и каждый читатель без перевода попадёт на испанскую страницу, что хуже поведения по умолчанию.

## Проверка в одну строку

Вся поломка видна из терминала. Пройдитесь по своим языкам и напечатайте, что заявляет каждая страница:

```bash
for p in "" es/ fr/ de/ pt/ it/ ja/ zh/ ko/ ru/; do
  printf '%-4s ' "${p:-en}"
  curl -s "https://example.com/${p}about" | grep -o '<link rel="canonical"[^>]*>'
done
```

В правильном выводе каждая строка называет свой собственный путь:

```
en   <link rel="canonical" href="https://example.com/about">
es   <link rel="canonical" href="https://example.com/es/about">
fr   <link rel="canonical" href="https://example.com/fr/about">
```

Десять строк с одним адресом — это баг из этой статьи. Прогоните проверку по всем шаблонам, которые трогали, а не только по главной: canonical обычно собирается на уровне макета, и сайт с тремя макетами может быть прав в двух.

## Как это ломается ещё

**Метки отслеживания внутри canonical.** Если собирать canonical из сырого URL запроса, посетитель, пришедший по `?utm_source=newsletter`, получит canonical, указывающий на `?utm_source=newsletter`. Каждый репост рождает новый адрес, объявляющий каноничным себя, — неограниченное множество URL на одну страницу. Стройте canonical из пути и возвращайте только те параметры, которые действительно задают другую страницу: отфильтрованный список, номер страницы. И в фиксированном порядке, чтобы одна страница не могла записать своё имя двумя способами.

**Canonical, указывающий на редирект.** Если `/es/about` объявляет `/es/about/`, а тот отдаёт 301 обратно на `/es/about`, вы вручили краулеру петлю и повод не доверять сигналу. Указывайте на адрес, который отвечает `200`.

**Два canonical, противоречащих друг другу.** `<link>` в HTML и HTTP-заголовок `Link: <...>; rel="canonical"` одинаково допустимы, и их охотно добавляют CDN, плагины и обратные прокси. Когда они расходятся, результат не определён и обычно не тот, которого вы ждали. Смотрите и заголовки:

```bash
curl -sI https://example.com/es/about | grep -i "^link:"
```

Пустой вывод здесь — хороший ответ, если только вы не поставили заголовок намеренно.

[Проверить сайт](/ru) · [Генератор мета-тегов](/ru/tools/meta-tags-generator) · [Чек-лист готовности к ИИ](/ru/tools/ai-readiness-checklist)
