# Como adicionar um formulário de lista de espera em HTML puro a qualquer landing page

> Uma página de lista de espera completa em HTML puro, sem build, que funciona com o JavaScript desligado, mostra um agradecimento na própria página quando carrega e tem plano B quando precisa.

Publicado em 2026-10-25 · https://ikiform.com/pt-br/guides/waitlist/html

Escreva um formulário HTML normal que envia o e-mail ao seu endpoint do Ikiform e redireciona para uma página de agradecimento, para que funcione em qualquer host sem nenhum JavaScript. Depois adicione um script pequeno que envia o mesmo formulário com `fetch` e mostra o agradecimento na página, e que devolve o controle ao envio simples sempre que não consegue terminar, como quando a verificação humana está ligada.

Uma página de lista de espera não precisa de framework, etapa de build nem servidor. Quatro arquivos estáticos e um backend de formulários dão conta, e a página carrega mais rápido do que qualquer coisa que um criador de sites exporta. Esta página monta tudo, camada por camada, para que cada camada funcione sozinha.

## O que importa sem etapa de build

- **O formulário é a integração.** Um `<form>` simples com um `action` é toda a conexão com o Ikiform. Sem pacote, sem chave de API, nada para compilar.
- **JavaScript é uma melhoria, não um requisito.** A página deve coletar inscrições quando o script está bloqueado, lento ou quebrado. Cada camada abaixo se soma à anterior e volta para ela quando falha.
- **O script nunca pode deixar alguém preso.** Se o `fetch` falhar ou o Ikiform pedir uma verificação humana que o `fetch` não consegue rodar, o script devolve o formulário para o navegador enviar do jeito normal.
- **A hospedagem pode ser em qualquer lugar.** GitHub Pages, Netlify, Cloudflare Pages, um bucket S3 ou uma hospedagem compartilhada servem estes arquivos como estão.

| Camada | O que ela adiciona | Se faltar |
| --- | --- | --- |
| Formulário HTML | Envia o e-mail, redireciona para `thanks.html` | Nada funciona, então ele vem primeiro |
| CSS | Uma página legível nos modos claro e escuro | Um formulário simples, mas funcionando |
| Script | Agradecimento na página, erros de campo, sem trocar de página | O envio simples e o redirecionamento |

Diagrama: Os três caminhos que uma inscrição pode seguir numa página de lista de espera em HTML puro

1. Com o JavaScript desligado, o navegador envia o formulário, o Ikiform redireciona com 303 para a sua URL de _redirect e o visitante vê o thanks.html.
2. Com o script carregado, ele envia com fetch e pede JSON. O Ikiform responde 201 ou um erro, e o script mostra o resultado na mesma página.
3. Se o fetch falhar, ou se o Ikiform responder challenge_required, o script chama form.submit().
4. Esse envio simples passa pela página de verificação humana do Ikiform, se necessário, e termina no thanks.html.

Todo caminho termina com uma inscrição armazenada. O script só muda como o visitante a vê.

## Os arquivos

### A 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>
```

- O `_redirect` manda o navegador para `thanks.html` depois de um envio simples. Ele só funciona com uma URL `https:` em um dos **Allowed domains** do formulário, então adicione seu domínio lá. **Redirect after submit**, nas configurações do formulário, faz o mesmo sem o input oculto.
- O input `_gotcha` é o honeypot. As pessoas nunca o veem; bots que preenchem todos os inputs são armazenados como spam.
- `aria-live="polite"` faz os leitores de tela anunciarem as mensagens do script sem mover o foco.
- A tag script usa `defer`, então roda depois que o formulário existe e nunca impede a página de ser desenhada.

### A melhoria

`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` faz o Ikiform responder com JSON em vez de um redirecionamento, para o script poder mostrar o resultado no lugar.
- A chave de idempotência é criada uma vez por carregamento de página. Um clique duplo, ou uma nova tentativa depois de uma resposta lenta, armazena uma única inscrição.
- `challenge_required` significa que a verificação de spam do formulário está como **Ikiform** e o `fetch` não enviou token. `form.submit()` envia do jeito tradicional, o que mostra a página de verificação do Ikiform e depois a página de agradecimento. Ele não dispara o evento `submit` de novo, então não há loop.
- Um erro de rede segue o mesmo plano B. Se o Ikiform estava inacessível, o envio simples mostra o erro do próprio navegador, o que ainda é mais honesto que um botão travado.
- Um `429` lê o `Retry-After` e diz quanto tempo esperar.

### A página de agradecimento

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

### Os 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; }
}
```

> Se a página estiver num domínio diferente de `example.com`, atualize o `_redirect` e o link de volta. O Ikiform ignora um `_redirect` que não consegue seguir e mostra a própria página de agradecimento hospedada, então uma divergência nunca faz você perder uma inscrição.

## Deploy e teste

Coloque os quatro arquivos numa pasta e suba-a para qualquer host estático. Depois adicione o domínio do host em **Allowed domains**: por exemplo `username.github.io`, `your-site.netlify.app` ou o seu próprio domínio. Enquanto trabalha localmente, adicione `localhost` também; sirva a pasta com qualquer servidor estático, já que uma página aberta como URL `file://` não envia origem.

## Confira se funciona

1. Em **Allowed domains** do formulário, adicione o domínio do seu site, mais `localhost` enquanto desenvolve. Deixe vazio para aceitar envios de qualquer lugar.
2. Desligue o JavaScript no navegador e inscreva-se. Você cai no `thanks.html`.
3. Ligue-o de novo e inscreva-se outra vez. O formulário dá lugar à frase de agradecimento sem sair da página.
4. Defina **Spam check** como **Ikiform** e inscreva-se mais uma vez. A página vai para a verificação e depois para o `thanks.html`.
5. Abra **Submissions** no painel. O novo envio está lá com todos os campos que você enviou.
6. Envie com o honeypot preenchido, `_gotcha=bot`. Ele ainda recebe uma resposta de sucesso, mas o envio é armazenado como spam e não dispara e-mail nem webhook.

## Pegadinhas

| Problema | O que fazer |
| --- | --- |
| O navegador cai na página de agradecimento do Ikiform em vez da sua | O `_redirect` é ignorado, a menos que o domínio dele esteja em **Allowed domains** e ele seja uma URL `https:`. Adicione o domínio, ou defina **Redirect after submit** nas configurações do formulário. |
| `403 origin_not_allowed`, ou todo envio cai no spam | O domínio da página não está em **Allowed domains** do formulário. Adicione-o, mais `localhost` para testes locais. Subdomínios de um domínio listado são permitidos. |
| Envios de uma página aberta como arquivo são recusados ou marcados como spam | Uma página `file://` não envia origem. Sirva a pasta com um servidor estático local e adicione `localhost` em **Allowed domains**. |
| A frase de agradecimento nunca aparece, mas as inscrições chegam | O script não carregou: confira o caminho no atributo `src`, ou se o host serve arquivos `.js`. O envio simples continua funcionando, e é por isso que as inscrições chegam. |
| `402 test_limit_reached` | O workspace não tem plano e já usou os 50 testes gratuitos de `localhost` deste mês. Escolha um plano ou use **Send test** no painel. |

A [documentação de formulários HTML](https://ikiform.com/pt-br/docs/html.md) lista todos os campos reservados, e o [modelo de lista de espera](https://ikiform.com/forms/waitlist) gera uma versão com nome e função. Exportar a lista e o e-mail de lançamento estão no [guia principal](https://ikiform.com/pt-br/guides/waitlist.md#launch-day).

## Perguntas

### Um formulário de lista de espera funciona sem JavaScript?

Sim. Um formulário HTML simples envia o e-mail ao seu endpoint do Ikiform, e o Ikiform redireciona o navegador para a sua página de agradecimento. O JavaScript só adiciona a mensagem na página.

### Onde posso hospedar uma página de lista de espera em HTML puro?

Em qualquer lugar que sirva arquivos estáticos: GitHub Pages, Netlify, Cloudflare Pages, S3 ou uma hospedagem compartilhada. O formulário envia ao Ikiform, então o host nunca roda código nenhum.

### O que acontece se a verificação humana estiver ligada e o script enviar com fetch?

O Ikiform responde 403 challenge_required. O script deste guia pega isso e envia o formulário do jeito tradicional, o que mostra a página de verificação e depois a sua página de agradecimento.

### O Ikiform hospeda a página do formulário para mim?

Não. O Ikiform não tem páginas de formulário hospedadas. Você coloca o formulário no seu próprio site, em qualquer criador de sites ou framework, e o aponta para o seu endpoint do Ikiform.

### O Ikiform é gratuito?

Não, não há plano gratuito. Testar é grátis: você pode enviar 50 envios de teste por mês a partir de localhost antes de pagar. Para entrar no ar, o Pro começa em US$ 6/mês para 500 envios, ou você pode pagar uma vez 10.000 envios por US$ 49 ou 50.000 envios por US$ 149, com créditos que nunca expiram.

## Mais guias

- [Lista de espera para um novo SaaS](https://ikiform.com/pt-br/guides/waitlist.md): Monte uma lista de espera pré-lançamento na sua própria landing page: o formulário, proteção contra spam, duplicados, e-mails de confirmação e uma exportação limpa no dia do lançamento.
- [Lista de espera com Next.js](https://ikiform.com/pt-br/guides/waitlist/nextjs.md): Uma lista de espera no App Router do Next.js: uma server action que encaminha as inscrições com chave secreta, useActionState, agradecimento otimista e limite por visitante.
- [Lista de espera no Carrd](https://ikiform.com/pt-br/guides/waitlist/carrd.md): Colete inscrições na lista de espera numa página do Carrd: um formulário Send to URL no Pro Plus com rastreamento de UTM, ou um formulário HTML incorporado com honeypot no Pro Standard.
- [Lista para lançamento no Product Hunt](https://ikiform.com/pt-br/guides/waitlist/product-hunt.md): Colete uma lista semanas antes do Product Hunt, marque cada inscrição pelo canal e envie o e-mail do dia do lançamento em duas ondas, no ritmo da virada do horário do Pacífico.
- [Inscrição para beta com triagem](https://ikiform.com/pt-br/guides/waitlist/beta-signup.md): Um formulário de candidatura ao beta com perguntas de triagem que o servidor exige, e um script que pontua os candidatos e escolhe cada turma da lista.
- [Lista de espera para app mobile](https://ikiform.com/pt-br/guides/waitlist/mobile-app.md): Uma lista de espera pré-lançamento para um app iOS ou Android que pergunta qual celular a pessoa usa, manda testadores de iPhone ao TestFlight e preenche um teste fechado do Play.
