mailhider

A ~1 KB JavaScript library that hides email addresses in your HTML source so scrapers can’t extract them, while still rendering a working mailto: link for humans. Below are the three modes, running live on this page.

GitHub  ·  npm  ·  npm i mailhider

1. Baseline — a plain mailto: link scrapeable

The control. A normal contact link, exactly the kind a scraper will harvest.

Renders as: plain@example.com
In HTML source: <a href="mailto:plain@example.com">plain@example.com</a>
Scanning HTML source for emails…

2. Reverse mode default

The username and domain are stored reversed in data-* attributes. A tiny runtime reverses them on page load and writes a real mailto: anchor. A regex over the HTML source finds nothing.

Renders as:
In HTML source:
<span class="mh-email" data-u="olleh" data-d="moc.elpmaxe">
  <noscript>Enable JavaScript to view email.</noscript>
</span>
Scanning HTML source for emails…

3. Click-to-reveal mode defeats headless browsers

The email isn’t assembled until the visitor clicks. This defeats non-interactive JS-running scrapers (headless Chrome, Playwright) because they don’t simulate clicks on every span on every page they crawl. A scraper that does click every span on every page can still extract it.

Try it: Show email
In HTML source:
<span class="mh-email" data-u="kcilc" data-d="moc.elpmaxe" data-mh-mode="click">
  Show email
</span>
Scanning HTML source for emails…

4. Bracket fallback weakest

For visitors with JavaScript disabled. The text reads user[at]domain[dot]tld. Scrapers that match [at]/[dot] patterns will still extract this, so it’s only worth using if no-JS reachability matters to you.

Renders as: info[at]example[dot]com
Scanning HTML source for emails…

What just happened

This page ran a regex over its own HTML source (the bytes the server sent — not the DOM after JavaScript decoded it) and looked for any string matching an email. The results above show what a typical scraper sees.

The reverse and click modes leak nothing. The baseline link and the bracket fallback leak. Pick your trade-off.

Use it

Three ways, depending on whether you have a build step.

No build — drop into any HTML page:

<span class="mh-email" data-u="olleh" data-d="moc.elpmaxe"></span>
<script src="https://unpkg.com/mailhider/dist/browser.js"></script>

Build-time encoding (Node):

import { obfuscate } from 'mailhider';
const html = obfuscate('hello@example.com', { mode: 'click' });

CLI:

npx mailhider hello@example.com --mode=click