fix(contact): keep the contact email editable and protect it from scrapers
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:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-26 20:35:50 +02:00
1 parent 12675f381a
commit 2943021fe9
9 files changed
+251 -35

No files matched your search

+12 -8
View File
@@ -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 });
}