From 28c05962ce5c1d6f4af000a12b09483cbb278ad6 Mon Sep 17 00:00:00 2001 From: Ryan Salazar Date: Thu, 28 May 2026 14:21:44 +0000 Subject: [PATCH] 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) --- src/app/about/team/page.tsx | 37 +++++++++++++++++++--- src/components/ObfuscatedEmail.tsx | 50 ++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 src/components/ObfuscatedEmail.tsx diff --git a/src/app/about/team/page.tsx b/src/app/about/team/page.tsx index be0aecf..edfe7bd 100644 --- a/src/app/about/team/page.tsx +++ b/src/app/about/team/page.tsx @@ -2,6 +2,7 @@ import { createClient } from "@supabase/supabase-js"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import SidebarAdStack from "@/components/SidebarAdStack"; +import ObfuscatedEmail from "@/components/ObfuscatedEmail"; export const dynamic = "force-dynamic"; export const revalidate = 600; @@ -75,6 +76,19 @@ export default async function TeamPage() { .map((w) => w.charAt(0).toUpperCase()) .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 (
@@ -111,6 +125,15 @@ export default async function TeamPage() {
)} {p.bio &&

{p.bio}

} + {(() => { + const e = editorialEmail(p); + if (!e) return null; + return ( +

+ +

+ ); + })()} ))} @@ -136,11 +159,15 @@ export default async function TeamPage() { {p.role}
- {p.email && ( -
- {p.email} -
- )} + {p.email && (() => { + const e = splitEmail(p.email); + if (!e) return null; + return ( +
+ +
+ ); + })()} {p.phone && (
diff --git a/src/components/ObfuscatedEmail.tsx b/src/components/ObfuscatedEmail.tsx new file mode 100644 index 0000000..fffaae0 --- /dev/null +++ b/src/components/ObfuscatedEmail.tsx @@ -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: + * [email protected] + * + * Post-mount (JS executed) becomes: + * ryan@broadcastbeat.com + * + * 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 ( + + [email protected] + + ); + } + + const addr = `${user}@${domain}`; + return ( + + {addr} + + ); +}