’ and é all over the page
The page renders, but don’t shows as don’t, café as café, or
every accented letter has turned into �. Nothing is wrong with the
text. Two programs disagreed about which encoding it was in, and the
pattern of garbage tells you which two.
Decode the garbage
| You see | It was | What happened |
|---|---|---|
’ |
’ |
UTF-8 file read as Windows-1252 |
“ / †|
“ ” |
UTF-8 read as Windows-1252 |
– / — |
– — |
UTF-8 read as Windows-1252 |
é ü ë |
é ü ë |
UTF-8 read as Windows-1252 |
 or £ |
non-breaking space, £ |
UTF-8 read as Windows-1252 |
€ |
€ |
UTF-8 read as Windows-1252 |
😀 |
an emoji | UTF-8 read as Windows-1252 |
é |
é |
Mis-decoded twice |
� |
anything non-ASCII | Windows-1252 file read as UTF-8 |
Two families, opposite fixes:
Ãandâ€sequences — the bytes are fine UTF-8. The reader guessed wrong. Fix the declaration.�— the bytes are not UTF-8 at all. Declaring UTF-8 harder changes nothing. Fix the file.
The full mechanism is on the mojibake glossary page.
How the browser picks an encoding
In order, first match wins:
- A byte order mark at the start of the file (BOM).
- The
charsetin the HTTPContent-Typeheader. - A
<meta charset>in the first 1024 bytes. - A guess — from the content and the browser's locale. On an English-language system that guess is often Windows-1252.
A UTF-8 file with no BOM, opened from disk or served without a charset,
and no meta tag, lands on step 4. That is where almost every ’ comes
from.
Fix: declare UTF-8
Make this the first element inside <head>, before the <title> and
before any long inline <style> that could push it past 1024 bytes:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Q3 report</title>
</head>
</html>
If a server is involved, the header must agree, because it beats the meta tag:
Content-Type: text/html; charset=utf-8
On S3, that is the object's metadata (--content-type "text/html; charset=utf-8" on upload). In nginx, charset utf-8; in
the server block. In Express, res.type("html") already adds it.
Fix: write the file as UTF-8
If you see �, the file was written in a legacy encoding. The usual
culprit is code that writes text without naming one:
- Python:
open(path, "w")uses the locale encoding on Windows. Always passencoding="utf-8". - Java before 18:
FileWriteruses the platform default. PassStandardCharsets.UTF_8. - Windows PowerShell 5.1:
>andOut-Filewrite UTF-16. UseOut-File -Encoding utf8orSet-Content -Encoding utf8. - Editors: check the encoding in the status bar. VS Code's "Reopen with Encoding" shows you what the bytes really are; "Save with Encoding → UTF-8" converts them.
Then add the meta tag, so the next reader doesn't have to guess.
Fix: text that is already garbled
If the garbage was saved — copied into a database, a CMS, another report
— fixing the header won't help; the é is now real characters. One
round of UTF-8-read-as-Windows-1252 reverses cleanly:
fixed = broken.encode("cp1252").decode("utf-8")
For mixed or multiple rounds (é), the ftfy library's
fix_text() detects the chain and undoes it. Text with � cannot be
recovered — the original byte was thrown away at decode time.
Where it shows up in reports
- Double-clicking a generated HTML file. No server, no header — the
file's own
<meta charset>is the only signal. Generators that emit UTF-8 without one rely on the browser guessing right. - A CSV opened in Excel. Same bug, different program: CSV opens wrong in Excel.
- Copy-paste from Word or Outlook. Smart quotes and em dashes are
the first characters outside ASCII most text contains, which is why
’is the face of this bug. - An S3 or static host serving
text/htmlwith no charset — the file's meta tag decides, if it has one.
Publish it once, encoded once
Comma reads uploaded HTML as UTF-8 and serves every report with
charset=utf-8, so a UTF-8 file renders the same for every reader on
every machine — no locale guess involved. If an old export shows �
after upload, it was saved in Windows-1252: re-save it as UTF-8 and
update the report at the same link.
Readers can then select the sentence that still looks wrong and comment
on it (comment on HTML), instead of sending a
screenshot of ’.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history.
Preview your HTML with the right encoding →