team page: publish employee emails with crawler obfuscation

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>
This commit is contained in:
Ryan Salazar
2026-05-28 14:21:44 +00:00
parent 74ab4dae3d
commit 28c05962ce
2 changed files with 82 additions and 5 deletions

View File

@@ -2,6 +2,7 @@ import { createClient } from "@supabase/supabase-js";
import Header from "@/components/Header"; import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import SidebarAdStack from "@/components/SidebarAdStack"; import SidebarAdStack from "@/components/SidebarAdStack";
import ObfuscatedEmail from "@/components/ObfuscatedEmail";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = 600; export const revalidate = 600;
@@ -75,6 +76,19 @@ export default async function TeamPage() {
.map((w) => w.charAt(0).toUpperCase()) .map((w) => w.charAt(0).toUpperCase())
.join(""); .join("");
// Derive @broadcastbeat.com email from the persona slug. Real mailboxes
// are set up centrally; this is the authoring address surfaced to readers.
const editorialEmail = (p: Persona): { user: string; domain: string } | null => {
if (!p.slug) return null;
return { user: p.slug, domain: "broadcastbeat.com" };
};
const splitEmail = (email: string): { user: string; domain: string } | null => {
const at = email.indexOf("@");
if (at <= 0) return null;
return { user: email.slice(0, at), domain: email.slice(at + 1) };
};
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<Header /> <Header />
@@ -111,6 +125,15 @@ export default async function TeamPage() {
</div> </div>
)} )}
{p.bio && <p className="text-sm text-[#9ca3af] mt-2 leading-relaxed">{p.bio}</p>} {p.bio && <p className="text-sm text-[#9ca3af] mt-2 leading-relaxed">{p.bio}</p>}
{(() => {
const e = editorialEmail(p);
if (!e) return null;
return (
<p className="text-xs text-[#9ca3af] mt-2">
<ObfuscatedEmail user={e.user} domain={e.domain} className="hover:text-[#60a5fa]" />
</p>
);
})()}
</div> </div>
</li> </li>
))} ))}
@@ -136,11 +159,15 @@ export default async function TeamPage() {
{p.role} {p.role}
</div> </div>
<div className="text-xs text-[#9ca3af] mt-2 space-y-0.5"> <div className="text-xs text-[#9ca3af] mt-2 space-y-0.5">
{p.email && ( {p.email && (() => {
<div> const e = splitEmail(p.email);
<a href={`mailto:${p.email}`} className="hover:text-[#60a5fa]">{p.email}</a> if (!e) return null;
</div> return (
)} <div>
<ObfuscatedEmail user={e.user} domain={e.domain} className="hover:text-[#60a5fa]" />
</div>
);
})()}
{p.phone && ( {p.phone && (
<div> <div>
<a href={`tel:${p.phone.replace(/[^\d+]/g, '')}`} className="hover:text-[#60a5fa]"> <a href={`tel:${p.phone.replace(/[^\d+]/g, '')}`} className="hover:text-[#60a5fa]">

View File

@@ -0,0 +1,50 @@
"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&nbsp;protected]
</span>
);
}
const addr = `${user}@${domain}`;
return (
<a href={`mailto:${addr}`} className={className}>
{addr}
</a>
);
}