Liste d'attente pour un nouveau SaaS

Liste d'attente pour un nouveau SaaS7 min de lecture

Comment ajouter un formulaire de liste d'attente en HTML simple à n'importe quelle landing page

Une page de liste d'attente complète en HTML simple, sans build, qui fonctionne sans JavaScript, remercie sur place quand il se charge, et se replie si besoin.

Preet SutharFondateur, Ikiform

Écrivez un formulaire HTML normal qui envoie l'e-mail à votre endpoint Ikiform et redirige vers une page de remerciement : il fonctionne ainsi sur n'importe quel hébergeur sans aucun JavaScript. Ajoutez ensuite un petit script qui envoie le même formulaire avec fetch et affiche le remerciement sur place, et qui rend la main à l'envoi classique chaque fois qu'il ne peut pas aller au bout, par exemple quand la vérification humaine est activée.

Une page de liste d'attente n'a besoin ni de framework, ni d'étape de build, ni de serveur. Quatre fichiers statiques et un backend de formulaires suffisent, et la page se charge plus vite que tout ce qu'exporte un créateur de sites. Cette page construit l'ensemble, couche par couche, pour que chaque couche fonctionne seule.

Ce qui compte sans étape de build

  • Le formulaire est l'intégration. Un simple <form> avec une action constitue toute la connexion à Ikiform. Pas de package, pas de clé d'API, rien à compiler.
  • JavaScript est une amélioration, pas une obligation. La page doit collecter des inscriptions quand le script est bloqué, lent ou cassé. Chaque couche ci-dessous s'ajoute à la précédente et se replie sur elle.
  • Le script ne doit jamais bloquer quelqu'un. Si fetch échoue ou si Ikiform demande une vérification humaine que fetch ne peut pas exécuter, le script rend le formulaire au navigateur pour un envoi classique.
  • L'hébergement peut être n'importe où. GitHub Pages, Netlify, Cloudflare Pages, un bucket S3 ou un hébergement mutualisé servent tous ces fichiers tels quels.
CoucheCe qu'elle ajouteSi elle manque
Formulaire HTMLEnvoie l'e-mail, redirige vers thanks.htmlRien ne fonctionne, elle passe donc en premier
CSSUne page lisible en mode clair et sombreUn formulaire sobre mais fonctionnel
ScriptRemerciement sur place, erreurs de champ, pas de changement de pageL'envoi classique et la redirection
Les trois chemins possibles d'une inscription sur une page de liste d'attente en HTML simpleSans JavaScriptScript chargéLe script ne peut pas finirenvoi duformulaireurlencodedIkiformredirection 303thanks.htmlvotre propre pagefetchAccept :application/jsonIkiform201 ou une erreurmessage surplacemême pagefetcherreur réseau ouvérificationrequiseform.submit()l'envoi classiquevérification,puis thanks.html_redirectJSONrepliLes trois chemins possibles d'une inscription sur une page de liste d'attente en HTML simpleSans JavaScriptScript chargéLe script ne peut pas finirenvoi du formulaireurlencodedIkiformredirection 303thanks.htmlvotre propre pagefetchAccept : application/jsonIkiform201 ou une erreurmessage sur placemême pagefetcherreur réseau ou vérificationrequiseform.submit()l'envoi classiquevérification, puisthanks.html_redirectJSONrepli
  1. Avec JavaScript désactivé, le navigateur envoie le formulaire, Ikiform redirige en 303 vers votre URL _redirect, et le visiteur voit thanks.html.
  2. Avec le script chargé, il envoie avec fetch et demande du JSON. Ikiform répond 201 ou une erreur, et le script affiche le résultat sur la même page.
  3. Si fetch échoue, ou si Ikiform répond challenge_required, le script appelle form.submit().
  4. Cet envoi classique passe par la page de vérification humaine d'Ikiform si besoin et se termine sur thanks.html.
Chaque chemin aboutit à une inscription stockée. Le script ne change que la façon dont le visiteur la voit.

Les fichiers

La page

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 envoie le navigateur vers thanks.html après un envoi classique. Il ne fonctionne que pour une URL https: sur l'un des Allowed domains du formulaire : ajoutez-y donc votre domaine. Redirect after submit dans les réglages du formulaire fait la même chose sans l'input masqué.
  • L'input _gotcha est le honeypot. Les humains ne le voient jamais ; les bots qui remplissent tous les inputs sont stockés comme spam.
  • aria-live="polite" fait annoncer les messages du script par les lecteurs d'écran sans déplacer le focus.
  • La balise script utilise defer : elle s'exécute après la création du formulaire et ne bloque jamais l'affichage de la page.

Le script d'amélioration

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 fait répondre Ikiform en JSON au lieu d'une redirection, pour que le script puisse afficher le résultat sur place.
  • La clé d'idempotence est créée une fois par chargement de page. Un double clic, ou une nouvelle tentative après une réponse lente, ne stocke qu'une inscription.
  • challenge_required signifie que la vérification anti-spam du formulaire est réglée sur Ikiform et que fetch n'a envoyé aucun jeton. form.submit() envoie de façon classique, ce qui affiche la page de vérification d'Ikiform puis la page de remerciement. Il ne déclenche pas à nouveau l'événement submit, il n'y a donc pas de boucle.
  • Une erreur réseau prend le même repli. Si Ikiform était injoignable, l'envoi classique affiche l'erreur du navigateur, ce qui reste plus honnête qu'un bouton bloqué.
  • Un 429 lit Retry-After et indique combien de temps attendre.

La page de remerciement

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>

Les styles

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 page est sur un autre domaine qu'example.com, mettez à jour _redirect et le lien de retour. Ikiform ignore un _redirect qu'il ne peut pas suivre et affiche sa propre page de remerciement hébergée : une incohérence ne fait donc jamais perdre d'inscription.

Déployer et tester

Placez les quatre fichiers dans un dossier et téléversez-le sur n'importe quel hébergeur statique. Ajoutez ensuite le domaine de l'hébergeur dans Allowed domains : par exemple username.github.io, your-site.netlify.app ou votre propre domaine. Pendant le travail en local, ajoutez aussi localhost ; servez le dossier avec n'importe quel serveur statique, car une page ouverte via une URL file:// n'envoie pas d'origine.

Vérifiez que tout fonctionne

  1. Dans Allowed domains du formulaire, ajoutez le domaine de votre site, plus localhost pendant le développement. Laissez vide pour accepter les envois de partout.
  2. Désactivez JavaScript dans le navigateur et inscrivez-vous. Vous arrivez sur thanks.html.
  3. Réactivez-le et inscrivez-vous à nouveau. Le formulaire est remplacé par la phrase de remerciement sans quitter la page.
  4. Réglez Spam check sur Ikiform et inscrivez-vous une fois de plus. La page passe par la vérification, puis par thanks.html.
  5. Ouvrez Submissions dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.
  6. Envoyez avec le honeypot rempli, _gotcha=bot. L'envoi reçoit quand même une réponse de succès, mais la soumission est stockée comme spam et n'envoie ni e-mail ni webhook.

Pièges

ProblèmeQue faire
Le navigateur arrive sur la page de remerciement d'Ikiform au lieu de la vôtre_redirect est ignoré sauf si son domaine figure dans Allowed domains et qu'il s'agit d'une URL https:. Ajoutez le domaine, ou définissez plutôt Redirect after submit dans les réglages du formulaire.
403 origin_not_allowed, ou tous les envois finissent en spamLe domaine de la page n'est pas dans les Allowed domains du formulaire. Ajoutez-le, plus localhost pour les tests en local. Les sous-domaines d'un domaine listé sont autorisés.
Les envois depuis une page ouverte comme fichier sont refusés ou marqués comme spamUne page file:// n'envoie pas d'origine. Servez le dossier avec un serveur statique local et ajoutez localhost aux Allowed domains.
La phrase de remerciement ne s'affiche jamais, mais les inscriptions arriventLe script ne s'est pas chargé : vérifiez le chemin dans l'attribut src, ou que l'hébergeur sert bien les fichiers .js. L'envoi classique fonctionne toujours, c'est pourquoi les inscriptions arrivent.
402 test_limit_reachedL'espace de travail n'a pas d'offre et a utilisé ses 50 tests gratuits depuis localhost ce mois-ci. Choisissez une offre, ou utilisez Send test dans le tableau de bord.

La documentation des formulaires HTML liste tous les champs réservés, et le modèle de liste d'attente génère une version avec nom et rôle. L'export de la liste et l'e-mail de lancement sont dans le guide principal.

Questions

Une étape vous bloque, ou vous voulez un guide pour votre cas ? Écrivez à hello@ikiform.com.