Acentos quebrados no HTML: como ler o sintoma e achar a camada errada
"ã" e o losango com interrogação contam histórias diferentes. Aprenda a ler cada sintoma, conferir cabeçalho, meta e arquivo, e use entidade só para o que precisa.
O site da Padaria São João (fictícia) mostra no cardápio "Pão de açúcar" e, na página de contato, "Rua da Concei��o". Alguém sugere trocar cada letra acentuada por ã e ç. Funciona, e transforma o texto numa sopa que ninguém mais consegue editar. A resposta curta: salve os arquivos em UTF-8, declare <meta charset="utf-8"> no começo do <head> e garanta que o servidor envie charset=utf-8 no cabeçalho, ou não envie charset nenhum. Com as três camadas de acordo, "ação" é "ação". Entidade fica só para os poucos caracteres que o HTML reserva.
O mais útil, porém, é aprender a ler o sintoma. "ã" e "�" parecem o mesmo defeito e apontam para causas opostas.
Por que o acento quebra: bytes lidos com a tabela errada
Um arquivo de texto é uma sequência de bytes. A codificação é a tabela que diz qual letra cada byte representa. Em UTF-8, o "ã" ocupa dois bytes, C3 A3. Na codificação antiga ISO-8859-1 (e na sua prima Windows-1252), o mesmo "ã" é um byte só, E3. O navegador não adivinha: ele usa a codificação que alguém declarou e lê os bytes com essa tabela.
- "ã", "ç", "ú": o arquivo está em UTF-8, mas o navegador leu como Windows-1252. Cada letra acentuada vira duas: o
C3é "Ã" e oA3é "£". A causa está na declaração, no cabeçalho ou na meta tag. - "�" (losango com interrogação): o navegador leu como UTF-8, mas o byte veio em Latin-1. O
E3sozinho não forma um caractere UTF-8 válido, e o navegador o troca pelo caractere de substituição. A causa está no arquivo ou no dado que veio do banco.
Saber isso já corta metade da investigação. No exemplo da padaria, o cardápio (com "ã") é um HTML salvo em UTF-8 e servido com declaração errada. O endereço (com "�") vem do banco, que entrega Latin-1 para uma página UTF-8.
Quem decide a codificação da página
O navegador segue uma ordem de prioridade definida na especificação do HTML:
- A marca BOM no início do arquivo, se existir.
- O cabeçalho HTTP
Content-Type: text/html; charset=.... - A meta tag
<meta charset>, que precisa estar nos primeiros 1024 bytes do documento. - Se nada disso existir, uma adivinhação do navegador, que varia.
A consequência prática: uma meta tag correta não vence um cabeçalho errado. Se o servidor diz charset=iso-8859-1, a sua meta UTF-8 é ignorada. É por isso que tanta gente "coloca a meta e não resolve".
Como diagnosticar e corrigir, passo a passo
- Veja o cabeçalho que o servidor envia.
- Veja a meta tag no código-fonte da página (Ctrl+U), logo no início do
<head>. - Veja a codificação do arquivo no disco.
- Converta o arquivo uma única vez, se ele estiver em Latin-1.
- Alinhe o servidor para enviar UTF-8.
- Se o defeito só aparece em texto do banco, siga para a conexão e as colunas.
$ curl -sI https://padaria.exemplo.com.br/cardapio | grep -i content-type
Content-Type: text/html; charset=iso-8859-1
$ file -bi cardapio.html
text/html; charset=utf-8
$ printf 'ã' | xxd
00000000: c3a3 ..
O diagnóstico do cardápio está na primeira linha: o arquivo é UTF-8, o servidor anuncia ISO-8859-1. O xxd confirma que o seu terminal grava "ã" como c3 a3, ou seja, UTF-8.
Se o arquivo estivesse em Latin-1, a conversão seria feita uma vez, gerando um arquivo novo:
$ iconv -f WINDOWS-1252 -t UTF-8 contato-antigo.html > contato.html
No editor, o VS Code mostra a codificação na barra de status; clicando nela, há as opções de reabrir e de salvar com outra codificação. Não converta um arquivo que já está em UTF-8: o resultado é "ã" gravado de verdade, e aí o defeito deixa de ser de leitura. O post acento não se conserta duas vezes trata desse estrago.
A declaração na página e no servidor
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Cardápio da Padaria São João</title>
</head>
# Apache (VirtualHost ou .htaccess)
AddDefaultCharset UTF-8
# nginx (bloco server)
charset utf-8;
Em PHP, a diretiva default_charset já vem como UTF-8 nas versões atuais, e é ela que entra no cabeçalho das páginas geradas. Se algum código antigo chama header('Content-Type: text/html; charset=iso-8859-1'), ele vence tudo; procure por charset no projeto inteiro.
Quando a entidade HTML ainda é necessária
Com UTF-8 de ponta a ponta, qualquer letra de qualquer idioma pode ser escrita diretamente. Entidades continuam necessárias só para caracteres que o HTML usa como marcação:
<e>para mostrar os sinais de menor e maior como texto;&para o "e comercial";"para aspas dentro de um atributo delimitado por aspas; quando você quer de propósito um espaço que não quebra linha.
Em PHP, ao imprimir texto vindo do usuário ou do banco, use htmlspecialchars($texto, ENT_QUOTES, 'UTF-8'). Ele escapa exatamente esses caracteres reservados e protege contra injeção de HTML. Evite htmlentities para isso: ele também converte cada letra acentuada em entidade, o que é desnecessário em UTF-8 e deixa o código-fonte ilegível. A diferença entre uma coisa e outra está detalhada em entidade HTML existe para caractere especial.
Quando o problema vem do banco
Se o HTML estático aparece certo e só o texto do banco quebra, a página não é a culpada. A conexão precisa declarar UTF-8 (no MySQL, utf8mb4) e as colunas precisam estar nessa codificação. O guia acentos que quebram só quando vêm do MySQL faz esse caminho com calma. Corrigir na tela, com utf8_encode ou substituições, esconde a coluna errada e quebra de novo no próximo relatório.
Erros comuns
- Trocar todas as letras por entidades. O texto fica ilegível no código e o problema real, a declaração, continua lá.
- Colocar a meta charset e ignorar o cabeçalho. O cabeçalho HTTP tem prioridade e a página continua quebrada.
- Meta charset depois de scripts e estilos longos. Fora dos primeiros 1024 bytes, ela pode não ser considerada.
- Converter um arquivo que já era UTF-8. O texto corrompido passa a ser gravado em disco e a correção vira trabalho manual.
- Usar
htmlentitiespara escapar saída. Funciona, mas enche o HTML de entidades sem necessidade. - Remendar na tela o texto que vem do banco. A causa continua na conexão ou na coluna e aparece em outro lugar.
Checklist
- O cabeçalho
Content-Typedizcharset=utf-8(confira comcurl -I). <meta charset="utf-8">é a primeira tag do<head>.- Os arquivos do projeto estão salvos em UTF-8.
- Nenhum código do projeto força outro charset por
header(). - A conexão com o banco e as colunas de texto usam
utf8mb4. - A saída dinâmica passa por
htmlspecialcharscomENT_QUOTESe'UTF-8'. - Entidades aparecem só para caracteres reservados.
Escreva uma frase com "ç", "ã" e aspas numa página do seu site e outra num registro do banco, e abra o código-fonte. As duas estão legíveis no fonte e na tela, ou uma delas mostra "ã" e a outra um losango, dizendo exatamente em qual camada procurar?
Referência: http://www.erikasarti.com/html/acentuacao-caracteres-especiais/