SEO в Next.js: метаданные, sitemap, robots.txt и OG-картинки
Практика4 минРедакция Innovialabs
Коротко
В 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 и Яндекс Вебмастер.