Lista de espera para um novo SaaS

Lista de espera para um novo SaaS7 min de leitura

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.

Preet SutharFundador, Ikiform

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.
CamadaO que ela adicionaSe faltar
Formulário HTMLEnvia o e-mail, redireciona para thanks.htmlNada funciona, então ele vem primeiro
CSSUma página legível nos modos claro e escuroUm formulário simples, mas funcionando
ScriptAgradecimento na página, erros de campo, sem trocar de páginaO envio simples e o redirecionamento
Os três caminhos que uma inscrição pode seguir numa página de lista de espera em HTML puroSem JavaScriptScript carregadoScript não consegue terminarenvio doformuláriourlencodedIkiformredirecionamento303thanks.htmlsua própria páginafetchAccept:application/jsonIkiform201 ou um erromensagem napáginamesma páginafetcherro de rede ouverificaçãonecessáriaform.submit()o envio simplespágina deverificação,depoisthanks.html_redirectJSONplano BOs três caminhos que uma inscrição pode seguir numa página de lista de espera em HTML puroSem JavaScriptScript carregadoScript não consegue terminarenvio do formuláriourlencodedIkiformredirecionamento 303thanks.htmlsua própria páginafetchAccept: application/jsonIkiform201 ou um erromensagem na páginamesma páginafetcherro de rede ou verificaçãonecessáriaform.submit()o envio simplespágina de verificação,depois thanks.html_redirectJSONplano B
  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
<!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
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
<!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
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

ProblemaO que fazer
O navegador cai na página de agradecimento do Ikiform em vez da suaO _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 spamO 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 spamUma 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 chegamO 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_reachedO 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 lista todos os campos reservados, e o modelo de lista de espera gera uma versão com nome e função. Exportar a lista e o e-mail de lançamento estão no guia principal.

Perguntas

Travou em algum passo ou quer um guia para o seu caso? Envie um e-mail para hello@ikiform.com.