Sitemap и robots в Next.js 15: индексация SEO-лендинга без ручного XML
Лендинг собран на Next.js 15, Core Web Vitals в зелёной зоне, JSON-LD и Open Graph на месте — а в Google Search Console через две недели «Обнаружено — не проиндексировано» для половины URL. Частая причина не в контенте, а в том, что поисковик не знает полный список страниц или тратит crawl budget на /api, preview-деплои и дубли с query-параметрами.
Ручной public/sitemap.xml устаревает после каждого поста в блоге или кейса из Sanity. Статический robots.txt с Disallow: / на staging случайно попадает в production. В App Router Next.js 15 эти файлы генерируются типобезопасно через app/sitemap.ts и app/robots.ts — они участвуют в build, подхватывают динамические маршруты и деплоятся на Vercel Edge вместе с остальным приложением.
В статье — production-подход для SEO-лендинга и мультиязычного сайта: robots с разделением окружений, sitemap из CMS и файловой системы, hreflang в sitemap, лимиты Google и проверка в Search Console.
Зачем sitemap и robots лендингу в 2026 году
Google по-прежнему использует sitemap как подсказку приоритетов и свежести, а robots.txt — как правила обхода на уровне хоста. Ни один файл не гарантирует индексацию, но их отсутствие или ошибки создают предсказуемые проблемы.
| Симптом в Search Console | Частая техническая причина |
|---|---|
| Новые URL не появляются неделями | Нет sitemap, нет внутренних ссылок |
Индексируются /api/*, /_next/* | robots не закрывает служебные пути |
| Staging попал в индекс | robots на preview без noindex + без Basic Auth |
Дубли ?utm= и /page/ vs /page | canonical есть, но краулер всё равно обходит мусор |
Для лендинга услуг с 5–15 статическими страницами и блогом на 20–50 статей динамический sitemap окупается с первого релиза: один источник правды, lastModified из CMS, changeFrequency по типу контента.
Robots на Next.js 15 — не замена <meta name="robots">. Страница «спасибо за заявку» или черновик в CMS должны иметь noindex в metadata, а robots лишь сужает обход тяжёлых или бесполезных путей.
robots.ts: окружения, служебные пути и ссылка на sitemap
Файл app/robots.ts экспортирует функцию, возвращающую объект MetadataRoute.Robots. Next.js отдаёт его по адресу /robots.txt с корректным Content-Type.
Базовый шаблон для production
// app/robots.ts
import type { MetadataRoute } from 'next';
const BASE = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://example.com';
export default function robots(): MetadataRoute.Robots {
const isProduction = process.env.VERCEL_ENV === 'production';
if (!isProduction) {
return {
rules: { userAgent: '*', disallow: '/' },
sitemap: undefined,
};
}
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/', '/_next/', '/private/'],
},
],
sitemap: `${BASE}/sitemap.xml`,
host: BASE.replace(/^https?:\/\//, ''),
};
}
Ключевые решения:
VERCEL_ENV !== 'production'— полныйDisallow: /на preview и local. Дополнительно на preview-деплоях в Vercel включайте Deployment Protection, иначе robots не спасёт от прямых ссылок.disallowдля/api/— Route Handlers и webhooks не должны попадать в индекс; краулер всё равно может найти их по ссылкам, поэтому для чувствительных endpoint добавляйте 401 или отсутствие HTML-ответа.sitemapс абсолютным URL — Google требует полный URL в директивеSitemap:.
Когда нужны отдельные правила для ботов
Для обычного лендинга одного блока userAgent: '*' достаточно. Отдельные правила для Googlebot имеют смысл, если вы сознательно разрешаете краулинг AI-ботам или наоборот закрываете их через Disallow — помните, что это не юридическая защита контента, только управление нагрузкой.
Если нужна подобная разработка — напишите в Telegram.
sitemap.ts: статика, блог и headless CMS
Файл app/sitemap.ts генерирует /sitemap.xml. Для крупных сайтов (>50 000 URL) используйте sitemap index и несколько файлов через generateSitemaps (Next.js 15); для лендинга + блога хватает одного массива записей.
Статические страницы и приоритеты
// app/sitemap.ts
import type { MetadataRoute } from 'next';
const BASE = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://example.com';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const staticRoutes: MetadataRoute.Sitemap = [
{ url: `${BASE}/`, lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
{ url: `${BASE}/services`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.9 },
{ url: `${BASE}/contacts`, lastModified: new Date(), changeFrequency: 'yearly', priority: 0.7 },
];
const posts = await getBlogPosts(); // см. ниже
const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${BASE}/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.6,
}));
return [...staticRoutes, ...postEntries];
}
Поле priority Google официально игнорирует в пользу внутренней логики, но lastModified полезен при частых обновлениях кейсов и цен. Берите дату из CMS (_updatedAt в Sanity), не new Date() на каждый build — иначе краулер получает ложный сигнал «всё изменилось».
Подключение Sanity / Contentful без лишних запросов
Запрос к CMS в sitemap() выполняется на build time (SSG) или при revalidation — в зависимости от export const revalidate. Для лендинга с on-demand revalidation из статьи про Sanity CMS синхронизируйте:
export const revalidate = 3600; // 1 час — sitemap обновляется вместе со страницами
Кешируйте список slug в unstable_cache или на уровне fetch с next: { tags: ['blog'] }, чтобы sitemap не дергал CMS на каждый запрос /sitemap.xml в dev.
Локальные файлы блога (Content Collections / MDX)
Если статьи лежат в src/content/blog/ru/*.md, обходите файловую систему на build:
import fs from 'node:fs/promises';
import path from 'node:path';
import matter from 'gray-matter';
async function getBlogPosts() {
const dir = path.join(process.cwd(), 'src/content/blog/ru');
const files = await fs.readdir(dir);
return Promise.all(
files
.filter((f) => f.endsWith('.md'))
.map(async (file) => {
const raw = await fs.readFile(path.join(dir, file), 'utf8');
const { data } = matter(raw);
return {
slug: file.replace(/\.md$/, ''),
updatedAt: new Date(data.publishedAt ?? Date.now()),
};
}),
);
}
Так sitemap всегда совпадает с реальным контентом репозитория — без ручного XML после каждого merge.
Мультиязычность: alternates и отдельные sitemap
Для пары локалей ru / en каждая языковая версия URL должна быть связана через hreflang. В Next.js 15 Metadata API это делается в layout.tsx через alternates.languages; в sitemap — поле alternates:
{
url: `${BASE}/ru/services`,
lastModified: new Date('2026-05-01'),
alternates: {
languages: {
ru: `${BASE}/ru/services`,
en: `${BASE}/en/services`,
'x-default': `${BASE}/en/services`,
},
},
}
Практика для лендинга в 2026:
- Один домен, префиксы
/ruи/en— проще для CWV и одного sitemap. - Отдельные домены
example.ru/example.com— два sitemap и перекрёстныеalternatesобязательны; ошибка вx-default— частая причина «неправильной» выдачи в Search Console → Международное таргетирование.
Не добавляйте в sitemap URL с noindex (страница благодарности, черновики, /search). Robots их не удалит из индекса, если на них уже есть ссылки.
Деплой на Vercel, лимиты и Search Console
Лимиты Google Sitemap
- До 50 000 URL и 50 MB несжатого XML на файл.
- При превышении — sitemap index и несколько
sitemap/0.xml,sitemap/1.xmlчерезgenerateSitemapsв Next.js.
Для типичного SEO-лендинга с блогом вы далеки от лимита; важнее корректность URL (без trailing slash дублей — выберите один стиль в next.config trailingSlash).
Проверка после релиза
- Откройте
https://your-domain.com/robots.txtиhttps://your-domain.com/sitemap.xml— статус 200, валидный XML. - Search Console → Файлы Sitemap → добавьте URL sitemap → статус «Успешно».
- Проверка URL для 2–3 свежих страниц из sitemap — «URL есть в Google».
- Отчёт Страницы → исключения «Заблокировано robots.txt» — не должно включать
/,/services, статьи блога.
Edge и кеширование
sitemap.xml и robots.txt по умолчанию статичны на build. Если sitemap полностью динамический (тысячи товаров), задайте export const revalidate или CDN-cache с коротким TTL — иначе Vercel отдаёт свежий sitemap только после redeploy.
Подключите уведомление в Slack/Telegram при падении build: битый sitemap.ts (throw в CMS fetch) иногда ломает весь /sitemap.xml, а не одну запись.
Типичные ошибки на Next.js 15 лендингах
| Ошибка | Последствие |
|---|---|
NEXT_PUBLIC_SITE_URL не задан на Vercel | В sitemap URL https://example.com или localhost |
Дубли с и без / в конце | Два URL в индексе, размытый сигнал canonical |
Sitemap включает ?ref= и UTM | Мусорные URL в отчётах GSC |
| Только robots, без sitemap на новом домене | Медленное обнаружение глубоких страниц блога |
allow: / + noindex на странице | Норма: robots разрешает, meta запрещает индекс |
| Забыли исключить OG-image route | Редко, но /opengraph-image не нужен в sitemap |
Связка с уже настроенным SEO-стеком: canonical в generateMetadata, JSON-LD для Organization, Open Graph для превью — sitemap дополняет, не заменяет их. После внедрения PPR и edge deploy проверьте, что sitemap генерируется на build с теми же slug, что видит пользователь в HTML.
Нужна помощь? Telegram → или vic.kell@ya.ru
FAQ
Нужен ли sitemap, если страниц меньше десяти?
Да, как страховка для блога и новых лендингов под кампании. Google находит и маленькие сайты по ссылкам, но sitemap ускоряет появление новых URL в отчётах и упрощает диагностику.
Можно ли оставить public/sitemap.xml вместо sitemap.ts?
Можно, но при CMS и i18n файл устареет. sitemap.ts версионируется в Git, проходит code review и не требует ручного экспорта из плагинов.
Блокирует ли Disallow в robots индексацию страницы?
Нет полностью: страница может быть проиндексирована, если на неё ссылаются с других сайтов, но контент Google может не качать. Для запрета индекса используйте robots: { index: false } в metadata.
Сколько sitemap указывать в robots.txt?
Обычно один — главный sitemap.xml. При sitemap index в robots перечисляют index-файл, дочерние подтянутся автоматически.
Нужно ли отправлять sitemap в Яндекс?
Для RU-трафика — да, в Яндекс.Вебмастер отдельно. Формат тот же; hreflang Яндекс тоже учитывает.
Влияет ли sitemap на Core Web Vitals?
Нет напрямую. Косвенно — если в sitemap попали тяжёные preview-URL, краулер нагружает origin; держите sitemap чистым.
Заключение
sitemap.ts и robots.ts в Next.js 15 App Router — минимальный, но обязательный слой технического SEO для лендинга и блога. Robots отсекает preview и служебные пути, sitemap сообщает Google актуальный список URL с правильными lastModified и hreflang.
В связке с headless CMS, edge deploy на Vercel и revalidation вы получаете индексацию без ручного XML после каждого релиза. Потратьте час на настройку и проверку в Search Console — это дешевле, чем недели ожидания, пока новые кейсы и статьи появятся в выдаче сами.