# hreflang

## Що це таке

Атрибут `hreflang` на посиланні повідомляє, якою мовою написано сторінку на
іншому його кінці. HTML дозволяє ставити його на елементи `a`, `area` і `link`,
але відомим його зробило одне застосування: набір елементів
`link rel="alternate"` у `head` сторінки, по одному на кожну мовну версію, яким
сайт повідомляє пошуковим системам, що кілька адрес містять той самий вміст
різними мовами.

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

Стандарт HTML називає цей атрибут «суто рекомендаційним». Його значення мусить
бути коректним мовним тегом за BCP 47, а браузер має визначати мову завантаженої
сторінки з самої сторінки, а не з посилання, яке до неї привело. У поєднанні з
`rel="alternate"` атрибут `hreflang`, що відрізняється від мови самого
документа, позначає ціль як переклад.

Пошукові системи перетворили цю рекомендаційну примітку на протокол. Google
описує три рівноцінні способи передати набір: елементи `link` у `head`,
заголовок HTTP `Link` для PDF та інших файлів без `head` і записи в XML-карті
сайту. Кожна версія перелічує саму себе й усі інші версії, з повними абсолютними
адресами. Пара враховується лише тоді, коли обидві сторінки називають одна одну,
тож чужий сайт не може оголосити себе перекладом ваших сторінок. Коди
складаються з мови за ISO 639-1 і, за потреби, регіону за ISO 3166-1, а сам
регіон без мови недійсний. Зарезервоване значення `x-default` вказує сторінку
для читачів, чия мова не збігається з жодною з інших, і, як зазначає Google,
задумане воно для сторінок вибору мови.

Із ним легко сплутати двох сусідів. Атрибут `lang` каже, якою мовою текст перед
читачем; `hreflang` каже, якою мовою сторінка, на яку веде посилання. Google
зазначає, що не використовує жодного з них, щоб визначити мову сторінки, тож
`hreflang` не зробить сторінку данською; він повідомляє пошуковій системі, що
данська сторінка, яку та вже розпізнала, належить до однієї групи з англійською.
А канонічна URL зливає дублікати в одну проіндексовану адресу, тоді як
`hreflang` лишає в індексі кожну версію й каже, яку з них кому показувати.

## Чому це важливо

`hreflang` фіксує одне рішення: чи утворюють мовні версії сторінки одну групу,
чи їх оцінюють як окремі сторінки. Окремо кожна версія змагається сама за себе,
і пошукова система може показати данському читачеві англійську сторінку або
вивести обидві версії в тих самих результатах. Оголошені групою, версії
сприймаються як один вміст, і пошукова система може запропонувати ту, що
відповідає мовним налаштуванням читача.

Група має сенс лише тоді, коли версії справді містять той самий вміст. Сайтові
однією мовою `hreflang` не потрібен узагалі, а два мовні розділи, що різняться
по суті, теж не група. Регіональні варіанти з невеликими відмінностями, як-от
сторінки `en-GB` і `en-US` з різними цінами, утворюють групу так само.

Ціна — облік, і він зростає з квадратом кількості мов. Кожна версія кожної
сторінки несе повний список, тож коли один переклад додають, переносять чи
знімають, змінюється `head` усіх сторінок-сестер, а список, правильний на
більшості сторінок і хибний на кількох, ламається тихо: пари, що ведуть в обидва
боки, використовуються, решта відкидається, і жодної видимої помилки немає. Тому
`hreflang` погано пасує ручному редагуванню й природно — генераторові, який
виводить набір кожної сторінки з одного списку. Від `hreflang` залежить, яку з
кількох версій побачить користувач пошуку.

## Як це виглядає на практиці

Цей сайт
[публікує кожну сторінку трьома мовами з одного дерева вмісту](/uk/portfolio/built-a-multilingual-static-site-in-hugo-126/),
тож кожна сторінка має сталий набір сестер, які треба оголосити.

Один шаблон вирішує, які переклади оголошувати, і п'ять місць читають його
рішення: набір `hreflang` у `head`, теги `og:locale:alternate`, якими
користуються попередні перегляди посилань, альтернативи в карті сайту,
`inLanguage` у структурованих даних сайту і, після ще одного віднімання, мовна
смуга, яку бачить читач. Список містить і саму сторінку, а отже дає те посилання
на себе, якого вимагають правила. Коди називають лише мови, `en`, `da` і `uk`,
тож кожну версію пропонують усім читачам цієї мови, де б вони не були, а
англійський запис пишеться вдруге як `x-default`. І `head`, і карта сайту несуть
набір, згенерований з того самого списку, тож розійтися вони не можуть.

Поділ між машинами й читачами навмисний.
[Українська версія проіндексована й указана в картах сайту, а перемикач у верхній панелі її не пропонує](/uk/credits/),
і в мовній конфігурації це один прапорець: пошуковому роботові й далі
повідомляють про українську в обидва боки, змінюється лише меню для читача.
Другий прапорець, для мови, яку вже збирають, але яка ще не готова, прибирає її
з обох списків і позначає її сторінки `noindex`. Сторінка такою мовою теж не
оголошує сестер, бо `hreflang` від неоголошеної сторінки до оголошеної — це
пара, яку пошукова система бачить лише з одного кінця.

Копії підкоряються тій самій логіці. Сторінка на одному з тематичних субдоменів,
чия канонічна URL вказує на основний сайт, узагалі не виводить `hreflang`: набір
читають на тій сторінці, яку пошукова система прийняла як канонічну, а копія, що
називала б переклади свого хоста, суперечила б власній канонічній URL. Перемикач
мов на ній усе одно працює, бо читач, якому потрібна данська, однаково має її
отримати.

Перевірка зібраних сторінок вимагає, щоб кожна сторінка з альтернативами містила
`x-default` і власну мову, а кожна альтернатива була зібраною сторінкою
оголошеною мовою й вела назад. Правило про посилання на себе потрібне не для
галочки. Генератор має два списки з майже однаковими назвами: `.Translations`,
що не містить поточної сторінки, і `.AllTranslations`, що містить, і помилка в
одне слово прибирає власну мову кожної сторінки і з `head`, і з мовної смуги
водночас, тож вони й далі узгоджені між собою. На навмисно зламаній збірці ця
одна вимога виявилася єдиною в перевірці, що зловила 528 із 792 сторінок.

## Де виникають проблеми

**Користувачам пошуку показують версію не тією мовою.** Данського читача ведуть
на англійську сторінку, або обидві версії стоять поруч. Пари ведуть в один бік:
данська сторінка називає англійську, але англійська досі перелічує лише ті мови,
що були раніше, а пошукові системи ігнорують пару без зворотного посилання.
Будуйте набір кожної сторінки з одного списку, що містить і саму сторінку, і
перевіряйте взаємність на зібраних сторінках.

**Анотації цілої мови нічого не дають.** Розмітка на місці й є коректним HTML,
але версії трактують як не пов'язані між собою. Коди означають щось інше, ніж
задумано: `ua` чи `dk` там, де мови мають коди `uk` і `da`, `be` в значенні
Бельгії, хоча це код білоруської мови, або `en-UK` замість `en-GB`, бо `UK` —
зарезервований код регіону, який Google ігнорує. Беріть мову з ISO 639-1,
додавайте регіон з ISO 3166-1 лише там, де вміст справді різниться між країнами,
і пам'ятайте, що коректний тег BCP 47 Google може й не підтримувати.

**Переклади випадають з індексу.** У пошуку з'являється лише мова оригіналу, а
переклади трактують як його дублікати. Кожен переклад називає своєю канонічною
URL оригінал, а це просить злити переклад з оригіналом, тоді як його `hreflang`
просить його показувати. Робіть кожну мовну версію власною канонічною URL;
Google радить, щоб канонічна сторінка була тією самою мовою.

**Сторінку, що просила лишитися поза пошуком, однаково оголошують.** Чорнеткова
мова з позначкою `noindex` з'являється серед альтернатив опублікованих сторінок
або сама перелічує їх у своєму `head`. Оголошення й директиву задано в різних
місцях, тож сайт дає пошуковому роботові дві відповіді про ту саму адресу.
Вирішуйте для кожної мови окремо, оголошена вона в обидва боки чи в жоден,
тримайте це рішення в одному параметрі, який читають усі списки, і звіряйте з
ним зібрані сторінки.

## Що почитати далі

- [HTML Standard: the hreflang attribute](https://html.spec.whatwg.org/multipage/links.html#attr-hyperlink-hreflang)
  — що атрибут означає на посиланні і чому браузер не має йому довіряти.
- [HTML Standard: link type "alternate"](https://html.spec.whatwg.org/multipage/links.html#rel-alternate)
  — як `hreflang` і `type` описують переклад або інший формат того самого
  документа.
- [RFC 5646: Tags for Identifying Languages](https://www.rfc-editor.org/rfc/rfc5646)
  — синтаксис BCP 47, якому має відповідати значення атрибута.
- [Google Search Central: localized versions of your pages](https://developers.google.com/search/docs/specialty/international/localized-versions)
  — три способи передати набір, правило взаємності та `x-default`.

<https://engineer.company/uk/glossary/hreflang/>
