Hreflang × subdomain × subdirectory: что выбрать
В апреле я докрутил hreflang на одном из проектов — три локали: en, ka, lt. Прописал теги в head через Next.js metadata, проверил руками на десятке страниц, открыл несколько URL в инкогнито с разных IP. Всё показывалось правильно: грузин видит ka, литовец — lt, остальной мир — en. Закрыл задачу, переключился на следующую.
Через две недели открываю Google Search Console. Раздел International Targeting горит красным: "Return tag missing" на 60 URL. Hreflang-теги ссылаются с en на ka и lt, но обратные ссылки часть страниц не отдаёт. Google такие связи игнорирует обе стороны — то есть половина моих hreflang просто не существует для него. И всё это время сайт продолжал показывать ka-версию пользователям из США, которые искали на английском.
Мультиязычный SEO — это отдельная подсистема, со своими файлами, своими проверками, своей логикой генерации URL. И решение о структуре URL — куда класть локаль, в домен, поддомен или папку — принимается ОДИН раз, на старте. Менять потом — это 301-редирект, миграция sitemap, новая регистрация в GSC, минимум полгода на восстановление позиций. Переиграть назад почти невозможно.
В этом посте — три стратегии URL, как они взаимодействуют с hreflang, и что я выбрал для hiregora.com.
Hreflang — что это
Hreflang — это сигнал для Google: "вот эта страница на этом языке для этой страны". Не более того. Не фактор ранжирования напрямую, не магия — просто метаданные, которые помогают Google понять, какую из твоих 3-4 языковых версий показать конкретному пользователю.
Формат стандартный:
<link rel="alternate" hreflang="ru" href="https://example.com/ru/page" />
<link rel="alternate" hreflang="en" href="https://example.com/en/page" />
<link rel="alternate" hreflang="x-default" href="https://example.com/page" />
Положить можно в три места: в <head> страницы, в sitemap.xml как <xhtml:link>, или в HTTP-заголовок Link (для не-HTML файлов вроде PDF). Я ставлю в head и дублирую в sitemap. Дублирование — это страховка: если на странице JS сломал рендеринг head, sitemap всё равно даёт Google правильную картину.
Покрытие — два уровня. Либо чистый язык (en, ru, de) — работает для всех стран, где говорят на этом языке. Либо язык плюс регион (en-US, en-GB) — точная привязка к стране. Если контент один для всех англоязычных — хватит en. Если у тебя реально разные цены для США и Британии — нужны en-US и en-GB как отдельные страницы.
x-default — fallback. Когда пользователь не подходит ни под одну из объявленных hreflang комбинаций, Google показывает эту версию. Обычно ставится на основную локаль или на специальную language picker страницу.
Главное: hreflang не влияет на ранжирование напрямую. Он влияет на то, какую версию увидит пользователь. Видит свою — не уходит через 3 секунды, поведенческие сигналы хорошие, позиции косвенно растут. Кривой hreflang = плохой UX = поведенческие вниз = позиции вниз.
Три стратегии URL
Решение про URL принимается до настройки hreflang. Можно положить локаль в три места: в сам домен, в поддомен, в папку. Каждый вариант работает с hreflang, но по-разному взаимодействует с авторитетностью домена, индексацией и инфраструктурой.
ccTLD — отдельный домен на страну
Структура: example.de, example.fr, example.com.br. У каждой страны свой домен с национальным окончанием.
Плюсы серьёзные. ccTLD — самый сильный гео-сигнал. .de — это Германия, без вариантов, никаких настроек в GSC не нужно. Доменная репутация копится per-country: ссылки с немецких сайтов на example.de бустят немецкую версию, не размазываются на остальные. И юридически удобно: отдельный домен можно зарегистрировать на местную компанию.
Минусы тоже серьёзные. Каждый ccTLD — отдельный домен с нуля. Никакого общего DR. Если основной example.com имеет DR 60, новый example.de стартует с DR 0 и его придётся раскачивать ссылками с нуля — 6-12 месяцев минимум. Плюс инфра: 5 стран — это 5 DNS-зон, 5 сертификатов, 5 отдельных GSC-property.
Когда выбирать: enterprise с большим контентом per-country, разными правовыми subjects, локальными командами. Если у тебя в Германии своё юрлицо, свои цены, своя команда — example.de оправдан. Один продукт на четырёх языках — перебор.
Subdomain — поддомен на локаль
Структура: de.example.com, fr.example.com, en.example.com. Локаль в поддомене.
Плюсы. Техническая изоляция. Каждый поддомен можно посадить на свой сервер, свою CMS, свой стек. Удобно когда de ведёт немецкая команда на WordPress, а en — другая команда на Next.js. И потом легко мигрировать в ccTLD: 301-редирект с de.example.com на example.de, и часть авторитетности перейдёт.
Минусы. Google по умолчанию считает каждый поддомен отдельным сайтом. DR с основного example.com не передаётся полностью на de.example.com — передаётся частично, но не как с папки. Если ты вложил 2 года в раскачку example.com до DR 50 — de.example.com стартует где-то с 20-30. Плюс GSC требует отдельную регистрацию каждого поддомена как property.
Когда выбирать: когда у языковых версий реально разные технические стеки, разные команды, разные циклы релизов. Или когда планируется мигрировать в ccTLD позже. Или для отдельного продукта на том же бренде — у меня так с seo.hiregora.com: это не язык, это отдельный инструмент, своя кодовая база.
Subdirectory — папка на локаль
Структура: example.com/de/, example.com/fr/, example.com/en/. Локаль — это просто первый сегмент пути.
Плюсы. Один домен, одна доменная авторитетность, всё суммируется. Ссылка на example.com/de/page бустит весь домен, включая русскую и английскую версии. Один GSC-property, одна аналитика, один SSL, один CDN. Админить проще на порядок. Весь контент работает на одну домен-сущность.
Минусы. Общая инфра — то же преимущество, что и недостаток. Положил основной сайт — лёг весь мультиязычный сайт. Нельзя дать локальной команде доступ только к их папке. Главное ограничение: гео-таргетинг в GSC настраивается только domain-wide. Нельзя сказать "вся /de/ таргетится на Германию, а /en/ — на США". Только через hreflang с регионом — работает, но слабее, чем ccTLD-сигнал.
Когда выбирать: единый продукт на нескольких языках, единая команда, единый стек. DR домена ценный и его не хочется размывать. Инфраструктурная простота важнее технической изоляции. В 9 из 10 SaaS / контент-проектов — это правильный выбор.
Что выбрал я
Для hiregora.com — subdirectory: /en, /ka, /lt и корень для русской версии. Решал так.
Один продукт, один владелец, один стек — Next.js 15. Одна команда контента (это я). Никаких локальных юрлиц, никаких разных цен. Типичная конфигурация, где subdirectory выигрывает по всем фронтам.
DR домена ценный — я его раскачиваю последние полгода, и каждая статья на любом языке работает на общую цифру. Если бы я разнёс языки по поддоменам, каждый пришлось бы качать отдельно — минимум год на восстановление позиций по тем же запросам.
Cross-locale ссылки. Я часто связываю русский пост с английским — пользователь видит "available in English" и переходит на en-версию. Когда обе в одной папке, это просто <Link href="/en/writings/..."> без выходов на другой домен. Аналитика тоже одна, сессия не рвётся при переключении языка.
Subdomain я использую только для seo.hiregora.com — но это уже не язык, а отдельный продукт. SEO-чекер живёт на своём кодбейзе, с другим стеком. Поэтому поддомен оправдан: техническая изоляция нужна, а DR суммируется частично за счёт общего родительского домена.
Если бы я завтра решил делать hiregora.de с локальной немецкой командой — тогда ccTLD. Но это гипотеза на 2027. Сейчас — папки.
Реализация в Next.js 15
В Next.js 15 с app router всё это собирается через динамический сегмент app/[lang]/. Структура папок зеркалит структуру URL:
app/
[lang]/
page.tsx → /, /en, /ka, /lt
writings/
page.tsx → /writings, /en/writings, ...
[slug]/
page.tsx → /writings/foo, /en/writings/foo, ...
Middleware ловит запросы, проверяет первый сегмент пути и решает, есть ли там валидная локаль. Если нет — определяет язык по Accept-Language, ставит cookie с выбранной локалью и редиректит. Один middleware, один источник правды, работает на всех страницах.
generateStaticParams для каждой страницы возвращает массив всех локалей — [{lang: 'ru'}, {lang: 'en'}, {lang: 'ka'}, {lang: 'lt'}]. Это даёт статическую генерацию: на билде Next.js собирает 4 версии каждой страницы как отдельные HTML-файлы. Никакого SSR на runtime, никакой нагрузки на сервер — просто статика, которую раздаёт CDN.
Hreflang-теги генерирует helper buildAlternates() из lib/seo.ts. Принимает текущий путь без локали — возвращает объект для Next.js metadata API:
alternates: {
canonical: 'https://hiregora.com/writings/foo',
languages: {
'ru': 'https://hiregora.com/writings/foo',
'en': 'https://hiregora.com/en/writings/foo',
'ka': 'https://hiregora.com/ka/writings/foo',
'lt': 'https://hiregora.com/lt/writings/foo',
'x-default': 'https://hiregora.com/writings/foo',
},
}
Next.js сам ставит это в <head> как корректные <link rel="alternate">. Никаких ручных вставок, никаких пропущенных страниц — потому что это часть metadata, которая генерится автоматически для каждого роута.
Sitemap я генерю отдельным route handler в app/sitemap.xml/route.ts. Для каждой страницы он отдаёт не один URL, а блок из 4 локалей с <xhtml:link rel="alternate" hreflang="..."> внутри. Это страховка: если в head что-то слетит, sitemap всё равно даст Google правильную картину связей между локалями.
Самая частая ошибка — return tags
Это та самая красная штука, с которой я начал пост. И это ошибка номер один в hreflang-настройках, которую я видел на каждом втором проекте.
Правило простое и не обсуждается. Если страница A ссылается на страницу B через hreflang — страница B ОБЯЗАНА ссылаться обратно на страницу A. Это называется reciprocal linking, и Google его проверяет жёстко. Если B не ссылается на A — Google игнорирует обе ссылки. Не одну, обе.
Пример. На ru-странице /writings/foo стоят hreflang-теги: ru → ru/foo, en → en/foo, ka → ka/foo, lt → lt/foo. Я открываю en/foo и проверяю — там тоже должны стоять все четыре, включая ссылку на ru/foo. Если в en/foo указано только en и ka — связь en↔ru сломана, Google её отбросит, и при поиске на русском моя ru-страница не получит правильный сигнал.
Откуда берутся такие битые связи на практике. Самая частая причина — динамическая генерация без общего источника. На en-страницах используется одна функция для генерации hreflang, на ka — другая (например, копипастом, который кто-то забыл обновить). Они расходятся, и тесты этого не ловят, потому что каждая страница изолированно валидна.
Лечится одним helper, который генерит hreflang-блок для любой локали по одному и тому же входу — пути без локали. Если входной параметр один и функция одна, все локали отдают одинаковый блок hreflang-ссылок. Тогда reciprocal linking — это не правило, которое надо помнить, а следствие архитектуры.
Проверять — через GSC International Targeting, раз в неделю. Там видно: сколько ссылок Google нашёл, сколько из них сломанные, на каких URL проблема. Без этого отчёта ты узнаешь о проблемах только когда позиции просядут.
x-default — когда нужен
x-default — отдельный hreflang-тег для пользователей, которые не подходят ни под одну из объявленных языковых комбинаций. Например, у тебя en, ru, ka, lt. Заходит пользователь из Японии с японским в браузере. Google смотрит: en — есть, ru — нет, ka — нет, lt — нет. Японского у тебя нет. И тут срабатывает x-default: Google показывает версию, помеченную как fallback.
Куда ставить x-default — зависит от продукта. Три варианта:
-
На основную локаль. Если 70% трафика говорит на русском —
x-default → /(русская версия). Японец увидит русский. Не идеально, но он хотя бы попадёт на работающую страницу, а не на 404 или редирект-петлю. -
На английскую версию. Если продукт международный —
x-default → /en/. Англоязычная страница работает как универсальный fallback, потому что английский понимает примерно всё население интернета. -
На language picker. Если у тебя одинаково сильны все локали и нет очевидного fallback —
x-default → /select-language/. Страница, где пользователь сам выбирает язык. Менее красиво, но честнее, чем навязывать ему ru, когда он не знает русского.
У меня — вариант 1: x-default → / (русская версия). Это сейчас основной язык контента, и пока остальные локали не догнали по объёму, fallback на русский логичен. Когда en-версия станет полноценной — переключу x-default на неё.
И ещё: x-default — это всегда отдельный тег, не замена обычным hreflang. То есть ru-страница должна иметь и hreflang="ru" (для русскоязычных), и hreflang="x-default" (как fallback). Два тега на одной странице — это нормально.
Итог
Выбор URL-структуры — это решение, которое ты принимаешь один раз, на старте. ccTLD — для enterprise с локальными юрлицами и большим бюджетом на инфраструктуру. Subdomain — когда стеки или команды реально разные, или как промежуточный шаг к ccTLD. Subdirectory — для всего остального, и это правильный выбор в 9 из 10 случаев.
Hreflang поверх любого варианта работает одинаково, но требует трёх вещей: один общий генератор для всех локалей (чтобы reciprocal linking не ломался), дублирование в sitemap (страховка от JS-сбоев в head), и x-default как fallback. Проверять — через GSC International Targeting, раз в неделю.
И помни, что hreflang — это не про ранжирование напрямую. Это про то, чтобы правильный человек видел правильную страницу. Когда это работает — поведенческие сигналы хорошие. Когда сломано — Google показывает ru-версию литовцу, тот уходит за 3 секунды, и весь твой SEO-фундамент крошится сверху. Подробнее про общий список факторов — 30 факторов SEO 2026. Про взаимодействие с canonical — когда нужен canonical URL. Про настройку индексации — что блокировать в robots.txt.