У масштабних веб-застосунках із десятками тисяч або мільйонами динамічних сторінок (платформи електронної комерції, агрегатори даних, портали інженерної документації) пошукові роботи (Googlebot, Bingbot, YandexBot) не в змозі виявити всі сторінки лише шляхом рекурсивного переходу за внутрішніми посиланнями. Якщо глибина кліків сторінки перевищує 3–4 переходи від головної, ймовірність її своєчасного сканування різко знижується через обмеження Crawl Budget.
Протокол XML Sitemaps (за стандартом sitemaps.org target="_blank" rel="noopener noreferrer nofollow") є формальним контрактом між веб-ресурсом і пошуковими системами, надаючи структурований каталог канонічних URL, часові мітки останньої модифікації (lastmod), частоту оновлень (changefreq) та мовні альтернативи (hreflang).
Проте протокол встановлює суворі обмеження:
Перевищення цих лімітів вимагає архітектури Sitemap Index (шардингу) — головного індексного XML-файлу з посиланнями на дочірні партиціоновані файли sitemap/1.xml, sitemap/2.xml.
Next.js 15 переводить генерацію карт сайту та файлу керування індексацією robots.txt із застарілої ручної склейки рядків у суворо типізований програмний код: спеціальні файли app/sitemap.ts та app/robots.ts. Вони генеруються на льоту або під час інкрементальної збірки, підтримують автоматичний шардинг через generateSitemaps() та дозволяють програмно ізолювати правила індексації для Production, Staging та Preview контурів.
Next.js 15 абстрагує генерацію низькорівневого XML та конфігурації через вбудовані інтерфейси TypeScript 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 URLgenerateSitemaps() із паралельним отриманням даних із бази.alternates.languages безпосередньо в XML-вузли карти сайту для миттєвої валідації зв'язків hreflang без очікування парсингу HTML-сторінок.robots.ts, що аналізує змінні оточення (VERCEL_ENV === 'production') та блокує сканування всіх тестових копій (Disallow: /)./api/, /dashboard/, /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,
};
}
// Конфігурація для робочого оточення (Production)
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(). Вона повертає масив ідентифікаторів, після чого Next.js викликає функцію sitemap({ id }) для кожного окремого шарда.
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,
},
]
: [];
// Вибірка записів для поточного шарда з обмеженням OFFSET / LIMIT
// Емуляція: 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}`,
es: `${BASE_URL}/es/products/${product.slug}`,
uk: `${BASE_URL}/uk/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(),
}));
}Повернення масиву з понад 50 000 елементів з єдиної функції sitemap() порушує стандарт sitemaps.org target="_blank" rel="noopener noreferrer nofollow". Пошукові системи зафіксують помилку і відхилять увесь XML-файл. Якщо розмір каталогу наближається до 50 000 адрес, впроваджуйте функцію generateSitemaps() завчасно з розміром пакета 25 000–30 000 записів.
Специфікація XML Sitemap вимагає виключно повних абсолютних URL-адрес (FQDN) із протоколом та доменом. Оголошення { url: '/products/laptop' } замість { url: 'https://agenticlab.dev/products/laptop' } призведе до помилок валідації у Search Console. Завжди додавайте префікс базового канонічного домену (process.env.NEXT_PUBLIC_APP_URL).
Встановлення правила allow: '/' у app/robots.ts без перевірки типу оточення призведе до індексації тестових URL Vercel Preview. Це створює дублікати сторінок та знижує авторитетність основного домену. Обов'язково перевіряйте змінні process.env.VERCEL_ENV або NODE_ENV.
Вибірка повних об'єктів бази даних (із контентом статей чи складними зв'язками) всередині sitemap() спричинить тайм-аут сервера. Карті сайту необхідні лише три поля: slug, updated_at та мовні коди. Застосовуйте оптимізований SELECT slug, updated_at із конструкціями LIMIT та OFFSET за індексованими стовпцями.
Бот перевіряє права обходу та знаходить посилання на індекс карти сайту: /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.