В масштабных веб-приложениях с десятками тысяч или миллионами страниц (e-commerce платформы, агрегаторы данных, технические порталы документации) поисковые роботы (Googlebot, Bingbot, YandexBot) не могут обнаружить все узлы исключительно путем рекурсивного перехода по внутренним ссылкам. Если глубина вложенности страницы превышает 3–4 клика от главной, вероятность ее своевременного обхода стремится к нулю из-за ограничений Crawl Budget.
Протокол Sitemaps XML (спецификация sitemaps.org) служит базовым контрактом между платформой и поисковыми системами, передавая структурированный список канонических URL, даты их последнего изменения (lastmod), частоту обновлений (changefreq) и альтернативные языковые версии (hreflang).
Однако протокол накладывает строгие технические ограничения:
При превышении этих лимитов инфраструктура обязана реализовать Sitemap Index (шардинг) — главный индексный XML-файл со ссылками на дочерние файлы sitemap/1.xml, sitemap/2.xml.
Next.js переводит генерацию карт сайта и файлов управления обходом robots.txt из статических текстовых файлов в строго типизированный программный код: специальные файлы app/sitemap.ts и app/robots.ts. Они компилируются на лету или в процессе инкрементальной сборки, поддерживают шардинг через generateSitemaps() и позволяют программно разделять правила индексации для Production и Staging сред.
Next.js абстрагирует ручную конкатенацию XML и текстовых строк через встроенные типы MetadataRoute.Sitemap и MetadataRoute.Robots.
При использовании функции generateSitemaps() Next.js автоматически разворачивает корневой эндпоинт /sitemap.xml как Sitemap Index, направляющий роботов к дочерним шардам.
Поисковый краулер (Googlebot)
│
▼
GET /robots.txt
│
▼
[ app/robots.ts (Проверка process.env.VERCEL_ENV) ]
- Disallow: /api/
- Sitemap: https://agenticlab.dev/sitemap.xml
│
▼
GET /sitemap.xml
│
▼
[ Sitemap Index (Генерируется автоматически) ]
├── <sitemap><loc>https://.../sitemap/0.xml</loc></sitemap>
├── <sitemap><loc>https://.../sitemap/1.xml</loc></sitemap>
└── <sitemap><loc>https://.../sitemap/2.xml</loc></sitemap>
│
┌────────────────────┴────────────────────┐
▼ ▼
GET /sitemap/0.xml GET /sitemap/1.xml
│ │
▼ ▼
[ sitemap({ id: 0 }) ] [ sitemap({ id: 1 }) ]
- Выборка товаров 0..50000 - Выборка товаров 50001..100000
- Потоковый возврат 50 000 URL - Потоковый возврат 50 000 URLКраулер проверяет права обхода и извлекает канонический URL карты сайта: /sitemap.xml.
Next.js через generateSitemaps вычисляет число шардов и отдает XML со списком /sitemap/0.xml, /sitemap/1.xml.
Краулер отправляет параллельные запросы к отдельным подкартам sitemap/{id}.xml.
Функция sitemap({ id }) извлекает легкие проекции строк (slug, updatedAt) с заданным смещением LIMIT/OFFSET.
Next.js сериализует объекты в XML-формат и отдает клиенту со сжатием gzip/brotli.
Что происходит при экспорте функции generateSitemaps() в app/sitemap.ts, если массив идентификаторов возвращает [{ id: 0 }, { id: 1 }]?
generateSitemaps() с параллельной выборкой из базы данных.alternates.languages непосредственно в записи sitemap для мгновенной валидации связей hreflang без ожидания краулинга тела страниц.robots.ts, который динамически проверяет переменные окружения (VERCEL_ENV === 'production') и блокирует индексацию всех непубличных копий приложения (Disallow: /)./api/, /admin/, /checkout/ и распределение активности второстепенных ботов через директивы задержки обхода.app/robots.ts)#Файл robots.ts располагается в корне каталога app и экспортирует функцию по умолчанию, возвращающую объект MetadataRoute.Robots.
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://agenticlab.dev';
const isProduction = process.env.VERCEL_ENV === 'production' || process.env.NODE_ENV === 'production';
// Защита непубличных контуров (Staging, Preview, Local)
if (!isProduction) {
return {
rules: {
userAgent: '*',
disallow: '/', // Полный запрет индексации для всех ботов на превью-доменах
},
sitemap: undefined,
};
}
// Продакшен-конфигурация для публичной индексации
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: [
'/api/',
'/dashboard/',
'/settings/',
'/auth/',
'/*?*utm_*', // Блокировка краулинга дублей с маркетинговыми метками
],
},
{
// Специальные правила для агрессивных AI-краулеров
userAgent: ['GPTBot', 'ChatGPT-User', 'PerplexityBot'],
allow: ['/lessons/', '/courses/', '/blog/'],
disallow: ['/api/', '/dashboard/'],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl,
};
}app/sitemap.ts)#Если общее количество сущностей превышает 50 000, в app/sitemap.ts объявляется вспомогательная функция generateSitemaps(). Она вычисляет необходимое число шардов, а основная функция sitemap({ id }) возвращает батч URL для конкретного идентификатора шарда.
import type { MetadataRoute } from 'next';
const BATCH_SIZE = 25000; // Безопасный размер шарда (вдвое ниже лимита в 50 000)
const BASE_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://agenticlab.dev';
// Вспомогательная функция для получения общего количества записей из БД
async function getTotalProductCount(): Promise<number> {
// Эмуляция запроса SELECT COUNT(*) FROM products
return 85000;
}
// 1. Определение списка доступных шардов карты сайта
// Next.js сформирует Sitemap Index, содержащий ссылки на /sitemap/0.xml, /sitemap/1.xml...
export async function generateSitemaps() {
const totalCount = await getTotalProductCount();
const totalShards = Math.ceil(totalCount / BATCH_SIZE);
return Array.from({ length: totalShards }, (_, index) => ({
id: index,
}));
}
// 2. Генератор конкретного шарда карты сайта
export default async function sitemap({
id,
}: {
id: number;
}): Promise<MetadataRoute.Sitemap> {
const shardId = Number(id);
const skip = shardId * BATCH_SIZE;
// Шард 0 может содержать статичные ключевые разделы сайта
const staticRoutes: MetadataRoute.Sitemap =
shardId === 0
? [
{
url: BASE_URL,
lastModified: new Date().toISOString(),
changeFrequency: 'daily',
priority: 1.0,
},
{
url: `${BASE_URL}/courses`,
lastModified: new Date().toISOString(),
changeFrequency: 'daily',
priority: 0.9,
},
{
url: `${BASE_URL}/about`,
lastModified: new Date().toISOString(),
changeFrequency: 'monthly',
priority: 0.5,
},
]
: [];
// Выборка пагинированных сущностей для текущего шарда
// Эмуляция: SELECT slug, updated_at FROM products LIMIT BATCH_SIZE OFFSET skip
const products = await fetchProductsFromDb(skip, BATCH_SIZE);
const dynamicRoutes: MetadataRoute.Sitemap = products.map((product) => ({
url: `${BASE_URL}/products/${product.slug}`,
lastModified: product.updatedAt,
changeFrequency: 'weekly',
priority: 0.8,
// Включение мультиязычных альтернатив прямо в XML-узел
alternates: {
languages: {
en: `${BASE_URL}/en/products/${product.slug}`,
ru: `${BASE_URL}/ru/products/${product.slug}`,
},
},
}));
return [...staticRoutes, ...dynamicRoutes];
}
// Заглушка функции доступа к БД для типизации
async function fetchProductsFromDb(skip: number, take: number) {
return Array.from({ length: Math.min(take, 25000) }, (_, i) => ({
slug: `product-${skip + i + 1}`,
updatedAt: new Date(Date.now() - (skip + i) * 100000).toISOString(),
}));
}В чем принципиальная разница между директивой Disallow: / в robots.txt и директивой noindex в метаданных для страниц staging-окружения?
Попытка вернуть массив из более чем 50 000 элементов из стандартной функции sitemap() нарушает стандарт sitemaps.org. Google Search Console и Яндекс Вебмастер зафиксируют ошибку парсинга и отклонят всю карту сайта целиком. Если в вашей базе данных количество записей приближается к порогу в 50 000, внедряйте функцию generateSitemaps() заранее с шагом разбиения в 25 000–30 000 записей.
Спецификация Sitemap требует строго абсолютных URL с указанием протокола и доменного имени (FQDN). Запись { url: '/products/laptop' } вместо { url: 'https://agenticlab.dev/products/laptop' } приведет к ошибке Invalid URL format в поисковых консолях. Всегда конкатенируйте пути с переменной базового домена (process.env.NEXT_PUBLIC_APP_URL).
Если в app/robots.ts жестко закодировано правило allow: '/' без проверки среды выполнения, все промежуточные сборки Vercel Preview или Git-ветки будут проиндексированы поисковыми роботами. Это приведет к санкциям за дублированный контент (Duplicate Content Penalty) и расщеплению ссылочного веса основного домена. Обязательно проверяйте переменные process.env.VERCEL_ENV или NODE_ENV.
Если внутри sitemap() запрашивать полные тяжелые записи (включая контент статей, комментарии или связи), генерация карты сайта займет минуты и упадет по серверному таймауту. Для sitemap требуются только три поля: slug, updated_at и языковые локали. Оптимизируйте SQL-запросы: делайте точечный SELECT slug, updated_at с LIMIT и OFFSET по индексированным полям.