SEO-оптимизация Next.js сайта

Практическое руководство по SEO-оптимизации сайта на Next.js. Настройка мета-тегов, скорости загрузки и краулингового бюджета.

SEO-оптимизация Next.js — это не надстройка, а архитектурное преимущество фреймворка. В отличие от одностраничных приложений на React, где поисковые системы могут не дожидаться клиентского рендера, Next.js 15 из коробки предоставляет серверный рендеринг, статическую генерацию и инкрементальную регенерацию. По состоянию на 2026 год Google и Яндекс обрабатывают JavaScript достаточно уверенно, но серверная отрисовка по-прежнему даёт предсказуемый результат и минимальное время до отрисовки основного контента.

Почему Next.js подходит для SEO

Традиционные React-приложения (Create React App, Vite без SSR) рендерятся в браузере. Поисковый робот получает пустой div и должен выполнить JavaScript, чтобы увидеть содержимое. Это добавляет задержку и нестабильность в индексации. Next.js предлагает три стратегии рендеринга, решающие эту проблему.

Первая стратегия — серверный рендеринг (SSR). При каждом запросе сервер формирует готовый HTML и отправляет его клиенту. Поисковый робот видит контент сразу, без выполнения скриптов. Вторая стратегия — статическая генерация (SSG). HTML генерируется на этапе сборки и раздаётся как статический файл. Это даёт минимальный TTFB и максимальную надёжность. Третья стратегия — инкрементальная регенерация (ISR). Статические страницы обновляются по расписанию, сохраняя преимущества SSG при частых изменениях данных.

Дополнительно Next.js автоматически оптимизирует изображения и шрифты. Компонент next/image генерирует современные форматы WebP и AVIF, адаптивные размеры и встроенную отложенную загрузку. Модуль next/font саморазмещает шрифты, исключая внешние запросы к Google Fonts и предотвращая CLS (сдвиг макета). Эти оптимизации напрямую улучшают Core Web Vitals — фактор ранжирования с 2021 года.

На проекте интернет-магазина с 20 000 товаров мы перевели фронтенд с CRA на Next.js SSG. Индексация страниц каталога ускорилась с 2 недель до 3 дней, а LCP на десктопе снизился с 3,8 до 1,4 секунд. Серверный рендеринг и автоматические оптимизации работают системно, без ручного тюнинга.

Metadata API в App Router

App Router, стабильный с версии 13.4, ввёл декларативный Metadata API для управления мета-тегами. Он базируется на React Server Components и поддерживает статические и динамические метаданные без лишних эффектов. Metadata API автоматически объединяет данные из корневых и дочерних шаблонов, генерирует теги title, meta, link, open-graph и другие.

Статические метаданные

В любом файле layout.tsx или page.tsx можно экспортировать объект metadata или функцию generateMetadata. Статические метаданные задают фиксированные значения и не требуют асинхронных запросов.

// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Мой сайт',
  description: 'Онлайн-магазин электроники',
  metadataBase: new URL('https://example.com'),
  openGraph: {
    type: 'website',
    locale: 'ru_RU',
    siteName: 'Мой сайт',
  },
  robots: { index: true, follow: true },
};

export default function RootLayout({ children }) {
  return children;
}

Этот код в корневом шаблоне задаёт базовый URL, описание по умолчанию и настройки для всех страниц. Важно указывать metadataBase — он используется для построения абсолютных URL у Open Graph и канонических ссылок.

Для отдельной страницы метаданные можно переопределить:

// app/about/page.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'О компании',
  description: 'История и команда проекта',
};

export default function AboutPage() {
  return 

О компании

; }

App Router автоматически сливает метаданные: description из дочерней страницы заменит корневое значение, а openGraph дополнится унаследованными полями. Каноническая ссылка формируется на основе metadataBase и текущего сегмента пути.

Динамические метаданные

Для страниц с динамическими параметрами (товар, статья) используют асинхронную функцию generateMetadata. Она получает объект с params и searchParams и может выполнять запросы к API или базе данных.

// app/product/[slug]/page.tsx
import { getProduct } from '@/lib/api';

export async function generateMetadata({ params }) {
  const product = await getProduct(params.slug);
  if (!product) {
    return { title: 'Товар не найден' };
  }
  return {
    title: product.title,
    description: product.description?.slice(0, 160),
    openGraph: {
      images: [product.image],
    },
    alternates: {
      canonical: `/product/${product.slug}`,
    },
  };
}

export default async function ProductPage({ params }) {
  const product = await getProduct(params.slug);
  return (
    

{product.title}

{product.description}

); }

Функция выполняется на сервере при каждом запросе (SSR) или во время статической генерации (SSG/ISR). Это позволяет формировать точные мета-теги на основе контента, не прибегая к клиентским хакам.

Если на одной странице несколько параметров, например фильтры, их можно получить через searchParams. Для SEO полезно задавать canonical и noindex на нежелательные комбинации, чтобы избежать дублирования.

Template для title

В корневом макете можно задать объект title с полем template. Тогда все дочерние страницы, предоставляющие строку title, будут автоматически обёрнуты в этот шаблон. Символ %s заменяется на значение title из дочерней страницы.

export const metadata: Metadata = {
  title: {
    template: '%s | Мой магазин',
    default: 'Мой магазин',
  },
};

Теперь страница «Контакты» получит title «Контакты | Мой магазин», а главная страница без указания title сохранит дефолтный «Мой магазин». Это избавляет от дублирования названия сайта в каждом page.tsx и централизует формат заголовков.

При использовании generateMetadata можно вернуть как строку, так и объект с полем absolute, чтобы переопределить шаблон полностью. Метод удобен при мультиязычности, когда суффикс зависит от языка.

Выбор стратегии рендеринга

Next.js 15 поддерживает все три стратегии в рамках одного приложения. Выбор между SSG, SSR и ISR зависит от частоты обновления контента, персонализации и требований к TTFB.

Критерий SSG (статическая генерация) SSR (серверный рендеринг) ISR (инкрементальная регенерация)
Формирование HTML На этапе сборки При каждом запросе При первом запросе, затем по таймеру
TTFB Минимальный (из CDN) Зависит от данных (обычно 100-500 мс) Как SSG для кэшированных страниц
Индексация Мгновенная при попадании в sitemap Стабильная, HTML приходит готовым Новые страницы сначала рендерятся SSR
Обновление контента Только через ребилд При каждом запросе Фоновое перестроение через revalidate
Подходит для Блоги, лендинги, витрины без персональных данных Страницы с пользовательскими данными, корзина, админка Каталог товаров, статьи с частыми правками

На практике большинство страниц — это смешанный подход. Главная страница и страницы блога генерируются статически, страницы товаров используют ISR с интервалом 60 секунд, а личный кабинет всегда SSR. Такая комбинация не усложняет сборку — Next.js позволяет определить стратегию на уровне route segment.

Для статической генерации динамических маршрутов (например, /blog/[slug]) применяется функция generateStaticParams. Она возвращает список значений параметров, для которых нужно сгенерировать HTML на этапе сборки.

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

Если параметр не попал в этот список, запрос может вернуть 404 или сгенерировать страницу динамически (fallback). Для ISR ключевой параметр — revalidate. Он указывается либо через fetch с опцией next: { revalidate: 60 }, либо в сегменте через export const revalidate = 60; в page.tsx.

Кэширование fetch в Server Components по умолчанию включено: все запросы fetch кэшируются навсегда (force-cache), что позволяет строить SSG-страницы без ручки. Для SSR или ISR запросы маркируются cache: 'no-store' либо задаётся revalidate. Такой подход предотвращает избыточные обращения к CMS при каждой перестройке страницы, кроме случая, когда данные нужно обновить.

// ISR с кэшированием на 120 секунд
const res = await fetch('https://api.example.com/products', {
  next: { revalidate: 120 },
});
const products = await res.json();

Оптимизация изображений

Компонент next/image — это не просто обёртка над img. Он автоматически преобразует изображения в формат WebP (а при поддержке — в AVIF), генерирует адаптивный srcset с разными разрешениями и добавляет атрибуты width и height для предотвращения CLS. Всё это работает без плагинов и дополнительной настройки.

Для правильной индексации изображений стоит заполнять alt, а для основного контента (LCP) — использовать атрибут priority. Он отключает ленивую загрузку для этого изображения и добавляет preload в head. Без него LCP-изображение может загружаться с задержкой, ухудшая показатель.

import Image from 'next/image';

export default function Hero() {
  return (
    Главный баннер
  );
}

Параметр sizes сообщает браузеру, какой ширины будет изображение на разных экранах, помогая выбрать нужный файл из srcset. При использовании адаптивных макетов без sizes браузер может загрузить слишком большое изображение для мобильной версии.

Для внешних изображений необходимо указать remotePatterns в next.config.js, иначе компонент откажется их оптимизировать. Это защита от злоупотреблений и резкого роста нагрузки на сервер оптимизации.

Параметр Назначение Влияние на SEO
alt Альтернативный текст Прямой сигнал для Image Search
priority Предзагрузка LCP-изображения Улучшает LCP, косвенно влияет на ранжирование
sizes Подсказки браузеру для srcset Снижает вес страницы на мобильных
quality Качество сжатия (по умолчанию 75) Баланс между весом и визуалом
loading lazy или eager Управляет приоритетом загрузки

На проекте с каталогом из 15 000 товаров мы столкнулись с тем, что автоматические форматы WebP снижали размер картинок в среднем на 40% относительно JPEG. На мобильных устройствах это сократило Largest Contentful Paint с 2,8 до 1,6 секунд. Такой прирост достигался только за счёт внедрения next/image вместо обычного img.

Шрифты и производительность

Подключение шрифтов через Google Fonts добавляет внешние запросы и может вызывать CLS при загрузке. Модуль next/font решает эту проблему: он загружает файлы шрифтов на этапе сборки и размещает их вместе с остальной статикой. Браузер получает шрифт с того же домена, без цепочек переадресаций.

import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['cyrillic', 'latin'],
  display: 'swap',
  variable: '--font-inter',
});

export default function RootLayout({ children }) {
  return (
    
      {children}
    
  );
}

Параметр display: 'swap' гарантирует, что текст будет немедленно отрисован системным шрифтом, а затем заменён на кастомный после загрузки. Это исключает период невидимого текста (FOIT). CSS-свойство font-display: swap теперь применяется автоматически.

Собранные шрифты попадают в .next/static/media и раздаются с долгим кэшированием. Никаких запросов к fonts.googleapis.com на продакшене не происходит. Для локальных шрифтов используется тот же API через localFont, что позволяет использовать фирменные шрифты без CDN.

CLS от шрифтов почти полностью устраняется, потому что Next.js генерирует корректный @font-face с указанием точных метрик size-adjust, ascent-override и других. При тестировании магазина после миграции на next/font CLS снизился с 0,12 до 0,01.

Структурированные данные

JSON-LD — рекомендуемый Google формат для структурированных данных. Next.js не имеет встроенных компонентов для JSON-LD, но легко интегрируется через серверные компоненты. Достаточно разместить script с нужным содержимым в head через Metadata API или непосредственно в JSX.

Простой компонент для генерации JSON-LD:

export function JsonLd({ data }) {
  return (