Lista de espera para un nuevo SaaS

Lista de espera para un nuevo SaaS7 min de lectura

Cómo añadir un formulario de lista de espera en HTML puro a cualquier landing page

Una página de lista de espera completa en HTML puro y sin build que funciona con JavaScript desactivado, muestra un agradecimiento en línea cuando carga y recurre a un plan B cuando hace falta.

Preet SutharFundador, Ikiform

Escribe un formulario HTML normal que envíe el email a tu endpoint de Ikiform y redirija a una página de agradecimiento, para que funcione en cualquier hosting sin nada de JavaScript. Luego añade un pequeño script que envíe el mismo formulario con fetch, muestre el agradecimiento en la página y devuelva el control al envío normal cuando el script no pueda terminar, por ejemplo cuando la verificación humana está activa.

Una página de lista de espera no necesita un framework, un paso de build ni un servidor. Cuatro archivos estáticos y un backend de formularios bastan, y la página carga más rápido que cualquier cosa que exporte un constructor. Esta página lo construye todo, capa a capa, para que cada capa funcione por sí sola.

Qué importa sin un paso de build

  • El formulario es la integración. Un <form> normal con un action es toda la conexión con Ikiform. Sin paquetes, sin clave de API, nada que compilar.
  • JavaScript es una mejora, no un requisito. La página debería recoger registros aunque el script esté bloqueado, sea lento o falle. Cada capa de abajo se suma a la anterior y vuelve a ella si falla.
  • El script nunca debe dejar a nadie atrapado. Si fetch falla o Ikiform pide una verificación humana que fetch no puede ejecutar, el script devuelve el formulario al navegador para que lo envíe de la forma normal.
  • Se aloja en cualquier sitio. GitHub Pages, Netlify, Cloudflare Pages, un bucket de S3 o un hosting compartido sirven estos archivos tal cual.
CapaQué aportaSi falta
Formulario HTMLEnvía el email, redirige a thanks.htmlNo funciona nada, por eso va primero
CSSUna página legible en modo claro y oscuroUn formulario sencillo pero funcional
ScriptAgradecimiento en la página, errores por campo, sin cambiar de páginaEl envío normal y la redirección
Los tres caminos que puede seguir un registro en una página de lista de espera en HTML puroSin JavaScriptScript cargadoEl script no puede terminarenvío delformulariourlencodedIkiformredirección 303thanks.htmltu propia páginafetchAccept:application/jsonIkiform201 o un errormensaje en lapáginamisma páginafetcherror de red o hacefalta verificarform.submit()el envío normalverificación,luegothanks.html_redirectJSONplan BLos tres caminos que puede seguir un registro en una página de lista de espera en HTML puroSin JavaScriptScript cargadoEl script no puede terminarenvío del formulariourlencodedIkiformredirección 303thanks.htmltu propia páginafetchAccept: application/jsonIkiform201 o un errormensaje en la páginamisma páginafetcherror de red o hace faltaverificarform.submit()el envío normalverificación, luegothanks.html_redirectJSONplan B
  1. Con JavaScript desactivado, el navegador envía el formulario, Ikiform redirige con un 303 a tu URL de _redirect y el visitante ve thanks.html.
  2. Con el script cargado, envía con fetch y pide JSON. Ikiform responde 201 o un error, y el script muestra el resultado en la misma página.
  3. Si fetch falla, o Ikiform responde challenge_required, el script llama a form.submit().
  4. Ese envío normal muestra la página de verificación humana de Ikiform si hace falta y termina en thanks.html.
Todos los caminos acaban con un registro guardado. El script solo cambia cómo lo ve el visitante.

Los archivos

La página

index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Acme is almost here</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <h1>Acme is almost here</h1>
    <p>Invoices that chase themselves. Leave your email and we'll invite you first.</p>

    <form id="waitlist" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
      <label for="email">Email</label>
      <div class="row">
        <input id="email" name="email" type="email" autocomplete="email" required placeholder="you@company.com">
        <button>Join the waitlist</button>
      </div>
      <input type="hidden" name="_redirect" value="https://example.com/thanks.html">
      <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden>
      <p class="message" role="status" aria-live="polite"></p>
    </form>
  </main>
  <script src="waitlist.js" defer></script>
</body>
</html>
  • _redirect lleva el navegador a thanks.html después de un envío normal. Solo funciona con una URL https: en uno de los Allowed domains del formulario, así que añade ahí tu dominio. Redirect after submit en los ajustes del formulario hace lo mismo sin el input oculto.
  • El input _gotcha es el honeypot. Las personas nunca lo ven; los bots que rellenan todos los inputs se guardan como spam.
  • aria-live="polite" hace que los lectores de pantalla anuncien los mensajes del script sin mover el foco.
  • La etiqueta del script usa defer, así que se ejecuta cuando el formulario ya existe y nunca bloquea el dibujado de la página.

La mejora

waitlist.js
const form = document.getElementById("waitlist");
const message = form.querySelector(".message");
const button = form.querySelector("button");
const key = crypto.randomUUID();

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  button.disabled = true;
  message.textContent = "Joining…";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json", "Idempotency-Key": key },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      form.replaceChildren(Object.assign(document.createElement("p"), {
        textContent: "You're on the list. We'll email you on launch day.",
        role: "status",
      }));
      return;
    }
    if (result.error.code === "challenge_required") {
      form.submit();
      return;
    }
    const wait = response.headers.get("Retry-After");
    message.textContent = wait
      ? `Too many tries. Wait ${wait} seconds and try again.`
      : result.error.fields?.email?.[0] ?? result.error.message;
  } catch {
    form.submit();
    return;
  }
  button.disabled = false;
});
  • Accept: application/json hace que Ikiform responda con JSON en lugar de una redirección, para que el script pueda mostrar el resultado en su sitio.
  • La clave de idempotencia se crea una vez por carga de página. Un doble clic, o un reintento tras una respuesta lenta, guarda un solo registro.
  • challenge_required significa que la verificación antispam del formulario está en Ikiform y fetch no envió ningún token. form.submit() envía de la forma normal, lo que muestra la página de verificación de Ikiform y luego la de agradecimiento. No vuelve a disparar el evento submit, así que no hay bucle.
  • Un error de red sigue el mismo plan B. Si Ikiform no era accesible, el envío normal muestra el error propio del navegador, que sigue siendo más honesto que un botón atascado.
  • Un 429 lee Retry-After y dice cuánto esperar.

La página de agradecimiento

thanks.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>You're on the list</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <h1>You're on the list</h1>
    <p>We'll email you the day Acme opens. Nothing before that.</p>
    <p><a href="/">Back to the page</a></p>
  </main>
</body>
</html>

Los estilos

style.css
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; color: #111; background: #fff; }
main { max-width: 34rem; margin: 0 auto; padding: 18vh 1rem 4rem; }
h1 { font-size: 2.25rem; line-height: 1.1; margin: 0 0 0.75rem; }
label { display: block; font-weight: 600; margin: 1.5rem 0 0.375rem; }
.row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
input[type="email"] { flex: 1 1 14rem; font: inherit; padding: 0.625rem 0.75rem; border: 1px solid #ccc; border-radius: 0.5rem; }
button { font: inherit; padding: 0.625rem 1rem; border: 0; border-radius: 0.5rem; background: #111; color: #fff; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: progress; }
.message { min-height: 1.5em; margin: 0.5rem 0 0; }
@media (prefers-color-scheme: dark) {
  body { color: #eee; background: #111; }
  input[type="email"] { background: #1b1b1b; color: inherit; border-color: #444; }
  button { background: #eee; color: #111; }
}
Si la página está en un dominio distinto de example.com, actualiza _redirect y el enlace de vuelta. Ikiform ignora un _redirect que no puede seguir y muestra en su lugar su propia página de agradecimiento alojada, así que un desajuste nunca hace perder un registro.

Despliega y prueba

Pon los cuatro archivos en una carpeta y súbela a cualquier hosting estático. Luego añade el dominio del hosting en Allowed domains: por ejemplo username.github.io, your-site.netlify.app o tu propio dominio. Mientras trabajas en local, añade también localhost; sirve la carpeta con cualquier servidor estático, porque una página abierta como URL file:// no envía origen.

Comprueba que funciona

  1. En Allowed domains del formulario, añade el dominio de tu sitio, y localhost mientras desarrollas. Déjalo vacío para aceptar envíos desde cualquier sitio.
  2. Desactiva JavaScript en el navegador y regístrate. Llegas a thanks.html.
  3. Vuelve a activarlo y regístrate otra vez. El formulario se cambia por la línea de agradecimiento sin salir de la página.
  4. Pon Spam check en Ikiform y regístrate una vez más. La página pasa a la verificación y luego a thanks.html.
  5. Abre Submissions en el panel. El nuevo envío está ahí con todos los campos que mandaste.
  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 email ni webhook.

Problemas habituales

ProblemaQué hacer
El navegador llega a la página de agradecimiento de Ikiform en vez 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.
403 origin_not_allowed, o todos los envíos acaban en spamEl dominio de la página no está en los Allowed domains del formulario. Añádelo, y también localhost para las pruebas locales. Los subdominios de un dominio de la lista están permitidos.
Los envíos desde una página abierta como archivo se rechazan o se marcan como spamUna página file:// no envía origen. Sirve la carpeta con un servidor estático local y añade localhost a Allowed domains.
La línea de agradecimiento nunca aparece, pero los registros lleganEl script no cargó: revisa la ruta del atributo src, o que el hosting sirva archivos .js. El envío normal sigue funcionando, por eso llegan los registros.
402 test_limit_reachedEl espacio de trabajo no tiene plan y ya usó sus 50 pruebas gratuitas desde localhost este mes. Elige un plan, o usa Send test en el panel.

La documentación de formularios HTML enumera todos los campos reservados, y la plantilla de lista de espera genera una versión con nombre y rol. Exportar la lista y el email de lanzamiento están en la guía principal.

Preguntas

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