Hreflang в Next.js при неполных переводах: проверенный пример
Neaptide · 4 октября 2026 г. · 9 мин чтения
Эксперимент в Next.js и проверка 18 опубликованных URL: как согласовать hreflang, canonical, sitemap и переключатель, когда переведены не все страницы.
В этой статье

Стройте языковые ссылки страницы по её опубликованным переводам, а не по всем языкам сайта. Используйте одну карту доступности для hreflang, sitemap и переключателя. Французский язык в общих настройках ещё не означает, что конкретная статья уже переведена на французский.
Мы проверили это 4 октября 2026 года. В стенде Next.js намеренно ошибочная настройка создала 14 hreflang-ссылок и 14 ссылок навигации на отсутствующие переводы, а также восемь лишних URL в sitemap. Карта переводов конкретной страницы устранила все три несоответствия. Отдельно проверку прошли 18 опубликованных URL Neaptide. Индексацию и позиции Google эти данные не измеряют.
Два набора данных с разными задачами
На опубликованном сайте выбрали три маршрута в шести локалях: GEO-услугу, руководство по мультиязычному сайту и статью о B2B-каталоге. Сделали два прохода HTTP-проверки, изучили каждую страницу в браузере и выполнили по одному переключению языка. Один запрос оборвался из-за сброса соединения; отдельно записанный повтор прошёл успешно. Итого 37 попыток и 36 успешных ответов — первый проход не был безошибочным.
| Маршрут | Проверено URL | Метаданные + sitemap | Смена языка |
|---|---|---|---|
| /services/geo | 6 | 6 / 6 | 6 / 6 |
| /blog/multilingual-website-guide | 6 | 6 / 6 | 6 / 6 |
| /blog/b2b-catalog-seo | 6 | 6 / 6 | 6 / 6 |
Все 18 URL ответили 200, указали canonical на себя и совпали с языковыми аннотациями в sitemap. На каждой странице было шесть языковых альтернатив и английская версия как x-default. Все 18 проверенных переключений сохранили маршрут. Это по одному переходу с каждого URL, а не все пары языков и не весь сайт. Проверяли опубликованную B2B-статью, а не её новый локальный исследовательский черновик.
Контролируемый эксперимент с неполными переводами
Создали отдельный стенд App Router на Next.js 15.5.22, React 19.2.6 и Node.js v25.2.1. До замеров задали три профиля: шесть переводов, три перевода и только английский оригинал. В каждом варианте 18 сочетаний маршрута и языка: 10 доступных страниц и восемь отсутствующих. Два HTTP-прохода до и после исправления дали 72 наблюдения; все 36 сочетаний также открыли в браузере.

В ошибочном варианте все три механизма брали общий список шести языков. В исправленном — реальные записи переводов. Доступные страницы в обоих случаях сохраняли canonical на себя. Мы не создавали подменный контент и не перенаправляли отсутствующие переводы на главную.
| Профиль | Ошибки hreflang: до → после | Ошибки переключателя: до → после | Лишние URL sitemap: до → после |
|---|---|---|---|
| 6 языков | 0 → 0 | 0 → 0 | 0 → 0 |
| 3 языка | 9 → 0 | 9 → 0 | 3 → 0 |
| 1 язык | 5 → 0 | 5 → 0 | 5 → 0 |
Единицы различаются: ссылка считается отдельно для каждой исходной страницы, URL sitemap — по записи. Складывать их в число сломанных страниц нельзя. Оба HTTP-прохода совпали, все 36 проверок DOM соответствовали HTML. Пять контрольных кликов подтвердили три корректных перехода и два намеренно ошибочных перехода на 404. Эксперимент не показывает распространённость этой ошибки.
Используйте одну карту доступности
В стенде TRANSLATIONS содержит только опубликованный контент. availableLocales получает его языки, а generateStaticParams создаёт соответствующие маршруты. При dynamicParams = false отсутствующие сочетания остаются недоступными; функции страницы и метаданных дополнительно проверяют запись перевода. Ниже используются те же функции, что и в проверенном стенде. В скачиваемом проекте есть оба варианта; для исправления смотрите ветку after.
export const availableLocales = profile => Object.keys(TRANSLATIONS[profile] || {});
export const translation = (profile, locale) => TRANSLATIONS[profile]?.[locale];
export const pageUrl = (profile, locale, phase) => `${ORIGIN}/${locale}/${phase}/${profile}`;export async function generateMetadata({ params }) {
const { phase, profile, locale } = await params;
const content = translation(profile, locale);
if (!PHASES.includes(phase) || !content) notFound();
return {
title: content.title,
alternates: { canonical: pageUrl(profile, locale, phase), languages: languageAlternates(profile, phase) },
};
}В варианте after функция languageAlternates возвращает только опубликованные эквиваленты. Для многоязычных профилей она добавляет английский x-default, а для английского оригинала без переводов не выводит языковые аннотации. Это наш осознанный выбор. Если оригинал на другом языке, выберите существующую запасную страницу этой группы. При разных slug храните соответствующие URL, а не заменяйте префикс: локализованные slug в эксперимент не входили.
У canonical, hreflang и навигации разные задачи
Canonical указывает предпочтительный URL, hreflang связывает языковые эквиваленты. Наши переводы используют canonical на себя. Переключатель должен вести на существующий перевод того же материала. Дополнительная, явно подписанная ссылка на главную другого языка остаётся обычной навигацией — она не заменяет перевод отсутствующей статьи.
Google описывает абсолютные взаимные языковые ссылки, включая самоссылку. HTML и sitemap — альтернативные способы аннотации; мы сравнивали оба, поскольку проект использует оба.
Что делать, пока перевод не готов
Уберите недоступную версию из группы переводов и списка эквивалентных страниц в переключателе. Не включайте черновик в публичный sitemap. Если предлагаете английский оригинал, так и подпишите ссылку и направьте её на настоящий английский URL. Ответ 200 с английским текстом по французскому адресу не подтверждает наличие французского перевода.
В нашем статическом стенде все восемь отсутствующих сочетаний возвращали 404 и noindex до и после исправления. Мы проверили ответы сервера. Для динамической или потоковой выдачи статусы нужно измерить отдельно: вызов notFound() в коде сам по себе ещё не является наблюдением.
Проверяйте результат после публикации перевода
- Откройте все опубликованные эквиваленты: проверьте статус, конечный URL, язык и canonical.
- Сопоставьте каждую языковую ссылку с картой переводов, проверьте целевую страницу и обратные ссылки.
- Сравните записи sitemap с опубликованным контентом. Если есть языковые аннотации, сопоставьте их с HTML.
- Нажмите переключатель на затронутых страницах. Проверьте и успешный перевод, и поведение при отсутствующем языке.
- Повторите проверку после добавления или снятия перевода. Сохраните дату, маршрут и результат; индексацию проверяйте отдельно через Search Console.
Повторите эксперимент
Скачайте исходники стенда, результаты и полную методику. Архив не содержит зависимостей и сборки. Соберите и запустите проект локально на указанном порту, затем выполните HTTP-сборщик. Исходные проверки браузера записаны отдельно: сборщик не выполняет их повторно.
Это техническая проверка одного дня одним исследователем. Она охватывает три маршрута опубликованного сайта и контролируемые статические примеры, но не качество всех переводов, выбранный Google canonical, индексацию, позиции, трафик или цитирование ИИ.