En aplicaciones web de gran escala con decenas de miles o millones de páginas dinámicas (catálogos de comercio electrónico, agregadores de información, plataformas de documentación), los rastreadores de búsqueda (Googlebot, Bingbot, YandexBot) no logran descubrir cada nodo basándose únicamente en el seguimiento recursivo de enlaces internos. Si la profundidad de clics de una página supera 3 o 4 niveles desde la página principal, la probabilidad de que sea indexada a tiempo decae drásticamente debido a los límites de Crawl Budget.
El protocolo XML Sitemaps (gobernado por sitemaps.org target="_blank" rel="noopener noreferrer nofollow") establece un contrato formal entre la aplicación web y los motores de búsqueda, suministrando un índice estructurado de URLs canónicas, marcas temporales de última modificación (lastmod), frecuencias de actualización (changefreq) y versiones lingüísticas alternativas (hreflang).
Sin embargo, el estándar impone restricciones técnicas estrictas:
Superar estos límites exige adoptar una arquitectura de Sitemap Index (sharding) — un archivo XML raíz que referencia submapas particionados como sitemap/1.xml, sitemap/2.xml.
Next.js 15 reemplaza la concatenación manual de cadenas XML y los archivos estáticos por manejadores programáticos fuertemente tipados: app/sitemap.ts y app/robots.ts. Estos se compilan bajo demanda o durante la generación estática incremental, admiten particionado automatizado mediante generateSitemaps() y permiten aislar de forma programática las políticas de rastreo entre entornos de Producción, Staging y Preview.
Next.js 15 abstrae la serialización XML y de texto plano de bajo nivel mediante las interfaces nativas de TypeScript MetadataRoute.Sitemap y MetadataRoute.Robots.
Cuando se exporta la función generateSitemaps(), Next.js convierte automáticamente el endpoint raíz /sitemap.xml en un Sitemap Index, delegando el tráfico de los rastreadores hacia los shards hijos.
Rastreador de Búsqueda (Googlebot)
│
▼
GET /robots.txt
│
▼
[ app/robots.ts (Evalúa process.env.VERCEL_ENV) ]
- Disallow: /api/
- Sitemap: https://agenticlab.dev/sitemap.xml
│
▼
GET /sitemap.xml
│
▼
[ Sitemap Index (Generado Automáticamente) ]
├── <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 }) ]
- Consulta productos 0..50000 - Consulta productos 50001..100000
- Envío de 50,000 URLs en stream - Envío de 50,000 URLs en streamgenerateSitemaps() con consultas concurrentes a la base de datos.alternates.languages dentro de cada nodo del sitemap para validar relaciones hreflang sin esperar al rastreo individual de páginas.robots.ts dinámico que verifica variables de entorno (VERCEL_ENV === 'production') para bloquear de forma tajante el acceso a copias de prueba (Disallow: /)./api/, /dashboard/, /checkout/) y regulación de la actividad de bots mediante directivas claras.app/robots.ts)#El archivo robots.ts se ubica en la raíz del directorio app y exporta una función por defecto que devuelve un objeto 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';
// Protección para entornos no productivos (Staging, Preview, Local)
if (!isProduction) {
return {
rules: {
userAgent: '*',
disallow: '/', // Bloqueo total de indexación en despliegues de prueba
},
sitemap: undefined,
};
}
// Configuración de rastreo para el entorno de Producción
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: [
'/api/',
'/dashboard/',
'/settings/',
'/auth/',
'/*?*utm_*', // Evitar rastreo de URLs duplicadas por parámetros de marketing
],
},
{
// Reglas específicas para bots de indexación de IA
userAgent: ['GPTBot', 'ChatGPT-User', 'PerplexityBot'],
allow: ['/lessons/', '/courses/', '/blog/'],
disallow: ['/api/', '/dashboard/'],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl,
};
}app/sitemap.ts)#Cuando el número total de registros excede los 50,000 elementos, app/sitemap.ts declara la función auxiliar generateSitemaps(). Esta define los identificadores de shards y la función principal sitemap({ id }) entrega el lote correspondiente.
import type { MetadataRoute } from 'next';
const BATCH_SIZE = 25000; // Tamaño conservador de shard (muy inferior al límite duro de 50,000)
const BASE_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://agenticlab.dev';
// Función auxiliar para obtener el conteo total de registros desde la base de datos
async function getTotalProductCount(): Promise<number> {
// Emula: SELECT COUNT(*) FROM products
return 85000;
}
// 1. Declaración de shards disponibles del mapa de sitio
// Next.js genera un Sitemap Index que enlaza a /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. Generador del contenido de cada shard individual
export default async function sitemap({
id,
}: {
id: number;
}): Promise<MetadataRoute.Sitemap> {
const shardId = Number(id);
const skip = shardId * BATCH_SIZE;
// El shard 0 puede alojar las rutas estáticas principales
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,
},
]
: [];
// Consulta de entidades paginadas para el shard activo
// Emula: 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,
// Incorporación directa de alternativas multilingües en el nodo 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];
}
// Simulación de función de acceso a datos para tipado estricto
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(),
}));
}Devolver un arreglo de más de 50,000 elementos desde una única función sitemap() vulnera el estándar de sitemaps.org target="_blank" rel="noopener noreferrer nofollow". Los motores de búsqueda registrarán un error de análisis y descartarán el documento completo. Cuando el catálogo supere las 50,000 entidades, implemente generateSitemaps() de forma preventiva con lotes de 25,000 a 30,000 URLs.
La especificación XML exige URLs absolutas completas (FQDN) con protocolo y host. Indicar { url: '/products/laptop' } en vez de { url: 'https://agenticlab.dev/products/laptop' } provocará errores de ingestión en Search Console. Concatene siempre las rutas relativas con su variable canónica de dominio (process.env.NEXT_PUBLIC_APP_URL).
Configurar allow: '/' en app/robots.ts sin validar las variables de entorno provocará que los rastreadores indexen despliegues efímeros de Vercel Preview. Esto genera penalizaciones por contenido duplicado y devalúa la autoridad de su dominio principal. Valide siempre las variables process.env.VERCEL_ENV o NODE_ENV.
Cargar modelos de datos completos con contenido textual pesado o árboles complejos de relaciones dentro de sitemap() causará tiempos de espera por timeout en el servidor. El mapa de sitio únicamente requiere tres atributos: slug, updated_at e identificadores de idioma. Optimice sus consultas SQL con proyecciones ligeras SELECT slug, updated_at aplicando LIMIT y OFFSET sobre columnas indexadas.
El bot examina los permisos de acceso y extrae la URL del índice de sitemaps: /sitemap.xml.
Next.js ejecuta generateSitemaps para calcular el número de shards y responde un XML con enlaces a /sitemap/0.xml, /sitemap/1.xml.
El rastreador emite peticiones en paralelo hacia los submapas individuales /sitemap/{id}.xml.
La función sitemap({ id }) recupera proyecciones ligeras de columnas (slug, updatedAt) mediante LIMIT/OFFSET.
Next.js serializa las entidades en formato XML válido y envía la respuesta con compresión gzip/brotli.