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.
Artigos relacionados
Automação de e-mail com Claude Code: de leads à monetização
Implemente automação de e-mail com Claude Code: lead magnet, consentimento, retries, analytics e receita.
Automatize a GitHub API com Claude Code sem vazar tokens
Guia prático para automatizar a GitHub API com Claude Code: permissões, paginação, limites, webhooks e exemplos Node.
Como criar um portfólio com Claude Code e Astro: implementação prática
Crie um portfólio com Claude Code e Astro: código copiável, SEO, casos reais, erros comuns e checklist de publicação.
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.