# 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.

Publicado el 2026-10-25 · https://ikiform.com/es/guides/waitlist/html

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.

| Capa | Qué aporta | Si falta |
| --- | --- | --- |
| Formulario HTML | Envía el email, redirige a `thanks.html` | No funciona nada, por eso va primero |
| CSS | Una página legible en modo claro y oscuro | Un formulario sencillo pero funcional |
| Script | Agradecimiento en la página, errores por campo, sin cambiar de página | El envío normal y la redirección |

Diagrama: Los tres caminos que puede seguir un registro en una página de lista de espera en HTML puro

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`:

```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`:

```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`:

```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`:

```text
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

| Problema | Qué 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 spam | El 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 spam | Una 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 llegan | El 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_reached` | El 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](https://ikiform.com/es/docs/html.md) enumera todos los campos reservados, y la [plantilla de lista de espera](https://ikiform.com/forms/waitlist) genera una versión con nombre y rol. Exportar la lista y el email de lanzamiento están en la [guía principal](https://ikiform.com/es/guides/waitlist.md#launch-day).

## Preguntas

### ¿Puede funcionar un formulario de lista de espera sin JavaScript?

Sí. Un formulario HTML normal envía el email a tu endpoint de Ikiform e Ikiform redirige el navegador a tu página de agradecimiento. JavaScript solo añade el mensaje en la página.

### ¿Dónde puedo alojar una página de lista de espera en HTML puro?

En cualquier sitio que sirva archivos estáticos: GitHub Pages, Netlify, Cloudflare Pages, S3 o un hosting compartido. El formulario envía a Ikiform, así que el hosting nunca ejecuta código.

### ¿Qué pasa si la verificación humana está activa y el script envía con fetch?

Ikiform responde 403 challenge_required. El script de esta guía lo detecta y envía el formulario de la forma normal, lo que muestra la página de verificación y luego tu página de agradecimiento.

### ¿Ikiform aloja la página del formulario por mí?

No. Ikiform no tiene páginas de formulario alojadas. Pones el formulario en tu propio sitio, en cualquier constructor o framework, y lo apuntas a tu endpoint de Ikiform.

### ¿Ikiform es gratis?

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 nunca caducan.

## Más guías

- [Lista de espera para un nuevo SaaS](https://ikiform.com/es/guides/waitlist.md): Crea una lista de espera previa al lanzamiento en tu propia landing page: el formulario, el antispam, los duplicados, los emails de confirmación y una exportación limpia el día del lanzamiento.
- [Lista de espera en Next.js](https://ikiform.com/es/guides/waitlist/nextjs.md): Una lista de espera con el App Router de Next.js: una server action que reenvía registros con una clave secreta, useActionState, un agradecimiento optimista y un límite por visitante.
- [Lista de espera en Carrd](https://ikiform.com/es/guides/waitlist/carrd.md): Recoge registros de lista de espera en una página de Carrd: un formulario Send to URL en Pro Plus con seguimiento UTM, o un formulario HTML incrustado con honeypot en Pro Standard.
- [Lista para lanzar en Product Hunt](https://ikiform.com/es/guides/waitlist/product-hunt.md): Reúne una lista semanas antes de Product Hunt, etiqueta cada registro por canal y envía el email del lanzamiento en dos tandas según el reinicio en hora del Pacífico.
- [Registro a una beta con filtro](https://ikiform.com/es/guides/waitlist/beta-signup.md): Un formulario de solicitud para una beta con preguntas de filtro que aplica el servidor, y un script que puntúa a los candidatos y elige cada grupo de la lista.
- [Lista de espera para app móvil](https://ikiform.com/es/guides/waitlist/mobile-app.md): Una lista de espera previa al lanzamiento para una app de iOS o Android que pregunta qué teléfono se usa, lleva a los testers de iPhone a TestFlight y llena una prueba cerrada de Play.
