How to Protect Email Addresses from Spam Bots in WordPress (the Modern, Accessible Way)

Date:

Publishing you@example.com in plain text is an open invitation to harvesting bots. The old fix — a document.write script — still works but breaks for screen readers and no-JavaScript visitors. Here’s the current, accessible approach, starting with a function WordPress already ships.

The short answer: don’t publish a plain-text address at all — the best fix is no visible address (a contact form with a honeypot). If you need a clickable one, WordPress core’s antispambot() is the native, accessible choice: it entity-encodes the address so scrapers miss it while screen readers still read it. A CDN-edge obfuscation (Cloudflare’s Scrape Shield) is the easiest site-wide win; CSS reversal and JavaScript reassembly are weaker fallbacks. The goal isn’t a perfect cipher — just to be more trouble than the millions of un-obfuscated addresses next door, without breaking accessibility.

Spam bots crawl the web looking for the name@domain pattern in page source, then flood whatever they find. You can defeat the naive crawlers cheaply — but the goal isn’t a bulletproof cipher (a determined scraper will win); it’s to be more trouble than the millions of un-obfuscated addresses next door, without making the address unusable for real people or invisible to assistive technology.

The methods below are ranked from “best practice” to “last resort.”

The goal isn’t a bulletproof cipher — just more trouble than the address next door.

Four options, strongest firstRanked on one tiebreak: does a screen reader still get the address?
Publish no address

A contact form exposes nothing to scrape. Every option below this one is damage limitation.

antispambot()

WordPress already ships it. Entity-encodes the address, and a screen reader still reads it out correctly.

CSS reversal

Reversed in the markup, restored in CSS. Needs no JavaScript — but it breaks copy and paste for everyone.

Plain mailto:

Published in the clear and harvested within days. Every technique above exists because of this one.

the one to stop doing

Accessibility is the tiebreak: an address a harvester cannot read but a screen reader can is the only good outcome.

Best practice: don’t publish the address at all

The address a bot can’t see is the one it can’t harvest. Two options cover almost every case:

  • Use a contact form instead of a visible address. The email lives server-side in the form config, never in the page. Add a honeypot field (a hidden input real users leave empty; bots fill it, and you reject those submissions) and basic rate-limiting — that stops far more spam than obfuscating a visible address ever will.
  • Route through a role alias (contact@, support@) you can filter or rotate, rather than a personal inbox you can never change once it’s scraped.

If you genuinely need a clickable address on the page, use one of the following.

A plain mailto link rendered in Chromium shows you@example.com, and the HTML the server sent contains the address in full twice, in the href and the link text. A simple regex harvester run over that HTML extracts you@example.com, which is exactly what an address-scraping bot does.

The address a bot can’t see is the one it can’t harvest.

WordPress already has a function for this: antispambot()

Before installing anything, know that WordPress core ships antispambot() — it converts an email into randomized HTML character entities (j etc.) that render as normal text in a browser but don’t match the plain name@domain pattern bots scan for. It’s the native, dependency-free answer.

A small Code Snippet (use a snippet manager, not functions.php) turns it into a reusable shortcode:

add_shortcode('safe_email', function ($atts) {
    $a = shortcode_atts(['address' => '', 'text' => ''], $atts);
    if (!is_email($a['address'])) return '';
    $enc  = antispambot($a['address']);
    $label = $a['text'] ? esc_html($a['text']) : $enc;
    return '<a href="mailto:' . $enc . '">' . $label . '</a>';
});
Code language: PHP (php)

Then write [safe_email address="you@example.com"] in any post. The rendered source contains only entities, the link works normally, and — crucially — it stays fully accessible: screen readers and no-JS visitors get a real, readable, clickable address, because nothing depends on JavaScript.

The WordPress antispambot() shortcode rendered in Chromium: the visitor sees an ordinary clickable you@example.com link, while the HTML the server sent is a mix of literal letters and character entities such as &#64; for the at sign. The same regex harvester finds no address in it.

Nothing here depends on JavaScript, so it stays fully accessible.

CSS reversal — no JavaScript, defeats simple scrapers

Store the address backwards in the markup and flip it visually with CSS, so what a bot reads is reversed nonsense while a human sees it correctly:

<span class="rmail">moc.elpmaxe@uoy</span>Code language: HTML, XML (xml)
.rmail { unicode-bidi: bidi-override; direction: rtl; }Code language: CSS (css)

Cheap and JS-free. Caveat: it’s display-only (not a working mailto: link), and copy-paste yields the reversed text — so it suits “here’s our address” more than a click-to-mail button. It also reads backwards to some screen readers, so pair it with an accessible label if assistive-tech support matters.

CSS reversal rendered in Chromium: the visitor sees you@example.com, but the HTML the server sent stores it backwards as moc.elpmaxe@uoy inside a span flipped with unicode-bidi and direction rtl. The regex harvester finds no address.

JavaScript reassembly — strongest obfuscation, with a fallback

Split the address into parts and assemble the link only in the browser, so it never appears whole in the served HTML:

<a id="c" href="#" data-u="you" data-d="example.com"></a>
<script>
  var a = document.getElementById('c');
  var e = a.dataset.u + '@' + a.dataset.d;
  a.href = 'mailto:' + e; a.textContent = e;
</script>
<noscript>Email: you [at] example [dot] com</noscript>
Code language: HTML, XML (xml)

The whole address is never in the static source, and the <noscript> block gives no-JavaScript visitors a human-readable fallback — the accessibility gap the old document.write method left open. This is the strongest of the on-page methods; the trade-off is that it relies on JavaScript for the clickable link.

JavaScript reassembly rendered in Chromium with JavaScript on: the visitor sees a working you@example.com link, while the HTML the server sent holds only the parts you and example.com in data attributes plus the script. The regex harvester finds no address.

Many versions of this pattern put placeholder text such as “email us” inside the link, as in these renders. With JavaScript off, that link stays on the page as a dead “email us” pointing at #, right next to the <noscript> fallback. That is why the code above leaves the link text empty: the script fills it in, so visitors without JavaScript see only the readable fallback.

The same JavaScript reassembly markup rendered in Chromium with JavaScript off: the noscript fallback reads "Email: you [at] example [dot] com", next to a leftover "email us" link that goes nowhere because the script never ran.

The whole address never appears whole in the served HTML.

The easiest win of all: let your CDN do it

If your site is behind Cloudflare, its Scrape Shield includes automatic email obfuscation — it rewrites addresses in your HTML on the fly, no code required. Turn it on and every address on the site is obfuscated at the edge. Many managed hosts offer an equivalent. This is often the highest value-for-effort option — a CDN in front of your site tends to earn its keep several ways at once.

The short version

Layer it, matched to the address’s exposure:

  1. Prefer a contact form + honeypot wherever a form fits — the address stays off the page entirely.
  2. For a visible, clickable address, use the antispambot() shortcode (accessible, native, no dependency), or Cloudflare’s automatic obfuscation if you’re behind it.
  3. Reserve JavaScript reassembly for a high-value address you want hidden from the static HTML, always with a <noscript> fallback.
  4. Treat CSS reversal as a display-only nicety, not your main defense.

No method is permanent — rotate a role alias if an address does start attracting spam, and lean on your mail provider’s spam filtering as the backstop. The point is to stop being the easy target — the same bot-hygiene mindset as making sure your maintenance page answers 503, not 200.

Frequently Asked Questions

Does email obfuscation actually stop spam?

It stops the cheap, high-volume harvesters that scan page source for the plain name@domain pattern — which is most of them. It won’t stop a determined scraper that renders JavaScript, so treat it as one layer alongside a contact form, a honeypot, and your mail provider’s spam filter rather than a complete fix.

Do I need a plugin to obfuscate an email in WordPress?

No. WordPress core includes antispambot(), which encodes an address into HTML entities. A few lines in a Code Snippet turn it into a reusable shortcode — no plugin required. If your site is behind Cloudflare, its automatic email obfuscation needs no code at all.

Is obfuscating an email bad for accessibility?

It can be, if done carelessly. The old document.write method leaves no-JavaScript users and some screen readers with nothing. Prefer the antispambot() entity method (fully readable and clickable with no JS), or if you use JavaScript reassembly, always include a <noscript> fallback with a human-readable address.

Should I just use a contact form instead?

Usually, yes. A contact form keeps the address off the page entirely, and adding a honeypot field stops far more spam than obfuscating a visible address. Use a displayed address only when people genuinely need to mail you directly.

Share post:

Subscribe

Popular

More like this
Related

How to Merge Spreadsheets by URL (and Why Our Tool Reported 5 Misses That Weren’t)

Matching two spreadsheets on a URL column is the same job as matching a stock list to a price list by product code. It goes wrong in the same two places: how you clean the code, and how you count what didn't match.

Many-to-One: Why a Mapping Backlog Outgrows Its Target Taxonomy

You're mapping a messy source vocabulary onto a clean target taxonomy. The target has about 5,600 slots, so the job must be roughly 5,600 decisions — right? The backlog is already past 8,900, and still climbing.

The Fix That Didn’t Take: When a Content-Hash Gate Skips Your Code Change

You shipped a fix to how one field is computed, re-ran the pipeline across everything, and it reported a clean run. Then the consistency check still flags those records as wrong. The fix is in the code, the rebuild "ran" — and the records never changed. Your own optimisation skipped them.

Your Dedup Scan Is Over-Counting — Because It Forgot What You Already Fixed

The report said 80 duplicate groups still to resolve. You'd been resolving them for days. You run it again after a big batch and it *still* says 80-ish. The backlog isn't stubborn — the scan is counting work you already finished.