Use Cases (Actualizado: 3/6/2026)

Generar sitemaps XML con Claude Code

Crea sitemaps en Astro y Node con hreflang, lastmod, robots.txt y verificación en Search Console.

Generar sitemaps XML con Claude Code

Un sitemap es un inventario público de URL, no una garantía de indexación

Cuando Claude Code ayuda a publicar artículos, documentación o páginas de producto, el problema no siempre está en la plantilla. Muchas veces está en si los buscadores pueden descubrir las URL correctas. Un sitemap XML indica qué URL canónicas quieres que se rastreen, cuándo tuvieron una actualización importante y cómo se relacionan sus versiones traducidas.

Conviene partir de la regla actual de Google: priority y changefreq se ignoran, y lastmod solo es útil si refleja cambios reales de forma consistente. Además, el antiguo endpoint de ping para sitemaps ya está obsoleto. En un flujo moderno debes usar robots.txt, Google Search Console y comprobaciones de despliegue, no llamadas a https://www.google.com/ping?sitemap=....

Esta guía cubre dos enfoques prácticos: la integración oficial de Astro y un generador en Node.js sin dependencias para colecciones MDX multilingües. Para encajar esto en una estrategia más amplia, revisa también optimización SEO con Claude Code y configuración CI/CD con Claude Code.

Reglas oficiales que debes respetar

PuntoDecisión práctica
URLUsa URL absolutas como https://example.com/blog/post/
LímiteDivide si un archivo llega a 50.000 URL o 50 MB sin comprimir
CodificaciónGuarda en UTF-8 y escapa los valores XML
lastmodUsa la fecha real de un cambio importante en contenido, datos estructurados o enlaces
priority / changefreqPuedes omitirlos para Google
Sitios multilingüesCada URL debe listar sus alternativas y también a sí misma
EnvíoUsa robots.txt y Search Console; elimina scripts de ping

Las fuentes base son la guía de sitemaps de Google, el aviso de retirada del ping, la guía de versiones localizadas y el protocolo de sitemaps.org.

Caso 1: páginas y rutas de blog en Astro

En un sitio Astro estático, empieza con @astrojs/sitemap. Se ejecuta durante astro build y puede generar relaciones de idioma si las rutas están bien definidas.

npx astro add sitemap
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://claudecodelab.com',
  integrations: [
    sitemap({
      filter: (page) => !page.includes('/draft/') && !page.includes('/preview/'),
      i18n: {
        defaultLocale: 'ja',
        locales: {
          ja: 'ja',
          en: 'en',
          zh: 'zh-CN',
          ko: 'ko',
          es: 'es',
          fr: 'fr',
          de: 'de',
          pt: 'pt-BR',
          hi: 'hi',
          id: 'id',
        },
      },
    }),
  ],
});

El fallo típico es dejar mal configurado site: localhost, un dominio de vista previa o una mezcla de http y https. Google intentará rastrear exactamente las URL que aparezcan en el archivo, así que el sitemap debe coincidir con tus URL canónicas.

Caso 2: generador Node.js para MDX multilingüe

Un generador propio es útil cuando el contenido vive en carpetas como blog, blog-en y blog-zh, o cuando updatedDate debe convertirse en lastmod. Este ejemplo usa solo módulos nativos de Node.js y crea public/sitemap.xml.

// scripts/generate-sitemap.mjs
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';

const SITE_URL = (process.env.SITE_URL ?? 'https://example.com').replace(/\/$/, '');
const OUT_DIR = 'public';
const OUT_FILE = path.join(OUT_DIR, 'sitemap.xml');

const collections = [
  { dir: 'site/src/content/blog', prefix: '/blog', hreflang: 'ja' },
  { dir: 'site/src/content/blog-en', prefix: '/en/blog', hreflang: 'en' },
  { dir: 'site/src/content/blog-zh', prefix: '/zh/blog', hreflang: 'zh-CN' },
  { dir: 'site/src/content/blog-ko', prefix: '/ko/blog', hreflang: 'ko' },
  { dir: 'site/src/content/blog-es', prefix: '/es/blog', hreflang: 'es' },
  { dir: 'site/src/content/blog-fr', prefix: '/fr/blog', hreflang: 'fr' },
  { dir: 'site/src/content/blog-de', prefix: '/de/blog', hreflang: 'de' },
  { dir: 'site/src/content/blog-pt', prefix: '/pt/blog', hreflang: 'pt-BR' },
  { dir: 'site/src/content/blog-hi', prefix: '/hi/blog', hreflang: 'hi' },
  { dir: 'site/src/content/blog-id', prefix: '/id/blog', hreflang: 'id' },
];

function escapeXml(value) {
  return String(value).replace(/[<>&'"]/g, (char) => ({
    '<': '&lt;',
    '>': '&gt;',
    '&': '&amp;',
    "'": '&apos;',
    '"': '&quot;',
  })[char]);
}

async function* walk(dir) {
  let items;
  try {
    items = await readdir(dir, { withFileTypes: true });
  } catch (error) {
    if (error.code === 'ENOENT') return;
    throw error;
  }

  for (const item of items) {
    const fullPath = path.join(dir, item.name);
    if (item.isDirectory()) {
      yield* walk(fullPath);
    } else if (/\.(md|mdx)$/.test(item.name)) {
      yield fullPath;
    }
  }
}

function frontmatterOf(source) {
  return source.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? '';
}

function dateField(frontmatter, key) {
  return frontmatter.match(new RegExp(`^${key}:\\s*["']?(\\d{4}-\\d{2}-\\d{2})`, 'm'))?.[1];
}

function routeSlug(collectionDir, filePath) {
  return path
    .relative(collectionDir, filePath)
    .replace(/\\/g, '/')
    .replace(/\.(md|mdx)$/, '')
    .replace(/\/index$/, '');
}

function encodeRoute(slug) {
  return slug.split('/').map(encodeURIComponent).join('/');
}

async function collectEntries() {
  const bySlug = new Map();

  for (const collection of collections) {
    for await (const filePath of walk(collection.dir)) {
      const source = await readFile(filePath, 'utf8');
      const frontmatter = frontmatterOf(source);
      if (/^draft:\s*true\s*$/m.test(frontmatter)) continue;

      const info = await stat(filePath);
      const slug = routeSlug(collection.dir, filePath);
      const lastmod =
        dateField(frontmatter, 'updatedDate') ??
        dateField(frontmatter, 'pubDate') ??
        info.mtime.toISOString().slice(0, 10);

      const route = `${collection.prefix}/${encodeRoute(slug)}/`;
      const variant = {
        loc: `${SITE_URL}${route}`,
        hreflang: collection.hreflang,
        lastmod,
      };

      const variants = bySlug.get(slug) ?? [];
      variants.push(variant);
      bySlug.set(slug, variants);
    }
  }

  return [...bySlug.values()].flatMap((variants) =>
    variants.map((variant) => ({
      ...variant,
      alternates: variants.map(({ hreflang, loc }) => ({ hreflang, loc })),
    })),
  );
}

function buildSitemap(entries) {
  const urls = entries.map((entry) => `  <url>
    <loc>${escapeXml(entry.loc)}</loc>
    <lastmod>${entry.lastmod}</lastmod>
${entry.alternates.map((alt) => `    <xhtml:link rel="alternate" hreflang="${escapeXml(alt.hreflang)}" href="${escapeXml(alt.loc)}" />`).join('\n')}
  </url>`).join('\n');

  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
${urls}
</urlset>
`;
}

const entries = await collectEntries();
if (entries.length === 0) {
  throw new Error('No se encontraron URL públicas para el sitemap.');
}

await mkdir(OUT_DIR, { recursive: true });
await writeFile(OUT_FILE, buildSitemap(entries), 'utf8');
console.log(`Se escribieron ${entries.length} URL en ${OUT_FILE}.`);

Ejecuta el script así:

SITE_URL=https://claudecodelab.com node scripts/generate-sitemap.mjs

Caso 3: dividir artículos, productos y documentación

Un blog pequeño puede vivir con un solo sitemap.xml. En sitios más grandes conviene dividir por tipo de contenido para mantenerse bajo los límites oficiales y depurar mejor en Search Console.

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://example.com/sitemap-pages.xml</loc>
    <lastmod>2026-06-03</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-blog.xml</loc>
    <lastmod>2026-06-03</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-products.xml</loc>
    <lastmod>2026-06-03</lastmod>
  </sitemap>
</sitemapindex>

Pide a Claude Code que registre cuántas URL contiene cada archivo, que divida antes de llegar a 50.000 URL y que el índice solo apunte a sitemaps del mismo sitio.

robots.txt, Search Console y verificación

Incluye el sitemap en robots.txt:

User-agent: *
Allow: /

Sitemap: https://claudecodelab.com/sitemap.xml

Después, envíalo una vez desde Google Search Console. En despliegues automáticos, comprueba que la URL pública responde con HTTP 200 y que el XML tiene una raíz válida.

// scripts/verify-sitemap.mjs
const sitemapUrl = process.env.SITEMAP_URL ?? 'https://example.com/sitemap.xml';
const response = await fetch(sitemapUrl);

if (!response.ok) {
  throw new Error(`Falló la solicitud del sitemap: HTTP ${response.status}`);
}

const xml = await response.text();
if (!xml.includes('<urlset') && !xml.includes('<sitemapindex')) {
  throw new Error('La respuesta no parece un sitemap XML.');
}

console.log(`Verificado ${sitemapUrl}. Tamaño: ${xml.length} bytes`);

Errores que debes evitar

El primero es poner la fecha del build en todos los lastmod. Si el contenido no cambió, esa fecha no debe cambiar.

El segundo es incluir borradores, páginas noindex, URL de redirección o duplicados. El sitemap debe contener solo las URL canónicas que quieres mostrar en búsqueda.

El tercero es dejar hreflang en una sola dirección. Cada versión de idioma debe apuntar a sí misma y a todas las demás versiones.

El cuarto es olvidar el escape XML. Un & en una cadena de consulta debe convertirse en &amp;.

Monetización y resultado práctico

El sitemap no monetiza por sí solo, pero protege la capa de descubrimiento de páginas que sí generan valor: tutoriales, comparativas, recursos gratuitos y páginas de consulta. Después de corregirlo, revisa enlaces internos y CTA para que el lector pueda avanzar hacia formación en Claude Code o recursos relacionados.

En el flujo de Masa para ClaudeCodeLab, los cambios con más impacto fueron eliminar el ping antiguo, hacer que lastmod siguiera updatedDate y unir las diez versiones de idioma con hreflang recíproco. La revisión editorial quedó más clara porque el sitemap reflejaba las mismas fechas y slugs que se comprobaban en el frontmatter MDX.

#Claude Code #mapa del sitio #SEO #XML #automatización
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.