IndexNow: avisando o Bing e o Yandex mais rápido
Olá meus Unicórnios! 🦄✨
Sabe aquela sensação de publicar um artigo novo, olhar para a tela toda orgulhosa, e depois ficar esperando? 😅 Esperando o buscador passar. Esperando ele reparar que a página existe. Às vezes um dia, às vezes uma semana, às vezes nunca.
É que o mundo dos buscadores sempre funcionou por visita. O robô decide quando volta ao seu site, e você não tem voz nenhuma nisso. Publicou? Ótimo, agora aguarde. 🙃
O IndexNow vira essa lógica do avesso: em vez de esperar o robô voltar, você bate na porta dele e avisa. "Ó, esta página aqui mudou, dá uma olhada." E o mais bonito é que isso cabe numa requisição HTTP só, sem SDK, sem cadastro, sem painel para configurar.
Este artigo é o caminho inteiro: como criar a chave, como publicar o arquivo de verificação e como mandar as URLs, com um script em Node.js e outro em PHP. E, principalmente, quais erros a API devolve quando você escorrega, porque alguns deles são bem traiçoeiros.
🎺 Por que isso importa (e por que não é bala de prata)
A ideia do protocolo é ridiculamente simples: você manda uma lista de URLs que mudaram, e os buscadores que participam recebem o aviso. Sem esperar o rastreamento. Sem torcer para o robô ter um dia de boa vontade.
E tem um detalhe que quase ninguém menciona: o IndexNow não serve só para página nova. Ele serve para qualquer mudança que você queira que o buscador saiba:
- Publicou um artigo. O caso óbvio.
- Editou um artigo antigo, corrigiu um preço, atualizou um tutorial que ficou desatualizado.
- Apagou uma página. Sim, isso também: você avisa a URL, o buscador vai lá, vê o 404 e tira do índice mais rápido do que faria sozinho.
Agora a parte chata, e prefiro dizer logo em vez de te deixar descobrir depois. 🙏
O Google não usa o IndexNow. Ele chegou a anunciar que estava avaliando o protocolo, e ficou nisso. Para o Google, continua valendo o sitemap e o Search Console, do mesmo jeito de sempre.
Então por que fazer? Porque é trabalho de uma vez só. São dez minutos para criar a chave e subir o arquivo, mais um script que você chama sempre que publica. Depois disso, roda sozinho para sempre. Não é a coisa mais importante do seu SEO, mas é provavelmente a de melhor relação entre esforço e retorno que existe.
🔑 A chave: você inventa, ninguém aprova
Esta é a parte que mais confunde quem chega, porque a gente está viciada em API que exige cadastro. No IndexNow não existe cadastro. Não tem painel, não tem e-mail de confirmação, não tem botão "gerar credencial".
Você inventa a chave. Sério. Ela é só um texto aleatório que você escolhe, e a única coisa que prova que ela é sua é o fato de ela estar publicada dentro do seu site, num lugar onde só quem manda no domínio conseguiria colocar.
As regras que a chave precisa obedecer são estas, e elas vêm da própria mensagem de erro da API:
- De 8 a 128 caracteres.
- Só letras minúsculas (
a-z), maiúsculas (A-Z), números (0-9) e traço (-).
Nada de acento, espaço, underline ou símbolo. O jeito mais prático de acertar sem pensar é gerar um punhado de caracteres hexadecimais, porque hexadecimal só tem número e letra de a a f, então cai naturalmente dentro da regra.
Este script gera a chave e já cria o arquivo de verificação junto:
// Gera a chave do IndexNow e grava o arquivo de verificacao.
// A chave e o nome do arquivo TEM de ser iguais, senao o buscador recusa.
const crypto = require("crypto");
const fs = require("fs");
function gerarChave() {
// 32 caracteres hexadecimais. So letras e numeros, como a API exige.
return crypto.randomBytes(16).toString("hex");
}
function main() {
const chave = gerarChave();
// O conteudo do arquivo e a propria chave, mais nada. Sem quebra de linha
// extra, sem aspas, sem JSON.
fs.writeFileSync(chave + ".txt", chave);
console.log("Sua chave e: " + chave);
console.log("Arquivo criado: " + chave + ".txt");
}
main();
Rodando com node gerar-chave.js, a saída é assim:
Sua chave e: 7c4bba7aa9ef3dfbea5dc9c481bf80f7
Arquivo criado: 7c4bba7aa9ef3dfbea5dc9c481bf80f7.txt
E se você preferir PHP, é a mesma ideia em três linhas:
<?php
// Gera a chave do IndexNow e grava o arquivo de verificacao.
function gerarChave()
{
// 32 caracteres hexadecimais. So letras e numeros, como a API exige.
return bin2hex(random_bytes(16));
}
$chave = gerarChave();
// O conteudo do arquivo e a propria chave, mais nada.
file_put_contents($chave . '.txt', $chave);
echo 'Sua chave e: ' . $chave . "\n";
echo 'Arquivo criado: ' . $chave . ".txt\n";
📄 O arquivo de verificação: o nome é a chave
Aqui mora a armadilha que derruba mais gente no primeiro dia, e ela é boba de tão simples: o nome do arquivo e o conteúdo dele são a mesma coisa.
Se a sua chave é 7c4bba7aa9ef3dfbea5dc9c481bf80f7, então:
- o arquivo se chama
7c4bba7aa9ef3dfbea5dc9c481bf80f7.txt; - e dentro dele está escrito
7c4bba7aa9ef3dfbea5dc9c481bf80f7, mais nada.
Mais nada mesmo. Sem aspas, sem JSON, sem título, sem linha em branco depois. Só a chave crua. É por isso que o script acima usa writeFileSync(chave + ".txt", chave) em vez de console.log com redirecionamento: o echo do terminal costuma acrescentar uma quebra de linha no fim, e aí o arquivo deixa de bater exatamente com o nome.
Esse arquivo vai na raiz do seu site, de forma que ele responda nesta URL:
https://exemplo.com.br/7c4bba7aa9ef3dfbea5dc9c481bf80f7.txt
E antes de seguir, abra essa URL no navegador. Ela tem de mostrar a chave na tela, em texto puro. Se aparecer a sua página de 404 bonitinha, ou se o servidor devolver o arquivo como download em vez de exibir, o buscador não vai conseguir validar e todos os seus envios vão ser silenciosamente ignorados.
Pelo terminal, dá para conferir assim:
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" \
"https://exemplo.com.br/7c4bba7aa9ef3dfbea5dc9c481bf80f7.txt"
Você quer ver 200 e um tipo de conteúdo de texto. Qualquer outra coisa e é melhor resolver isso antes de continuar.
📮 Mandando as URLs: a requisição de verdade
Com a chave publicada, o envio é um POST só. O corpo é um JSON com três campos:
{
"host": "exemplo.com.br",
"key": "7c4bba7aa9ef3dfbea5dc9c481bf80f7",
"urlList": [
"https://exemplo.com.br/meu-artigo-novo/",
"https://exemplo.com.br/uma-pagina-que-mudou/"
]
}
O host é o seu domínio sem o https://. O key é a chave. E o urlList é a lista de endereços completos, esses sim com o https://. Essa diferença entre um campo e outro parece pegadinha, mas é assim mesmo.
Do jeito mais direto possível, sem escrever nenhum script, é isto:
curl -X POST "https://api.indexnow.org/indexnow" \
-H "Content-Type: application/json; charset=utf-8" \
-d '{"host":"exemplo.com.br","key":"7c4bba7aa9ef3dfbea5dc9c481bf80f7","urlList":["https://exemplo.com.br/meu-artigo-novo/"]}'
Se der certo, a resposta é um 202 com o corpo vazio. Nenhuma mensagem, nenhum JSON de confirmação, nada. O silêncio é o sucesso aqui. 😄
🟢 Node.js: o script que você chama ao publicar
Este é o arquivo completo. Ele roda no Node 18 ou mais novo sem instalar absolutamente nada, porque o fetch passou a vir junto com o Node a partir dessa versão. Sem npm install, sem package.json, sem dependência.
// Avisa os buscadores que estas URLs mudaram, usando o IndexNow.
// Roda com Node 18 ou mais novo, sem instalar nada: o fetch ja vem junto.
const HOST = "exemplo.com.br";
const CHAVE = "7c4bba7aa9ef3dfbea5dc9c481bf80f7";
async function enviarUrls(listaDeUrls) {
const corpo = {
host: HOST,
key: CHAVE,
// Se a chave nao estiver na raiz do site, descomente a linha abaixo:
// keyLocation: "https://" + HOST + "/" + CHAVE + ".txt",
urlList: listaDeUrls
};
const resposta = await fetch("https://api.indexnow.org/indexnow", {
method: "POST",
// Sem este cabecalho a API responde 415 e nao explica o motivo.
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify(corpo)
});
const texto = await resposta.text();
return { status: resposta.status, texto: texto };
}
async function main() {
const urls = [
"https://" + HOST + "/meu-artigo-novo/",
"https://" + HOST + "/uma-pagina-que-mudou/"
];
try {
const r = await enviarUrls(urls);
console.log("HTTP " + r.status);
if (r.status === 200 || r.status === 202) {
console.log("Enviado. " + urls.length + " URLs na fila.");
} else {
console.log("A API recusou. Resposta: " + r.texto);
}
} catch (erro) {
console.log("Nao consegui falar com a API: " + erro.message);
}
}
main();
Três detalhes que valem o parágrafo:
O Content-Type não é opcional. Aquele comentário na linha dele não está lá de enfeite. Sem esse cabeçalho a API responde 415 com o corpo vazio, sem dizer o que faltou. É o tipo de erro que a gente fica meia hora olhando sem entender. 😤
Eu leio a resposta como texto, não como JSON. Repare no resposta.text(). Se eu tivesse escrito resposta.json(), o caminho de sucesso quebraria, porque o 202 vem com corpo vazio e vazio não é JSON válido. O script morreria justamente quando tudo deu certo, que é o bug mais irritante que existe.
O keyLocation é para quem não pode mexer na raiz. Deixei comentado de propósito. Se o seu site não deixa você colocar arquivo na raiz (uma hospedagem compartilhada, um CMS teimoso), você pode pôr o .txt em outra pasta e informar o endereço dele nesse campo. Se o arquivo está na raiz, não precisa mandar nada: a API procura ali sozinha.
Rodando o script, a saída é esta:
HTTP 202
Enviado. 2 URLs na fila.
🐘 PHP: a mesma coisa com curl
Se o seu site é PHP, faz mais sentido chamar o aviso de dentro do próprio site, na hora em que você salva o post. Aqui não precisa de biblioteca nenhuma também: o curl já vem em praticamente toda hospedagem PHP.
<?php
// Avisa os buscadores que estas URLs mudaram, usando o IndexNow.
// So precisa do PHP com a extensao curl, que quase toda hospedagem ja tem.
$HOST = 'exemplo.com.br';
$CHAVE = '7c4bba7aa9ef3dfbea5dc9c481bf80f7';
function enviarUrls($host, $chave, $listaDeUrls)
{
$corpo = array(
'host' => $host,
'key' => $chave,
'urlList' => $listaDeUrls,
);
$ch = curl_init('https://api.indexnow.org/indexnow');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Sem este cabecalho a API responde 415 e nao explica o motivo.
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json; charset=utf-8'));
// JSON_UNESCAPED_SLASHES deixa as barras das URLs legiveis no envio.
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($corpo, JSON_UNESCAPED_SLASHES));
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$texto = curl_exec($ch);
// curl_exec devolve false quando nem chegou a falar com o servidor.
if ($texto === false) {
$erro = curl_error($ch);
curl_close($ch);
throw new Exception('Nao consegui falar com a API: ' . $erro);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return array('status' => $status, 'texto' => $texto);
}
function main($host, $chave)
{
$urls = array(
'https://' . $host . '/meu-artigo-novo/',
'https://' . $host . '/uma-pagina-que-mudou/',
);
try {
$r = enviarUrls($host, $chave, $urls);
echo 'HTTP ' . $r['status'] . "\n";
if ($r['status'] == 200 || $r['status'] == 202) {
echo 'Enviado. ' . count($urls) . " URLs na fila.\n";
} else {
echo 'A API recusou. Resposta: ' . $r['texto'] . "\n";
}
} catch (Exception $e) {
echo $e->getMessage() . "\n";
}
}
main($HOST, $CHAVE);
Duas linhas aqui merecem explicação, porque cada uma evita um erro específico.
A primeira é o CURLOPT_RETURNTRANSFER. Sem ele, o curl_exec imprime a resposta na tela em vez de devolver, e a sua variável $texto recebe apenas true. Aí o seu tratamento de erro nunca consegue mostrar a mensagem da API, porque ele não tem a mensagem em mãos.
A segunda é aquele if ($texto === false), com três sinais de igual. Isso não é preciosismo: quando dá tudo certo, a API responde com o corpo vazio, e string vazia no PHP é considerada falsa numa comparação normal. Com ==, o sucesso cairia dentro do bloco de erro e o script anunciaria uma falha que não existiu. O === compara o tipo junto, então só o false de verdade (a falha de conexão) entra ali.
A saída, rodando com php enviar.php:
HTTP 202
Enviado. 2 URLs na fila.
🚨 Os erros que a API devolve (e o que cada um quer dizer)
Esta é a seção que eu queria ter achado pronta quando comecei, porque as mensagens do IndexNow são curtas e algumas chegam sem nenhum texto. Vamos aos casos.
URL de um domínio que não é o seu. Se a urlList tiver endereço de outro site, a API recusa na hora com 422:
{
"errorCode": "InvalidRequestParameters",
"message": "One or more URLs are not related to your verified domain. Please verify URLs before submitting.",
"details": null
}
Vale para o caso óbvio (misturar dois sites no mesmo pedido) e para um bem menos óbvio: www e sem www contam como hosts diferentes. Se o seu host diz exemplo.com.br e a URL da lista está escrita como https://www.exemplo.com.br/..., é este o erro que você recebe.
Chave fora das regras. Chave curta demais, ou com símbolo que não é permitido, também dá 422, e essa mensagem é generosa: ela lista a regra inteira.
{
"errorCode": "InvalidRequestParameters",
"message": "Given key is not valid. Your key should have a minimum of 8 and a maximum of 128 alphanumeric characters. The key can contain only the following characters: lowercase characters (a-z), uppercase characters (A-Z), numbers (0-9), and dashes (-).",
"details": null
}
JSON malformado. Se o corpo não for um JSON válido, aí é 400, com uma mensagem bem mais seca:
{
"errorCode": "InvalidRequestParameters",
"message": "Given request parameters are null or invalid",
"details": null
}
Cabeçalho faltando. E o pior de todos: sem o Content-Type, a resposta é 415 e o corpo vem completamente vazio. Nenhuma pista. É por isso que insisti tanto nele lá em cima.
🤨 O 202 que engana: silêncio não é aprovação
Se você só for ler um pedaço deste artigo, leia este. 🙏
É natural olhar para o 202 e entender "deu tudo certo, minhas URLs foram aceitas". Não é isso que ele significa. O 202 quer dizer apenas "recebi o seu pedido, vou olhar depois". É um recibo de entrega, não um atestado de aprovação.
E a diferença dói na prática, porque estas três situações devolvem 202 alegremente:
- Um
hostde um domínio que nem existe. - Uma chave que nunca foi publicada no site, ou que está no arquivo errado.
- Uma
urlListvazia, sem nenhuma URL dentro.
Nos três casos a resposta é 202, corpo vazio, e o seu script imprime um lindo "Enviado" na tela. 😳
O motivo é que a validação da chave não acontece durante a sua requisição. O buscador aceita o pedido, coloca na fila, e só mais tarde vai até o seu site buscar aquele .txt para conferir se a chave confere. Se não conferir, ele descarta tudo em silêncio, e ninguém te avisa.
É exatamente por isso que a conferência manual do arquivo, lá na seção da verificação, não é passo opcional. Ela é a única confirmação que você vai ter, porque o 202 não confirma nada. Abra a URL do .txt no navegador e veja a chave com os próprios olhos, uma vez, antes de confiar no resto.
🔁 Um envio basta: eles conversam entre si
Uma dúvida que aparece sempre: preciso avisar o Bing e depois avisar o Yandex separadamente?
Não precisa. Os buscadores que participam do IndexNow compartilham entre si o que recebem. Mandando para api.indexnow.org, o aviso chega em todos eles.
Os endereços individuais existem e funcionam. Tanto https://www.bing.com/indexnow quanto https://yandex.com/indexnow aceitam exatamente o mesmo corpo JSON e devolvem o mesmo 202. Mas mandar para os três é fazer o mesmo trabalho três vezes: escolha um endereço, de preferência o api.indexnow.org, que é o neutro, e siga a vida.
🧩 Encaixando no seu fluxo
A última peça é decidir quando chamar isso. E a resposta curta é: no mesmo lugar onde você já considera o conteúdo publicado.
Se você tem um painel, chame o envio logo depois de salvar o post, com a URL daquele post só. Se o seu site é estático e você faz deploy por script, ponha a chamada no fim do deploy, com a lista do que mudou naquele deploy. Se você publica na mão, deixe o script numa pasta e rode depois de subir o arquivo.
O que não vale a pena é montar uma fila, um sistema de retentativa e um painel de acompanhamento para isso. É um POST que ou vai ou não vai, e se não for hoje, vai no próximo artigo que você publicar. Guarde a sua energia de engenharia para outra coisa. 😉
E, honestamente, o melhor de tudo é o que este protocolo não tem. Sem OAuth, sem token que expira, sem biblioteca para atualizar, sem cadastro para renovar. Um arquivo de texto e uma requisição. Num mundo de integrações que quebram sozinhas a cada seis meses, isso é quase um descanso. 🌈
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Perguntas frequentes
O Google usa o IndexNow?
Como eu crio a chave do IndexNow?
crypto.randomBytes(16).toString("hex") no Node ou bin2hex(random_bytes(16)) no PHP. Depois é só publicar um arquivo de texto na raiz do site cujo nome é a chave e cujo conteúdo é a mesma chave.Por que a API responde 202 mesmo quando eu erro alguma coisa?
202 significa só "recebi o seu pedido e vou olhar depois", não "está tudo certo". Mandando um host que não existe, uma chave que nunca foi publicada ou uma urlList vazia, a resposta é 202 do mesmo jeito, com o corpo vazio. A validação da chave acontece depois, quando o buscador vai buscar o seu arquivo .txt. Os erros que ele recusa na hora são outros: URL de domínio diferente, chave malformada e falta do cabeçalho de tipo.Por que dá erro 415 ao enviar com curl?
Content-Type: application/json; charset=utf-8. A API responde 415 com o corpo vazio, sem nenhuma mensagem explicando, então é um erro que trava bastante gente. No fetch do Node e no curl_setopt do PHP esse cabeçalho tem de ser escrito à mão: nenhum dos dois manda JSON por conta própria.Posso mandar URLs de vários sites no mesmo pedido?
urlList precisam ser do mesmo host que você declarou no campo host. Misturando domínios, a API recusa na hora com 422 e a mensagem One or more URLs are not related to your verified domain. Para vários sites, um pedido para cada.Preciso enviar para o Bing e para o Yandex separadamente?
api.indexnow.org basta. Os endereços www.bing.com/indexnow e yandex.com/indexnow aceitam o mesmo pedido e devolvem o mesmo 202, mas mandar para os três é repetir trabalho à toa.Leia também
OpenSEO: instalando na VPS e auditando um site
Como instalar o OpenSEO, alternativa aberta ao Semrush, numa VPS com Docker, auditar um site de verdade e entender a chave da DataForSEO.
API Mágica: notificações Web Push no navegador com PHP
Web Push com a API Mágica em PHP puro: o navegador se inscreve, o seu site envia a notificação e a API conta o clique. Sem Composer e sem biblioteca.
API Mágica: CEP e Pix de graça, sem cartão
Lancei a API Mágica: CEP, QR Code Pix, geradores e mais, de graça. Veja como consultar e gerar com Node.js em poucas linhas.