Kurzfassung
reCAPTCHA v3 zeigt keine „Ich bin kein Roboter“-Checkbox — es liefert einen Score von 0,0–1,0 und ein kurzlebiges Token, das Sie serverseitig per siteverify prüfen müssen, bevor ein Lead gespeichert oder eine E-Mail versendet wird. Auf einer produktiven Next.js-15-Unternehmensseite lädt das Skript per lazyOnload in layout.tsx (LCP-Schutz), und das Kontaktformular wartet auf grecaptcha.ready mit 12 s Timeout — sonst bleibt das Token oft leer und die API antwortet mit 400. Schwelle 0,5 ist ein sinnvoller Start; dazu IP-Rate-Limit (5 Anfragen/Minute in diesem Projekt), optional Honeypot und dasselbe Muster für den Chatbot. Im Folgenden: Ablauf, DSGVO, False Positives und Tests.
Für wen ist das
- Dienstleister mit Kontaktformular oder Chatbot für Lead-Generierung
- Next.js / React-Entwickler, die Schutz ohne nerviges v2-Captcha wollen
- Verantwortliche für DSGVO / Datenschutz — Hinweis am Formular und Datenübermittlung an Google
- Teams mit Spam im Postfach trotz „irgendeinem Captcha“ im WordPress-Theme — auf der Suche nach serverseitigem Muster
Keyword (SEO)
recaptcha v3 kontaktformular, formular spam schutz, recaptcha nextjs api route, recaptcha score schwelle, recaptcha server verifizierung, lazyOnload recaptcha layout, dsgvo recaptcha google
Warum v3 statt v2-Checkbox
Google reCAPTCHA entwickelte sich von sichtbaren Rätseln zu unsichtbarer Risikobewertung. Bei v3 klickt der Nutzer meist nichts — das Skript wertet Signale aus (Domain-Historie, Bewegungsmuster, Fingerprint) und liefert einen Score. Das ist besseres UX auf B2B-Seiten, wo „Wählen Sie alle Ampeln“ die Conversion killt.
| Aspekt | reCAPTCHA v2 (Checkbox) | reCAPTCHA v3 |
|---|---|---|
| UX | Sichtbare Aufgabe | Unsichtbar; Badge in der Ecke |
| Ergebnis | bestanden / nicht | Score 0,0–1,0 |
| Entscheidung | vor allem in Google-UI | Sie serverseitig (Schwelle) |
| Performance | Schwerer iframe | Leichter, trotzdem externes Skript |
v3 ersetzt nicht Feldvalidierung, Rate Limits oder Plausibilitätschecks — es ist eine Schicht in Defence in Depth.
Architektur auf Next.js 15 (Produktionsmuster)
Im Software-House-Projekt (mehrsprachig /pl, /en, /de) sitzt der Schutz an drei Stellen: globales Skript im Layout, execute() beim Submit, Verifizierung in /api/submissions.
Skript in layout.tsx — lazyOnload
Das reCAPTCHA-Tag lädt nicht afterInteractive wie GA4 — bewusst lazyOnload, damit es nicht mit Analytics um den First Paint konkurriert und LCP nicht leidet. Der Site Key ist öffentlich und steht in der URL:
<Script
src="https://www.google.com/recaptcha/api.js?render=6Lcwsx0sAAAAAO4sbP31qTdgjuoGLgMmp9HyxhYB"
strategy="lazyOnload"
/>Der Code-Kommentar ist eindeutig: Skript ist lazy — das Formular muss auf ready warten, sonst entsteht kein Token.
Badge und Rechtstext — zwei Ebenen
Google verlangt ein sichtbares reCAPTCHA-Badge. Im Layout schneidet CSS das Badge auf ein kleines Icon unten links zu (overflow hidden), der vollständige Rechtstext steht am Kontaktformular in page.tsx — mit Links zu Google-Datenschutz und Nutzungsbedingungen (Übersetzungen in messages/pl.json, en.json, de.json):
{translations.contact.form.recaptchaBeforePrivacy}
<a href="https://policies.google.com/privacy">...</a>
{translations.contact.form.recaptchaBetween}
<a href="https://policies.google.com/terms">...</a>
{translations.contact.form.recaptchaAfter}Das ist wichtig für die DSGVO: Nutzer wissen, dass Daten zur Anti-Spam-Analyse an Google LLC gehen können — ein verstecktes Badge allein reicht nicht.
Clientseitig — Token beim Formular-Submit
In handleSubmit des Kontaktformulars execute() nicht blind bei onClick aufrufen — bei lazyOnload ist window.grecaptcha die ersten Sekunden oft undefined.
Muster: auf ready warten, dann execute
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('recaptcha_timeout')), 12000);
const done = () => { clearTimeout(t); resolve(); };
if (window.grecaptcha?.ready) {
window.grecaptcha.ready(done);
} else {
const iv = setInterval(() => {
if (window.grecaptcha?.ready) {
clearInterval(iv);
window.grecaptcha.ready(done);
}
}, 100);
setTimeout(() => {
clearInterval(iv);
if (!window.grecaptcha?.ready) reject(new Error('recaptcha_no_script'));
}, 12000);
}
});
const recaptchaToken = await window.grecaptcha.execute(
'6Lcwsx0sAAAAAO4sbP31qTdgjuoGLgMmp9HyxhYB',
{ action: 'submit_contact_form' }
);Die Action (submit_contact_form) erscheint in der reCAPTCHA-Konsole und in Logs — separate Actions für den Chatbot (submit_chatbot_form) erleichtern die Score-Analyse pro Kanal.
Token im POST-Body zusammen mit Lead-Feldern senden:
const response = await fetch('/api/submissions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, locale, recaptchaToken }),
});Schlägt execute fehl, klare Meldung anzeigen („Bitte kurz warten nach dem Laden…“) — kein leeres Token erzwingen.
TypeScript-Typen für grecaptcha in src/types/gtag.d.ts neben gtag pflegen.
Serverseitig — Verifizierung in der API-Route
Secret Key niemals in den Browser. In Vercel / .env RECAPTCHA_SECRET_KEY setzen und in POST /api/submissions aufrufen:
const recaptchaResponse = await fetch(
'https://www.google.com/recaptcha/api/siteverify',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `secret=${recaptchaSecret}&response=${recaptchaToken}`,
}
);
const recaptchaData = await recaptchaResponse.json();Entscheidungsreihenfolge in Produktion:
- Kein Token → HTTP 400 (Bots überspringen oft JS).
success: false(z. B.timeout-or-duplicate, falscher Secret) → 400 + Logerror-codes.score < 0.5→ 400 (Bot / verdächtiger Traffic).- Erst dann Feldvalidierung und Prisma-Speicherung + Transaktions-Mails.
if (recaptchaData.score !== undefined && recaptchaData.score < 0.5) {
return NextResponse.json(
{ error: 'Verifizierung fehlgeschlagen. Seite neu laden und erneut versuchen.' },
{ status: 400 }
);
}Token ist Einmalnutzung und kurzlebig — nicht zwischen Requests cachen. Bei Netzwerkfehlern zu Google Submission blockieren (fail closed); ein offener Endpoint ohne Prüfung lädt Spammer ein.
Score-Schwelle — 0,5, 0,7 oder „weich“
Google behandelt den Score als Signal, nicht als Absolutwert. In der Praxis:
| Schwelle | Effekt | Wann |
|---|---|---|
| 0,3 | Weniger False Positives, mehr Spam | Sehr wenig Traffic, A/B-Tests |
| 0,5 | Balance (Default in vielen Guides) | Unternehmensseiten, B2B-Leads |
| 0,7 | Aggressiver Filter | Formulare mit Anreiz (E-Book, Gutschein) |
In diesem Projekt Start bei 0,5. Nach einer Woche reCAPTCHA Admin → Analytics für Action submit_contact_form prüfen. Liegen 5 % legitimer Leads bei 0,45–0,49, eher 0,45 oder zusätzliches Rate Limit statt harter 0,7.
Weiche Schwelle: Score 0,3–0,5 → Queue „manuelle Prüfung“ statt harter Ablehnung — sinnvoll bei Ads-Traffic von neuen Geräten (VPN, Safari ITP).
Rate Limit — erste Verteidigungslinie (bereits im Projekt)
Bevor Google ins Spiel kommt, ruft /api/submissions checkEmailRateLimit(ip) aus src/lib/email-rate-limit.ts auf:
- 5 Anfragen pro IP pro 60 Sekunden
- bei Überschreitung: HTTP 429 + Header
Retry-After
Schützt vor einfachen Skripten und Floods, die E-Mail-Kontingente leeren würden. Derselbe Helper schützt weitere Mail-Endpoints (Chatbot, Verträge, Testimonials).
Rate Limit ersetzt nicht reCAPTCHA — Botnetze mit vielen IPs kommen durch. Zusammen erhöhen sie Angriffskosten.
Honeypot — wann eine dritte Schicht
In diesem Repo noch kein Honeypot-Feld — OK bei v3 + Rate Limit bei moderatem Traffic. Honeypot ergänzen, wenn:
- wiederholter Spam mit akzeptablem Score (Bot-Farmen),
- einfaches HTML-Formular leicht scrapbar,
- null Kosten gewünscht (verstecktes Feld
website,company_url— CSSposition:absolute; left:-9999px,tabIndex={-1},autoComplete="off").
Server lehnt ab, wenn Honeypot nicht leer — ohne „Sie sind ein Bot“ (weniger Feedback für Angreifer). Reihenfolge: Rate Limit → Honeypot → reCAPTCHA → fachliche Validierung.
Chatbot — gleiches Muster, eigener Endpoint
Das Kontaktformular nutzt /api/submissions mit voller reCAPTCHA-Prüfung. Der Chatbot-Endpoint /api/chatbot/submit hat derzeit Rate Limit, aber keine Token-Prüfung — typische Lücke, sobald Spammer die JSON-API finden.
Empfohlener Plan (passend zur Architektur):
verifyRecaptchaToken(token, minScore)nachsrc/lib/recaptcha.tsauslagern.- In der Chatbot-Komponente — derselbe
ready+execute-Block mit Actionsubmit_chatbot_form. - In
POST /api/chatbot/submit— identische Verifizierung wie bei submissions. - reCAPTCHA-Hinweis beim letzten Chatbot-Schritt (Kurzfassung des Formulartexts).
Ein Site Key, ein Secret, zwei Actions in der Google-Konsole — einfacheres Debugging als zwei reCAPTCHA-Projekte.
DSGVO, Cookies und Datenschutzerklärung
reCAPTCHA v3 verarbeitet Nutzerdaten bei Google (USA — Transfermechanismen: SCC, DPF je nach aktuellem Rechtsstand). Mindest-Paket Compliance:
- Hinweis am Formular (Google Privacy + Terms) — bereits in der UI.
- In der Datenschutzerklärung: Zweck (Anti-Spam), Datenkategorien (IP, Google-Cookies, Verhaltenssignale), Rechtsgrundlage (berechtigtes Interesse Art. 6 Abs. 1 lit. f oder Einwilligung — Anwalt bei Marketing-Cookies).
- Blockiert ein Cookie-Banner Google-Skripte bis zur Einwilligung — reCAPTCHA läuft ggf. nicht; entweder Kategorie „notwendig“ für Anti-Spam oder Laden nach Consent mit klarer Formular-Meldung.
- Aufbewahrung: Score-Logs in der API kurz halten; reCAPTCHA-Tokens nicht dauerhaft in der Lead-Tabelle speichern.
v3 ist nicht „cookie-frei“ — Badge und Google-Doku sagen das Gegenteil.
False Positives — was tun
Nutzer mit VPN, Tor, frischen Browser-Profilen, Adblockern gegen google.com/recaptcha oder Firmen-Proxy bekommen manchmal niedrige Scores trotz echter Anfrage.
Symptome in Logs:
recaptcha_no_script/recaptcha_timeoutclientseitig,score too lowmit 0,1–0,4 bei sinnvoller Projektbeschreibung,- „Formular geht nicht“ nur unter Firefox mit ETP.
Gegenmaßnahmen:
- Meldung mit Aufforderung zum Reload / direkte E-Mail (
kontakt@...). - Schwelle temporär senken + Monitoring.
- Fallback: Telefon oder Calendly neben dem Formular.
- Schutz nicht komplett abschalten zugunsten nur Honeypot — gezielter Spam bleibt.
Von der Produktionsdomain testen — localhost muss in der reCAPTCHA-Domainliste stehen, sonst success: false.
Testen — 15-Minuten-Checkliste
Google-reCAPTCHA-Konsole
- Key-Typ: v3, Domains: Produktion + optional localhost.
- Site Key (öffentlich) und Secret kopieren →
RECAPTCHA_SECRET_KEYauf Vercel. - Nach Deploy: Tab Overview — Anfragen für Action
submit_contact_formsteigen.
DevTools
- Network → Submit → JSON mit
recaptchaToken(langer String). - Response 200 nur bei OK-Score.
- Bewusst ohne Token senden (curl) → 400.
Niedrigen Score simulieren
Google bietet in v3 keinen „Bot-Schalter“; Test-Keys aus der Doku oder temporär niedrigere Schwelle auf Staging.
Checkliste vor Google-Ads-Kampagne
- Secret auf Produktion gesetzt
-
lazyOnload+ 12 s Timeout aufready - Hinweis am Formular in allen Locales
- Rate Limit aktiv
- Chatbot (falls öffentlich) — gleiche Prüfung wie Formular
- Server-Logs ohne Secret-Leak
Performance vs. Sicherheit
lazyOnload für reCAPTCHA ist derselbe Kompromiss wie bei Google-Ads-Tags: LCP wichtiger als sofortige Captcha-Bereitschaft. B2B-Formulare werden meist nach 30+ Sekunden ausgefüllt — dann ist das Skript geladen. Ausnahme: Landing mit Formular above the fold und sehr kurzer Entscheidungszeit — afterInteractive nur für reCAPTCHA oder Preload auf der Kontaktseite.
reCAPTCHA nicht auf jeder Blog-Seite laden, wenn nur auf der Startseite ein Formular ist — dieses Projekt nutzt ein globales Skript (einfacher), bei Performance-Tuning <Script> nur dort, wo <form> steht.
Typische Implementierungsfehler (und wie man sie vermeidet)
- Secret nur in
.env.local, nicht auf Vercel — lokal OK, Produktion antwortet mit 500 „reCAPTCHA-Konfiguration unvollständig“. Nach jedem neuen EnvironmentRECAPTCHA_SECRET_KEYim Hosting-Dashboard prüfen. - Verifizierung nur wenn Token da ist — im guten Muster bedeutet fehlendes Token Blockade, nicht „durchwinken wegen Adblock“. In
/api/submissionsführt leeresrecaptchaTokenzu 400. - Derselbe Token zweimal — Doppelklick auf „Senden“; zweite Anfrage erhält
timeout-or-duplicate. UI sollte den Button nach erstem Klick deaktivieren (isSubmittingin React). - Keine action in
execute— erschwerte Analyse in der Google-Konsole; immer aussagekräftige Action-Namen pro Formular. - Vollständiges Token in Produktionslogs — auch Einmal-Tokens gehören nicht in öffentliche Logs oder Sentry-Breadcrumbs.
Mehrsprachigkeit (/pl, /en, /de)
Das reCAPTCHA-Skript im gemeinsamen layout.tsx gilt für alle Locales — keine Keys pro Sprache duplizieren. Der Rechtstext kommt aus Übersetzungsdateien (messages/*.json), jede Sprachversion hat korrekte Google-Links ohne Hardcode in der Komponente. API-Fehlermeldungen (400/429) sollten perspektivisch ebenfalls nach locale aus dem Body lokalisiert werden — derzeit sind Teile unabhängig von /en auf Polnisch, was beim Backend-i18n-Refactoring nachgezogen werden sollte.
Integration mit E-Mail und CRM
Nach erfolgreicher reCAPTCHA-Prüfung legt /api/submissions den Lead in Prisma an und versendet Transaktions-Mails (Admin + Bestätigung an den Kunden) über sendTransactionalEmail. Spam, der reCAPTCHA passiert, landet trotzdem im Postfach — deshalb Rate Limit und optional Honeypot vor dem teuren E-Mail-Versand. Score-Werte müssen nicht ins CRM; reicht serverseitiges Logging mit IP-Zeitstempel für spätere Schwellen-Anpassung. Bei hohem Spam-Aufkommen: Score-Schwelle erhöhen oder verdächtige Domains in einer einfachen Blockliste auf API-Ebene filtern — nach reCAPTCHA, nicht stattdessen.
Vergleich mit anderen Anti-Spam-Methoden
| Methode | Stärke | Schwäche |
|---|---|---|
| reCAPTCHA v3 | Verhaltensanalyse, unsichtbar | Google-Abhängigkeit, DSGVO-Hinweis |
| Honeypot | Einfach, kostenlos | Leicht von gezielten Bots umgangen |
| Rate Limit | Stoppt Floods | Kein Schutz bei vielen IPs |
| E-Mail-Double-Opt-in | Qualität der Adressen | Kein Bot-Schutz vor Submit |
| Akismet / ML-Filter | Gut für Kommentare | Weniger Standard für B2B-Formulare |
Die produktive Kombination in diesem Projekt: Rate Limit + reCAPTCHA v3 + (optional) Honeypot — drei Schichten mit unterschiedlichen Angriffsvektoren.
FAQ
Darf der Site Key im Repo liegen?
Ja — Site Key ist öffentlich (im HTML sichtbar). Secret Key niemals — nur Server-Umgebungsvariablen.
Reicht clientseitige Prüfung?
Nein. Token lassen sich aus DevTools kopieren oder wiederverwenden. Entscheidung muss serverseitig nach siteverify fallen.
Warum bleibt das Token trotz Layout-Skript leer?
Meist execute vor grecaptcha.ready oder Skript durch Adblocker blockiert. Lösung: Polling + Timeout wie in page.tsx.
Ist 0,5 offizielle Google-Empfehlung?
Google erklärt Score-Interpretation, nicht eine feste Schwelle. 0,5 ist praktischer Default — am eigenen Traffic kalibrieren.
reCAPTCHA v3 vs. Cloudflare Turnstile?
Turnstile oft leichter und einfacheres UX; reCAPTCHA v3 passt zum Google-Ökosystem. Marke ist weniger wichtig als serverseitige Prüfung + Rate Limit.
Muss das Badge sichtbar sein?
Ja — Google-Nutzungsbedingungen. Platzierung stylen (wie im Projekt: kleines Icon + Volltext am Formular).
Zusammenfassung
Wirksamer Formular-Schutz ist nicht Tutorial-Skript einfügen, sondern eine konsistente Pipeline: lazyOnload im Layout, geduldiges execute nach ready, pflichtige siteverify mit Score-Schwelle, Rate Limit an der API, DSGVO-Hinweis beim Submit und dasselbe Schema für den Chatbot. 0,5 ist der Startpunkt — aus Logs nachjustieren. Bei False Positives Schwelle senken oder Honeypot ergänzen, statt Schutz ganz abzuschalten.
Formulare absichern lassen?
- Kontakt — reCAPTCHA v3, Rate Limits und Anti-Spam-Audit für Ihre Website
- Webseiten — Next.js, Lead-Formulare und DSGVO aus einer Hand
- Referenzen — Produktivseiten mit unsichtbarem Bot-Schutz