Favicon não aparece: caminho, servidor, cache e Google
Favicon que some em algumas páginas, fica antigo ou não sai no Google: teste com um script, corrija o caminho relativo, o fallback do SPA e o cache.
Quando o favicon falha, quase sempre é um destes três sintomas. A home mostra o ícone, mas a página de produto mostra o globo cinza. O ícone novo subiu e algumas pessoas continuam vendo o antigo. Ou o navegador mostra o ícone certo e a busca do Google mostra um genérico. Cada sintoma tem uma causa diferente, e dá para separar as três sem abrir o DevTools em cada página.
Este post trata da entrega do arquivo. Se o ícone aparece, mas ninguém reconhece o desenho, o problema é outro e está em favicon nítido em 16 pixels.
Um teste que faz o caminho do navegador
O script abaixo baixa uma página, lê os <link> de ícone do <head>, resolve cada endereço como o navegador faria (com urljoin, a partir da URL da página) e baixa o arquivo. Para cada ícone, mostra o status, o tipo declarado e o que o arquivo é de fato. Também confere as regras do Google para a busca: formato aceito, imagem quadrada e tamanho mínimo.
# conferir_favicon.py: lê o <head> de uma página, resolve cada ícone declarado
# e confere o que o servidor devolve. Uso: python3 conferir_favicon.py URL
import io, sys, urllib.request, urllib.error
from html.parser import HTMLParser
from urllib.parse import urljoin
from PIL import Image
REL = {"icon", "shortcut icon", "apple-touch-icon"}
GOOGLE = {"BMP", "GIF", "ICO", "PNG", "JPEG", "PPM", "TIFF"} # formatos aceitos na busca
class Links(HTMLParser):
def __init__(self):
super().__init__(); self.icones = []
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "link" and a.get("rel", "").lower() in REL and a.get("href"):
self.icones.append((a["rel"], a["href"]))
def baixar(url):
try:
with urllib.request.urlopen(url) as r:
return r.status, r.headers.get("Content-Type", ""), r.read()
except urllib.error.HTTPError as e:
return e.code, e.headers.get("Content-Type", ""), b""
pagina = sys.argv[1]
status, tipo, html = baixar(pagina)
p = Links(); p.feed(html.decode("utf-8", "replace"))
if not p.icones:
print("nenhum <link rel=icon>; o navegador vai tentar /favicon.ico")
p.icones = [("(padrão)", "/favicon.ico")]
for rel, href in p.icones:
url = urljoin(pagina, href)
status, tipo, corpo = baixar(url)
linha = f"{rel:16} {href:22} -> {url}\n{'':17}{status} {tipo or '-'} {len(corpo)} bytes"
if status == 200 and corpo.lstrip().startswith(b"<svg"):
linha += " SVG (navegador sim, Google não)"
elif status == 200:
try:
im = Image.open(io.BytesIO(corpo)); w, h = im.size
ok = im.format in GOOGLE and w == h and w >= 8
linha += f" {im.format} {w}x{h}" + ("" if ok else " FORA DA REGRA DO GOOGLE")
if ok and w <= 48: linha += " (Google recomenda maior que 48x48)"
except Exception:
linha += " NÃO É IMAGEM"
print(linha)
Rodando contra dois sites locais, um bem configurado e outro com os dois erros mais comuns:
## site certo, página de produto
icon /favicon.ico -> http://127.0.0.1:8078/favicon.ico
200 image/vnd.microsoft.icon 3384 bytes ICO 48x48 (Google recomenda maior que 48x48)
icon /icone.svg -> http://127.0.0.1:8078/icone.svg
200 image/svg+xml 636 bytes SVG (navegador sim, Google não)
icon /icon-192.png -> http://127.0.0.1:8078/icon-192.png
200 image/png 8415 bytes PNG 192x192
apple-touch-icon /apple-touch-icon.png -> http://127.0.0.1:8078/apple-touch-icon.png
200 image/png 6619 bytes PNG 180x180
## site errado, home
icon favicon.ico -> http://127.0.0.1:8079/favicon.ico
200 image/vnd.microsoft.icon 3384 bytes ICO 48x48 (Google recomenda maior que 48x48)
## site errado, página de produto
icon favicon.ico -> http://127.0.0.1:8079/produtos/favicon.ico
200 text/html 150 bytes NÃO É IMAGEM
Caminho relativo: funciona na home, quebra no resto
No site errado, o head declara href="favicon.ico", sem a barra. Na home, isso resolve para /favicon.ico e funciona. Em /produtos/cafe-especial, resolve para /produtos/favicon.ico, que não existe. Por isso o ícone aparece numa página e some em outra. A correção é começar todo href de ícone com /:
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/icone.svg" type="image/svg+xml"> <link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192"> <link rel="apple-touch-icon" href="/apple-touch-icon.png">
O servidor que responde 200 para tudo
A última linha do teste mostra o segundo erro, que esconde o primeiro: o pedido de /produtos/favicon.ico recebeu 200 com text/html. O site é uma aplicação de página única, e o servidor devolve o index.html para qualquer caminho desconhecido, assim as rotas do front-end funcionam ao recarregar a página. O efeito colateral é que nenhum arquivo estático dá 404. O navegador recebe HTML onde esperava imagem, não mostra nada, e o log do servidor não acusa erro.
Restrinja o fallback às rotas e deixe os arquivos estáticos responderem 404 quando faltarem:
# nginx: arquivo estático que não existe responde 404 de verdade
location ~* \.(ico|png|svg|webmanifest)$ {
try_files $uri =404;
}
location / {
try_files $uri /index.html; # o fallback do SPA fica só para as rotas
}
Com essa configuração no nginx 1.26, as rotas / e /produtos/cafe-especial continuam devolvendo o app com 200, /favicon.ico sai como image/x-icon e o pedido errado de /produtos/favicon.ico passa a dar 404, que aparece no log e no DevTools.
Confira também o tipo de cada arquivo. O SVG precisa sair como image/svg+xml; com text/plain ou application/octet-stream, o navegador pode ignorar o ícone.
O ícone antigo que não vai embora
Os navegadores guardam o favicon por bastante tempo, e o Chrome mantém um banco próprio de ícones, separado do cache das páginas. Se você troca o desenho e mantém o mesmo arquivo, cada pessoa vê a troca num momento diferente, e você mesmo pode achar que o deploy falhou.
Quando o desenho mudar, mude também o nome do arquivo e atualize o <link>:
<link rel="icon" href="/icone-2026.svg" type="image/svg+xml">
Isso vale para trocas de desenho, que são raras. Não gere um nome novo a cada deploy: a documentação do Google pede que a URL do favicon seja estável.
O Google mostra outro ícone
A busca tem regras próprias, descritas em Define a favicon to show in search results:
- O
<link>precisa estar na home. O Google lê o favicon da página inicial e aceita os valores derelicon,shortcut icon,apple-touch-iconeapple-touch-icon-precomposed. - Um favicon por hostname.
loja.devbras.deveblog.devbras.devpodem ter ícones diferentes. Jáloja.devbras.dev/cafesé um subdiretório e usa o da raiz. - Rastreável. O Googlebot-Image precisa conseguir baixar o arquivo e o Googlebot precisa conseguir rastrear a home. Uma regra no
robots.txtque bloqueia/assets/bloqueia o ícone junto. - Formato e tamanho. BMP, GIF, ICO, PNG, JPEG, PPM ou TIFF; quadrado; com pelo menos 8 por 8 pixels, e o Google recomenda mais de 48 por 48. O SVG não entra na lista, por isso o teste acima marca o SVG como "navegador sim, Google não" e o site certo também declara um PNG de 192 pixels.
Depois de corrigir, espere. Segundo o Google, o novo rastreamento da home pode levar de alguns dias a algumas semanas. A ferramenta de inspeção de URL do Search Console permite pedir a indexação da home, e mesmo com tudo certo o ícone não é garantido. O título e a descrição que aparecem junto dele seguem outras regras, que estão em meta tags que o Google realmente lê.