Programação Por Equipe DevBras

Capa do artigo: Favicon não aparece: caminho, servidor, cache e Google

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 de rel icon, shortcut icon, apple-touch-icon e apple-touch-icon-precomposed.
  • Um favicon por hostname. loja.devbras.dev e blog.devbras.dev podem 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.txt que 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ê.

Avalie este artigo

Referência: http://www.lojas-na.net/dicas-epages/Alterar-aspectos-graficos-com-HTML-e-CSS/Colocar-um-Faviconico-logo-na-barra-de-endereco.html