Странствующий техник
Подкинуть работёнку
Назад к статьям
DEV27 января 2026·5 MIN

Next.js App Router и SEO: почему клиентский роутинг ломает индексацию Google

Клиентский роутинг через useRouter даёт ноль проиндексированных страниц в Google. Разбор, как Next.js App Router и generateStaticParams решают SEO-индексацию через серверный рендеринг.

Приложение на Next.js с клиентским роутингом через useRouter работало быстро, а Google через месяц показывал ноль проиндексированных страниц — весь контент жил в клиентском JavaScript, который поисковые роботы не выполняют так же, как браузер пользователя. Решение — Next.js App Router с файловой маршрутизацией, generateStaticParams и generateMetadata, которые рендерят контент на сервере ещё до того, как страница попадает к боту.

Стек AI-инструментов

Начинал с React, где всё крутится вокруг useRouter и push — привычно и удобно, пока не узнаёшь, что поисковые роботы не выполняют JavaScript так, как браузер пользователя.

Проблема

Когда я только начал работать с Next.js 13 и App Router, меня смущала одна вещь: почему нельзя просто управлять состоянием маршрута через хуки, как в обычном React? Я привык к useRouter и push, и переход к серверному роутингу казался шагом назад.

Проблема в том, что клиентский роутинг работает только для людей с включенным JavaScript. Боты поисковиков видят пустые страницы или не ждут выполнения кода. Результат — контент не индексируется, сайт теряет органический трафик.

Я пытался обойти это через next/link и useEffect, но это костыли. Клиентская навигация не меняет URL на сервере, поэтому каждый переход — это новый запрос с нулевым кешем.

Три дня я боролся с тем, что мои страницы не индексировались. Я пробовал добавить meta теги динамически, но они не попадали в исходный HTML. Я пробовал Server Components, но не понимал, как управлять маршрутизацией.

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

Ключевой инсайт: URL — это не просто переменная состояния, это идентификатор ресурса, который должен работать на сервере.

Решение

Переломный момент наступил, когда я начал использовать App Router правильно. Вместо useRouter().push() я научился работать with файловой системой и маршрутизацией через папки и файлы.

Основная идея: каждый маршрут — это папка с page.tsx внутри. URL строится автоматически на основе структуры директорий. Не нужно программно указывать пути — файловая система делает это за вас.

Давайте начнем с базового примера структуры директорий:

app/
├── page.tsx              # /
├── about/
│   └── page.tsx          # /about
├── blog/
│   ├── page.tsx          # /blog
│   └── [slug]/
│       └── page.tsx      # /blog/[slug]
└── shop/
    ├── page.tsx          # /shop
    └── [category]/
        └── [product]/
            └── page.tsx  # /shop/[category]/[product]

Символ [slug] означает динамический сегмент. Это параметр, который попадает в props страницы как объект с данными запроса.

Динамические маршруты

Для работы с динамическими параметрами используется компонент page.tsx внутри папки с квадратными скобками. Параметр маршрута доступен через props:

// app/blog/[slug]/page.tsx
 
export default function BlogPost({ params }: { params: { slug: string } }) {
  return (
    <article>
      <h1>Post: {params.slug}</h1>
      {/* Контент поста */}
    </article>
  )
}

Обратите внимание на типизацию — params — это объект, где ключи соответствуют именам динамических сегментов. Это типобезопасно, TypeScript подскажет, если вы напутаете.

Данные на сервере

Для загрузки данных используйте async/await прямо в компоненте. Это серверный код, который выполняется до рендера HTML:

// app/blog/[slug]/page.tsx
 
async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  return res.json()
}
 
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

Серверные компоненты по умолчанию — это не просто "рендер на сервере", это полноценная архитектурная парадигма для SEO-оптимизации.

Генерация статических параметров

Для SSG (Static Site Generation) нужно сказать Next.js, какие варианты параметров существуют. Это делается через generateStaticParams:

// app/blog/[slug]/page.tsx
 
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json())
  
  return posts.map((post: any) => ({
    slug: post.slug,
  }))
}
 
async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  return res.json()
}
 
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

Эта функция запускается во время сборки. Next.js перебирает все варианты параметров и генерирует статические HTML-файлы. Это значит, что все ваши страницы будут доступны поисковикам без выполнения JavaScript.

Метаданные для SEO

Для управления мета-тегами используйте generateMetadata. Это функция, которая возвращает объект с метаданными страницы:

// app/blog/[slug]/page.tsx
 
import type { Metadata } from 'next'
 
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  const post = await getPost(params.slug)
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.date,
      authors: [post.author],
    },
  }
}

Эта функция тоже выполняется на сервере во время сборки. Результат — корректные <title>, <meta> теги и OpenGraph разметка, которые видны поисковикам.

Навигация между страницами

Для навигации используйте next/link. Это клиентский компонент, который prefetches данные для быстрого перехода:

import Link from 'next/link'
 
export default function BlogList() {
  return (
    <div>
      <Link href="/blog/first-post">
        First Post
      </Link>
      <Link href="/blog/second-post">
        Second Post
      </Link>
    </div>
  )
}

next/link автоматически prefetches данные для hovered ссылок, поэтому переходы мгновенные — это лучшее из обоих миров: серверный рендер для SEO и клиентская навигация для UX.

Pro Tips: Как получить максимум

  • Используйте generateStaticParams для статического контента — это даст вам предсказуемую производительность и мгновенную загрузку для всех известных путей.
  • Комбинируйте SSG и ISR — для статических страниц используйте generateStaticParams, для динамического контента с редкими обновлениями добавьте revalidate: 60 к fetch-запросам.
  • Не забывайте про generateMetadata — это единственный способ добавить SEO-оптимизированные мета-теги для динамических страниц.
  • Разделяйте серверные и клиентские компоненты — всё, что можно рендерить на сервере, рендерите там. Клиентские компоненты только для интерактивности.

Common Pitfalls: Где люди ошибаются

  • Путаница между useRouter и файловой системой — не пытайтесь программно управлять маршрутами через хуки. URL должен определяться структурой директорий, а не клиентским кодом.
  • Отсутствие generateStaticParams — без этой функции Next.js не знает, какие варианты параметров существуют, и не может генерировать статические страницы для каждого.
  • Игнорирование типизации params — TypeScript помогает избежать ошибок, но только если вы правильно типизируете параметры маршрута.
  • Использование useEffect для загрузки данных — это антипаттерн для Next.js. Загружайте данные напрямую в компоненте с async/await.

Переход к App Router — это не просто смена фреймворка, это смена мышления. URL перестаёт быть переменной состояния и становится частью архитектуры приложения. И именно это даёт вам SEO-оптимизацию без лишних костылей.

App Router меняет URL без перезагрузки страницы — та же особенность ломает и другие клиентские интеграции, завязанные на смену маршрута. Пример из практики — аналитика Яндекс.Метрики, которая без ручной подписки на pathname видит только первую страницу визита.

Идентификация

Денис Заплахов

Исследую Пустоши рутинных задач: настраиваю AI-инструменты, создаю скиллы и тестирую нейросети на реальных кейсах.

Досье в Pip-Boy 3000
© 2026 79z.ru · Странствующий техник