Obfuscating Email Addresses with Eleventy
I recently updated the About page here and wanted a way for people to email me without using a form. This site doesn’t currently have a backend to handle one and I didn’t want to introduce a 3rd-party dependency, so I decided to look at a classic mailto link with my email address.
But what about spammers and email harvesters? Sure, it’s 2026 and spam filters are pretty good, but also it’s 2026 and those harvesters are probably really good at getting around the ancient rites used to protect email addresses. Well, it turns out that apparently they still aren’t very good at getting past various obfuscation methods!
According to Spencer Mortensen, harvesters still struggle with old-school, easy-to-implement methods of obfuscating email addresses in ways that are usable to actual people. When I implemented this, the stats on that page were from April 2026 so there has been plenty of time for LLMs (and whatever their content stealing methods are) to use more advanced tools like a browser through Puppeteer. I’m sure the address is still making its way into LLM datasets though, but that isn’t causing spam (yet?).
I decided to write a shortcode for Eleventy (uh, now Build Awesome) so I could drop in an email address wherever I needed without worrying about it getting hammered with spam. Out of the available methods Spencer lists, I wasn’t willing to risk usability and wanted it to be simple to implement. I used a combination of methods for the displayed text and mailto href attribute to make it clickable.
For the displayed email address:
- HTML entity encoding
- An HTML comment to split text in source
- Visually hiding a fake subdomain using CSS
For the href attribute:
- HTML entity encoding
The shortcode takes a few parameters. Firstly, and required, is the email address to protect. Optional parameters are the text to display and a boolean to not generate some CSS to hide the fake subdomain. Here are some example usages:
Note that I’m using Nunjucks as my templating language.
{% email "me@example.com" %}
{% email "me@example.com", "Email me!" %}
{% email "me@example.com", "no-fake-subdomain@example.com", true %}
The output is a an HTML link with the email address in the href attribute encoded to HTML entities (the ones that look like Ӓ). If the text wrapped by the link is an email address (for simplicity, includes an @), it gets a fake email. subdomain and an HTML comment with another fake domain part added, and the whole thing is encoded. CSS is automatically generated to hide the fake subdomain from view. This last part is optional if you want to handle it yourself for some reason. Here’s what the first example above would render:
<a class="email-enc" href="mailto:me@example.com">me@<span>email.</span>example<!--.totallyreal-->.com</a><style type="text/css">a.email-enc span { display: none; }</style>
To use this yourself, create a file for the shortcode function and import it into your Eleventy config: eleventyConfig.addShortcode("email", emailObfuscator);
As a final note, I added my email address to the about page in April 2026 and didn’t get around to writing this blog post until October. I’m not seeing any additional spam to the address.
Here is the full code I’m using, and good luck avoiding spam!
/**
* Build a `mailto:` link in a way that protects the email address from spammers.
* @param {string} email The email address itself
* @param {string=} text The text of the link. If empty, the email will be used instead.
* @param {boolean=} forceNoCss If `text` is an email address, do not add CSS to hide the fake `<span>`. You will need to manage this yourself.
* @returns {string} HTML string
* @example {% email "me@example.com" %}
* @example {% email "me+site@example.com", "me@example.com" %}
* @example {% email "me@example.com", "contact me" %}
* @example {% email "me@example.com" "", true %}
* @example {% email "me+site@example.com", "me@example.com", true %}
* @see https://spencermortensen.com/articles/email-obfuscation/#text-display
*/
export default function emailObfuscator(email, text = "", forceNoCss = false) {
email = email.trim();
text = text.trim();
text = text || email;
let useCss = false;
const emailEncoded = encode("mailto:" + email);
let textEncoded = text;
if (text.includes("@")) {
const parts = text.split("@");
const domainTld = parts[1].split(/(\..+?$)/);
textEncoded =
encode(parts[0] + "@") +
"<span>email.</span>" +
encode(domainTld[0]) +
"<!--.totallyreal-->" +
encode(domainTld[1]);
useCss = !forceNoCss;
}
return `<a class="email-enc" href="${emailEncoded}">${textEncoded}</a>${
useCss
? '<style type="text/css">a.email-enc span { display: none; }</style>'
: ""
}`;
}
/**
* Convert a string to HTML entities
* @param {string} str
* @returns {string} HTML entities representation of `str`
*/
function encode(str) {
return str.replace(/./g, i => "&#" + i.charCodeAt(0) + ";");
}