’ 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:

  1. A byte order mark at the start of the file (BOM).
  2. The charset in the HTTP Content-Type header.
  3. A <meta charset> in the first 1024 bytes.
  4. 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 pass encoding="utf-8".
  • Java before 18: FileWriter uses the platform default. Pass StandardCharsets.UTF_8.
  • Windows PowerShell 5.1: > and Out-File write UTF-16. Use Out-File -Encoding utf8 or Set-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/html with 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 →

Related