cf-email-rehydrate
v0.1.1
Published
Restore Cloudflare-obfuscated email addresses after client-side navigation (SPA, View Transitions, swup, Turbo).
Downloads
48
Maintainers
Readme
cf-email-rehydrate
Restore Cloudflare-obfuscated email addresses after client-side navigation.
Zero dependencies. ~1 kB. TypeScript types included.
The problem
Cloudflare's Email Address Obfuscation (part of Scrape Shield, on by default) rewrites email addresses in your HTML:
<!-- displayed text -->
<span class="__cf_email__" data-cfemail="a1c4d9c0…">[email protected]</span>
<!-- link target -->
<a href="/cdn-cgi/l/email-protection#a1c4d9c0…">…</a>It also injects a script that walks the DOM once, on page load, and puts the real addresses back.
"Once, on page load" is the problem. If your site swaps content client-side — swup, Astro View Transitions, Turbo, a Next.js route change — then any HTML inserted after that first pass never gets restored. Visitors see the literal string [email protected], and clicking the link sends them to Cloudflare's /cdn-cgi/l/email-protection interstitial instead of opening their mail client.
It only reproduces in production (there is no Cloudflare in front of your dev server), and only when the page is reached by client-side navigation. Loading the same page directly works fine.
Install
Not on npm. Install straight from the repository:
npm install github:roukara/cf-email-rehydrateOr just copy src/index.ts into your project — it is one self-contained file with no dependencies.
Related
cf-email-decode on npm converts the hex string in both directions and never touches the DOM. Reach for that one if you are decoding scraped HTML rather than repairing a live page.
Usage
Automatic
observeCfEmails restores what is already in the page, then watches for inserted nodes and restores those too. Call it once during startup and you are done — no framework-specific hook required.
import { observeCfEmails } from 'cf-email-rehydrate';
observeCfEmails();It returns a function that stops the observer:
const stop = observeCfEmails();
// later
stop();Manual
If you would rather not keep a MutationObserver alive, call decodeCfEmails() yourself after each navigation.
import { decodeCfEmails } from 'cf-email-rehydrate';
document.addEventListener('astro:after-swap', () => {
decodeCfEmails();
});Where to call it:
| Setup | Hook |
| --- | --- |
| Astro View Transitions / @swup/astro | astro:after-swap |
| swup | hook: 'content:replace' |
| Turbo | turbo:load |
| Next.js App Router | a useEffect that runs after the swap |
Calling it on the initial load too is harmless and keeps behaviour consistent whether or not Cloudflare's own script ran: once restored, there is nothing left to match.
API
decodeCfEmails(root?): number
Restores every obfuscated address under root (default document) and returns how many elements were restored. Handles both shapes Cloudflare emits:
- elements carrying
data-cfemail— the displayed text - anchors pointing at
/cdn-cgi/l/email-protection#…— thehref
Idempotent, and safe to call on a subtree. Values that fail to decode are left untouched rather than throwing.
root may be a Document, an Element, or a DocumentFragment. An element that is itself a match is included, not just its descendants.
observeCfEmails(options?): () => void
Runs decodeCfEmails, then keeps restoring inserted nodes via MutationObserver. Returns a disposer.
observeCfEmails({ root: document.querySelector('#app') });Only inserted subtrees are rescanned, so this does not re-walk the whole document on every DOM change.
decodeCfEmail(encoded): string | null
The raw decoder, if you need it on its own.
decodeCfEmail('a1c4d9c0ccd1cdc4e1c6ccc0c8cd8fc2cecc'); // '[email protected]'
decodeCfEmail('not-hex'); // nullHow the obfuscation works
The first byte of the hex string is an XOR key. Every following byte, XORed with that key, gives one character:
a1 c4 d9 c0 …
│ └──┴──┴── payload
└─ key
0xc4 ^ 0xa1 = 0x65 = 'e'
0xd9 ^ 0xa1 = 0x78 = 'x'
0xc0 ^ 0xa1 = 0x61 = 'a'This is obfuscation, not encryption. It raises the cost for naive scrapers and nothing more — do not treat it as a way to keep an address secret.
Do you actually need this?
Maybe not. Two alternatives worth considering first:
- Turn the feature off. Cloudflare dashboard → Scrape Shield → Email Address Obfuscation. If you do not put raw addresses in your HTML, it is doing nothing for you anyway.
- Do not ship the address as text. A contact form, or assembling the address at runtime, sidesteps the whole issue.
Use this package when you want to keep the obfuscation and still have working addresses after a client-side navigation.
License
MIT © Tsukasa Aoki
