Formulaire de contact sur un site statique7 min de lecture
Ajouter un formulaire de contact à un site Cloudflare Pages
Un formulaire de contact sur Cloudflare Pages sans écrire de Function : envoyez à un backend, définissez les en-têtes dans _headers, utilisez vos clés Turnstile.
Les ressources statiques de Cloudflare Pages répondent 405 à un POST : écrivez une Pages Function, ou envoyez le formulaire à un backend de formulaires. Avec Ikiform, vous n'écrivez aucune Function : réglez l'action du formulaire sur votre endpoint, ajoutez your-project.pages.dev aux Allowed domains, ce qui couvre aussi toutes les URL de préproduction, et listez Ikiform dans la Content-Security-Policy de votre _headers. Si vous utilisez déjà Cloudflare, vous pouvez brancher vos propres clés Turnstile sur le formulaire.
Ce qui change sur Cloudflare Pages
Pages sert le résultat de votre build depuis le réseau de Cloudflare. Les ressources statiques répondent à GET ; un POST vers l'une d'elles reçoit 405 sans corps :
HTTP/2 405
server: cloudflare
content-length: 0Pages peut exécuter du code, via les Pages Functions d'un dossier functions/. Un functions/contact.ts pourrait accepter l'envoi, mais vous écririez alors un backend : analyse, validation, fournisseur d'e-mail, filtrage du spam et stockage. La documentation de Cloudflare recommande aussi de démarrer les nouveaux projets sur Workers plutôt que Pages. Envoyer vers un backend de formulaires évite les deux, et le même formulaire fonctionne tel quel si vous passez plus tard aux ressources statiques de Workers.
| Particularité de Pages | Conséquence pour le formulaire |
|---|---|
Les URL de préproduction sont des sous-domaines, comme abc123.your-project.pages.dev | Un seul domaine autorisé, your-project.pages.dev, couvre la production et toutes les préproductions. Les préproductions Vercel et Netlify demandent leurs propres entrées |
Les en-têtes viennent d'un fichier _headers dans le résultat du build | Jusqu'à 100 règles, 2 000 caractères par ligne. Ils ne s'appliquent pas aux réponses des Pages Functions |
| Turnstile est un produit Cloudflare | Vous pouvez utiliser la vérification gérée d'Ikiform sans clés, ou votre propre widget depuis le tableau de bord Cloudflare |
- Pages sert la page de contact, avec la Content-Security-Policy de votre fichier _headers.
- Le widget Turnstile s'exécute avec votre clé de site et place un jeton dans le formulaire.
- Le formulaire envoie ses champs et le jeton à Ikiform.
- Ikiform vérifie le jeton avec votre clé secrète et compare le nom d'hôte du jeton à vos domaines autorisés.
- Un envoi valide est stocké et envoyé par e-mail. Un jeton absent, expiré ou réutilisé reçoit 403 captcha_failed, et la page réinitialise le widget.
Le formulaire
Cette version utilise votre propre widget Turnstile, ce qui a du sens si vous gérez déjà Turnstile dans votre compte Cloudflare. Dans les réglages du formulaire, réglez Spam check sur Your own Turnstile keys et collez la clé de site et le secret. Le widget ajoute un champ cf-turnstile-response au formulaire, et le select team vous indique qui doit répondre :
<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>Le script garde les visiteurs sur la page. Un jeton Turnstile est à usage unique : après tout envoi échoué, il réinitialise donc le widget pour la tentative suivante :
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.";
});- Dans les réglages du widget Turnstile chez Cloudflare, listez les noms d'hôte où tourne le formulaire. Ikiform compare aussi le nom d'hôte du jeton aux Allowed domains du formulaire.
- Les envois sans jeton valide sont refusés avec
403 captcha_failed. Cela inclut un envoi HTML classique parti avant que le widget n'ait terminé. - Vous préférez ne gérer aucune clé ? Réglez Spam check sur Ikiform, supprimez le widget et son script, et l'envoi HTML passe la vérification sur ikiform.com. La version
fetcha alors besoin dechallenge()du SDK ; voir la vérification humaine.
Vos clés ou la vérification d'Ikiform
| Item | Vérification d'Ikiform | Vos propres clés Turnstile |
|---|---|---|
| Clés à gérer | Aucune | Une clé de site et un secret depuis votre tableau de bord Cloudflare |
| Modification du formulaire HTML | Aucune. La vérification s'exécute sur une page ikiform.com | Ajouter le widget et son script |
Version fetch | Appeler challenge() du SDK | Envoyer le cf-turnstile-response du widget |
| Content-Security-Policy | Seulement form-action et connect-src pour Ikiform | Aussi challenges.cloudflare.com dans script-src et frame-src |
| Statistiques Turnstile | Pas dans votre compte | Dans votre tableau de bord Cloudflare, à côté de vos autres widgets |
La vérification d'Ikiform demande moins de configuration. Vos propres clés ont du sens si vous suivez déjà les statistiques Turnstile, ou si vous voulez maîtriser l'apparence et l'emplacement du widget.
Tester le widget en local
Cloudflare publie des clés de site et des secrets de test qui réussissent ou échouent toujours, comme la paire toujours valide 1x00000000000000000000AA et 1x0000000000000000000000000000000AA. Les secrets de production rejettent les jetons créés avec une clé de test : ne les mélangez donc pas sur un même formulaire. Créez un second formulaire Ikiform pour le travail en local, donnez-lui la paire de clés de test, et changez ensemble le data-sitekey et l'id du formulaire. Laissez vides les Allowed domains de ce formulaire : Ikiform ne vérifie le nom d'hôte d'un jeton que lorsque la liste est définie, et Cloudflare ne documente pas le nom d'hôte que renvoie un jeton de test.
Le fichier _headers
Placez _headers dans votre répertoire de ressources statiques, comme public/, pour qu'il se retrouve dans le résultat du build. Turnstile a besoin de script-src et frame-src pour https://challenges.cloudflare.com, et Ikiform de form-action et 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.comSi vous ajoutez plus tard une Pages Function au même projet, ses réponses ne reçoivent pas ces en-têtes. Définissez-les dans le code de la Function.
Si vous passez à Workers
Passer le site sur les ressources statiques de Workers change la façon dont il se déploie, pas le fonctionnement du formulaire. Le formulaire envoie toujours vers ikiform.com, donc rien côté Cloudflare ne le traite. Vérifiez deux choses après la migration : le nouvel hôte workers.dev ou la nouvelle route figure dans Allowed domains, et vos en-têtes atteignent toujours la page, puisque Workers a sa propre façon de les définir.
your-project.pages.dev et votre domaine personnalisé dans Allowed domains. N'ajoutez jamais pages.dev seul : tous les projets Pages correspondraient.Vérifiez que tout fonctionne
- Poussez une branche et ouvrez son URL de préproduction. Envoyez, et vérifiez que le message arrive sans être marqué comme spam : la préproduction est un sous-domaine de l'hôte de votre projet.
- Envoyez à nouveau en production, puis revenez en arrière et renvoyez le même formulaire sans compléter le widget. Attendez-vous au message de réinitialisation, pas à une soumission stockée.
- Ouvrez Submissions dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.
- Vérifiez la boîte de réception de chaque destinataire des notifications. Chacun reçoit un e-mail avec les champs, et Reply répond à l'expéditeur quand le formulaire a un champ
email. - Rien n'est arrivé ? Passez le filtre de statut du tableau de bord sur Spam. Le spam reçoit la même réponse de succès qu'une vraie soumission.
Pièges
| Problème | Que faire |
|---|---|
405 avec un corps vide | Le formulaire a envoyé vers une URL Pages. Vérifiez l'action déployée, et qu'aucune Function ni règle de redirection ne réécrit /contact. |
| Le widget Turnstile n'apparaît jamais | La Content-Security-Policy bloque challenges.cloudflare.com. Ajoutez-le à script-src et frame-src, et vérifiez que _headers est bien dans le résultat du build. |
403 captcha_failed à chaque envoi | La clé de site et le secret viennent de widgets différents, le nom d'hôte n'est pas listé sur le widget, ou un secret de test est associé à une vraie clé de site. |
403 origin_not_allowed, ou tous les envois finissent en spam | Le domaine de la page n'est pas dans les Allowed domains du formulaire. Ajoutez-le, plus localhost pour les tests en local. Les sous-domaines d'un domaine listé sont autorisés. |
Questions
Non. Une Function est une façon d'accepter l'envoi, mais vous écrivez alors vous-même la validation, l'envoi d'e-mails et le filtrage du spam. Envoyer vers un backend de formulaires garde le projet entièrement statique.
Non. Les URL de préproduction de Pages sont des sous-domaines de your-project.pages.dev : autoriser cet hôte les couvre toutes.
Oui. Réglez la vérification anti-spam du formulaire sur vos propres clés Turnstile et collez la clé de site et le secret. Ikiform vérifie chaque jeton et compare son nom d'hôte à vos domaines autorisés.
Il a envoyé vers votre propre URL Pages, qui sert des ressources statiques ne répondant qu'aux requêtes GET. Faites pointer l'action du formulaire vers un backend de formulaires, ou ajoutez une Pages Function pour ce chemin.
Non, il n'y a pas d'offre gratuite. Les tests sont gratuits : vous pouvez envoyer 50 soumissions de test par mois depuis localhost avant de payer. Pour passer en production, Pro commence à 6 $/mois pour 500 soumissions, ou vous pouvez payer une seule fois 10 000 soumissions pour 49 $ ou 50 000 soumissions pour 149 $, avec des crédits qui n'expirent jamais.
Une étape vous bloque, ou vous voulez un guide pour votre cas ? Écrivez à hello@ikiform.com.