Sitemap index para SaaS multi-tenant: cómo Google descubre a tus usuarios sin que toques nada
El único mecanismo del protocolo de sitemaps diseñado para escala — y prácticamente nadie lo usa.
Un SaaS multi-tenant tiene un problema de SEO que no aparece en los tutoriales de Google: cada tenant es un subdominio nuevo, y Google no lo sabe.
El workflow "por defecto" que te encuentras en Search Console es: entras, pulsas "Add a new sitemap", pegas la URL, pulsas Submit. Funciona para un sitio. Funciona regular para cinco. A partir de ahí, cada nuevo usuario que se registra es una fricción manual más — alguien tiene que acordarse de submitir su sitemap, cada vez, para que Google lo indexe esta semana en vez de la que viene.
Si tu modelo de negocio depende del volumen (un blog gratuito, un editor open source, un builder de landing pages), este proceso no solo es frustrante — es un cuello de botella de crecimiento. Cada fricción entre "el usuario crea su sitio" y "el sitio aparece en Google" reduce conversiones. Y nadie encuentra tiempo para submitir manualmente cada tenant.
La buena noticia: hay un mecanismo oficial de sitemaps para resolverlo exactamente. Se llama sitemap index. La mala: está tan poco documentado que la mayoría de SaaS acaba montando alternativas frágiles (scripts cron, hacks en el onboarding) cuando existe una forma directa y limpia.
Esto es lo que hicimos en Agentikas.
El problema real: Google no auto-descubre subdominios
La creencia de que "Google encuentra todo solo" es parcialmente falsa. Google descubre URLs por varios caminos:
- Enlaces externos — algún sitio apunta a tu subdominio. Puede no pasar nunca para un tenant nuevo.
- Internal links desde páginas ya indexadas — si tu dominio principal tiene un link a
maria.tu-saas.com, Google sigue el link y crawlea. Pero solo cuando revisita tu dominio, y solo para los subdominios linkeados explícitamente. - Submissions en Search Console — manual, y solo cubre lo que tú le das.
Sitemap:en robots.txt del subdominio — Google lo lee, pero solo cuando ya conoce el subdominio (círculo vicioso).
Ninguno de estos escala. El más cercano a "automático" es el (2), pero requiere que cada tenant tenga un link desde una página indexada. En la práctica, los SaaS listan tenants en páginas tipo "explora otros blogs" o "descubre", pero esas páginas suelen tener un subconjunto (los featured, los recientes), no el catálogo completo. Los tenants que no están en esa vista se quedan invisibles.
La solución: sitemap index
Un sitemap index es un archivo XML con una estructura específica que lista otros sitemaps. No contiene URLs de páginas — contiene punteros a más sitemaps. Ejemplo:
<?xml version="1.0" encoding="utf-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://tu-saas.com/sitemap.xml</loc>
<lastmod>2026-04-21T11:42:23.017Z</lastmod>
</sitemap>
<sitemap>
<loc>https://maria.tu-saas.com/sitemap.xml</loc>
<lastmod>2026-04-20T09:15:00.000Z</lastmod>
</sitemap>
<sitemap>
<loc>https://juan.tu-saas.com/sitemap.xml</loc>
<lastmod>2026-04-19T14:00:00.000Z</lastmod>
</sitemap>
<!-- ... uno por cada tenant -->
</sitemapindex>
La diferencia crítica con un sitemap normal es que submites este URL UNA VEZ en Search Console y Google descubre todos los sub-sitemaps por sí solo. Cuando añades un tenant nuevo, tu endpoint regenera el índice incluyéndolo, Google lo re-lee en su siguiente crawl, y el nuevo tenant entra al pipeline de indexación sin que nadie toque nada.
Es el único mecanismo del protocolo de sitemaps que está diseñado específicamente para escala. Y casi nadie lo usa.
Cómo construirlo en Next.js
El sitemap index debe ser una ruta dinámica, no un archivo estático — porque cada vez que hay un tenant nuevo, el XML cambia. En Next.js App Router:
// app/sitemap-index.xml/route.ts
import { NextResponse } from "next/server";
import { getPublicTenants } from "@/data/tenants";
export const dynamic = "force-dynamic";
export const revalidate = 3600;
export async function GET() {
const tenants = await getPublicTenants();
const now = new Date().toISOString();
const entries = [
` <sitemap>
<loc>https://tu-saas.com/sitemap.xml</loc>
<lastmod>${now}</lastmod>
</sitemap>`,
...tenants.map(t => ` <sitemap>
<loc>https://${t.slug}.tu-saas.com/sitemap.xml</loc>
<lastmod>${t.updatedAt}</lastmod>
</sitemap>`),
];
const xml = `<?xml version="1.0" encoding="utf-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${entries.join("\n")}
</sitemapindex>`;
return new NextResponse(xml, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=7200",
},
});
}
El patrón clave aquí es la combinación de dos cosas:
- Consulta a la base de datos en cada render (
getPublicTenants()hace unselect * from tenants where public = truesin caché en memoria). - Cache a nivel de edge con TTL corto (
s-maxage=3600) para no hacer la consulta en cada request.
Con 3600 segundos de TTL, un tenant nuevo aparece en el índice entre 0 y 1 hora después de ser creado. Una hora es aceptable para la mayoría de los SaaS — Google en cualquier caso no va a crawlear el nuevo sitemap en menos de eso.
La regla de los dos niveles
Google permite UN NIVEL de anidación en sitemaps. Puedes tener:
sitemap-index.xml→ Nsitemap.xml(urlsets) ✅sitemap.xml(urlset) con URLs directamente ✅
Pero NO puedes tener:
sitemap-index.xml→otro-index.xml→sitemap.xml❌
Esto importa si tu SaaS crece al punto donde cada tenant tiene miles de páginas. El límite oficial de URLs por sitemap es 50.000 y 50MB. Si un tenant lo excede, SU sitemap debería dividirse en varios — pero no con un index propio, sino con sitemaps paralelos que todos cuelgan del index raíz. En la práctica, muy pocos tenants llegan a ese volumen, así que la estructura simple index → urlset cubre el 99% de casos.
robots.txt: la red de seguridad
El sitemap index en Search Console es el mecanismo "push" — le dices a Google dónde buscar. Pero hay también un canal "pull": el Sitemap: en el robots.txt de cada subdominio. Cuando Google crawlea maria.tu-saas.com por cualquier motivo (link externo, sitemap-index, recrawl programado), lee el robots.txt y descubre el sitemap desde ahí.
La configuración defensiva es declarar AMBAS señales:
En tu-saas.com/robots.txt:
Sitemap: https://tu-saas.com/sitemap.xml
Sitemap: https://tu-saas.com/sitemap-index.xml
En <tenant>.tu-saas.com/robots.txt (cada subdominio):
Sitemap: https://<tenant>.tu-saas.com/sitemap.xml
Con esto, cualquiera de las tres rutas (submit directo del index, descubrimiento via enlaces al dominio raíz, descubrimiento del subdominio en sí) lleva a Google al sitemap correcto. Resilencia por redundancia.
El trade-off del TTL
Reducir el TTL del edge cache hace que tenants nuevos aparezcan antes, pero aumenta la carga en tu base de datos. Para un SaaS con 10 tenants y 100 requests/día al sitemap-index, el coste es trivial. Para 10.000 tenants y 100.000 requests/día, cada request extra cuenta.
La alternativa es purgar el cache on-demand: cuando tu endpoint de "crear blog" recibe un POST exitoso, haces un fetch adicional a la API de Cloudflare (u otro CDN) para invalidar el cache del sitemap-index. Pros: el tenant aparece en el índice en segundos. Cons: acoplas tu ruta de creación al CDN (si falla, el sitemap se queda stale pero no rompes nada crítico).
Nosotros optamos por el TTL simple de una hora. Cuando las métricas digan que importa (Google taarda mucho en indexar nuevos tenants, los usuarios se quejan de que sus posts no aparecen), migraremos al purge on-demand. YAGNI primero, optimización después.
Qué hace Google realmente con el index
Google no procesa el sitemap-index al instante. El flujo real es aproximadamente:
- Submites
sitemap-index.xmluna vez en Search Console - Google lo fetchea dentro de los siguientes minutos-horas
- Enqueues cada sub-sitemap listado para crawl posterior
- Crawlea cada sub-sitemap en su orden (puede tardar horas-días)
- Enqueues cada URL del sub-sitemap para indexación
- Indexa las URLs según su propia prioridad interna (días-semanas)
En cada re-crawl del index, Google usa el <lastmod> de cada <sitemap> para decidir si vale la pena re-crawlear ese sub-sitemap. Por eso es importante que el lastmod de cada entrada refleje realmente cuándo cambió contenido — no simplemente "ahora". Si siempre pones new Date().toISOString(), Google te marca como ruidoso y reduce la frecuencia de re-crawl.
El patrón correcto: lastmod de cada entrada = max(updated_at) del contenido de ese tenant. Si no cambia nada en maria.tu-saas.com durante una semana, su <lastmod> no se mueve, y Google no re-crawlea su sitemap innecesariamente.
Agentikas es open source. El sitemap-index que describe este post está en producción en https://agentikas.ai/sitemap-index.xml y el código fuente en github.com/agentikas/agentikas-blog. El PR que lo introdujo es el #40, y el helper buildSitemapIndex que usa está en el package compartido @agentikas/blog-ui — reutilizable en cualquier Next.js.
Comentarios
Cargando comentarios…
Inicia sesión en tu dashboard para participar.