Sitemaps para sitios headless: lo que necesitas saber

Diagrama de un CMS que usa tecnologías Next.js, Nuxt.js y Gatsby.js para generar sitemaps y optimizar el SEO.

En arquitecturas desacopladas (headless), la generación del sitemap deja de depender por completo del CMS. Aunque el backend sigue administrando el contenido, las rutas finales se construyen en el frontend (Next.js, Nuxt, Gatsby, etc.). Por eso, generar un sitemap que refleje correctamente las URLs finales requiere un enfoque distinto al de un sitio tradicional. Si quieres tener la base clara antes de avanzar, aquí explicamos qué es un sitemap XML y su relevancia en SEO.

Generar un sitemap desde el frontend (Next.js + CMS headless)

Cuando las páginas existen como entradas dentro de un CMS headless (Contentful, Sanity, Strapi, Drupal en modo JSON:API, etc.), lo más común es:

  • Consultar el CMS vía API
  • Construir las páginas en el frontend
  • Generar un sitemap dinámicamente con la información final (slugs, estructura, fecha de actualización)
Ejemplo: Sitemap en Next.js

Primero definimos una página especial sitemap.xml.tsx:

// pages/sitemap.xml.tsx
export default class Sitemap extends Component {
  static async getInitialProps({ res }: GetServerSidePropsContext): Promise<void> {
    const pages = await getPages()
    res.writeHead(200, { 'Content-Type': 'text/xml' })
    res.write(createSitemap(pages))
    res.end()
  }
}

Este endpoint:

  • Obtiene las páginas desde el CMS (getPages)
  • Genera el XML (createSitemap)
  • Lo sirve directamente como sitemap.xml
Definir el tipo de datos del CMS
export type ContentfulPage = {
  title: string
  slug: string
  header: ContentfulOrHeader
  blocks?: ContentfulBlock[]
  footer: ContentfulOrFooter
  updatedAt?: string
}

Este tipo representa cómo llega una página desde Contentful, pero el concepto aplica igual a cualquier CMS: título, slug, componentes y metadatos como updatedAt.

Función para generar el sitemap XML
const createSitemap = (pages: ContentfulPage[]) => {
  return `<?xml version="1.0" encoding="UTF-8"?>
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    ${generateLinks(pages)}
  </urlset>`
}

Lo único que hace es envolver los items dentro del <urlset>.

Generar los items individuales del sitemap
const generateLinks = (pages: ContentfulPage[]) => {
  const pageItems = pages.map((page) => {
    const slugPath = page.slug === '/' ? '' : `/${page.slug}`
    const url = `${process.env.ORIGIN_URL}${slugPath}`
    return `
        <url>
          <loc>${url}</loc>
          <changefreq>daily</changefreq>
          <lastmod>${page.updatedAt}</lastmod>
          <priority>0.8</priority>
        </url>
      `
  })
  return pageItems.join('')
}

Aquí ocurre lo clave: construir URLs reales desde los slugs y metadatos del CMS.

Obtener las páginas desde el CMS
import { client } from 'services/contentful'

export const getPages = async (): Promise<ContentfulPage[]> => {
  const collection = await client.getEntries({ 'content_type': 'page' })
  const pages = collection?.items?.length ? collection.items : null

  if (pages) return pages.map((page) => ({
    title: page.fields.title,
    slug: page.fields.slug,
    header: page.fields.header,
    blocks: page.fields.blocks,
    footer: page.fields.footer,
    updatedAt: page.sys.updatedAt,
  }))

  return []
}

Contentful (y la mayoría de los CMS headless) entregan la data así:

{
  "fields": { "...": "..." },
  "sys": { "updatedAt": "2023-01-04T00:50:34.525Z" }
}

Con esto basta para armar URLs correctas.

Usar un sitemap generado por Drupal (y servirlo en Node.js/Express)

En sitios desacoplados con Drupal como backend, a veces es más eficiente dejar que Drupal genere el sitemap usando el módulo XML Sitemap y simplemente exponer ese archivo desde el frontend.

Este enfoque evita tener que replicar lógica en el frontend o desarrollar generadores personalizados.

Pasos del enfoque Drupal → Node.js
1. Instalar y configurar XML Sitemap en Drupal

Drupal genera automáticamente:

  • /sitemap.xml
  • Items de contenido configurables
  • Regeneración automática cuando hay cambios
2. En Node.js/Express, “streamear” el sitemap

Después de probar diferentes NPM modules, request suele ser la forma rápida de implementar el stream:

app.get('/sitemap.xml', function (req, res) {
  var sitemap = request(sitemapxml);
  req.pipe(sitemap);
  sitemap.pipe(res);
});

/sitemap.xml del frontend sirve directamente la versión generada por Drupal.

Este enfoque funciona especialmente bien porque evita duplicar la lógica de rutas entre backend y frontend: Drupal gestiona el contenido y el sitemap se mantiene siempre sincronizado con cualquier actualización. Además, Google solo necesita consultar un único endpoint (https://frontend.com/sitemap.xml), lo que simplifica el rastreo. Es una solución muy eficiente para frontends ligeros, sitios con gran volumen de páginas o proyectos donde no tiene sentido mantener dos sistemas generando URLs.

¿Qué método elegir?

Para elegir el método adecuado, depende de cómo se gestionan las rutas en tu sitio:

  • Frontend define las rutas finales: genera el sitemap directamente desde el frontend.
  • Drupal controla la estructura y las URLs: sirve el sitemap que genera Drupal.
  • Sitios muy dinámicos con generación estática (Next.js, Gatsby): el frontend puede encargarse del sitemap.
  • Menor presupuesto o evitar desarrollos custom: usar el XML Sitemap de Drupal suele ser más conveniente.

Ambos métodos son válidos; la elección final depende del modelo de enrutamiento y de dónde se construyen realmente las URLs.

Foco en la implementación

En un sitio desacoplado, generar un sitemap implica elegir correctamente dónde viven las rutas reales.
Si el frontend construye las URLs, él debe generar el sitemap.
Si Drupal controla la estructura, puede generarlo y el frontend solo lo expone.

Lo importante es que el sitemap refleje las URLs finales que Google puede rastrear.
Sin eso, cualquier arquitectura headless pierde capacidad de indexación.