Formulario de contacto en un sitio estático7 min de lectura
Añade un formulario de contacto a un sitio de Cloudflare Pages
Añade un formulario de contacto a Cloudflare Pages sin escribir una Function: envía a un backend de formularios, define cabeceras en _headers y usa tus propias claves de Turnstile.
Los assets estáticos de Cloudflare Pages responden a un POST con 405, así que o escribes una Pages Function o envías el formulario a un backend de formularios. Con Ikiform no escribes ninguna Function: pon tu endpoint en el action del formulario, añade your-project.pages.dev a Allowed domains, que también cubre todas las URL de preview, e incluye Ikiform en la Content-Security-Policy de tu _headers. Si ya usas Cloudflare, puedes conectar tus propias claves de Turnstile al formulario.
Qué cambia en Cloudflare Pages
Pages sirve el resultado de tu build desde la red de Cloudflare. Los assets estáticos responden a GET; un POST a uno recibe 405 sin cuerpo:
HTTP/2 405
server: cloudflare
content-length: 0Pages puede ejecutar código, mediante Pages Functions en una carpeta functions/. Un functions/contact.ts podría aceptar el envío, pero entonces estás escribiendo un backend: lectura, validación, un proveedor de email, filtrado de spam y almacenamiento. La propia documentación de Cloudflare recomienda además empezar los proyectos nuevos en Workers en lugar de Pages. Enviar a un backend de formularios evita ambas cosas, y el mismo formulario funciona sin cambios si más adelante pasas a los static assets de Workers.
| Rasgo de Pages | Qué significa para el formulario |
|---|---|
Las URL de preview son subdominios, como abc123.your-project.pages.dev | Un solo dominio permitido, your-project.pages.dev, cubre producción y todas las previews. Las previews de Vercel y Netlify necesitan sus propias entradas |
Las cabeceras salen de un archivo _headers en el resultado del build | Hasta 100 reglas, 2.000 caracteres por línea. No se aplican a las respuestas de Pages Functions |
| Turnstile es un producto de Cloudflare | Puedes usar la verificación gestionada de Ikiform sin claves, o tu propio widget desde el panel de Cloudflare |
- Pages sirve la página de contacto, con la Content-Security-Policy de tu archivo _headers.
- El widget de Turnstile se ejecuta con tu site key y pone un token en el formulario.
- El formulario envía sus campos y el token a Ikiform.
- Ikiform verifica el token con tu clave secreta y compara el hostname del token con tus dominios permitidos.
- Un envío válido se guarda y se manda por email. Un token que falta, caducado o reutilizado recibe 403 captcha_failed, y la página reinicia el widget.
El formulario
Esta versión usa tu propio widget de Turnstile, lo que tiene sentido cuando ya gestionas Turnstile en tu cuenta de Cloudflare. En los ajustes del formulario, pon Spam check en Your own Turnstile keys y pega la site key y el secreto. El widget añade un campo cf-turnstile-response al formulario, y el select team te dice quién debe responder:
<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>El script mantiene a los visitantes en la página. Un token de Turnstile es de un solo uso, así que tras cualquier envío fallido reinicia el widget para el siguiente intento:
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.";
});- En los ajustes del widget de Turnstile en Cloudflare, indica los hostnames en los que funciona el formulario. Ikiform también compara el hostname del token con Allowed domains del formulario.
- Los envíos sin un token válido se rechazan con
403 captcha_failed. Eso incluye un envío HTML clásico hecho antes de que el widget terminara. - ¿Prefieres no tener ninguna clave? Pon Spam check en Ikiform, borra el widget y su script, y el envío HTML hace la verificación en ikiform.com. La versión con
fetchnecesita entonceschallenge()del SDK; consulta la verificación humana.
Tus claves o la verificación de Ikiform
| Item | Verificación de Ikiform | Tus propias claves de Turnstile |
|---|---|---|
| Claves que gestionar | Ninguna | Una site key y un secreto de tu panel de Cloudflare |
| Cambio en el formulario HTML | Ninguno. La verificación se hace en una página de ikiform.com | Añadir el widget y su script |
Versión con fetch | Llamar a challenge() del SDK | Enviar el cf-turnstile-response del widget |
| Content-Security-Policy | Solo form-action y connect-src para Ikiform | También challenges.cloudflare.com en script-src y frame-src |
| Analíticas de Turnstile | No en tu cuenta | En tu panel de Cloudflare, junto a tus otros widgets |
La verificación de Ikiform requiere menos configuración. Tus propias claves tienen sentido cuando ya sigues las analíticas de Turnstile, o quieres controlar el aspecto y la posición del widget.
Probar el widget en local
Cloudflare publica site keys y secretos de prueba que siempre aprueban o siempre fallan, como el par que siempre aprueba 1x00000000000000000000AA y 1x0000000000000000000000000000000AA. Los secretos de producción rechazan los tokens generados con una clave de prueba, así que no los mezcles en un mismo formulario. Crea un segundo formulario de Ikiform para trabajar en local, dale el par de claves de prueba y cambia el data-sitekey y el id del formulario a la vez. Deja vacío Allowed domains en ese formulario: Ikiform solo comprueba el hostname de un token cuando la lista está configurada, y Cloudflare no documenta qué hostname indica un token de prueba.
El archivo _headers
Pon _headers en tu directorio de assets estáticos, como public/, para que acabe en el resultado del build. Turnstile necesita script-src y frame-src para https://challenges.cloudflare.com, e Ikiform necesita form-action y 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 más adelante añades una Pages Function al mismo proyecto, sus respuestas no reciben estas cabeceras. Defínelas en el código de la Function.
Si te pasas a Workers
Mover el sitio a los static assets de Workers cambia cómo se despliega, no cómo funciona el formulario. El formulario sigue enviando a ikiform.com, así que nada en el lado de Cloudflare lo gestiona. Comprueba dos cosas después del cambio: que el nuevo host workers.dev o la ruta esté en Allowed domains, y que tus cabeceras sigan llegando a la página, ya que Workers tiene su propia forma de definirlas.
your-project.pages.dev y tu dominio personalizado en Allowed domains. Nunca añadas pages.dev por sí solo: coincidiría cualquier proyecto de Pages.Comprueba que funciona
- Sube una rama y abre su URL de preview. Envía y confirma que el mensaje llega y no se marca como spam: la preview es un subdominio del host de tu proyecto.
- Envía otra vez en producción, luego vuelve atrás y envía el mismo formulario una segunda vez sin completar el widget. Deberías ver el mensaje de reinicio, no un envío guardado.
- Abre Submissions en el panel. El nuevo envío está ahí con todos los campos que enviaste.
- Revisa la bandeja de entrada de cada destinatario de notificaciones. Cada uno recibe un email con los campos, y Reply va a quien envió el formulario cuando este tiene un campo
email. - ¿No llegó nada? Cambia el filtro de estado del panel a Spam. El spam recibe la misma respuesta de éxito que un envío real.
Problemas habituales
| Problema | Qué hacer |
|---|---|
405 con el cuerpo vacío | El formulario envió a una URL de Pages. Revisa el action desplegado, y que ninguna Function ni regla de redirección reescriba /contact. |
| El widget de Turnstile no aparece nunca | La Content-Security-Policy bloquea challenges.cloudflare.com. Añádelo a script-src y frame-src, y comprueba que _headers está en el resultado del build. |
403 captcha_failed en todos los envíos | La site key y el secreto son de widgets distintos, el hostname no está en la lista del widget, o un secreto de prueba está emparejado con una site key real. |
403 origin_not_allowed, o todos los envíos acaban en spam | El dominio de la página no está en Allowed domains del formulario. Añádelo, más localhost para las pruebas locales. Los subdominios de un dominio de la lista están permitidos. |
Preguntas
No. Una Function es una forma de aceptar el envío, pero entonces escribes tú la validación, el envío de emails y el filtrado de spam. Enviar a un backend de formularios mantiene el proyecto totalmente estático.
No. Las URL de preview de Pages son subdominios de your-project.pages.dev, así que permitir ese host las cubre todas.
Sí. Configura la comprobación antispam del formulario con tus propias claves de Turnstile y pega la site key y el secreto. Ikiform verifica cada token y compara su hostname con tus dominios permitidos.
Envió a tu propia URL de Pages, que sirve assets estáticos que solo responden a peticiones GET. Apunta el action del formulario a un backend de formularios, o añade una Pages Function para esa ruta.
No, no hay plan gratuito. Probar es gratis: puedes enviar 50 envíos de prueba al mes desde localhost antes de pagar. Para pasar a producción, Pro empieza en 6 $/mes por 500 envíos, o puedes pagar una sola vez 10.000 envíos por 49 $ o 50.000 envíos por 149, $ con créditos que no caducan nunca.
¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.