Guías por framework
Añade un formulario de contacto a un sitio Astro
La salida estática por defecto de Astro no puede recibir un POST por sí sola, y pasar a renderizado en servidor solo por un formulario de contacto es excesivo. Apunta el formulario a embedio y mantén el sitio totalmente estático.
1. Crea el formulario en embedio
Regístrate (el plan Gratis incluye 500 envíos al mes y no pide tarjeta), crea un formulario y copia su clave. Tiene la forma frm_ seguida de diez caracteres. En todo lo que sigue, cambia frm_your_form_key por la tuya.
La clave del formulario está pensada para ser pública: solo permite enviar a ese formulario, nunca leer nada. Puedes ponerla en código del cliente y en tu repositorio.
2. Un componente de formulario
Esto no envía JavaScript. El navegador envía el formulario y embedio manda a la persona a /thanks de tu sitio después.
---
const endpoint = "https://api.tryembedio.com/f/frm_your_form_key";
---
<form id="contact" action={endpoint} method="POST">
<label for="email">Email</label>
<input id="email" type="email" name="email" required>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<!-- Honeypot: hidden from people, filled in by bots -->
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none">
<!-- Where to send people after a successful submit (same site) -->
<input type="hidden" name="_redirect" value="/thanks">
<button type="submit">Send</button>
</form>Úsalo en cualquier página con <ContactForm /> y añade src/pages/thanks.astro para la confirmación.
3. Opcional: enviar sin salir de la página
Añade este script al mismo componente. Astro lo empaqueta una sola vez, y si no carga, el formulario simple de arriba sigue funcionando.
<p id="contact-status" role="status" aria-live="polite"></p>
<script>
const form = document.querySelector<HTMLFormElement>("#contact");
const status = document.querySelector("#contact-status");
form?.addEventListener("submit", async (event) => {
event.preventDefault();
if (status) status.textContent = "Sending…";
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const body = await response.json().catch(() => ({}));
if (response.ok) {
form.reset();
if (status) status.textContent = body.message ?? "Thanks! Your message is on its way.";
} else if (status) {
status.textContent =
body.error === "validation_failed"
? body.fields.map((f: { message: string }) => f.message).join(" ")
: "Something went wrong. Please try again.";
}
});
</script>Enviar FormData mantiene funcionando los campos de archivo: llama file al campo y activa el módulo Uploads. Como la petición no usa un content type JSON, es la cabecera Accept la que hace que embedio responda con JSON.
Spam y validación
- Honeypot: un campo oculto
_gotcha. Las personas nunca lo ven; los bots que lo rellenan reciben un éxito aparente y no se guarda nada. - Doble clic: un envío idéntico segundos después del primero se descarta en lugar de guardarse dos veces.
- Nivel de confianza: cada formulario tiene un ajuste relajado, equilibrado o estricto para ráfagas y contenido repetido. Los envíos marcados quedan aparte para revisarlos, no se borran.
- Reglas en el servidor: las validaciones como obligatorio, email o longitud máxima se ejecutan en embedio, así que se cumplen aunque alguien se salte tu front end.
Recibir avisos
Cada envío aparece en tu panel al instante. Para recibir además un email por cada uno, añade una conexión de Email al formulario; funciona en todos los planes, incluido Gratis. Slack, Discord, Zapier, webhooks y las demás conexiones vienen con cualquier plan de pago, desde $9/mes.
Errores que puedes ver
Con una petición JSON, cada fallo vuelve como un código de estado más { "error": "..." }. Un envío HTML normal recibe en su lugar una página de error legible.
| Estado | error | Qué significa |
|---|---|---|
| 404 | form_not_found | La clave es incorrecta o el formulario se borró. |
| 422 | validation_failed | Falló una regla de validación; fields lista { field, message } por cada una. |
| 402 | monthly_limit_reached | La cuenta agotó sus envíos del mes. |
| 413 | file_too_large | Un archivo subido supera los 10MB. |
| 400 | invalid_file_type | El tipo de archivo no se acepta (sí se aceptan imágenes, PDF, documentos de Office, TXT y CSV). |
| 429 | Más de 20 envíos por minuto desde una misma IP. Espera y reintenta. |
La lista completa, con los errores propios de cada módulo, está en la referencia de la API.