Fixing Character Encoding Errors
Computers store text as numbers, and an encoding is the table that turns those numbers back into letters. When the page is written with one table and read with another, a pound sign becomes £, a curly apostrophe becomes ’ and accented names fall apart.
The HTML Checker reports the causes before visitors see them: no declared encoding, a declaration placed too late, a server header that disagrees with the page, or a legacy encoding instead of UTF-8. This guide shows how to read the symptoms, find the layer that is wrong and repair text already damaged.
Reading the symptoms
The garbled characters are not random. Each pattern points to a particular mismatch, so it is worth looking closely before changing anything.
| What visitors see | What it should say | What happened |
|---|---|---|
£, ©, a stray  before spaces | £,
©, a non-breaking space | UTF-8 text read as windows-1252 or ISO-8859-1. Two-byte characters show as two symbols. |
’, “, — | ’, “,
— | The same mismatch for three-byte characters such as curly quotes and dashes. |
é, è, ü | é, è, ü | Accented letters in UTF-8 read as a single-byte encoding. |
 and longer chains | One character | Double encoding: text was misread once, saved, then converted to UTF-8 again. |
� (a diamond with a question mark) | Any accented or special character | The reverse: windows-1252 text read as UTF-8. The byte is not valid UTF-8, so the browser shows the replacement character. |
? in place of letters or emoji | The original character | The character was lost when it was saved, often by a database or export using an encoding that cannot hold it. |
The first five are display problems: the right bytes exist somewhere and are being misread, so fixing the declaration may be enough. The last one is data loss. Question marks saved into a database cannot be turned back into the original letters; you need a backup or the original source.
How a browser decides the encoding
A browser has to choose an encoding before it can read the page, so it checks several places in a fixed order and uses the first one it finds:
- A byte order mark. If the file starts with the UTF-8 BOM, the browser uses UTF-8 and ignores everything else. A BOM is allowed in HTML, including before the doctype.
- The HTTP header. A
charsetparameter on theContent-Typeresponse header, for exampletext/html; charset=utf-8. - The meta tag.
<meta charset="utf-8">, which the browser looks for by pre-scanning the start of the document. - A guess. If none of these exist, the browser guesses from the content, the user's locale and other signals. In the UK that guess is often windows-1252.
This order explains the most confusing situations. A page can have a perfect meta tag and still display mojibake, because the server's header says ISO-8859-1 and the header wins. A file can declare windows-1252 in its meta tag and display correctly as UTF-8, because an editor added a BOM. The fix is to make all three agree, not to adjust whichever one you happen to find first.
The 1024-byte rule
The browser's pre-scan for the meta tag only reads the first 1024 bytes of the document. If the charset declaration comes after a long block of inline CSS, a large comment, a set of preload links or a tracking snippet, the browser may never see it and falls back to guessing. The HTML specification requires the declaration to sit entirely within those first 1024 bytes, which is why the HTML Checker flags a late declaration even when the value is right.
The checker messages and what to do about each
| Message | Meaning | Fix |
|---|---|---|
| The character encoding was not declared | No BOM, no charset in the header and no meta tag. The checker, like a browser, has to guess. | Add <meta charset="utf-8">
as the first element in the head and set the header. |
| A charset attribute on a meta element found after the first 1024 bytes | The declaration exists but is too far down the page. | Move it directly after the opening <head>
tag. |
| Legacy encoding windows-1252 used. Documents must use UTF-8 | The page is valid in its own encoding, but HTML now requires UTF-8. | Convert the files and data to UTF-8, then change the declarations. |
| Internal encoding declaration disagrees with the actual encoding of the document | The meta tag names one encoding, but the header or BOM made the page use another. | Decide on UTF-8 and correct whichever layer is wrong. |
| Malformed byte sequence | The page is declared UTF-8 but contains bytes that are not valid UTF-8. | Find the file, include or database field holding legacy-encoded text and convert it. |
| A meta element with http-equiv content-type and a charset attribute both present | Two declarations in the head. | Keep only the short meta charset form. |
The long form <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
is still valid, but it does exactly the same job as the short form. Old themes often carry both, sometimes
with different values. Delete the long one.
The fix, step by step
Step 1
Identify the pattern of broken characters
Before you change anything, collect examples of the broken text and where they appear.
Compare them with the symptoms table: sequences starting with  or à mean UTF-8
is being read as a legacy encoding, the diamond replacement character means the opposite, and plain question
marks mean the characters were lost when saved. Note whether the problem affects the whole site, one
template, database content only or a single old page.
Step 2
Check the header, the meta tag and the BOM
Run curl -sI against the page and read the Content-Type
line, then view the source and find the meta charset tag. Finally check the first bytes of the response
for a byte order mark. Write down all three values. Any disagreement between them, or a missing value,
is a cause; remember that a BOM overrides the header, and the header overrides the meta tag.
Step 3
Declare UTF-8 in the first 1024 bytes
In the base layout or header template, make <meta charset="utf-8"> the
very first element inside <head>, before the title, inline styles, comments, preload
hints and analytics code. Remove any older http-equiv content type tag and any second charset
tag added by a plugin. Placing it first guarantees the browser finds it during its pre-scan, whatever
else is later added to the head.
Step 4
Make the server send charset=utf-8
Configure
the web server or application so HTML responses carry Content-Type: text/html; charset=utf-8.
On Apache that is AddDefaultCharset UTF-8; on nginx it is charset utf-8; in
the server block; in PHP set default_charset to UTF-8. Also check CDN or proxy rules that
rewrite headers. Test again with curl to confirm the header changed on the live address, not just locally.
Step 5
Convert template and content files to UTF-8
Find files saved in a legacy encoding with file -i on the server, or by
opening them in an editor that shows the encoding in its status bar. Convert them with the editor's save
with encoding option or with iconv -f WINDOWS-1252 -t UTF-8, writing to a new file and checking
it before replacing the original. Include CSV imports, translation files and old static pages, which
are often overlooked.
Step 6
Align the database connection and tables
In MySQL or MariaDB, check the table and column character sets and the connection character
set. Use utf8mb4 rather than the older utf8, which cannot store four-byte characters
such as emoji. Set the connection to utf8mb4 in your application, for example the DB_CHARSET
constant in WordPress or the charset in a PDO connection string, because a mismatched connection garbles
text on the way in and out.
Step 7
Repair text that was stored damaged
Once
new text is saved correctly, fix old records that already contain mojibake. For a small number, edit
them by hand. For many, run a search and replace on a copy of the database, mapping sequences such as
’ back to the right character, or convert double-encoded columns in a controlled way.
Always take a backup first, test on staging, and rerun the HTML Checker on affected pages.
Server and application settings
The header is the layer most often forgotten, because it is invisible in the page source. These are the usual places to set it.
# Apache (.htaccess or virtual host)
AddDefaultCharset UTF-8
AddCharset UTF-8 .html .css .js
# nginx (server block)
charset utf-8;
charset_types text/html text/css application/javascript;
# PHP (php.ini)
default_charset = "UTF-8"
# Check the result
curl -sI https://www.example.co.uk/ | grep -i content-type
content-type: text/html; charset=utf-8
Stylesheets and scripts can have encoding problems too. A CSS file containing a pound sign in a content
property, or a script containing an accented string, needs to be served as UTF-8 or begin with @charset "UTF-8";
in the case of CSS. JSON responses are UTF-8 by definition.
Frameworks and platforms
- WordPress stores the site encoding under Settings, Reading on older installs and
in
wp-config.phpasDB_CHARSET. New installs use UTF-8 and utf8mb4 by default; migrated sites from long ago may not. - Shopify serves pages as UTF-8. Encoding problems there almost always come from content imported from a spreadsheet saved in a legacy encoding, so re-export the CSV as UTF-8 and import again.
- Node, Python and PHP frameworks generally default to UTF-8, but check any code that reads files or calls external systems, and state the encoding explicitly when opening files.
- Static site generators read source files as UTF-8. A single Markdown file saved in windows-1252 by an older editor can produce the malformed byte sequence error on one page only.
The byte order mark: allowed, but watch PHP
A UTF-8 byte order mark is three bytes at the start of a file that announce it as UTF-8. HTML allows it, and browsers treat it as the strongest signal of all. For a static HTML file it is harmless and can even help.
The problem is with server-side includes. PHP sends everything outside its tags as output, and a BOM
counts. If functions.php, wp-config.php or a language file starts with a BOM,
those three bytes are sent before the page, which leads to the familiar "headers already sent" warning,
broken redirects and cookies that fail to set. Because the bytes are invisible, the page source just
looks as if something strange is going on at the top.
# List PHP files in a theme that start with a UTF-8 BOM
grep -rlI $'^\xEF\xBB\xBF' --include=*.php wp-content/themes/larchfield-child/
Most code editors have an option to save as "UTF-8" rather than "UTF-8 with BOM". Use that for code files, and re-save any file the command above finds.
Edge cases worth knowing
- HTML entities hide the problem. Writing
£or’instead of the character works in any encoding, which is why some old pages look fine. It does not fix the underlying mismatch, and content added later in plain characters will still break. - Emails and exports. Order confirmation emails, feeds and CSV downloads have their own encoding settings. A product feed with mojibake can be rejected or shown wrongly by the service reading it.
- Copied text from word processors. Pasting from a word processor brings curly quotes and non-breaking spaces. They are fine in UTF-8 but are the first characters to break when something in the chain is wrong, which makes them useful test cases.
- Search snippets. If a search engine crawled the page while it was broken, the mojibake can appear in the snippet until the page is crawled again. Request indexing in Search Console after the fix if the page is important.
- Double conversion. Running a conversion on files that are already UTF-8 damages them. Always check the current encoding first, and convert a copy.
Preventing it
- UTF-8 everywhere. Editor defaults, file storage, server headers, database tables, database connection and the meta tag should all say UTF-8, with utf8mb4 in MySQL.
- Charset first in the head. Treat the meta charset line as fixed, with nothing allowed above it inside the head.
- Test with awkward text. Keep a test page or product containing £, €, curly quotes, an em dash, an accented name and an emoji. If any of them breaks after a change, you know immediately.
- Check imports. Ask suppliers for UTF-8 CSV files, and check a sample of imported records before publishing.
1. Why does my pound sign show as £?
Your text is stored as UTF-8, where the pound sign takes two bytes, but the browser is reading it with a single-byte encoding such as windows-1252, so each byte becomes its own character. The first one displays as Â. Check the Content-Type header your server sends and the meta charset tag in your head. Usually one of them is missing or names ISO-8859-1. Make both say UTF-8 and the pound sign will display correctly without changing the text.
2. What causes the black diamond question mark symbol?
The diamond with a question mark is the Unicode replacement character. Browsers show it when a page is declared as UTF-8 but contains bytes that are not valid UTF-8, typically text saved in windows-1252 by an old editor, a legacy include file or content imported from a spreadsheet. The fix is to convert that file or data to UTF-8 rather than to change the page declaration, because the rest of the site is correctly UTF-8 and would break if you switched.
3. Does the meta charset have to be the first thing in the head?
Strictly, the specification requires it to fall within the first 1024 bytes of the document, and it must come before any text that uses special characters, such as the title. Putting it first inside the head is the simplest way to guarantee both, whatever scripts, styles and comments get added later. If a plugin or tag manager injects large blocks above it, the browser may miss it during its pre-scan and guess the encoding instead.
4. Is a byte order mark before the doctype an error?
No. HTML permits a UTF-8 byte order mark at the very start of the document, before the doctype, and browsers use it to identify the encoding with the highest priority. It is not needed if your server sends the correct header and your meta tag is in place. The only real problem is in PHP files and other server-side includes, where the invisible bytes are sent as output and can trigger headers already sent warnings.
5. Should I use utf8 or utf8mb4 in MySQL?
Use utf8mb4. In MySQL, the older utf8 character set only stores characters up to three bytes long, so emoji and some rarer characters cannot be saved and are lost or rejected. utf8mb4 is full UTF-8. Convert both the tables and columns and make sure the application connects using utf8mb4, because a correct table with a mismatched connection still garbles text. Back up the database first and test the conversion on a staging copy.
6. Why is only some of my text garbled?
Because different parts of the page come from different places with different encodings. If template text is fine but product descriptions or blog posts are broken, the database or the connection to it is the likely cause. If one old page is broken, that file was probably saved in a legacy encoding. If a single widget is wrong, its content comes from an external source. Trace the broken text to where it is stored and fix that layer.
7. Will fixing the encoding fix text already saved as question marks?
No. When a character is replaced by a question mark during saving, the original information is gone, and no encoding change can bring it back. That is different from mojibake such as £ or ’, where the right bytes still exist and can be reinterpreted or mapped back. For question marks, restore the text from a backup, the original source document or the supplier file, after fixing the layer that lost it in the first place.
The HTML Checker validates your markup against W3C standards and reports errors and warnings covering the doctype, character encoding, tag nesting, ARIA, forms and the language attribute, so you can fix each template at its source.
Open HTML Checker Fixes →