Puedes crear un formulario de contacto en Astro con Resend y Cloudflare y mantener tus páginas estáticas. El formulario envía la solicitud a un pequeño Worker de Cloudflare, que valida los datos y llama a Resend. El mismo backend permite ofrecer una suscripción opcional al newsletter y confirmar el correo antes de añadirlo a un segmento.
Esta guía se basa en los formularios de contacto y suscripción que implementamos en FastWeb. El ejemplo descargable incluye interfaces en español e inglés, protección antispam y el flujo completo de confirmación.
Descargar el ejemplo completo (ZIP). Incluye el componente de Astro, el Worker, el esquema SQL, los mensajes en ambos idiomas, instrucciones y pruebas automatizadas con envíos simulados. Los fragmentos de esta guía explican las partes importantes; utiliza la descarga para obtener la implementación completa.
Qué vas a construir
- Contacto: el visitante envía su nombre, email y consulta. Recibes un correo y puedes responderle directamente.
- Suscripción opcional: una casilla sin marcar permite solicitar el newsletter sin convertirlo en una condición para contactarte.
- Suscripción independiente: un componente reutilizable al final de las guías recoge el email y el consentimiento explícito.
- Confirmación por correo: la persona abre el email y pulsa un botón antes de que el Worker la añada al segmento correspondiente de Resend.
Astro → Worker de Cloudflare → Resend. Turnstile verifica los envíos; D1 guarda las confirmaciones pendientes, el consentimiento y los controles temporales de solicitudes. Resend gestiona los correos y los contactos. Esta arquitectura no requiere React ni renderizar el sitio en el servidor.
Un archivo HTML estático no puede procesar una solicitud POST por sí solo: aquí el Worker funciona como backend. La receta de formularios de Astro con renderizado en el servidor es otra alternativa si tu proyecto ya utiliza un adaptador; este ejemplo utiliza un Worker independiente.
Prepara el sitio y los servicios
- Un sitio en Astro y conocimientos básicos de componentes, variables de entorno y terminal. Comprobamos el ejemplo con Node.js 24, Astro 7.3.5 y Wrangler 4.147.0.
- Una cuenta de Cloudflare con acceso a Workers y D1, y un dominio con zona activa y un registro DNS con el proxy de Cloudflare habilitado. Puedes conservar el hosting estático que ya utilizas.
- Una cuenta de Resend con un dominio de envío verificado. Añade los registros DNS que indique Resend; el remitente debe pertenecer a ese dominio.
- Un widget de Cloudflare Turnstile configurado para el hostname del sitio. Distingue su clave pública de su clave secreta.
- Dos segmentos en Resend, por ejemplo Newsletter Español y Newsletter Inglés. Copia sus identificadores.
Este Worker combina contacto y suscripción, por lo que necesita una clave dedicada de Resend con
permiso full_access: enviará correos y administrará contactos y segmentos. Una clave
sending_access sirve para un formulario que solo envía consultas, pero no completa
este flujo de suscripción. Guarda la clave con acceso completo únicamente como secreto del Worker.
Abre el ejemplo y configura tu dominio
Descomprime la descarga y ejecuta estos comandos desde su carpeta raíz:
npm install
npm test
npm run dev
La vista previa incluye /contact/, /es/contacto/,
/newsletter/ y /es/newsletter/. Los envíos permanecen desactivados hasta
que los habilites. Las páginas de demostración tienen noindex: sirven para probar e integrar el
componente en tu sitio.
Estos son los archivos principales:
-
src/components/EmailForm.astro: ambos formularios y sus mensajes en el navegador. -
workers/forms/index.ts: validación, envío de consultas, suscripción y confirmación. -
workers/forms/page.tsymessages.ts: correos, página de confirmación y traducciones. workers/forms/schema.sql: tablas de la base de datos.-
workers/forms/wrangler.jsonc: configuración del despliegue y valores que no son secretos.
Sustituye el dominio, la marca y el correo de ejemplo en los archivos y la configuración.
Reemplaza los identificadores de segmentos y de D1 por los tuyos. Estas son las opciones
relevantes de wrangler.jsonc; el fragmento va dentro del archivo completo incluido en
la descarga:
"routes": [
{ "pattern": "example.com/api/forms/*", "zone_name": "example.com" }
],
"vars": {
"SITE_ORIGIN": "https://example.com",
"TURNSTILE_HOSTNAME": "example.com",
"RESEND_SEGMENT_EN": "YOUR_ENGLISH_SEGMENT_ID",
"RESEND_SEGMENT_ES": "YOUR_SPANISH_SEGMENT_ID",
"MAIL_FROM": "Example <hello@example.com>",
"MAIL_TO": "hello@example.com"
}
SITE_ORIGIN debe contener el origen HTTPS exacto, sin barra final. Si utilizas
www.example.com, úsalo tanto en el origen como en el hostname de Turnstile y en el
patrón de la ruta; zone_name sigue siendo example.com.
La ruta solo cubre /api/forms/*. No asignes el Worker del formulario a todo el sitio.
Revisa las rutas existentes: cuando coinciden varias, Cloudflare utiliza la más específica.
Crea D1 y guarda los secretos
Autentica Wrangler y crea la base de datos. Copia el identificador obtenido en
d1_databases[0].database_id antes de ejecutar el comando SQL:
npx wrangler login
npx wrangler d1 create astro-forms
npx wrangler d1 execute astro-forms --remote \
--config workers/forms/wrangler.jsonc \
--file workers/forms/schema.sql
npx wrangler secret put RESEND_API_KEY \
--config workers/forms/wrangler.jsonc
npx wrangler secret put TURNSTILE_SECRET \
--config workers/forms/wrangler.jsonc
Los dos comandos secret put solicitan los valores de forma interactiva. Si todavía no
existe el Worker, acepta la propuesta de Wrangler para crearlo. La clave de Resend y el secreto de
Turnstile se guardan en Cloudflare, nunca en componentes de Astro, variables públicas o el
repositorio.
Usamos D1 porque la confirmación necesita conservar su estado. El ejemplo guarda el hash del token aleatorio, su vencimiento y el idioma solicitado. Los comprobantes de consultas guardan un hash del contenido, sin almacenar el mensaje. Los registros de consentimiento conservan email, ruta de origen, idioma, fechas y versión del texto aceptado; define una política de conservación y eliminación apropiada para tu sitio.
Añade el formulario de contacto en Astro
Si ya tienes un sitio, copia EmailForm.astro a tu carpeta de componentes e impórtalo
en la página de contacto. Ajusta la ruta del import a tu estructura:
---
import EmailForm from '../components/EmailForm.astro';
---
<EmailForm locale="es" kind="contact" />
En la página en inglés utiliza locale="en". La casilla del newsletter es opcional y
aparece sin marcar. Enviar una consulta sin seleccionarla genera únicamente el correo de contacto.
Configura estos valores públicos de compilación en el entorno de tu hosting, o en
un archivo local .env excluido de Git al compilar:
PUBLIC_TURNSTILE_SITE_KEY=YOUR_PUBLIC_SITE_KEY
PUBLIC_FORMS_ENABLED=true
La clave del widget es pública por diseño. La clave de Resend y el secreto de Turnstile nunca
deben llevar el prefijo PUBLIC_. Vuelve a compilar Astro después de cambiar estos
valores y habilita los envíos cuando el backend esté configurado.
El componente carga Turnstile cuando alguien interactúa con el formulario, muestra el resultado en un área accesible y renueva la verificación después de intentar un envío. La versión incluida admite un formulario por página. Actualiza los enlaces a tu política de privacidad y tu dirección de contacto antes de utilizarlo.
Valida la solicitud y envía con Resend
El navegador envía JSON a /api/forms/es/contact o /api/forms/en/contact.
El Worker comprueba el origen, el tamaño, los campos y los límites de solicitudes. También envía
el token de Turnstile a Siteverify y comprueba success, hostname y
action. Mostrar el widget no sustituye la validación en el servidor.
Una vez realizadas esas comprobaciones, la llamada de envío sigue este patrón. El Worker completo también controla el tamaño de las respuestas, el estado en la base de datos y los errores:
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `contact-${requestId}`,
},
body: JSON.stringify({
from: env.MAIL_FROM,
to: [env.MAIL_TO],
reply_to: email,
subject: 'Website inquiry',
text: `${name}\n\n${message}`,
}),
signal: AbortSignal.timeout(8000),
});
if (!response.ok) throw new Error('Email was not accepted');
from contiene tu remitente verificado, to tu buzón fijo de recepción y
reply_to el email validado del visitante. Así puedes responder normalmente sin
intentar enviar desde el dominio de otra persona. La versión HTML del ejemplo escapa el texto
introducido por el usuario.
El formulario conserva el identificador al reintentar una consulta que no cambió. El Worker lo
utiliza en la cabecera Idempotency-Key de Resend para reducir envíos duplicados;
Resend conserva esas claves durante 24 horas. Si la suscripción opcional falla después de aceptar
la consulta, el mensaje lo explica sin pedir que se envíe otra vez la consulta.
Añade la suscripción al final de los artículos
Coloca el componente después del contenido en el layout compartido por tus artículos:
---
import EmailForm from '../components/EmailForm.astro';
---
<article><slot /></article>
<EmailForm locale="es" kind="newsletter" />
Utiliza el idioma inglés en su layout correspondiente, o pasa la variable de idioma que ya tenga tu sitio. Se modifica el layout una sola vez: todos los artículos que lo usan incorporan el formulario. No necesitas editar cada entrada.
El formulario de newsletter exige marcar el consentimiento. Llama a
/api/forms/{locale}/subscribe; el Worker envía el correo de confirmación, pero
todavía no crea un contacto de marketing. El idioma de la ruta determina el segmento en el
servidor. El navegador no elige identificadores de segmentos.
Confirma el correo antes de añadir el contacto
- Genera un token aleatorio mediante criptografía y guarda únicamente su hash SHA-256 en D1, con vencimiento a las 24 horas.
-
Envía un enlace como
https://example.com/api/forms/es/confirm#TOKEN. El fragmento evita que el token viaje en la solicitud HTTP inicial. - Muestra la página de confirmación. Su script elimina el fragmento de la barra de direcciones y espera a que la persona pulse el botón.
- Envía el token por POST al endpoint de confirmación. Comprueba hash, idioma, vencimiento y uso previo.
- Busca o crea el contacto en Resend, añádelo al segmento configurado para ese idioma y registra el consentimiento.
Abrir el enlace mediante GET no suscribe a nadie. Esto reduce las confirmaciones accidentales provocadas por verificadores simples de enlaces de correo. Un token usado no puede reactivar a alguien que luego se dio de baja, y el ejemplo no revierte automáticamente las bajas globales existentes. Se conservan los nombres y las pertenencias a otros segmentos de los contactos ya registrados.
El correo de confirmación es transaccional. Los newsletters posteriores son campañas de marketing independientes: prepáralos en Broadcasts de Resend, selecciona el segmento correcto e incluye el mecanismo de baja. El formulario no programa ni envía campañas.
Prueba, publica y comprueba el flujo completo
Ejecuta las verificaciones, publica el Worker independiente y compila el sitio estático:
npm run types
npm run check:worker
npm run check
npm test
npm run dry-run
npx wrangler deploy --config workers/forms/wrangler.jsonc
npm run build
Publica la carpeta dist/ con el proceso habitual de tu hosting de Astro. Desplegar el
Worker no publica las páginas. En desarrollo local, la vista previa de Astro no ejecuta el Worker:
utiliza las pruebas automatizadas o configura un proxy local y credenciales de prueba separadas
antes de probar todo el flujo.
Las pruebas incluidas simulan Resend y Turnstile: no envían correos ni consultan servicios de producción. Antes de habilitar el formulario para visitantes, prueba con tu propio buzón en el dominio configurado:
- Una consulta sin marcar la casilla llega una sola vez y no genera una suscripción.
- Una consulta con la casilla marcada llega y genera un correo de confirmación independiente.
- Una solicitud de newsletter no crea el contacto de marketing antes de confirmarse.
- El correo confirmado entra en el segmento del idioma correcto; un enlace vencido o reutilizado no cambia el estado de suscripción.
- La validación, los desafíos de Turnstile vencidos, los fallos del proveedor y los reintentos muestran mensajes útiles.
- El diseño móvil, la navegación con teclado y los enlaces de privacidad funcionan. Revisa los eventos de entrega en Resend y el buzón: la aceptación de la API no demuestra que el mensaje haya llegado.
Problemas frecuentes
-
404 al enviar: revisa el hostname con proxy, la ruta del Worker y la URL exacta
/api/forms/{locale}/.... -
Error de origen o Turnstile: alinea hostname,
SITE_ORIGIN, dominios permitidos del widget y acción esperada. Un token vencido requiere una verificación nueva. - Se envía el correo pero no aparece la suscripción: comprueba la confirmación, el permiso para administrar contactos y el identificador del segmento.
- Contactos importados antiguos aparecen duplicados: revisa mayúsculas, minúsculas e identificadores existentes antes de importar o sincronizar. Normalizar los datos nuevos no corrige por sí solo los duplicados históricos.
- 503 o límite de solicitudes: revisa estados y límites del proveedor sin registrar secretos ni el contenido de las consultas. El ejemplo limita las solicitudes y su tamaño; un sitio con más tráfico puede necesitar una cola y otra política de reintentos.
Documentación y próximos pasos
- Astro: crear formularios HTML
- Resend: envíos desde Cloudflare Workers
- Permisos de las claves de Resend y añadir un contacto a un segmento
- Validación de Turnstile en el servidor
- Secretos de Cloudflare, rutas del Worker y configuración de D1
- Claves de idempotencia en Resend
¿Utilizas WordPress? Consulta nuestra guía de formularios de contacto para WordPress. Si necesitas adaptar esta integración a tu sitio en Astro, contacta con FastWeb.

