Which characters need encoding, and where
Only a handful of characters are structurally significant in HTML: the angle brackets that delimit tags, the ampersand that begins an entity, and the quote characters that terminate attribute values. Encoding these prevents text being read as markup.
The rest is optional. Named entities such as or © were essential when documents were served in single-byte encodings, but with UTF-8 — now effectively universal — you can write the characters directly. A document declaring a UTF-8 charset handles accented letters, symbols and emoji without any encoding at all.
The exception worth keeping is characters that are invisible or easily confused: a non-breaking space looks identical to a normal space in source, and a zero-width character is invisible entirely. Writing those as entities makes them visible to whoever edits the file next, which is a real maintenance benefit.
Escaping is contextual, and this is where XSS comes from
The critical thing to understand is that there is no single correct escaping. What is safe depends on where the value lands, and applying HTML escaping everywhere is a widespread and dangerous simplification.
In HTML body text, escaping the structurally significant characters is sufficient. In an unquoted attribute it is not, because a value containing a space can introduce a new attribute — which is why attributes must always be quoted and the quote character escaped. Inside a script block, HTML escaping does nothing useful and JavaScript string escaping is required. In a URL, percent-encoding is required, and HTML-escaping a URL leaves an injection open. Inside a CSS context, yet another set of rules applies.
The reliable approach is not to hand-escape at all. Use a templating engine that escapes by default and knows its context, set text through textContent rather than innerHTML, and treat any use of an explicit "raw" or "unsafe" helper as something requiring justification. Where untrusted HTML genuinely must be rendered, sanitise it with an allowlist-based library rather than filtering with regular expressions.
Encoding problems that are not escaping problems
Mojibake — text appearing as strings of accented nonsense — is a character encoding mismatch rather than an escaping failure. It happens when UTF-8 bytes are decoded as a single-byte encoding such as Windows-1252, so a two-byte sequence is read as two separate characters. The fix is to declare UTF-8 consistently in the document, the HTTP Content-Type header and the database connection, since a mismatch anywhere in that chain produces it.
Double encoding is the related failure: text escaped twice, so an escaped ampersand is itself escaped again and the entity becomes visible on the page. It comes from escaping at more than one layer — typically once when storing and once when rendering. Escape at output only, and store raw values, which also keeps the data usable for searching and comparison.
Numeric references are worth knowing as a fallback: — and — both denote an em dash. Every character has one, whereas named entities exist only for a subset, so numeric form works where no name is defined.