Marketing Por Equipe DevBras

Capa do artigo: Sitemap que ajuda a busca: só URLs que respondem 200 e que você quer no índice

Sitemap que ajuda a busca: só URLs que respondem 200 e que você quer no índice

Gere o sitemap a partir do que está publicado, tire redirects, rascunhos e páginas noindex, use lastmod de verdade e confira cada URL com um script antes de enviar.

Um blog com 300 posts publicados tinha 1.140 URLs no sitemap. Estavam lá o slug antigo de cada post que mudou de nome, os rascunhos, as páginas de tag marcadas com noindex e as mesmas categorias com ?ordem=recente. A resposta curta: o sitemap deve listar só as URLs canônicas que respondem 200 e que você quer ver na busca. Ele é a lista de recomendações, não o inventário de tudo que já existiu.

Um sitemap sujo não derruba o site, mas manda a busca gastar visitas em endereço que você mesmo não recomenda. E esconde o sinal que importa: quando você publica um post novo, ele se perde no meio de mil URLs ruins.

O que o sitemap faz e o que ele não faz

A documentação do Google descreve o sitemap como um arquivo que informa quais páginas e arquivos você considera importantes, e quando mudaram. Ele ajuda a descobrir URLs, principalmente em sites grandes, novos ou com poucos links internos. Mas ele não garante indexação: é uma sugestão, e a busca decide.

Algumas regras que o Google documenta e que mudam como você monta o arquivo:

  • Limite por arquivo: 50.000 URLs ou 50 MB sem compressão. Acima disso, divida em vários sitemaps e crie um arquivo índice que aponta para eles.
  • URLs absolutas e canônicas: com protocolo e host exatamente como na canonical da página. Se o site responde em https://www., o sitemap não lista http:// nem o domínio sem www.
  • lastmod conta se for confiável. O Google usa a data quando ela é consistente com a realidade. Data que muda em todo deploy, sem o conteúdo mudar, perde valor.
  • priority e changefreq são ignorados pelo Google. Não gaste tempo calibrando esses campos.

O que entra e o que fica de fora

Use esta pergunta para cada URL: "Eu quero que esta página apareça na busca, neste endereço exato?" Se a resposta for não, ela sai.

  • Entra: posts publicados, categorias que você quer ranquear, páginas institucionais úteis, sempre no endereço canônico.
  • Fica de fora: URLs que redirecionam, rascunhos, páginas com noindex, versões com parâmetros de ordenação ou rastreamento, páginas de busca interna, páginas que exigem login.

A combinação mais confusa é a URL no sitemap com canonical apontando para outra. Você diz "indexe esta" no sitemap e "a certa é a outra" na página. O guia sobre canonical que aponta para a URL certa explica como alinhar as duas.

Como gerar o sitemap a partir do banco

O sitemap confiável é gerado da mesma fonte que decide o que é publicado. Nada de arquivo editado à mão. Exemplo em Node com mysql2 e Express:

import express from 'express';
import mysql from 'mysql2/promise';

const BASE = 'https://www.exemplo.com.br';
const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
});

function escaparXml(texto) {
  return String(texto)
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

async function gerarSitemap() {
  const [posts] = await pool.query(
    "SELECT slug, atualizado_em FROM posts WHERE status = 'publicado' AND noindex = 0 ORDER BY atualizado_em DESC"
  );
  const itens = posts.map((p) =>
    '  <url><loc>' + escaparXml(BASE + '/blog/' + p.slug) + '</loc>' +
    '<lastmod>' + new Date(p.atualizado_em).toISOString().slice(0, 10) + '</lastmod></url>'
  );
  return [
    '<?xml version="1.0" encoding="UTF-8"?>',
    '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
    ...itens,
    '</urlset>',
  ].join('\n');
}

const app = express();

app.get('/sitemap.xml', async (req, res, next) => {
  try {
    const xml = await gerarSitemap();
    res.type('application/xml').send(xml);
  } catch (err) {
    next(err);
  }
});

O ponto central está no WHERE: o mesmo critério que decide se o post aparece no site decide se ele entra no sitemap. Se a página de um post responde 404 para rascunho, o rascunho nunca chega ao arquivo.

O campo atualizado_em deve mudar quando o conteúdo muda de verdade (texto, título, código), não quando alguém conta uma visualização. Se o seu sistema atualiza essa coluna por qualquer motivo, crie uma coluna separada para "conteúdo alterado em".

Para site com muitas visitas de robô, guarde o XML gerado em cache por alguns minutos ou regenere ao publicar. A consulta acima é leve, mas não precisa rodar a cada pedido.

Avise onde está o sitemap

Coloque a linha no robots.txt, com URL absoluta:

User-agent: *
Allow: /

Sitemap: https://www.exemplo.com.br/sitemap.xml

E envie o endereço no Search Console, na seção de sitemaps. Lá você vê quantas URLs foram lidas e se houve erro de leitura.

Como conferir cada URL antes de enviar

Este script lê o sitemap e pede cada URL sem seguir redirecionamento. Tudo que não for 200, ou que tiver noindex, aparece na saída:

// conferir-sitemap.mjs (Node 18 ou mais novo)
const SITEMAP = 'https://www.exemplo.com.br/sitemap.xml';
const NOINDEX = new RegExp('<meta[^>]+name=["\']robots["\'][^>]+noindex', 'i');

const xml = await (await fetch(SITEMAP)).text();
const urls = [...xml.matchAll(new RegExp('<loc>([^<]+)</loc>', 'g'))].map((m) => m[1].trim());
let problemas = 0;

for (const url of urls) {
  try {
    const res = await fetch(url, { redirect: 'manual' });
    const html = res.status === 200 ? await res.text() : '';
    const robotsHeader = res.headers.get('x-robots-tag') || '';
    if (res.status !== 200) {
      console.log(res.status + '  ' + url + '  ->  ' + (res.headers.get('location') || ''));
      problemas++;
    } else if (NOINDEX.test(html) || robotsHeader.includes('noindex')) {
      console.log('noindex  ' + url);
      problemas++;
    }
  } catch (err) {
    console.log('falhou  ' + url + '  ' + err.message);
    problemas++;
  }
}

console.log(urls.length + ' URLs, ' + problemas + ' com problema');
process.exitCode = problemas > 0 ? 1 : 0;

Como o script termina com código 1 quando acha problema, ele pode rodar no CI depois de cada deploy. No blog do exemplo, a primeira execução listou 612 redirects, 140 páginas noindex e 88 rascunhos. Depois da correção do WHERE, o arquivo ficou com as 300 URLs que importavam.

Se a saída mostrar redirects encadeados, vale ler sobre cadeias de redirect que gastam a paciência e a busca.

Erros comuns

  • Sitemap gerado uma vez e esquecido. Posts novos não entram e os removidos ficam lá, apontando para 404.
  • URLs que redirecionam. Cada uma obriga a busca a seguir um salto para chegar ao destino que você deveria ter listado direto.
  • Host misturado. Metade das URLs com www, metade sem. A busca recebe sinais contraditórios sobre qual é a canônica.
  • lastmod sempre igual à data de hoje. A data perde credibilidade e deixa de ajudar a busca a priorizar o que mudou.
  • Rascunho e noindex no arquivo. Você pede indexação e proíbe ao mesmo tempo. O relatório do Search Console se enche de avisos que escondem os reais.
  • Caracteres sem escape. Um & cru numa URL com parâmetro torna o XML inválido, e o arquivo inteiro pode ser rejeitado.

Checklist

  • O sitemap é gerado pela mesma consulta que decide o que está publicado.
  • O script de conferência termina com zero problemas.
  • Todas as URLs usam o mesmo protocolo e host da canonical.
  • lastmod muda só quando o conteúdo muda.
  • O robots.txt aponta para o sitemap com URL absoluta.
  • O sitemap foi enviado no Search Console e não mostra erro de leitura.
  • Arquivos acima de 50.000 URLs foram divididos com um índice.

Para fechar: conte as URLs do seu sitemap e compare com o número de páginas que você publicou de verdade. Os dois números batem?

Avalie este artigo

Referência: https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview