Чек-лист SEO для Next.js App Router: SSR-видимость, canonical, hreflang и ИИ-краулеры
Neaptide · 30 октября 2026 г. · 8 мин чтения
Проверяем, что Next.js App Router реально отдаёт в исходном HTML: видимость <h1> без opacity:0, наследование alternates, ссылки переключателя языков и редиректы.
В этой статье

Сайт на Next.js App Router может безупречно выглядеть в браузере, но при этом скрывать главный заголовок <h1> за начальным стилем анимации opacity:0, терять теги <link rel="alternate"> при переопределении метаданных в дочерних маршрутах или прятать ссылки на языковые версии внутри несмонтированного клиентского меню. Перед релизом проверяйте сырой HTTP-ответ и серверный HTML, а не только дерево элементов после гидратации в DevTools.
Хотя Server Components по умолчанию рендерят разметку на сервере, клиентские обёртки анимаций, функция generateMetadata() на уровне страниц и логика в middleware.ts напрямую влияют на то, что получают Googlebot, Яндекс и краулеры ИИ-поиска (GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot). Ниже — пять реальных технических проблем, которые мы нашли и устранили на собственном мультиязычном сайте Neaptide на Next.js 15, и пошаговый протокол проверки.
Что показал аудит нашего собственного сайта на Next.js 15?
В октябре 2026 года мы проверили серверный HTML, HTTP-заголовки, метаданные и карту сайта Neaptide на шести языках. В таблице собраны пять обнаруженных дефектов рендеринга и маршрутизации и результаты после выкатки исправлений.
| Проверенный узел | Проблема в исходном SSR-ответе | Внедрённая правка и результат |
|---|---|---|
| Обёртки анимаций Reveal.tsx и Hero.tsx | Заголовок <h1>, лид и карточки отдавались в SSR со стилем style="opacity:0;transform:translateY(12px)" до гидратации (~1,2 с задержки) | Убрали opacity:0 из начального SSR-состояния; заголовки и текст сразу видимы в исходном HTML |
| Хелпер generateMetadata() в lib/meta.ts | Поле alternates: { canonical, languages } на страницах затирало alternates из layout и удаляло <link rel="alternate" type="application/rss+xml"> | Добавили types: { "application/rss+xml": "https://www.neaptide.ai/feed.xml" } в общий хелпер pageMetadata() |
| Переключатель языков components/LangSwitcher.tsx | Языковые версии рендерились только после клика как <button> без атрибута href | Все 6 локалей рендерятся в SSR DOM как ссылки <a href={...} hrefLang={l} lang={l}> |
| Редирект корневого маршрута / в middleware.ts | При запросе / ботами без гео-заголовков и Accept-Language не отдавался полный заголовок Vary | Добавили детерминированный переход на x-default (/en) и Vary: Accept-Language, Cookie, x-vercel-ip-country, cf-ipcountry |
| Микроразметка lib/schema.ts и app/sitemap.ts | На трёх страницах услуг в JSON-LD был зашитый areaServed: United States; в sitemap не входили иллюстрации кейсов и статей | Выровняли areaServed и добавили локализованные обложки и графики доказательств в sitemap.xml |
Эти наблюдения описывают конкретные инженерные правки на нашем сайте. Устранение таких ошибок убирает технические барьеры для индексации и LCP, однако позиции в поиске и цитирование в ИИ-ответах также зависят от конкуренции в выдаче, ссылочного профиля и качества контента.
Почему style="opacity:0" в анимациях появления вредит LCP и краулерам?
На многих сайтах на Next.js первый экран, заголовки и карточки оборачивают в клиентские компоненты плавного появления или Framer Motion с начальным состоянием opacity: 0. Поскольку Next.js предварительно рендерит Client Components на сервере в их стартовом состоянии, в исходном HTML главный заголовок <h1> и первый абзац приходят с инлайновым стилем style="opacity:0".
Это создаёт сразу две проблемы. Во-первых, браузер не засчитывает заголовок или лид-абзац как отрисованный элемент Largest Contentful Paint (LCP), пока не загрузится и не выполнится клиентский JavaScript — на мобильной сети это легко добавляет 800–1200 мс. Во-вторых, поисковые роботы и краулеры ИИ-систем, читающие первичный HTML без выполнения скриптов, видят основной контент страницы явно помеченным как невидимый.
Оставляйте текст полностью видимым в SSR-разметке с первого байта: используйте мягкий сдвиг transform: translateY(12px) без opacity: 0 либо включайте прозрачность только после инициализации клиентского скрипта с учётом prefers-reduced-motion.
Как generateMetadata может незаметно удалить теги alternates из layout?
В Next.js App Router объекты метаданных объединяются поверхностно (shallow merge). Если в корневом или языковом layout.tsx задан блок alternates (например, ссылка на RSS-ленту), а дочерняя страница возвращает в generateMetadata() собственный объект alternates с полями canonical и languages, дочерний объект полностью заменяет родительский alternates.
Чтобы ни один маршрут не терял x-default, языковые альтернативы или RSS-фид, формируйте весь объект alternates в единой функции-хелпере:
export function pageMetadata(locale: Locale, opts: PageMetaInput): Metadata {
const canonicalUrl = `${SITE.url}${localeHref(locale, opts.path)}`;
const languages: Record<string, string> = {};
for (const l of LOCALES) {
languages[l] = `${SITE.url}${localeHref(l, opts.path)}`;
}
languages["x-default"] = `${SITE.url}${localeHref(X_DEFAULT_LOCALE, opts.path)}`;
return {
alternates: {
canonical: canonicalUrl,
languages,
types: {
"application/rss+xml": `${SITE.url}/feed.xml`,
},
},
};
}- Документация Next.js по generateMetadata
- Руководство Google Поиска по локализованным версиям (hreflang)
Почему переключатель языков и корневой редирект должны быть явными для роботов?
Если список языков в шапке монтируется в DOM только после клика по кнопке или переключает локаль через router.push() на элементе <button>, поисковый робот не находит в HTML ни одной прямой ссылки на другие языковые ветки сайта. Даже при наличии <link rel="alternate" hreflang="..."> в <head> рендеринг обычных ссылок <a href="/en/..." hrefLang="en" lang="en"> в DOM шапки ускоряет переобход всех локалей.
Отдельно проверьте поведение корневого маршрута / при запросе без cookie, гео-заголовков и Accept-Language. Middleware должен стабильно перенаправлять таких роботов на локаль по умолчанию (x-default, например /en) и отдавать заголовок Vary: Accept-Language, Cookie, x-vercel-ip-country, cf-ipcountry, чтобы CDN не закешировал случайный гео-редирект для поискового бота.
Как проверить релиз сайта на Next.js App Router за шесть шагов
Перед выкаткой изменений прогоните шесть быстрых проверок на тестовом или боевом домене:
- Видимость контента в сыром SSR HTML: запросите страницу через curl -s и убедитесь, что единственный <h1> и основной текст присутствуют в HTML без style="opacity:0" и атрибута hidden.
- Полнота canonical и hreflang: проверьте, что каждая языковая страница содержит самоссылающийся <link rel="canonical">, все активные локали, x-default и ссылку на RSS.
- Краулинговые ссылки в навигации и переключателе языков: убедитесь, что меню, футер и переключатель языков отрендерены как теги <a href="...">.
- Маршрутизация корня и адресов без префикса языка: проверьте / и старые URL через curl -I с пустым Accept-Language — должен возвращаться один постоянный переход (308 или 301) на целевую страницу 200 OK.
- Согласованность JSON-LD с контентом страницы: сверьте поля WebPage, Service, Article, BreadcrumbList и FAQPage с видимым текстом, языком, датами и географией услуги.
- Покрытие в sitemap.xml и llms.txt: убедитесь, что все коммерческие страницы, статьи и их изображения включены в /sitemap.xml, а /llms.txt актуален для ИИ-поисковиков.
curl -s https://www.neaptide.ai/en | grep -E '<h1|rel="canonical"|hrefLang|application/rss\+xml'
curl -I -H 'Accept-Language:' https://www.neaptide.ai/Если вам нужен инженерный разбор сайта или веб-приложения на Next.js, мы проведём технический SEO- и GEO-аудит с воспроизводимыми проверками и готовыми задачами для разработчиков.