Guías

Guías17 min de lectura

Cómo añadir un formulario de contacto a un sitio estático sin backend

Añade un formulario de contacto que funcione a un sitio estático sin servidor: a dónde va el POST, el spam, las respuestas por email y código para cada host y framework.

Preet SutharFundador, Ikiform

Un host estático sirve archivos y rechaza el POST de un formulario, normalmente con 405 o 404, así que el formulario tiene que enviar a otro sitio. Apunta el action del formulario a un backend de formularios como Ikiform, que guarda cada mensaje, filtra el spam y te envía un email con el remitente como dirección de respuesta. Tu sitio sigue siendo estático: sin código de servidor, sin claves secretas en la página y sin cambios en cómo despliegas.

Un sitio estático es una carpeta de HTML, CSS y JavaScript que un host entrega como archivos. Por eso es rápido, barato y difícil de romper. Y también por eso el formulario de contacto es lo primero que deja de funcionar: el envío de un formulario es una petición que algo tiene que recibir, y en un host estático no hay nada escuchando.

Por qué falla el formulario en un host estático

Un <form method="post"> sin action envía a la misma página en la que está. En un host estático eso es un archivo, y los archivos solo responden a GET y HEAD. El 2026-10-25 enviamos un POST a una página estática publicada en cada uno de los hosts principales. Ninguno guardó nada:

HostQué recibe un POST a una página estática
GitHub Pages405 Not Allowed, como una pequeña página HTML
Vercel405 con el cuerpo vacío
Netlify404, con la página 404 del sitio si tiene una, salvo que Netlify Forms haya detectado ese formulario al desplegar
Cloudflare Pages405 con el cuerpo vacío
Endpoint de sitio web de Amazon S3405 Method Not Allowed, con el código MethodNotAllowed

La solución es la misma en todas partes: manda el envío a una URL que lo acepte. Esa URL puede ser código que escribes y alojas tú, una función de tu host o un backend de formularios. El resto de esta guía usa un backend de formularios, porque así el sitio sigue siendo estático.

A dónde va el envío de un formulario de contacto, antes y después de la soluciónaction apunta a tu propio sitioaction apunta a Ikiformpágina de contactoHTML estáticohost estáticosirve archivos,responde a GET405 o 404mensaje perdidopágina de contactomismo HTML, nuevoactionIkiformrevisa, guarda, avisapágina de graciasde vuelta en tu sitioPOSTPOST303A dónde va el envío de un formulario de contacto, antes y después de la soluciónaction apunta a tu propio sitioaction apunta a Ikiformpágina de contactoHTML estáticohost estáticosirve archivos, responde a GET405 o 404mensaje perdidopágina de contactomismo HTML, nuevo actionIkiformrevisa, guarda, avisapágina de graciasde vuelta en tu sitioPOSTPOST303
  1. Sin action, o con un action que apunta a una ruta de tu propio sitio, el navegador envía al host estático.
  2. El host no tiene código que ejecutar para un POST, así que responde 405 o 404 y el mensaje se pierde.
  3. Con el action apuntando a tu endpoint de Ikiform, el envío llega a un servidor que lo acepta.
  4. Ikiform revisa el mensaje en busca de spam, lo guarda y te envía un email.
  5. Ikiform responde con una redirección 303 y el navegador carga la página de agradecimiento de tu propio sitio.
Solo cambia el action del formulario. La página, el host y el despliegue se quedan como están.

Qué pasa con cada mensaje

Ikiform es el endpoint al que envía el formulario. No aloja tu página ni renderiza tu formulario; ambos siguen siendo tuyos. Con cada envío ejecuta las comprobaciones antispam, guarda el mensaje y lo reparte a los sitios donde lo lees.

Qué hace Ikiform con un mensaje del formulario de contactoformulario decontactoen cualquier hostestáticoIkiformrevisa spam y guardaemail denotificaciónLa respuesta va alremitentepanelbúsqueda, filtro de spam,CSVwebhook firmadoSlack, Discord, tu códigoenvíoQué hace Ikiform con un mensaje del formulario de contactoformulariode contactoen cualquierhost estáticoIkiformrevisa spam yguardaemail denotificaciónLa respuesta va alremitentepanelbúsqueda, filtro despam, CSVwebhook firmadoSlack, Discord, tucódigoenvío
  1. Un visitante rellena el formulario de contacto y este se envía a tu endpoint de Ikiform.
  2. Ikiform comprueba el honeypot, el límite de frecuencia y la puntuación de spam, y luego guarda el mensaje.
  3. Cada destinatario de notificaciones recibe un email, con la dirección de respuesta configurada como la del remitente.
  4. El mensaje aparece en el panel, donde puedes buscarlo, recuperar spam y exportar a CSV.
  5. Si añades un webhook, Ikiform envía una copia firmada a Slack, Discord, una herramienta de automatización o tu propio código.
El email basta para la mayoría de los formularios de contacto. Añade un webhook cuando los mensajes deban llegar a donde ya trabajas.

Cinco formas de recibir el formulario de un sitio estático

EnfoqueDestaca enContras
Un enlace mailto: o action="mailto:..."Cero configuración. Sirve para una página personal con poco tráfico.Abre la app de correo del visitante, si tiene una configurada. No se guarda ni se filtra nada, y tu dirección queda en el HTML al alcance de los scrapers.
La función de formularios de tu host, como Netlify FormsUn atributo, ninguna otra cuenta y normalmente incluido en el plan de hosting.Solo funciona en ese host. Cambiar de host implica rehacer el formulario, y los formularios renderizados con JavaScript necesitan apaños.
Una función serverless que escribes túControl total sobre la validación, el almacenamiento y el email que envías.Ahora tienes un backend que mantener: una clave de API de email, filtrado de spam, límites de frecuencia, reintentos y logs. El formato de las funciones cambia en cada host.
Un formulario alojado incrustado, como Google Forms, Tally o TypeformSin código. Bueno para formularios largos o de varios pasos.Un iframe o un enlace a otro sitio, con su aspecto y sus scripts en tu página.
Un backend de formularios, como Ikiform, Formspree, Basin o Web3FormsTu propio HTML en cualquier host. Almacenamiento, filtrado de spam y email sin servidor.Otra cuenta y otra factura, y los datos del formulario viven en ese servicio.

Si estás en Netlify y no piensas irte nunca, Netlify Forms es difícil de superar en esfuerzo. Si quieres escribir el backend, una función es un buen proyecto de fin de semana. Si quieres que el formulario siga funcionando cuando cambies de host o de framework, usa un backend de formularios. Las comparativas ponen los backends uno al lado del otro.

El formulario

Todos los ejemplos de abajo envían desde el navegador, porque un despliegue estático no tiene servidor desde el que enviar. Elige tu stack; la elección se mantiene en todos los ejemplos de código de estas guías. Los generadores de sitios estáticos tienen sus propias páginas de configuración: Jekyll, Hugo, Eleventy y Gatsby.

Esta es toda la integración para un sitio escrito a mano. El formulario envía de la forma clásica, así que funciona con JavaScript desactivado, y el navegador llega a tu thanks.html tras un envío correcto.

contact.html
<form id="contact" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" maxlength="120" required />
  <p class="field-error" data-error-for="name" hidden></p>

  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <p class="field-error" data-error-for="email" hidden></p>

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" minlength="10" maxlength="5000" required></textarea>
  <p class="field-error" data-error-for="message" hidden></p>

  <input type="hidden" name="_redirect" value="https://example.com/thanks.html" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button type="submit">Send message</button>
  <p id="contact-status" role="status"></p>
</form>
<p id="contact-sent" role="status" hidden>Thanks. Your message is on its way, and I'll reply by email.</p>

<script src="/contact.js" defer></script>

El script es opcional. Mantiene al visitante en la página, pone cada error de campo debajo de su input y conserva el mensaje escrito cuando algo falla.

contact.js
const form = document.querySelector("#contact");
const status = document.querySelector("#contact-status");
const done = document.querySelector("#contact-sent");
const button = form.querySelector("button");

function showErrors(fields = {}) {
  for (const slot of form.querySelectorAll("[data-error-for]")) {
    const name = slot.dataset.errorFor;
    const message = fields[name]?.[0] ?? "";
    slot.textContent = message;
    slot.hidden = !message;
    form.elements[name]?.setAttribute("aria-invalid", message ? "true" : "false");
  }
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  button.disabled = true;
  status.textContent = "Sending";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      form.hidden = true;
      done.hidden = false;
      return;
    }
    showErrors(result.error.fields);
    status.textContent = result.error.message;
  } catch {
    status.textContent = "Couldn't send. Check your connection and try again.";
  }
  button.disabled = false;
});
  • Sustituye frm_xxxxxxxxxxxxxxxx por el id de tu formulario que aparece en el panel. El id es público por diseño: está en tu HTML desde el momento en que despliegas. Nunca pongas una clave de API secreta en un sitio estático.
  • Mantén email como nombre del campo de email. Ikiform lo usa como dirección de respuesta de los emails de notificación y lo compara con los dominios de email desechables.
  • El input oculto _gotcha es el honeypot. Las personas nunca lo ven; los bots que rellenan todos los inputs se marcan solos como spam.
  • Las versiones con JavaScript envían Accept: application/json, así que Ikiform responde 201 si todo va bien, o 422 con un objeto error.fields que nombra cada campo y su problema.

Si tu framework sí se ejecuta en un servidor en producción, puedes enviar desde ahí y dejar el endpoint fuera de la página. Las guías de Next.js, SvelteKit y Nuxt muestran las versiones de servidor, y Desde un servidor explica las claves secretas.

Las versiones de servidor envían con una clave secreta, así que Ikiform se salta Allowed domains (dominios permitidos) y su verificación humana. Todos los envíos vienen entonces de la IP de tu servidor, y el límite Standard de 5 por minuto por visitante cuenta a todos tus visitantes juntos. Elige el límite de frecuencia Relaxed o Custom para un formulario que envías a través de un servidor, y guarda IKIFORM_SECRET_KEY en variables de entorno solo de servidor.

Campos de un formulario de contacto

Nombre, email y mensaje cubren casi cualquier formulario de contacto. Merece la pena añadir un select de tema cuando distintas personas responden distintos tipos de mensaje. Prueba el formulario de abajo: ejecuta el validador de Ikiform en tu navegador y muestra lo que se guardaría, o el 422 que recibirías.

Vista previa

Ejecuta el validador de Ikiform en tu navegador. No se envía nada.

La misma definición que la plantilla de formulario de contacto. Pégala en la pestaña Fields del formulario e Ikiform la aplica en el servidor, diga lo que diga el HTML.
CampoPor quéTen en cuenta
nameCómo lo saludas en la respuestaUn enlace en un campo de nombre es una señal fuerte de spam, así que mantén el id name
emailA dónde va la respuestaObligatorio. type="email" muestra el teclado adecuado en el móvil
topicManda ventas y soporte a la persona adecuadaUn select solo acepta los valores de la lista, así que es seguro filtrar por él
messageLa razón de ser del formularioUna longitud mínima de 10 frena los mensajes de una sola palabra

Deja fuera los números de teléfono, el tamaño de la empresa y todo lo que no vayas a usar en tu primera respuesta. Cada campo que añades te cuesta mensajes. Sin una definición de campos, Ikiform guarda cualquier input con nombre que envíe el formulario; con una, rechaza los envíos que no encajan.

La subida de archivos aún no está disponible. Un envío con un archivo recibe 422 files_not_supported.

Responde desde tu bandeja de entrada

Los emails de notificación van a hasta 10 destinatarios confirmados por formulario. Cuando el formulario tiene un campo email, la dirección de respuesta del email es la del remitente, así que Reply en tu app de correo le escribe directamente. Nunca tienes que copiar una dirección desde un panel.

Las notificaciones tienen un máximo de 50 emails por formulario por hora y luego un resumen por hora, así que una oleada de spam no puede enterrar tu bandeja de entrada. Los mensajes marcados como spam no envían ningún email. _subject no cambia el asunto de los emails de notificación. Se pasa a los webhooks y a la API como meta.subject.

Ikiform no envía emails a la persona que rellena el formulario. Los emails de notificación van solo a ti y a tu equipo, así que no hay respuesta automática, email de confirmación ni doble opt-in. Envíalos desde tu herramienta de email, activada por un webhook.

¿Quieres los mensajes en un chat de equipo en lugar del email, o además de él? Añade un webhook y sigue la guía de Slack, Discord o Telegram.

Qué ve el visitante después de enviar

Un envío HTML clásico termina en una redirección. Ikiform responde a un envío correcto del navegador con 303 See Other, y el navegador carga el destino con un GET, que tu host estático sirve sin problema.

Las tres peticiones detrás de un envío de formulario HTML clásiconavegadorel visitante pulsaEnviarikiform.com/f/…guarda el mensajetu /thanks.htmlun archivoestáticoPOST303, luego GETLas tres peticiones detrás de un envío de formulario HTML clásiconavegadorel visitante pulsa Enviarikiform.com/f/…guarda el mensajetu /thanks.htmlun archivo estáticoPOST303, luego GET
  1. El navegador envía los campos del formulario a tu endpoint de Ikiform.
  2. Ikiform guarda el mensaje y responde 303 See Other con una cabecera Location.
  3. El navegador sigue la redirección con un GET a tu página de agradecimiento, que el host estático sirve como cualquier otro archivo.
Sin un destino de redirección, el paso 3 carga la página de agradecimiento alojada de Ikiform en lugar de la tuya.

Ikiform elige el destino en este orden:

  • Redirect after submit en los ajustes del formulario. Funciona cualquier URL http: o https:, porque solo tu equipo puede configurarlo. Es la opción más sencilla para un solo sitio.
  • Un campo _redirect en el formulario, si es una URL https: en uno de los Allowed domains del formulario. Úsalo cuando un formulario sirve a varios sitios o idiomas.
  • Si no, la página de agradecimiento alojada de Ikiform, que enlaza de vuelta a tu sitio cuando el envío vino de una página https: permitida.

Haz que la página de agradecimiento sea un archivo real y mantenla fuera de los resultados de búsqueda. Di cuándo vas a responder; te ahorra un segundo email de la misma persona.

thanks.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="robots" content="noindex" />
    <title>Message sent</title>
  </head>
  <body>
    <h1>Thanks, your message is on its way</h1>
    <p>I read every message and reply within two working days, from the address on this site.</p>
    <p><a href="/">Back to the home page</a></p>
  </body>
</html>

Las versiones con JavaScript de arriba se saltan la redirección. Un envío con Accept: application/json recibe JSON, y la propia página muestra el mensaje de éxito.

Spam en un formulario estático

Tu endpoint está en el código fuente de tu página, y los bots leen el código fuente. Cuenta con recibir spam a los pocos días de publicar. Ikiform apila sus defensas para que puedas empezar sin configurar nada:

DefensaQué frenaConfiguración
HoneypotBots que rellenan todos los inputsEl input oculto _gotcha
Puntuación de spamEnlaces en el nombre (+3), muchos enlaces (+2), frases de spam conocidas (+2) y direcciones desechables. Con 5, el mensaje se guarda como spamNinguna
Límites de frecuenciaScripts que envían en bucleActivo por defecto: Standard, 5 por minuto por visitante
Dominios permitidosCopias de tu formulario que envían desde otros sitiosAñade tus dominios en los ajustes del formulario
Verificación humanaBots que manejan un navegador realPon Spam check en Ikiform para usar Cloudflare Turnstile sin claves

El spam recibe la misma respuesta de éxito que un mensaje real, así que un bot no aprende nada, y no envía ningún email ni webhook. Un formulario de contacto rara vez necesita más de un envío por persona en pocos minutos, así que Strict, 2 cada 10 minutos por visitante, le va bien cuando ya estás en producción. La documentación de spam explica cada capa, y la verificación humana necesita una llamada extra en las versiones con JavaScript: challenge() del SDK.

Dominios permitidos y despliegues de preview

Allowed domains acepta un dominio y sus subdominios. Los hosts estáticos te dan más dominios de los que crees: el dominio de producción, el subdominio propio del host y una dirección de preview por rama o pull request. Algunas previews son subdominios de la dirección de tu sitio en el host y ya coinciden; otras no, y necesitan su propia entrada.

HostAñadePreviews
GitHub Pagesusername.github.io y tu dominio personalizadoSin previews por defecto
Vercelyour-project.vercel.app y tu dominioLas URL de rama no coinciden; añade el host exacto mientras pruebas
Netlifyyour-site.netlify.app y tu dominiodeploy-preview-12--your-site.netlify.app no coincide
Cloudflare Pagesyour-project.pages.dev y tu dominioabc123.your-project.pages.dev ya coincide

Nunca añadas un host compartido por sí solo, como github.io, vercel.app o netlify.app. Coincidiría cualquier sitio de ese host, incluidos los de otras personas.

Si tu sitio envía una Content-Security-Policy

Muchas plantillas estáticas traen una Content-Security-Policy estricta. Dos directivas deciden si el formulario funciona: form-action para el envío HTML clásico y connect-src para las versiones con fetch. Si falta cualquiera de las dos, el navegador bloquea la petición antes de que salga de la página, y lo único que verás es un mensaje en la consola.

Cabecera de respuesta
Content-Security-Policy: default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com

Mantén 'self' en form-action: algunos navegadores también comprueban con ella la redirección de vuelta a tu página de agradecimiento. Dónde va la cabecera depende del host: _headers en Netlify y Cloudflare Pages, vercel.json en Vercel, una etiqueta <meta> en GitHub Pages y una política de cabeceras de respuesta de CloudFront delante de S3. Las páginas de cada host, más abajo, muestran cada caso.

Prueba antes de desplegar

Abre la página a través de un servidor local, no haciendo doble clic en el archivo. Una página abierta como file:// envía Origin: null, que no coincide con ningún dominio permitido y no cuenta como prueba local. Cualquiera de estos sirve la carpeta actual:

Terminal
npx serve .

python3 -m http.server 8000

Añade localhost a Allowed domains mientras pruebas. Los envíos desde localhost cuentan como pruebas: un espacio de trabajo sin plan tiene 50 al mes, así que puedes montar todo el formulario antes de pagar.

Elige tu caso

El formulario de arriba funciona en cualquier host estático. Cada host tiene su propia forma de fallar, y sus propios dominios de preview y archivos de cabeceras. Estas páginas los cubren.

Comprueba que funciona

Haz este recorrido una vez en tu dominio en producción y otra en una preview, si tu host las genera. Ikiform no tiene plan gratuito. Probar es gratis: un espacio de trabajo sin plan acepta 50 envíos de prueba al mes desde localhost. Para pasar a producción, elige Pro desde 6 $/mes por 500 envíos, o paga una sola vez: 10.000 envíos por 49 $ o 50.000 envíos por 149, $ con créditos que no caducan nunca. Consulta los precios.

  1. En Allowed domains del formulario, añade el dominio de tu sitio, más localhost mientras desarrollas. Déjalo vacío para aceptar envíos desde cualquier sitio.
  2. Envía desde tu servidor local y confirma que el mensaje aparece en Submissions.
  3. Despliega y envía desde el sitio en producción con JavaScript desactivado. Deberías llegar a tu propia página de agradecimiento.
  4. Vuelve a activar JavaScript y envía con el mensaje vacío. El error debería aparecer en la página, no como una página de error alojada.
  5. 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.
  6. Envía con el honeypot relleno, _gotcha=bot. Sigue recibiendo una respuesta de éxito, pero el envío se guarda como spam y no manda ningún email ni webhook.
  7. ¿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.

Solución de problemas

ProblemaQué hacer
405 Method Not Allowed o 404 justo después de pulsar EnviarEl formulario envió a tu propio sitio. Revisa el atributo action en el HTML desplegado, no en tu código fuente; un paso de build o un componente <Form> del framework puede haberlo reemplazado.
No pasa nada y la consola menciona Content-Security-PolicyAñade https://ikiform.com a form-action para los envíos clásicos y a connect-src para fetch.
403 origin_not_allowed, o todos los envíos acaban en spamEl 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.
Las pruebas locales se rechazan con 402 plan_requiredLa página se abrió como una URL file://, así que el envío no tenía un origen válido y contó como real. Sirve la carpeta con un servidor local y prueba desde localhost.
El navegador llega a la página de agradecimiento de Ikiform en lugar de a la tuya_redirect se ignora salvo que su dominio esté en Allowed domains y sea una URL https:. Añade el dominio, o configura Redirect after submit en los ajustes del formulario.
422 validation_failedEl envío no encaja con la definición de campos del formulario. error.fields nombra cada campo y el motivo. Haz que el name del input coincida con el id del campo.
El envío está en el panel pero no llegó ningún email ni webhookSe guardó como spam. El spam no envía nada. Ábrelo con el filtro Spam para ver los motivos y márcalo como no spam para que envíe su email y sus webhooks.
429 rate_limitedUn visitante envió más de lo que permite el límite de frecuencia del formulario. Standard permite 5 por minuto por visitante. Espera lo que indique Retry-After, o elige Relaxed o Custom para formularios que envían desde una IP compartida.

Preguntas

¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.