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

SEO в Next.js: метаданные, sitemap, robots.txt и OG-картинки

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

Коротко

В App Router SEO задаётся экспортом metadata или generateMetadata, специальными файлами sitemap.ts, robots.ts и opengraph-image.tsx и скриптом JSON-LD; в многоязычных маршрутах каждой странице нужны canonical и альтернативы hreflang.

Коротко

В проекте на Next.js App Router SEO-плагин не нужен — всё встроено:

  • metadata или generateMetadata в layout.tsx и page.tsx — для title, description, canonical, hreflang и Open Graph;
  • app/sitemap.ts и app/robots.ts — генерируют sitemap.xml и robots.txt;
  • opengraph-image.tsx — рисует картинку для соцсетей из кода;
  • тег <script type="application/ld+json"> — для структурированных данных.

Примеры ниже следуют соглашениям App Router; сверяйтесь с официальной документацией для вашей версии Next.js — детали, например то, что params стал Promise, со временем менялись.

Базовые настройки в корневом layout

Задайте metadataBase один раз, чтобы относительные URL в canonical и Open Graph стали абсолютными:

// app/[lang]/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: { default: 'Studio', template: '%s | Studio' },
}

template автоматически добавляет название сайта к заголовку каждой страницы.

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

Для страниц из данных используйте generateMetadata. На многоязычном сайте у каждой языковой версии свой canonical и полный набор альтернатив hreflang, включая x-default:

// app/[lang]/blog/[slug]/page.tsx
type Props = { params: Promise<{ lang: string; slug: string }> }

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { lang, slug } = await params
  const post = await getPost(slug, lang)
  return {
    title: post.title,
    description: post.description,
    alternates: {
      canonical: `/${lang}/blog/${slug}`,
      languages: {
        ru: `/ru/blog/${slug}`,
        en: `/en/blog/${slug}`,
        uz: `/uz/blog/${slug}`,
        'x-default': `/ru/blog/${slug}`,
      },
    },
    openGraph: { type: 'article', title: post.title, description: post.description },
  }
}

Главные правила:

  • canonical указывает на саму страницу, а не на основную языковую версию;
  • каждая языковая версия перечисляет все версии, включая себя;
  • указывайте только реально существующие переводы.

Динамический sitemap

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

const base = 'https://example.com'
const langs = ['ru', 'en', 'uz']

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts()
  return posts.flatMap((p) =>
    langs.map((lang) => ({
      url: `${base}/${lang}/blog/${p.slug}`,
      lastModified: p.updatedAt,
      alternates: {
        languages: Object.fromEntries(langs.map((l) => [l, `${base}/${l}/blog/${p.slug}`])),
      },
    })),
  )
}

В одном файле sitemap может быть не больше 50 000 URL. Для крупных сайтов в Next.js есть generateSitemaps, который разбивает карту на несколько файлов. Указывайте реальную дату lastModified, а не время сборки, иначе поисковики перестанут ей доверять.

robots.txt

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

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

Помните: disallow запрещает сканирование, а не индексацию. Чтобы убрать страницу из выдачи, укажите в её метаданных robots: { index: false } и оставьте её доступной для сканирования, чтобы поисковик увидел этот тег.

Генерируемые Open Graph картинки

Файл opengraph-image.tsx рядом со страницей создаёт её превью для соцсетей. ImageResponse превращает JSX в PNG:

// app/[lang]/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'

export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

export default async function Image({ params }: { params: Promise<{ lang: string; slug: string }> }) {
  const { lang, slug } = await params
  const post = await getPost(slug, lang)
  return new ImageResponse(
    <div style={{ display: 'flex', width: '100%', height: '100%', padding: 80, fontSize: 64, background: '#120B24', color: '#fff' }}>
      {post.title}
    </div>,
    size,
  )
}

Теги og:image Next.js добавит сам. Поддерживается только flexbox и часть CSS; для кириллицы и узбекских символов подключите шрифт, в котором они есть.

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

const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: post.title,
  datePublished: post.date,
  inLanguage: lang,
}

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }}
/>

Экранирование < защищает от внедрения HTML через контент. Проверьте результат в Google Rich Results Test.

Чек-лист перед релизом

  • Уникальные title и description на каждой странице.
  • Canonical и hreflang на всех языковых версиях.
  • Все публичные страницы в sitemap, без редиректов и 404 внутри.
  • Внутренние ссылки между связанными страницами, чтобы роботы находили каждый URL и без sitemap.
  • Sitemap добавлен в Google Search Console и Яндекс Вебмастер.

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

Sitemap помогает только обнаружить страницы; индексация зависит от качества и уникальности контента, внутренних ссылок и сигналов важности. Проверьте дубли, слабые страницы и неверные canonical, ставьте ссылки на новые страницы с уже существующих.

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

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

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

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

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

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