Перейти к содержанию
Innovialabs

SEO в Next.js: метаданные, sitemap и robots в App Router

Практика3 мин

Коротко

В App Router SEO настраивается кодом — объект metadata или функция generateMetadata для заголовков, canonical и hreflang, файлы sitemap.ts и robots.ts для сканирования, opengraph-image для превью; главное — абсолютные URL через metadataBase и свой canonical у каждой страницы.

Как устроено SEO в App Router

В App Router не нужно вручную писать теги в <head>. Next.js собирает их из нескольких источников:

  • metadata — статический объект в layout.tsx или page.tsx;
  • generateMetadata — функция для динамических страниц;
  • специальные файлы sitemap.ts, robots.ts, opengraph-image.tsx в папке app.

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

Базовые метаданные в layout

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

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: { default: 'Компания', template: '%s — Компания' },
  description: 'Короткое описание сайта',
}

metadataBase превращает относительные пути (/blog/post) в абсолютные URL в canonical, hreflang и Open Graph. Без него ссылки в превью соцсетей могут сломаться.

generateMetadata для динамических страниц

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

type Props = { params: Promise<{ slug: string }> }

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)

  return {
    title: post.title,
    description: post.description,
    alternates: {
      canonical: `/blog/${slug}`,
      languages: {
        ru: `/ru/blog/${slug}`,
        en: `/en/blog/${slug}`,
        'x-default': `/en/blog/${slug}`,
      },
    },
    openGraph: { type: 'article', title: post.title },
  }
}

В свежих версиях Next.js params — это Promise, поэтому его нужно await. Запросы fetch с одинаковыми параметрами в generateMetadata и в самой странице дедуплицируются, лишней нагрузки нет. Для несуществующего slug вызывайте notFound(), чтобы сервер вернул 404, а не пустую страницу с кодом 200.

sitemap.ts и robots.ts

// app/sitemap.ts
import type { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts()
  return posts.map((p) => ({
    url: `https://example.com/ru/blog/${p.slug}`,
    lastModified: p.updatedAt,
    alternates: {
      languages: { en: `https://example.com/en/blog/${p.slug}` },
    },
  }))
}
// app/robots.ts
import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/', disallow: '/admin/' },
    sitemap: 'https://example.com/sitemap.xml',
  }
}

В sitemap кладите только канонические URL с кодом 200. Если адресов больше лимита одного файла (50 000), разбейте карту через generateSitemaps.

Open Graph изображения

Файл opengraph-image.tsx (или .png/.jpg) рядом со страницей автоматически добавляет og:image. Через ImageResponse из next/og картинку можно генерировать из заголовка статьи. Рекомендуемый размер — 1200×630.

Типичные ошибки, из-за которых страницы не индексируются

  • Canonical в корневом layout. alternates.canonical: '/' наследуется всеми страницами, и все они указывают на главную. Задавайте canonical на уровне каждой страницы.
  • Нет metadataBase — относительные canonical и og:image собираются неверно.
  • robots: { index: false } остался со staging или задан в общем layout.
  • hreflang без взаимности: русская версия ссылается на английскую, а английская — нет. Каждая версия должна перечислять все остальные и себя.
  • В sitemap редиректы, 404 и неканонические адреса — робот тратит бюджет и меньше доверяет карте.
  • Пустой HTML. Если контент подгружается только на клиенте через 'use client' и useEffect, робот может увидеть пустую страницу. Рендерьте основной текст на сервере.
  • Мягкие 404: страница «не найдено» отдаёт код 200 вместо notFound().
  • Разнобой со слешем: /blog и /blog/ доступны оба без редиректа.
  • Тонкие и дублирующие страницы: тысячи почти одинаковых страниц Google часто сканирует, но не индексирует.

Как проверить результат

  1. Откройте исходный код страницы и найдите <title>, canonical, hreflang, og:image.
  2. Проверьте /sitemap.xml и /robots.txt в браузере.
  3. В Search Console используйте проверку URL: она покажет, какой canonical выбрал Google и видит ли он контент.

Частые вопросы

Общие поля (metadataBase, шаблон title) — в layout, а canonical — в каждой странице или её generateMetadata. Иначе canonical унаследуется и будет одинаковым у многих страниц.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.