← Home

Obfuscating Contact Data on a Static Site

German law (§ 5 TMG) requires a publicly accessible imprint with a real name, address, and working email address. That’s a gift to spam harvesters: a legal obligation to publish exactly the data they’re looking for.

This post covers the approach I settled on — combining two independent techniques to protect that data on a static site without sacrificing usability.

The Problem

A static site has no server-side rendering to help. The HTML is delivered as-is. Any email address that appears in the source can be found by a simple regex scan.

The most common approach — <a href="mailto:hey@example.com"> — is essentially a harvester’s dream. No JavaScript required, no interaction needed.

First Layer: Base64 + JavaScript

The first technique moves the actual contact data out of the HTML source entirely. Instead of rendering the text directly, the element is left empty and a data-obf attribute carries the content as Base64:

<span data-obf="cmVudGxBIG5haXJkQQ=="></span>

A small script decodes and injects it at runtime:

document.querySelectorAll('[data-obf]').forEach(el => {
  el.textContent = atob(el.dataset.obf);
});

For links, data-obf-href works the same way — the mailto: is never in the HTML:

<a data-obf-href="bWFpbHRvOmhleUBleGFtcGxlLmNvbQ=="></a>

This stops static HTML scrapers cold. The source contains only Base64, and the decoded text only exists in the DOM after JavaScript runs.

What it doesn’t protect: copy-paste. Once the script runs and the real text lands in the DOM, selecting and copying gives the actual address.

Second Layer: CSS RTL Reversal

The second technique addresses copy-paste. The idea comes from a common trick used by privacy-focused blogs: store the text reversed in the DOM, then use CSS to flip it back visually.

.r {
  direction: rtl;
  unicode-bidi: bidi-override;
}

With unicode-bidi: bidi-override, the browser renders characters strictly right-to-left. So the string rentlA nairdA stored in the DOM displays as Adrian Altner. When a visitor copies the text, they get rentlA nairdA — not the real name.

Combining Both

The two techniques compose cleanly. The data-obf attribute stores a Base64-encoded reversed string. JavaScript decodes it and writes the reversed text into the DOM. CSS then renders it visually correct.

<span class="r" data-obf="cmVudGxBIG5haXJkQQ=="></span>

The flow:

  1. Static HTML: empty element, opaque Base64 attribute
  2. After JS: rentlA nairdA in the DOM
  3. After CSS: displays as Adrian Altner
  4. On copy-paste: rentlA nairdA lands in the clipboard

A static scraper sees nothing. A JS-capable scraper sees reversed text. A human copying sees the reversed string.

Generating the Reversed Strings

For plain text, reversing is straightforward. For email addresses, the @ and . need placeholder substitutions so they survive the round-trip legibly. The placeholders also need to account for another browser behavior: bidi mirroring.

Bracket characters like {, }, (, ) are Unicode bidi-mirror pairs. When rendered in RTL context, the browser swaps them: { becomes } and vice versa. This means that to display {at}, the stored string must contain {ta} — the reversed character order of {at} — because the browser mirrors the braces as part of the RTL rendering:

  • Stored: {ta} → reversed char order: }at{ → bidi-mirrored: {at}
  • Stored: }ta{ → reversed char order: {at} → bidi-mirrored: }at{

The same applies to {dot}:

  • Stored: {tod} → reversed: }dot{ → mirrored: {dot}

So to display hey{at}adrian-altner{dot}com, the stored string is:

moc{tod}rentla-nairda{ta}yeh

A small Node script generates all the values for address fields:

const obfuscate = s =>
  s.split('').reverse().join('')
   .replace(/@/g, '{ta}')
   .replace(/\./g, '{tod}');

Then Base64-encode the result for the data-obf attribute.

What This Protects (and What It Doesn’t)

ThreatProtected
Static HTML scrapers (most spam bots)
Regex email harvesters
Copy-paste by visitors✅ (reversed text in clipboard)
Headless browsers running JS (Puppeteer)⚠️ reversed text, no plain email
Someone inspecting the decoded DOM
Manual reading by a human

The last two cases are unavoidable: if the data is visible to a human, a determined human can get it. The goal isn’t perfect protection — it’s raising the bar high enough to stop the automated tools responsible for the vast majority of harvested-address spam.

For a public imprint on a personal blog, this is a reasonable trade-off.

← Home