fix(contact): keep the contact email editable and protect it from scrapers
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m20s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m20s
An earlier change rejected obfuscated entries such as
"info [á] wabenhain.com" in Admin -> Inhalte and replaced them with the
default address. The obfuscation was intentional, so:
- The admin accepts the entry as written (plain or obfuscated with
[at], (at), {ät}, [á], <@>, [dot], [punkt]) and stores it unchanged;
only entries that yield no valid address are rejected.
- lib/contact-email.ts turns the entry into the real address for
mailto links and DHL labels.
- Public pages never contain the address in clear text: footer,
contact page and every email in legal texts (Impressum, Datenschutz,
Widerruf) render "info [at] wabenhain.com" plus an XOR-encoded payload
with a fresh random key; the browser builds the clickable mailto link
after load (components/protected-email.tsx).
Verified with a production build: the raw HTML of /, /kontakt,
/impressum, /datenschutz and /widerrufsbelehrung contains no plain
address, and after load each page shows mailto:[email protected].
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
12675f381a
commit
2943021fe9
9 files changed
+251
-35
No files matched your search
@@ -7,7 +7,7 @@ import { CONTENT_REGISTRY } from "@/lib/content-registry";
|
||||
import { isValidUploadPath, isValidVideoUploadPath } from "@/lib/uploads";
|
||||
import { requireAdmin } from "./_shared";
|
||||
import { fail, succeed } from "@/lib/flash";
|
||||
import { z } from "zod";
|
||||
import { normalizeContactEmail } from "@/lib/contact-email";
|
||||
|
||||
const COMPANY_EMAIL_KEY = "company.email";
|
||||
|
||||
@@ -51,13 +51,17 @@ export async function updateSiteContentAction(formData: FormData): Promise<void>
|
||||
if (def.type === "video" && valueDe !== "" && !isValidVideoUploadPath(valueDe)) {
|
||||
continue;
|
||||
}
|
||||
// Die Firmen-E-Mail steht im Impressum (§ 5 Abs. 1 Nr. 2 DDG verlangt
|
||||
// eine direkt nutzbare Adresse), in der Widerrufsbelehrung und in
|
||||
// mailto-Links. Eine verschleierte Schreibweise ("info [at] …") waere
|
||||
// dort rechtlich angreifbar und technisch unbrauchbar — daher wird der
|
||||
// ganze Speichervorgang abgelehnt statt still uebernommen.
|
||||
if (def.key === COMPANY_EMAIL_KEY && !z.string().trim().email().safeParse(valueDe).success) {
|
||||
return fail("/admin/inhalte", "Bitte gib als Firmen-E-Mail eine vollständige, gültige Adresse an (z. B. [email protected]). Sie steht im Impressum und muss direkt nutzbar sein.");
|
||||
// Die Firmen-E-Mail darf so eingetragen werden, wie sie gemeint ist —
|
||||
// auch verschleiert ("info [at] …", "info [á] …"). Gespeichert wird der
|
||||
// Eintrag unveraendert; die Website rechnet ihn in die echte Adresse um
|
||||
// und zeigt sie geschuetzt an (lib/contact-email.ts). Abgelehnt wird nur,
|
||||
// was sich in keine gueltige Adresse umrechnen laesst — sonst stuende im
|
||||
// Impressum ein Kontaktweg, der ins Leere fuehrt.
|
||||
if (def.key === COMPANY_EMAIL_KEY && normalizeContactEmail(valueDe) === null) {
|
||||
return fail(
|
||||
"/admin/inhalte",
|
||||
"Die Firmen-E-Mail ergibt keine gültige Adresse. Möglich sind z. B. [email protected], info [at] wabenhain.com oder info [á] wabenhain.com."
|
||||
);
|
||||
}
|
||||
updates.push({ key: def.key, valueDe: def.key === COMPANY_EMAIL_KEY ? valueDe.trim() : valueDe, valueEn: null });
|
||||
}
|
||||
|
||||
+20
-16
@@ -3,6 +3,7 @@ import { Container, SectionHeading } from "@/components/ui";
|
||||
import { HoneycombPattern, HexMark } from "@/components/honeycomb";
|
||||
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||
import { getCompanyInfo } from "@/lib/company-info";
|
||||
import { ContactEmail } from "@/components/contact-email";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
|
||||
@@ -32,22 +33,25 @@ export default async function KontaktPage() {
|
||||
<div className="relative h-full overflow-hidden bg-forest px-8 py-10 text-cream md:px-10">
|
||||
<HoneycombPattern className="text-honey-light/[0.1]" size={24} fade="right" />
|
||||
<div className="relative">
|
||||
<a
|
||||
href={`mailto:${company.email}`}
|
||||
className="group inline-flex items-center gap-3 font-serif text-2xl sm:text-3xl md:text-4xl text-cream"
|
||||
>
|
||||
<span className="link-draw pb-1">{company.email}</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
className="h-5 w-5 text-honey-light transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
>
|
||||
<path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</a>
|
||||
{/* Vor Adress-Sammlern geschuetzt: im HTML steht nur
|
||||
"info [at] …", den mailto-Link baut erst der Browser. */}
|
||||
<ContactEmail
|
||||
email={company.email}
|
||||
linkClassName="group inline-flex max-w-full items-center gap-3 break-all font-serif text-2xl sm:text-3xl md:text-4xl text-cream"
|
||||
textClassName="link-draw pb-1"
|
||||
trailing={
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
className="h-5 w-5 shrink-0 text-honey-light transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
>
|
||||
<path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
{/* Ohne gepflegte Nummer entfaellt die Zeile ganz. Eine
|
||||
Platzhalternummer waere schlimmer als keine: sie sieht nach
|
||||
einem Kontaktweg aus, an dessen Ende niemand abnimmt. */}
|
||||
|
||||
Reference in new issue
Block a user