SEO Técnico
Sitemap dinâmico em escala, sem estourar memória
Como gerar e atualizar sitemap de milhões de URLs sem derrubar o servidor. Arquitetura em três camadas, delta updates, e os sete erros que fazem o Google parar de ler seu sitemap do dia pra noite.
· 12 min de leitura
Programmatic SEO no WordPress (ou em qualquer stack) gera uma conta que o time de engenharia nem sempre espera: um site de trezentas URLs vira um site de dois milhões, e de repente o sitemap que era um arquivo de 40kb precisa virar uma infraestrutura. Abordagem naïve (juntar tudo em memória, renderizar, servir em uma rota dinâmica) estoura em dois lugares, ou o servidor trava tentando montar o XML, ou o Googlebot desiste porque o arquivo é maior que 50 MB e não lê mais nada.
Este post é como eu monto a estrutura de sitemap em sites de catálogo grande, programmatic SEO ou e-commerce com milhões de SKUs. Técnico, mas não precisa ser dev sênior pra acompanhar, o objetivo é deixar claro o que precisa acontecer e onde as decisões importam.
O que o sitemap precisa fazer bem
O sitemap tem três funções práticas, e só três:
- Listar as URLs canônicas do site. A versão oficial de cada página, sem duplicatas com parâmetro.
- Sinalizar quando cada URL mudou de verdade. É o campo
lastmod, e é o único sinal forte que o Google ainda leva a sério no protocolo. - Respeitar os dois limites do formato. No máximo 50.000 URLs por arquivo e no máximo 50 MB descomprimido. Passou disso, o Google simplesmente ignora.
Três coisas que o sitemap não precisa fazer, e que a gente perde tempo configurando à toa:
- priority. O Google ignora esse campo há anos. Pode deletar.
- changefreq. Idem. O Google confia no
lastmodreal, não na estimativa de frequência. - Listar URLs com parâmetro de tracking. UTMs e similares entram como ruído e geram duplicata. Só a URL canônica.
Os dois limites do protocolo, e como dar folga
O limite oficial é 50.000 URLs ou 50 MB por arquivo. Na prática, eu trabalho com 40.000 URLs por arquivo, pra dar folga caso o conteúdo cresça entre uma geração e a próxima. Passou de 40k, divido em um sitemap a mais.
Quando o site tem mais que 40k URLs, a estrutura correta é um sitemap de sitemaps (o nome formal é sitemap index). Fica assim:
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://exemplo.com.br/sitemap-produtos-1.xml</loc>
<lastmod>2026-04-21T14:00:00Z</lastmod>
</sitemap>
<sitemap>
<loc>https://exemplo.com.br/sitemap-produtos-2.xml</loc>
<lastmod>2026-04-20T09:30:00Z</lastmod>
</sitemap>
<sitemap>
<loc>https://exemplo.com.br/sitemap-categorias.xml</loc>
<lastmod>2026-04-15T18:00:00Z</lastmod>
</sitemap>
</sitemapindex>
Só o sitemap index é submetido no Search Console. O Google segue os ponteiros e lê cada arquivo filho sozinho.
Por que gerar ingênuo estoura
O jeito errado (que eu já vi em produção várias vezes) é:
- A rota
/sitemap.xmlaciona um controller no framework. - O controller faz um
SELECT *em toda a tabela de produtos. - Coloca o resultado inteiro num array na memória do processo.
- Monta a string do XML concatenando linha por linha.
- Devolve a resposta.
Em um site de 50 mil URLs isso funciona mal, mas funciona. Em um site de 2 milhões, o processo consome 3 GB de RAM, estoura o limite do worker, e o Googlebot recebe um erro 500 ou um tempo de resposta altíssimo. No primeiro caso o crawl fica capado. No segundo, o Google reduz a frequência de visita no domínio todo, o que arrasta outras páginas junto.
A correção é parar de gerar sob demanda e parar de carregar tudo em memória. As duas coisas.
Arquitetura em três camadas
O padrão que eu uso em site grande tem três camadas bem separadas.
1. Fonte, o banco de dados
A verdade sobre quais URLs existem está no banco. Uma tabela (ou view materializada) que tem as URLs canônicas do site, com o timestamp real da última modificação de cada uma. Essa view é atualizada via triggers ou via um job quando os dados de origem mudam.
O importante aqui é que consultar essa view tem que ser rápido. Indexe pela coluna que você usa pra particionar (tipo de conteúdo, range de ID, data). Se a consulta sozinha demora dois minutos, o job de sitemap fica escravo disso.
2. Geração, um worker que escreve em streaming
O sitemap é gerado em worker separado, não na rota que o Googlebot chama. Esse worker roda em horário controlado (1x por hora ou por cron mais espaçado pra partes que mudam pouco), abre um cursor no banco (não um SELECT *, um cursor que busca em blocos de 1000 linhas), escreve o XML direto em arquivo usando escritor em streaming (XMLWriter em PHP, xml2js streaming em Node, lxml.etree em Python), e faz upload do resultado pra um storage (S3, CloudFront origin, disco local servido via CDN).
O consumo de memória dessa versão é constante, não importa se o site tem 10 mil ou 10 milhões de URLs. Sempre 20-40 MB de RAM no processo, porque a qualquer momento só mil linhas estão carregadas.
3. Servir, via CDN, nunca dinâmico
O Googlebot chama /sitemap.xml e recebe um arquivo estático do storage, via CDN. A rota nunca aciona código de aplicação em tempo real. O tempo de resposta fica em dezenas de milissegundos, independente do tamanho do arquivo.
Se o framework obriga a passar por alguma rota (Next.js, Laravel), a rota lê o arquivo do storage e devolve, sem regerar. A geração é uma coisa, o serviço é outra.
Estratégia de particionamento, o que vai em cada arquivo
Com um sitemap index, você decide como quebrar o site em partições. Existem dois padrões comuns.
Por tipo de conteúdo (recomendado)
Um arquivo pra produtos, um pra categorias, um pra blog, um pra páginas institucionais. Esse padrão ganha em um ponto crítico, invalidação seletiva. Se só os produtos mudaram, você regera só o sitemap de produtos e atualiza o lastmod daquela entrada no index. Os outros arquivos ficam parados.
Quando produtos passam de 40k URLs, o sitemap de produtos vira dois (sitemap-produtos-1.xml, sitemap-produtos-2.xml). Você pode dividir por range de ID, alfabético, por categoria principal, não importa muito desde que seja determinístico, ou seja, a URL X cai sempre na mesma partição.
Por range de ID (mais simples, pior pra manutenção)
Todos os tipos misturados, divididos só por ID. É mais simples de gerar, mas qualquer mudança em qualquer tipo invalida o arquivo inteiro. Só uso esse padrão em protótipo.
Lastmod de verdade vs lastmod de enfeite
O Google aprendeu, com o tempo, quais sites mentem no lastmod. Quando o sistema detecta que o campo sempre muda pra "agora" em toda requisição, mas o conteúdo da página não mudou de fato, ele começa a ignorar o lastmod daquele site inteiro. Isso é ruim, porque é o único sinal forte que o sitemap ainda carrega.
Regra de bolso: o lastmod tem que vir do timestamp real da última edição de conteúdo, o campo updated_at (ou equivalente) da tabela. Nunca NOW() na hora de gerar o sitemap.
No sitemap index, o lastmod de cada entrada deve ser o maior updated_at das URLs dentro daquele arquivo filho. Isso sinaliza pro Google exatamente quais arquivos vale a pena rechecar desde a última visita, e ele consegue priorizar.
Delta updates, regerar só o que mudou
Em site de escala, regerar o sitemap inteiro a cada cron é desnecessário e caro. O padrão maduro é:
- Uma tabela de controle marca quais partições estão "sujas" (ou seja, tiveram alguma mudança desde a última geração).
- O cron lê essa tabela, regera só as partições sujas, atualiza o
lastmoddelas no sitemap index, e marca como limpas. - Partições sem mudança ficam intocadas, servidas direto da CDN com o mesmo arquivo da semana passada.
Efeito prático, em um site de 2 milhões de URLs que eu trabalhei, o sitemap completo regerava em 12 minutos. Depois do delta update, a execução típica regerava 3 a 5 partições de 40k URLs e terminava em 40 segundos. O cron passou de 1x por dia pra 1x por hora sem encostar em custo.
gzip, economizando 80% de banda
O sitemap é texto XML, comprime muito bem. O Google aceita .xml.gz sem cerimônia. Sempre gere o arquivo e suba comprimido. A economia de banda é significativa em site grande, e o tempo de download pra o bot também cai.
A única ressalva, submeta a URL com a extensão .xml.gz no Search Console pra deixar explícito.
Imagem, vídeo e news, quando vale a pena
Existem extensões do protocolo pra imagem, vídeo e news. Minha recomendação:
- Imagem. Vale se o site depende de tráfego em Google Imagens (moda, catálogo, receita). O ganho é real. Pode ser embutido nos sitemaps existentes (namespace extra) em vez de arquivo separado.
- Vídeo. Vale só se o site hospeda vídeo próprio com ambição de ranquear. Embutar direto em YouTube sem vídeo no próprio site dispensa.
- News. Só pra publicações registradas no Google News. Requer formato específico e regras de frescor, não é drop-in.
Em geral, não crie tipo de sitemap que você não vai manter. Manter vazio ou desatualizado é pior que não ter.
Submissão e monitoramento
Dois lugares pra declarar o sitemap:
- Search Console. Submeta a URL do sitemap index. O Search Console vai ler recursivamente. Acompanhe a coluna "Last read" (Última leitura), se o valor não atualizar em mais de 7 dias, algo está errado.
- robots.txt. Adicione uma linha
Sitemap: https://exemplo.com.br/sitemap.xml. Alguns crawlers (incluindo motores menores) descobrem o sitemap por aí.
Monitoramento semanal mínimo, veja no Search Console:
- Status de cada sitemap (Success vs Couldn't fetch).
- Discovered URLs vs Indexed URLs. Se a diferença é grande e aumenta, algo na qualidade das URLs está afastando o Google (canônica errada, thin content, duplicata).
- "Última leitura" próxima da data do cron. Se o Google está lendo rápido, seu sitemap está saudável.
Sete anti-padrões que fazem o Google parar de confiar
- Colocar URL que retorna 404. É o pior erro possível. O Google penaliza fortemente domínios que listam URLs mortas no sitemap, a leitura pode parar inteira.
- Colocar URL que redireciona (301). Segunda pior coisa. O sitemap é pra versão final, não pra alias que redireciona.
- Colocar URL com
noindexna página. Contradição direta, o bot lê, verifica a página, encontra noindex, perde a confiança no seu sitemap em geral. lastmodsempre igual ao momento atual. Mencionei antes. Se for assim, melhor não terlastmoddo que terlastmodfake.- URL com parâmetro de tracking. Entra como duplicata da versão canônica e gera ruído no índice.
- Gerar na hora da requisição em site grande. Tempo de resposta de segundos, bot desiste, crawl budget vai embora.
- Não declarar o charset em UTF-8. Caracteres acentuados quebram. O XML precisa começar com
<?xml version="1.0" encoding="UTF-8"?>.
Esqueleto em PHP, em streaming
Pra quem vai implementar, esqueleto mínimo em PHP usando XMLWriter (streaming) e PDO com cursor:
<?php
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // cursor server-side real
]);
$xml = new XMLWriter();
$xml->openUri('php://output');
$xml->startDocument('1.0', 'UTF-8');
$xml->startElement('urlset');
$xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
$stmt = $pdo->prepare(
'SELECT slug, updated_at FROM produtos WHERE ativo = 1 ORDER BY id'
);
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$xml->startElement('url');
$xml->writeElement('loc', "https://exemplo.com.br/produto/{$row['slug']}/");
$xml->writeElement('lastmod', (new DateTime($row['updated_at']))->format('c'));
$xml->endElement();
$xml->flush(); // descarrega pro buffer, evita acúmulo
}
$xml->endElement();
$xml->endDocument();
Esse worker escreve o arquivo inteiro com consumo constante de memória, independente do número de produtos. Depois, um passo adicional comprime em gzip e sobe pro S3 ou equivalente.
Quando dinâmico em requisição ainda vale
Sitemap gerado na hora faz sentido só em dois casos. Site pequeno (menos de 10 mil URLs totais), onde o custo de gerar é desprezível. E site com requisito de freshness extremo (ex: conteúdo de notícia em tempo real), onde atrasar o sitemap em 15 minutos é problema. Fora desses dois casos, sempre gere em background e sirva estático.
Se o site é Next.js, o padrão sitemap.ts do App Router roda no build, ou em ISR, e funciona bem até uns 30-50 mil URLs. Passou disso, também migre pra worker externo que publica o arquivo, e transforme o endpoint do Next em um proxy que lê do storage.
Fechando
Três ideias pra guardar. Sitemap é arquivo, não aplicação, separe quem gera de quem serve. lastmod é o único sinal forte que ainda importa no protocolo, trate com cuidado e nunca minta. E delta update é a diferença entre sitemap saudável em escala e servidor travado toda hora.
Pra conectar com o resto da trilha, o guia de programmatic SEO em WordPress mostra o lado de gerar as URLs em volume, e o case do scaffold de importação em PHP traz o mesmo padrão de streaming que descrevi aqui, aplicado à ingestão em vez de à exposição.