Kontaktformular auf einer statischen Site6 Min. Lesezeit
Ein Kontaktformular in eine Cloudflare-Pages-Site einbauen
Bau ein Kontaktformular in Cloudflare Pages ein, ohne eine Function zu schreiben: an ein Formular-Backend senden, Header in _headers setzen und eigene Turnstile-Schlüssel nutzen.
Statische Assets auf Cloudflare Pages beantworten einen POST mit 405, also schreibst du entweder eine Pages Function oder sendest das Formular an ein Formular-Backend. Mit Ikiform schreibst du keine Function: Setze die action des Formulars auf deinen Endpunkt, füge your-project.pages.dev zu Allowed domains hinzu, was auch jede Preview-URL abdeckt, und nimm Ikiform in die Content-Security-Policy in deiner _headers auf. Wenn du Cloudflare schon nutzt, kannst du deine eigenen Turnstile-Schlüssel im Formular hinterlegen.
Was bei Cloudflare Pages anders ist
Pages liefert deinen Build-Output aus dem Netzwerk von Cloudflare aus. Statische Assets beantworten GET; ein POST an eines davon bekommt 405 ohne Body:
HTTP/2 405
server: cloudflare
content-length: 0Pages kann Code ausführen, über Pages Functions in einem Ordner functions/. Eine functions/contact.ts könnte den Post annehmen, aber dann schreibst du ein Backend: Parsing, Validierung, einen E-Mail-Anbieter, Spamfilter und Speicherung. Die Dokumentation von Cloudflare selbst rät außerdem, neue Projekte auf Workers statt auf Pages zu starten. Ein Post an ein Formular-Backend umgeht beides, und dasselbe Formular funktioniert unverändert, falls du später zu Workers Static Assets wechselst.
| Pages-Eigenschaft | Was das für das Formular bedeutet |
|---|---|
Preview-URLs sind Subdomains, etwa abc123.your-project.pages.dev | Eine erlaubte Domain, your-project.pages.dev, deckt die Produktion und jede Preview ab. Previews bei Vercel und Netlify brauchen eigene Einträge |
Header kommen aus einer _headers-Datei im Build-Output | Bis zu 100 Regeln, 2.000 Zeichen pro Zeile. Sie gelten nicht für Antworten von Pages Functions |
| Turnstile ist ein Cloudflare-Produkt | Du kannst die verwaltete Prüfung von Ikiform ohne Schlüssel nutzen oder dein eigenes Widget aus dem Cloudflare-Dashboard |
- Pages liefert die Kontaktseite aus, mit der Content-Security-Policy aus deiner _headers-Datei.
- Das Turnstile-Widget läuft mit deinem Site Key und legt ein Token im Formular ab.
- Das Formular sendet seine Felder und das Token an Ikiform.
- Ikiform verifiziert das Token mit deinem Secret und prüft den Hostnamen des Tokens gegen deine erlaubten Domains.
- Ein gültiger Post wird gespeichert und per E-Mail gemeldet. Ein fehlendes, abgelaufenes oder wiederverwendetes Token bekommt 403 captcha_failed, und die Seite setzt das Widget zurück.
Das Formular
Diese Version nutzt dein eigenes Turnstile-Widget, was sinnvoll ist, wenn du Turnstile schon in deinem Cloudflare-Konto verwaltest. Setze in den Einstellungen des Formulars Spam check auf Your own Turnstile keys und füge Site Key und Secret ein. Das Widget fügt dem Formular ein Feld cf-turnstile-response hinzu, und das Select team sagt dir, wer antworten soll:
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form id="contact" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="team">Who should read this?</label>
<select id="team" name="team">
<option value="support">Support</option>
<option value="sales">Sales</option>
<option value="press">Press</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" minlength="10" required></textarea>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAAxxxxxxxxxxxxxx"></div>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button type="submit">Send</button>
<p id="contact-status" role="status"></p>
</form>
<script src="/contact.js" defer></script>Das Skript hält Besucher auf der Seite. Ein Turnstile-Token ist nur einmal gültig, daher setzt es nach jedem fehlgeschlagenen Senden das Widget für den nächsten Versuch zurück:
const form = document.querySelector("#contact");
const status = document.querySelector("#contact-status");
form.addEventListener("submit", async (event) => {
event.preventDefault();
status.textContent = "Sending";
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
}).catch(() => null);
const result = await response?.json().catch(() => null);
if (result?.ok) {
form.replaceWith(Object.assign(document.createElement("p"), { textContent: "Sent. We'll reply by email." }));
return;
}
window.turnstile?.reset();
if (result?.error?.code === "captcha_failed") {
status.textContent = "The human check expired. Complete it again, then send.";
return;
}
const fields = result?.error?.fields ?? {};
status.textContent = Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.";
});- Trag in den Einstellungen des Turnstile-Widgets in Cloudflare die Hostnamen ein, auf denen das Formular läuft. Ikiform prüft den Hostnamen des Tokens außerdem gegen Allowed domains des Formulars.
- Posts ohne gültiges Token werden mit
403 captcha_failedabgelehnt. Das gilt auch für einen einfachen HTML-Post, der gesendet wird, bevor das Widget fertig ist. - Lieber ganz ohne Schlüssel? Setze Spam check auf Ikiform, lösche das Widget und sein Skript, und der HTML-Post führt die Prüfung auf ikiform.com aus. Die
fetch-Version braucht dannchallenge()aus dem SDK; siehe die Menschlichkeitsprüfung.
Deine Schlüssel oder die Prüfung von Ikiform
| Item | Prüfung von Ikiform | Eigene Turnstile-Schlüssel |
|---|---|---|
| Zu verwaltende Schlüssel | Keine | Ein Site Key und ein Secret aus deinem Cloudflare-Dashboard |
| Änderung am HTML-Formular | Keine. Die Prüfung läuft auf einer Seite von ikiform.com | Widget und sein Skript hinzufügen |
Version mit fetch | challenge() aus dem SDK aufrufen | cf-turnstile-response des Widgets mitsenden |
| Content-Security-Policy | Nur form-action und connect-src für Ikiform | Zusätzlich challenges.cloudflare.com in script-src und frame-src |
| Turnstile-Analytics | Nicht in deinem Konto | In deinem Cloudflare-Dashboard, neben deinen anderen Widgets |
Die Prüfung von Ikiform ist weniger Einrichtung. Eigene Schlüssel lohnen sich, wenn du die Turnstile-Analytics ohnehin im Blick hast oder Aussehen und Platzierung des Widgets selbst bestimmen willst.
Das Widget lokal testen
Cloudflare veröffentlicht Test-Site-Keys und -Secrets, die immer bestehen oder immer scheitern, etwa das Paar 1x00000000000000000000AA und 1x0000000000000000000000000000000AA, das immer besteht. Produktions-Secrets lehnen Tokens ab, die mit einem Testschlüssel erzeugt wurden, also misch sie nicht in einem Formular. Leg ein zweites Ikiform-Formular für die lokale Arbeit an, gib ihm das Test-Schlüsselpaar und wechsle data-sitekey und Formular-ID gemeinsam. Lass Allowed domains dieses Formulars leer: Ikiform prüft den Hostnamen eines Tokens nur, wenn die Liste gesetzt ist, und Cloudflare dokumentiert nicht, welchen Hostnamen ein Test-Token meldet.
Die _headers-Datei
Leg _headers in dein Verzeichnis für statische Assets, etwa public/, damit sie im Build-Output landet. Turnstile braucht script-src und frame-src für https://challenges.cloudflare.com, und Ikiform braucht form-action und connect-src:
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.comWenn du später eine Pages Function zum selben Projekt hinzufügst, bekommen ihre Antworten diese Header nicht. Setze sie im Code der Function.
Wenn du zu Workers wechselst
Der Umzug der Site zu Workers Static Assets ändert, wie sie deployt wird, nicht, wie das Formular funktioniert. Das Formular sendet weiterhin an ikiform.com, also verarbeitet nichts auf Cloudflares Seite den Post. Prüfe nach dem Umzug zwei Dinge: Der neue workers.dev-Host oder die Route steht unter Allowed domains, und deine Header erreichen weiterhin die Seite, da Workers sie auf eigene Weise setzt.
your-project.pages.dev und deine eigene Domain unter Allowed domains hinzu. Füge niemals pages.dev allein hinzu: Jedes Pages-Projekt würde passen.Prüfen, ob es funktioniert
- Pushe einen Branch und öffne seine Preview-URL. Sende ab und prüfe, ob die Nachricht ankommt und nicht als Spam markiert ist: Die Preview ist eine Subdomain des Hosts deines Projekts.
- Sende in der Produktion erneut ab, geh dann zurück und sende dasselbe Formular ein zweites Mal, ohne das Widget abzuschließen. Erwarte die Meldung zum Zurücksetzen, keine gespeicherte Einsendung.
- Öffne Submissions im Dashboard. Die neue Einsendung ist da, mit jedem Feld, das du gesendet hast.
- Prüfe das Postfach jedes Benachrichtigungsempfängers. Jeder bekommt eine E-Mail mit den Feldern, und Reply geht an den Absender, wenn das Formular ein
email-Feld hat. - Nichts angekommen? Stell den Statusfilter im Dashboard auf Spam. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.
Stolperfallen
| Problem | Was zu tun ist |
|---|---|
405 mit leerem Body | Das Formular hat an eine Pages-URL gesendet. Prüfe die deployte action und ob keine Function oder Redirect-Regel /contact umschreibt. |
| Das Turnstile-Widget erscheint nie | Die Content-Security-Policy blockiert challenges.cloudflare.com. Füge es zu script-src und frame-src hinzu und prüfe, ob _headers im Build-Output liegt. |
403 captcha_failed bei jedem Post | Site Key und Secret stammen von verschiedenen Widgets, der Hostname ist beim Widget nicht eingetragen, oder ein Test-Secret ist mit einem echten Site Key kombiniert. |
403 origin_not_allowed, oder jeder Post landet im Spam | Die Domain der Seite steht nicht unter Allowed domains des Formulars. Füge sie hinzu, plus localhost für lokale Tests. Subdomains einer eingetragenen Domain sind erlaubt. |
Fragen
Nein. Eine Function ist ein Weg, den Post anzunehmen, aber dann schreibst du Validierung, E-Mail-Versand und Spamfilter selbst. Ein Post an ein Formular-Backend hält das Projekt vollständig statisch.
Nein. Preview-URLs von Pages sind Subdomains von your-project.pages.dev, wer diesen Host erlaubt, deckt also alle ab.
Ja. Setze die Spamprüfung des Formulars auf deine eigenen Turnstile-Schlüssel und füge Site Key und Secret ein. Ikiform verifiziert jedes Token und prüft seinen Hostnamen gegen deine erlaubten Domains.
Es hat an deine eigene Pages-URL gesendet, die statische Assets ausliefert, die nur GET-Requests beantworten. Richte die action des Formulars auf ein Formular-Backend aus oder füge eine Pages Function für diesen Pfad hinzu.
Nein, es gibt keinen kostenlosen Tarif. Testen ist kostenlos: Du kannst 50 Testeinsendungen pro Monat von localhost senden, bevor du zahlst. Für den Livebetrieb startet Pro bei $6/Monat für 500 Einsendungen, oder du zahlst einmalig 10.000 Einsendungen für $49 oder 50.000 Einsendungen für $149, mit Guthaben, das nie verfällt.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.