Encoding HTML for Email Templates: Entities, Special Characters, Links and Quoted-Printable

HTML email is written in the same markup as a web page but delivered through a 40-year-old transport and rendered by dozens of different clients, from Gmail and Apple Mail to the Word-based engine in Outlook for Windows. Characters that display fine on a website can arrive as `’`, `=3D` or a stray question mark in an inbox. Some of those bugs are in the template, others in the transfer encoding underneath it, and the fix depends on which layer broke.

Declare UTF-8, then type most characters directly

The single most important step is declaring the character set correctly, in the MIME headers of the message and in the HTML itself. Email service providers set the header for you. Check it when you send through your own code.

Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

With UTF-8 declared, you can type é, €, — and curly quotes directly into the template. The classic mojibake, such as ’ shown as ’, means UTF-8 bytes were read as Windows-1252, almost always because the charset was missing or wrong somewhere in the chain.

Some teams still write every non-ASCII character as a numeric entity (&#8217;) to survive broken pipelines, for example templates pasted through systems that re-encode text. It is a valid belt-and-braces approach for transactional templates maintained by developers; for marketing copy edited in an ESP, fix the charset instead.

Always escape the five markup characters in data

Anything inserted into the template at send time, such as a customer's name, an order note or a product title, must be HTML-escaped, exactly as on a web page. & becomes &amp;, < becomes &lt;, > becomes &gt;, " becomes &quot; and ' becomes &#39;.

// JavaScript (Node) — escape before inserting into an HTML email
const escapeHtml = (s) => String(s)
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  .replace(/"/g, '&quot;').replace(/'/g, '&#39;');

const html = `<p>Hi ${escapeHtml(user.firstName)},</p>`;

Webmail clients sanitise scripts, so the risk is less about JavaScript and more about content injection. A "name" such as <a href="https://evil.example">Click to verify your account</a> turns your trusted transactional email into a phishing link. Use your template engine's escaping, such as Handlebars {{ }}, Blade {{ }} or Jinja autoescape, and treat raw-output syntax ({{{ }}}, {!! !!}, |safe) as a code-review red flag.

Ampersands in link URLs

Query strings in links contain &, and inside an HTML attribute it should be written &amp;. The browser or mail client decodes it back to & before following the link.

<a href="https://example.com/offer?utm_source=newsletter&amp;utm_medium=email&amp;utm_campaign=sept">

Most clients tolerate a bare &, but some tracking-link rewriters and older clients cut the URL at &utm_… or mangle it when the text happens to look like a named entity, such as &copy= or &reg=. Values inside the URL must still be percent-encoded first (encodeURIComponent), then the whole URL HTML-escaped. That is two different encodings, applied in that order.

Non-breaking spaces, zero-width joiners and the preheader

&nbsp; keeps words together (10&nbsp;%, Mr&nbsp;Smith) and gives empty table cells a height in Outlook. Avoid long runs of &nbsp; for layout, which break on narrow screens.

The preheader, the grey preview text shown after the subject in the inbox, is taken from the first text in the email. To stop the client pulling in navigation or "View in browser" after your preheader, many templates pad it with a hidden sequence of zero-width non-joiners and non-breaking spaces:

<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;">
  Your order has shipped — track it here.
  &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;
</div>

Keep the padding modest. Some spam filters and accessibility tools treat very long runs of invisible characters as suspicious, and screen readers may announce them.

Below the HTML: quoted-printable and the =3D bug

Email lines were historically limited in length and to 7-bit ASCII, so the HTML body is usually sent with Content-Transfer-Encoding: quoted-printable. In that encoding, lines are wrapped at 76 characters with a soft break = at the end, = itself becomes =3D, and non-ASCII bytes become =C3=A9 and similar.

<a href=3D"https://example.com/very/long/path?utm_source=3Dnews=
letter">      <- quoted-printable on the wire, correct

<a href=3D"https://...">   <- shown in the email: the body was NOT decoded

If recipients see =3D, =20 or lines ending in = in the rendered email, something decoded the message twice or not at all, usually a custom mailer that set the Content-Transfer-Encoding header but did not encode the body, or the reverse. Let your mail library handle transfer encoding, such as Nodemailer, Symfony Mailer or Python's email package, and never write =3D into a template by hand.

Subject lines and sender names

Headers such as Subject and the display name in From cannot contain raw non-ASCII bytes. RFC 2047 encoded words wrap them, and mail libraries generate these automatically when given a normal Unicode string.

Subject: =?UTF-8?B?Q2Fmw6kgbWVudSDimJU=?=      (Base64: "Café menu ☕")
Subject: =?UTF-8?Q?Caf=C3=A9_menu?=           (Q-encoding: "Café menu")

HTML entities do not work in subject lines: &amp; appears literally. Emoji in subjects are supported by most modern clients, but the older Outlook for Windows may show them as boxes, so test before relying on them.

Sources and further reading

RFC 2045 defines MIME and quoted-printable, and RFC 2047 defines encoded words for non-ASCII header text. The WHATWG HTML Standard defines character references and attribute parsing. The OWASP Cross Site Scripting Prevention Cheat Sheet covers output encoding, and Can I Email (caniemail.com) documents HTML and CSS support across email clients.

Frequently asked questions

Do I need HTML entities in email templates?

Only for the markup characters `& < > " '` in dynamic content, and optionally for invisible characters like `&nbsp;`. With UTF-8 declared, accented letters and symbols can be typed directly.

Why does my email show ’ instead of an apostrophe?

UTF-8 text was read as Windows-1252 because the charset was missing or wrong. Declare `charset=UTF-8` in the `Content-Type` header and in a meta tag, or encode the character as `&#8217;`.

Should & in email links be written as &amp;?

Yes, inside the `href` attribute. The mail client decodes `&amp;` back to `&` before opening the link, and it avoids mangled URLs in stricter clients and link rewriters.

Why do I see =3D in my HTML email?

`=3D` is the quoted-printable encoding of `=`. It appears when the body was encoded but not decoded, or the transfer-encoding header does not match the body. Let your mail library handle the encoding.

How do I escape user data in an HTML email?

Use your template engine's default escaping, or a function that replaces `&`, `<`, `>`, `"` and `'` with entities. Never output customer-supplied text raw, even in internal emails.

Can I use HTML entities in the subject line?

No. Subject lines are plain text headers, so `&amp;` shows literally. Use normal Unicode text and let the mail library encode it per RFC 2047.