New ObfuscatedEmail client component splits user + domain into two data attributes server-side, then assembles the mailto on mount. Pre-JS DOM shows "[email protected]" placeholder; only after client-side hydration does the real address render. Most simple email scrapers won't execute JS or follow the data-u/data-d pattern, so addresses stay protected while remaining one click away for humans. Editorial persona emails derive from slug (firstname-lastname@broadcastbeat.com). Ops persona emails (Chloe, Riley) come from pulsedesk.personas.email. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
51 lines
1.2 KiB
TypeScript
51 lines
1.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
/**
|
|
* Render an email address split into user + domain attributes so simple
|
|
* HTML scrapers don't see a usable address in the server-rendered DOM.
|
|
* On mount we assemble the address client-side and wrap in a real mailto.
|
|
*
|
|
* Pre-mount HTML looks like:
|
|
* <span data-u="ryan" data-d="broadcastbeat.com">[email protected]</span>
|
|
*
|
|
* Post-mount (JS executed) becomes:
|
|
* <a href="mailto:ryan@broadcastbeat.com">ryan@broadcastbeat.com</a>
|
|
*
|
|
* Plus the @ is rendered with an HTML entity so even rare bots reading the
|
|
* pre-JS DOM won't get a clean string.
|
|
*/
|
|
export default function ObfuscatedEmail({
|
|
user,
|
|
domain,
|
|
className,
|
|
}: {
|
|
user: string;
|
|
domain: string;
|
|
className?: string;
|
|
}) {
|
|
const [revealed, setRevealed] = useState(false);
|
|
useEffect(() => setRevealed(true), []);
|
|
|
|
if (!revealed) {
|
|
return (
|
|
<span
|
|
data-u={user}
|
|
data-d={domain}
|
|
className={className}
|
|
suppressHydrationWarning
|
|
>
|
|
[email protected]
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const addr = `${user}@${domain}`;
|
|
return (
|
|
<a href={`mailto:${addr}`} className={className}>
|
|
{addr}
|
|
</a>
|
|
);
|
|
}
|