Gerar sitemaps XML com Claude Code
Crie sitemaps em Astro e Node com hreflang, lastmod, robots.txt e validação no Search Console.
Um sitemap é um inventário de URLs públicas, não uma garantia de indexação
Quando Claude Code ajuda a publicar muitos artigos, páginas de produto ou documentação, o ponto fraco nem sempre é o layout. Muitas vezes é a descoberta das URLs certas pelos mecanismos de busca. Um sitemap XML informa quais URLs canônicas devem ser rastreadas, quando elas tiveram uma atualização relevante e como as versões traduzidas se relacionam.
O ponto central é manter a informação verdadeira. A documentação atual do Google diz que priority e changefreq são ignorados, e que lastmod só é útil quando reflete mudanças reais de forma consistente. O antigo endpoint de ping para sitemaps também foi descontinuado. Portanto, o fluxo moderno deve usar robots.txt, Google Search Console e verificações de deploy, não chamadas para https://www.google.com/ping?sitemap=....
Este guia mostra dois caminhos práticos: a integração oficial do Astro e um gerador em Node.js sem dependências para coleções MDX multilíngues. Para encaixar isso na estratégia geral, veja também otimização de SEO com Claude Code e configuração de CI/CD com Claude Code.
Regras oficiais para seguir
| Item | Decisão prática |
|---|---|
| URL | Use URLs absolutas, como https://example.com/blog/post/ |
| Limite | Divida ao chegar perto de 50.000 URLs ou 50 MB sem compactação |
| Codificação | Salve em UTF-8 e escape valores XML |
lastmod | Use a data real de uma mudança importante no conteúdo, dados estruturados ou links |
priority / changefreq | Podem ser omitidos para Google |
| Páginas multilíngues | Cada URL lista a si mesma e todas as versões alternativas |
| Envio | Use robots.txt e Search Console; remova scripts de ping |
As fontes principais são o guia de sitemaps do Google, o aviso sobre o fim do ping, o guia de versões localizadas e o protocolo do sitemaps.org.
Caso 1: páginas Astro e rotas de blog
Em um site Astro estático, comece com @astrojs/sitemap. A integração roda durante astro build e pode gerar relações de idioma quando a estrutura de rotas é previsível.
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',
},
},
}),
],
});
O erro mais comum é deixar o valor de site errado. localhost, domínio de prévia ou mistura de http com https não devem aparecer no sitemap público. O Google tenta rastrear exatamente as URLs listadas, então elas precisam bater com as URLs canônicas.
Caso 2: gerador Node.js para MDX multilíngue
Um gerador próprio é útil quando o conteúdo fica em coleções como blog, blog-en e blog-zh, ou quando updatedDate precisa virar lastmod. O exemplo abaixo usa apenas módulos nativos do Node.js e grava 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) => ({
'<': '<',
'>': '>',
'&': '&',
"'": ''',
'"': '"',
})[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('Nenhuma URL pública foi encontrada para o sitemap.');
}
await mkdir(OUT_DIR, { recursive: true });
await writeFile(OUT_FILE, buildSitemap(entries), 'utf8');
console.log(`${entries.length} URLs gravadas em ${OUT_FILE}.`);
Execute assim:
SITE_URL=https://claudecodelab.com node scripts/generate-sitemap.mjs
Caso 3: separar artigos, produtos e documentação
Um blog pequeno pode usar apenas sitemap.xml. Em sites maiores, separar por tipo de conteúdo facilita a manutenção, evita os limites oficiais e ajuda a depurar no 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>
Peça ao Claude Code para registrar a quantidade de URLs por arquivo, dividir antes de 50.000 URLs e garantir que o índice aponte apenas para sitemaps do mesmo site.
robots.txt, Search Console e validação
Adicione o sitemap ao robots.txt:
User-agent: *
Allow: /
Sitemap: https://claudecodelab.com/sitemap.xml
Depois, envie a URL uma vez no Google Search Console. No deploy, confirme que a URL pública retorna HTTP 200 e parece um arquivo de sitemap.
// 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(`Falha ao buscar o sitemap: HTTP ${response.status}`);
}
const xml = await response.text();
if (!xml.includes('<urlset') && !xml.includes('<sitemapindex')) {
throw new Error('A resposta não parece um sitemap XML.');
}
console.log(`${sitemapUrl} validado. Tamanho: ${xml.length} bytes`);
Erros comuns antes de publicar
O primeiro erro é definir todos os lastmod como a data do build. Se o conteúdo não mudou, a data também não deve mudar.
O segundo é incluir rascunhos, páginas noindex, URLs de redirecionamento ou duplicadas. O sitemap deve listar apenas URLs canônicas que você quer ver nos resultados de busca.
O terceiro é criar hreflang em uma direção só. Cada versão de idioma precisa apontar para si mesma e para todas as outras.
O quarto é esquecer o escape XML. Um & em uma query string deve virar &.
Monetização e resultado prático
O sitemap não monetiza sozinho, mas protege a descoberta das páginas que geram valor: tutoriais, comparativos, materiais gratuitos e páginas de consultoria. Depois de corrigir o sitemap, revise links internos e CTA para levar o leitor naturalmente à formação em Claude Code ou a recursos relacionados.
No fluxo de Masa para o ClaudeCodeLab, as mudanças mais úteis foram remover o ping antigo, alinhar lastmod com updatedDate e conectar as dez versões de idioma com hreflang recíproco. A revisão editorial ficou mais simples porque o sitemap passou a refletir as mesmas datas e slugs verificados no frontmatter MDX.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.