cómo crear un sitemap automático en NextJS 16 con App Router
guía completa paso a paso para crear un sitemap.xml automático en NextJS 16 usando App Router. Mejora tu SEO con sitemaps dinámicos.
cómo crear un sitemap automático en NextJS 16 con App Router
Un sitemap automático en NextJS 16 es esencial para el SEO de tu sitio web. Le dice a Google y otros motores de búsqueda que páginas tienes, cuando se actualizaron y que tan importantes son.
En esta guía vas a crear un sitemap.xml completamente automático con App Router que se actualiza solo cuando agregas nuevas páginas. Desde lo básico hasta un enfoque que lee tu filesystem sin mantenimiento.
¿Qué es un sitemap y por qué lo necesitas?
Un sitemap es un archivo XML que lista todas las URLs publicas de tu sitio web junto con metadata útil para motores de búsqueda. El formato esta definido por el protocolo de sitemaps.
Ejemplo de sitemap básico:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://tudominio.com/</loc>
<lastmod>2025-09-29</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://tudominio.com/about</loc>
<lastmod>2025-09-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>¿Por qué es importante?
- Indexación más rápida: Google descubre tus páginas nuevas más rápido
- Mejor SEO: Los motores entienden la estructura de tu sitio
- Control: Decides que páginas son más importantes
- Analytics: Ves en Google Search Console que páginas estan indexadas
Impacto en SEO
Sitios con sitemap bien configurado pueden ver sus páginas indexadas hasta 70% más rápido que sin sitemap.
Como funcionan los sitemaps en NextJS 16
NextJS 16 tiene soporte nativo para sitemaps con el App Router. No necesitas librerías externas ni configuración compleja. Si estas usando MDX para tu contenido, Asegúrate de revisar la solución al error de Turbopack con MDX para evitar problemas de build.
El sistema
NextJS reconoce automáticamente un archivo llamado sitemap.ts (o .js) en la carpeta app/ y lo transforma en un sitemap.xml accesible en /sitemap.xml. Puedes ver los detalles en la documentación oficial de NextJS para sitemaps.
app/
├── sitemap.ts <- Defines tu sitemap aquí
├── page.tsx
└── about/
└── page.tsxCuando un usuario (o Google) visita /sitemap.xml, NextJS ejecuta tu archivo sitemap.ts y genera el XML automáticamente.
No necesitas crear un archivo .xml manualmente. NextJS lo genera por ti basandose en tu código TypeScript/JavaScript.
Creando tu primer sitemap
Vamos a crear un sitemap paso a paso, desde lo más básico hasta uno completamente dinámico.
Sitemap estático básico
Crea el archivo app/sitemap.ts
En la raiz de tu carpeta app/
Agrega el código básico
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://tudominio.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://tudominio.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://tudominio.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.5,
},
]
}Verifica /sitemap.xml
Visita http://localhost:3000/sitemap.xml
verás tu sitemap generado automáticamente en formato XML
Cambia el dominio
Recuerda reemplazar 'https://tudominio.com' con tu dominio real en producción.
Entendiendo los campos del sitemap
Cada URL en tu sitemap puede tener estos campos:
url (Requerido)
La URL completa de la página:
url: 'https://tudominio.com/blog/mi-post'Reglas:
- Debe ser URL absoluta (incluir https://)
- No debe tener fragmentos (#sección)
- No debe tener query params si son para filtros (?page=2)
lastModified (Opcional pero recomendado)
Cuando se modifico la página por última vez:
lastModified: new Date('2025-09-29')
// O dinámico:
lastModified: new Date()Beneficio: Google prioriza re-crawlear páginas que cambiaron recientemente.
changeFrequency (Opcional)
Con que frecuencia cambia tipicamente la página:
changeFrequency: 'weekly'Opciones válidas:
always- Cambia cada vez que se accedehourly- Cada horadaily- Diarioweekly- Semanalmonthly- Mensualyearly- Anualnever- Nunca cambia
Google usa esto como sugerencia, no como comando. No garantiza que crawlee con esa frecuencia.
priority (Opcional)
Importancia relativa de la página (0.0 a 1.0):
priority: 0.8guía de prioridades:
1.0- Homepage, páginas principales0.8- páginas de categoría importantes0.6- páginas de subcategoria0.5- Posts de blog, artículos0.3- páginas secundarias
La prioridad es relativa a tu sitio, no al internet. Una página con priority 0.5 en tu sitio puede ser más importante que una con 1.0 en otro sitio.
Sitemap dinámico con páginas reales
Un sitemap estático no es útil para sitios que crecen. Vamos a crear uno que se actualice automáticamente.
Ejemplo: Blog con posts dinámicos
// app/sitemap.ts
import type { MetadataRoute } from 'next'
// Función para obtener todos tus posts
async function getAllPosts() {
// Esto depende de donde guardes tus posts
// Puede ser una base de datos, CMS, o archivos
const posts = [
{ slug: 'mi-primer-post', date: '2025-09-20' },
{ slug: 'segundo-post', date: '2025-09-25' },
{ slug: 'tercer-post', date: '2025-09-29' },
]
return posts
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://tudominio.com'
// páginas estáticas
const staticPages = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'yearly' as const,
priority: 1,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.9,
},
]
// páginas dinámicas (posts de blog)
const posts = await getAllPosts()
const postPages = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: 'monthly' as const,
priority: 0.6,
}))
// Combinar todas las páginas
return [...staticPages, ...postPages]
}Ahora cada vez que agregas un post nuevo, automáticamente aparece en tu sitemap. Sin configuración adicional!
Sitemap completamente automático leyendo el filesystem
Si quieres cero mantenimiento y que el sitemap se genere solo al leer tus carpetas, puedes usar el módulo fs de Node.js.
Este enfoque es perfecto para sitios con estructura basada en carpetas, como blogs o documentación donde cada carpeta es una página.
¿cuándo usar este método?
Usa automatización completa cuando:
- Tus páginas estan organizadas en carpetas (
app/blog/mi-post/) - No necesitas control fino sobre prioridades o frecuencias
- Quieres agregar páginas sin tocar el sitemap
- Tienes una estructura consistente y predecible
Usa el método manual cuando:
- Tus páginas vienen de una base de datos o CMS
- Necesitas lógica compleja para decidir que incluir
- Quieres prioridades diferentes para cada página
- Las páginas no siguen un patron de carpetas
Implementación completa
// app/sitemap.ts
import type { MetadataRoute } from 'next'
import fs from 'fs'
import path from 'path'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://tudominio.com'
const currentDate = new Date()
// páginas estáticas principales
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: currentDate,
changeFrequency: 'weekly',
priority: 1,
},
{
url: `${baseUrl}/sobre-mi`,
lastModified: currentDate,
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/blog`,
lastModified: currentDate,
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${baseUrl}/docs`,
lastModified: currentDate,
changeFrequency: 'weekly',
priority: 0.9,
},
]
// Generar páginas de blog automáticamente
const blogPages = await generateBlogPages(baseUrl)
// Generar páginas de docs automáticamente
const docsPages = await generateDocsPages(baseUrl)
return [...staticPages, ...blogPages, ...docsPages]
}
// Función para generar páginas de blog automáticamente
async function generateBlogPages(baseUrl: string): Promise<MetadataRoute.Sitemap> {
const blogDir = path.join(process.cwd(), 'app', 'blog')
try {
const entries = fs.readdirSync(blogDir, { withFileTypes: true })
return entries
.filter((entry) => entry.isDirectory() && entry.name !== '[slug]')
.map((entry) => {
const slug = entry.name
const dirPath = path.join(blogDir, slug)
const stats = fs.statSync(dirPath)
return {
url: `${baseUrl}/blog/${slug}`,
lastModified: stats.mtime, // Fecha real de modificación
changeFrequency: 'monthly' as const,
priority: 0.7,
}
})
} catch (error) {
console.error('Error generando páginas de blog:', error)
return []
}
}
// Función para generar páginas de docs automáticamente
async function generateDocsPages(baseUrl: string): Promise<MetadataRoute.Sitemap> {
const docsDir = path.join(process.cwd(), 'app', 'docs')
const pages: MetadataRoute.Sitemap = []
function scanDirectory(dir: string, basePath: string = '/docs') {
try {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
const urlPath = `${basePath}/${entry.name}`
const stats = fs.statSync(fullPath)
pages.push({
url: `${baseUrl}${urlPath}`,
lastModified: stats.mtime,
changeFrequency: 'weekly',
priority: basePath === '/docs' ? 0.9 : 0.7,
})
// Escanear recursivamente subcarpetas
scanDirectory(fullPath, urlPath)
}
}
} catch (error) {
console.error('Error escaneando directorio:', dir, error)
}
}
scanDirectory(docsDir)
return pages
}¿cómo funciona?
-
generateBlogPages():- Lee todas las carpetas dentro de
app/blog/ - Excluye la carpeta dinámica
[slug] - Convierte cada carpeta en una URL
- Usa la fecha de modificación real del filesystem
- Lee todas las carpetas dentro de
-
generateDocsPages():- Escanea recursivamente toda la estructura de
app/docs/ - Genera URLs para cada carpeta encontrada
- Mayor prioridad para niveles superiores
- Escanea recursivamente toda la estructura de
-
Ventajas:
- Cero mantenimiento manual
- Fechas de modificación reales
- Se adapta automáticamente a cambios
- Funciona con estructuras anidadas
Completamente automático
Con este enfoque, solo creas una nueva carpeta de blog o docs y automáticamente aparece en tu sitemap. Literalmente cero mantenimiento!
Combinando ambos enfoques
Puedes combinar páginas automaticas con manuales según necesites:
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://tudominio.com'
// páginas principales (manual para control fino)
const staticPages = [/* ... */]
// Blog automático (filesystem)
const blogPages = await generateBlogPages(baseUrl)
// Productos desde base de datos (manual)
const products = await db.product.findMany()
const productPages = products.map((product) => ({
url: `${baseUrl}/productos/${product.slug}`,
lastModified: product.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}))
return [...staticPages, ...blogPages, ...productPages]
}Ejemplo real: Sitio de documentación
Para un sitio de documentación técnica (como el que estamos construyendo):
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://rodalexanderson.com'
const currentDate = new Date()
// páginas principales
const mainPages = [
{
url: baseUrl,
lastModified: currentDate,
changeFrequency: 'weekly' as const,
priority: 1,
},
{
url: `${baseUrl}/sobre-mi`,
lastModified: currentDate,
changeFrequency: 'monthly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/blog`,
lastModified: currentDate,
changeFrequency: 'weekly' as const,
priority: 0.9,
},
{
url: `${baseUrl}/docs`,
lastModified: currentDate,
changeFrequency: 'weekly' as const,
priority: 0.9,
},
]
// Documentación de NextJS
const docsPages = [
'v16',
'v16/instalacion',
'v16/routing',
'v16/rendering',
'v16/rendering/client-components',
'v16/data-fetching',
].map((path) => ({
url: `${baseUrl}/docs/nextjs/${path}`,
lastModified: currentDate,
changeFrequency: 'monthly' as const,
priority: 0.7,
}))
return [...mainPages, ...docsPages]
}Sitemap con datos de base de datos
Si usas Prisma, Drizzle u otro ORM, puedes generar el sitemap consultando directamente tu base de datos. Asegúrate de manejar correctamente las llamadas asíncronas (revisa la guía de Fetch API en JavaScript si necesitas refrescar el patron):
// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { db } from '@/lib/database'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://tudominio.com'
// Obtener posts de la base de datos
const posts = await db.post.findMany({
where: { published: true },
select: {
slug: true,
updatedAt: true,
},
})
// Convertir a formato de sitemap
const postPages = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'monthly' as const,
priority: 0.7,
}))
// páginas estáticas
const staticPages = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 1,
},
]
return [...staticPages, ...postPages]
}Performance
Si tienes muchas páginas (1000+), considera usar Sitemap Index para dividir el sitemap en múltiples archivos.
Configurar robots.txt
después de crear tu sitemap, debes decirle a Google donde encontrarlo usando robots.txt:
Crea app/robots.ts
En la raiz de tu carpeta app/
Agrega la configuración
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/'],
},
sitemap: 'https://tudominio.com/sitemap.xml',
}
}Verifica /robots.txt
Visita http://localhost:3000/robots.txt
Esto crea un archivo robots.txt que:
- Permite crawlear todo el sitio
- Bloquea rutas privadas como
/admin/ - Apunta a tu sitemap
Verificar tu sitemap
después de crear tu sitemap, verifica que funciona correctamente:
1. Prueba local
Visita http://localhost:3000/sitemap.xml y verifica que:
- Muestra XML válido
- Todas las URLs son correctas
- Incluye todas tus páginas importantes
2. Validador XML
Usa un validador online:
Copia y pega tu sitemap para verificar que el XML es válido.
3. Google Search Console
Una vez en producción:
Inicia sesión con tu cuenta de Google
Agrega tu sitio web
Si aun no lo has hecho
Ve a Sitemaps
En el menu lateral
Agrega tu sitemap
Ingresa sitemap.xml y presiona Submit
Google comenzara a crawlear tu sitio usando el sitemap.
Puede tomar de 24 horas a varias semanas para que Google indexe completamente todas tus páginas.
Mejores prácticas
1. Solo incluye páginas canonicas
No incluyas en tu sitemap:
- URLs con parámetros de query (ejemplo: ?page=2)
- páginas duplicadas
- páginas que redirigen a otras
- páginas privadas que requieren login
Un buen SEO también incluye seguridad. Antes de hacer deploy, verifica que tu repo no exponga API keys o credenciales que puedan afectar tu posicionamiento. Herramientas como datahogo escanean tu repositorio de GitHub y te detectan vulnerabilidades automáticamente.
2. Usa URLs absolutas
// Correcto
url: 'https://tudominio.com/blog/post'
// Incorrecto
url: '/blog/post'3. Manten actualizado lastModified
Usa la fecha real de modificación cuando sea posible:
// Bueno - Fecha real del post
lastModified: post.updatedAt
// Malo - Siempre la fecha actual
lastModified: new Date()4. No exageres con las prioridades
No pongas priority 1.0 en todas las páginas. Usa la prioridad para indicar importancia relativa.
5. límite de URLs
Un sitemap puede tener hasta 50,000 URLs y máximo 50MB. Si tienes más, usa Sitemap Index.
Troubleshooting común
Sitemap no se genera
Problema: Visitas /sitemap.xml y obtienes 404.
Soluciones:
- Verifica que el archivo este en
app/sitemap.ts(no en otra carpeta) - Reinicia el servidor de desarrollo
- Limpia el cache:
rm -rf .next && npm run dev
URLs incorrectas en producción
Problema: Las URLs tienen localhost en lugar de tu dominio.
Solución: Usa variables de entorno:
// app/sitemap.ts
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://tudominio.com'En .env.local:
NEXT_PUBLIC_SITE_URL=http://localhost:3000En producción (Vercel):
NEXT_PUBLIC_SITE_URL=https://tudominio.comSitemap no se actualiza
Problema: Agregaste páginas pero no aparecen en el sitemap.
Solución: El sitemap se genera en build time. En producción, necesitas:
- Hacer un nuevo deploy
- O usar Incremental Static Regeneration
Conclusion
Crear un sitemap automático en NextJS 16 es sorprendentemente simple:
- Crea
app/sitemap.ts - Retorna un array de URLs con metadata
- NextJS genera el XML automáticamente
- Submit a Google Search Console
Tienes dos enfoques principales:
Manual/Hibrido: Control total sobre cada URL, perfecto para bases de datos o CMS automático: Lee el filesystem, cero mantenimiento, perfecto para estructuras basadas en carpetas
Beneficios que obtienes:
- Mejor indexación en Google
- páginas nuevas descubiertas más rápido
- Mejor SEO general
- Control sobre que páginas indexar
Elige el enfoque que mejor se adapte a tu proyecto. Incluso puedes combinar ambos según tus necesidades. Una vez que Google indexe tus páginas, complementa con una búsqueda instantanea usando Fuse.js para que los usuarios encuentren tu contenido fácilmente.
Recursos adicionales
Preguntas frecuentes
¿Por qué es importante tener un sitemap.xml en mi sitio web?
Un sitemap.xml le dice a Google y otros motores de búsqueda que páginas tiene tu sitio, cuando se actualizaron y que tan importantes son. Sitios con sitemap bien configurado pueden ver sus páginas indexadas hasta 70% más rápido que sin sitemap.
¿Cuál es la diferencia entre un sitemap dinámico y uno estático en NextJS?
Un sitemap estático lista URLs fijas que tu defines manualmente. Un sitemap dinámico genera las URLs automáticamente leyendo el filesystem, una base de datos o un CMS, y se actualiza solo cuando agregas nuevas páginas sin necesidad de editar código.
¿Cómo se relaciona el sitemap.xml con el robots.txt en NextJS 16?
El robots.txt le indica a los motores de búsqueda donde encontrar tu sitemap mediante la directiva sitemap. En NextJS 16 puedes crear ambos archivos como funciones TypeScript en la carpeta app/, y NextJS los genera automáticamente en formato XML y texto plano.
Articulos relacionados
12 Errores SEO que Arruinan tu Blog (y Como Arreglarlos)
Los errores SEO mas comunes que destrozan blogs de desarrolladores. Diagnostico, fix y verificacion para cada uno. Para blogs en Next.js y cualquier stack.
Indexar tu Blog en Google: Guia Tecnica
Como indexar un blog en Google Search Console: sitemap, robots.txt, canonical URLs, structured data, Core Web Vitals e IndexNow. Guia tecnica completa.
IndexNow en Next.js: guía completa para indexación rápida
Implementa IndexNow en tu app Next.js con TypeScript. Aprende a generar tu API key, enviar URLs a Bing automáticamente y configurar el webhook de Vercel.