Warteliste für ein neues SaaS

Warteliste für ein neues SaaS6 Min. Lesezeit

So fügst du jeder Landingpage ein Wartelisten-Formular in reinem HTML hinzu

Eine komplette Wartelisten-Seite in reinem HTML ohne Build, die ohne JavaScript funktioniert, nach dem Laden ein Danke inline zeigt und bei Bedarf zurückfällt.

Preet SutharGründer, Ikiform

Schreib ein normales HTML-Formular, das die E-Mail an deinen Ikiform-Endpunkt sendet und auf eine Dankeseite weiterleitet, damit es auf jedem Host ganz ohne JavaScript funktioniert. Füge dann ein kleines Skript hinzu, das dasselbe Formular mit fetch sendet, das Danke inline zeigt und an den einfachen Post zurückgibt, wann immer das Skript nicht fertig werden kann, etwa wenn die Menschlichkeitsprüfung aktiv ist.

Eine Wartelisten-Seite braucht kein Framework, keinen Build-Schritt und keinen Server. Vier statische Dateien und ein Formular-Backend reichen, und die Seite lädt schneller als alles, was ein Baukasten exportiert. Diese Seite baut das Ganze Schicht für Schicht, sodass jede Schicht für sich funktioniert.

Worauf es ohne Build-Schritt ankommt

  • Das Formular ist die Integration. Ein einfaches <form> mit einer action ist die ganze Verbindung zu Ikiform. Kein Paket, kein API-Schlüssel, nichts zu kompilieren.
  • JavaScript ist ein Upgrade, keine Voraussetzung. Die Seite sollte Anmeldungen sammeln, wenn das Skript blockiert, langsam oder kaputt ist. Jede Schicht unten baut auf der vorherigen auf und fällt auf sie zurück.
  • Das Skript darf niemanden festhalten. Wenn fetch scheitert oder Ikiform eine Menschlichkeitsprüfung verlangt, die fetch nicht ausführen kann, gibt das Skript das Formular an den Browser zurück, der es auf normalem Weg sendet.
  • Hosting geht überall. GitHub Pages, Netlify, Cloudflare Pages, ein S3-Bucket oder ein Shared Host liefern diese Dateien so aus, wie sie sind.
SchichtWas sie hinzufügtWenn sie fehlt
HTML-FormularSendet die E-Mail, leitet zu thanks.html weiterNichts funktioniert, deshalb kommt es zuerst
CSSEine lesbare Seite im hellen und dunklen ModusEin schlichtes, aber funktionierendes Formular
SkriptDanke inline, Feldfehler, kein SeitenwechselDer einfache Post und die Weiterleitung
Die drei Wege, die eine Anmeldung auf einer Wartelisten-Seite in reinem HTML nehmen kannKein JavaScriptSkript geladenSkript wird nicht fertigFormular-PosturlencodedIkiformWeiterleitung mit303thanks.htmldeine eigene SeitefetchAccept:application/jsonIkiform201 oder ein FehlerMeldung inlinegleiche SeitefetchNetzwerkfehler oderPrüfung nötigform.submit()der einfache PostPrüfseite, dannthanks.html_redirectJSONFallbackDie drei Wege, die eine Anmeldung auf einer Wartelisten-Seite in reinem HTML nehmen kannKein JavaScriptSkript geladenSkript wird nicht fertigFormular-PosturlencodedIkiformWeiterleitung mit 303thanks.htmldeine eigene SeitefetchAccept: application/jsonIkiform201 oder ein FehlerMeldung inlinegleiche SeitefetchNetzwerkfehler oder Prüfungnötigform.submit()der einfache PostPrüfseite, dannthanks.html_redirectJSONFallback
  1. Ohne JavaScript sendet der Browser das Formular, Ikiform leitet mit 303 auf deine _redirect-URL weiter, und der Besucher sieht thanks.html.
  2. Ist das Skript geladen, sendet es mit fetch und verlangt JSON. Ikiform antwortet mit 201 oder einem Fehler, und das Skript zeigt das Ergebnis auf derselben Seite.
  3. Wenn fetch scheitert oder Ikiform mit challenge_required antwortet, ruft das Skript form.submit() auf.
  4. Dieser einfache Post führt bei Bedarf Ikiforms Prüfseite aus und endet auf thanks.html.
Jeder Weg endet mit einer gespeicherten Anmeldung. Das Skript ändert nur, wie der Besucher sie sieht.

Die Dateien

Die Seite

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 schickt den Browser nach einem einfachen Post zu thanks.html. Es funktioniert nur für eine https:-URL auf einer der Allowed domains des Formulars, füge deine Domain also dort hinzu. Redirect after submit in den Einstellungen des Formulars tut dasselbe ohne das versteckte Eingabefeld.
  • Das Eingabefeld _gotcha ist der Honeypot. Menschen sehen es nie; Bots, die jedes Eingabefeld ausfüllen, werden als Spam gespeichert.
  • aria-live="polite" lässt Screenreader die Meldungen des Skripts ansagen, ohne den Fokus zu verschieben.
  • Das Skript-Tag nutzt defer, es läuft also, nachdem das Formular existiert, und blockiert nie das Zeichnen der Seite.

Die Erweiterung

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 lässt Ikiform mit JSON statt mit einer Weiterleitung antworten, damit das Skript das Ergebnis an Ort und Stelle zeigen kann.
  • Der Idempotenzschlüssel wird einmal pro Seitenaufruf erzeugt. Ein Doppelklick oder ein erneuter Versuch nach einer langsamen Antwort speichert eine Anmeldung.
  • challenge_required heißt, die Spamprüfung des Formulars steht auf Ikiform und fetch hat kein Token gesendet. form.submit() sendet auf die einfache Art, was Ikiforms Prüfseite und dann die Dankeseite zeigt. Es löst das submit-Event nicht erneut aus, es gibt also keine Schleife.
  • Ein Netzwerkfehler nimmt denselben Fallback. War Ikiform nicht erreichbar, zeigt der einfache Post den eigenen Fehler des Browsers, was immer noch ehrlicher ist als ein hängender Button.
  • Ein 429 liest Retry-After und sagt, wie lange zu warten ist.

Die Dankeseite

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>

Die 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; }
}
Wenn die Seite auf einer anderen Domain als example.com liegt, passe _redirect und den Link zurück an. Ikiform ignoriert ein _redirect, dem es nicht folgen kann, und zeigt stattdessen seine eigene gehostete Dankeseite, eine Abweichung kostet also nie eine Anmeldung.

Deployen und testen

Leg die vier Dateien in einen Ordner und lade ihn auf einen beliebigen statischen Host hoch. Füge dann die Domain des Hosts unter Allowed domains hinzu: zum Beispiel username.github.io, your-site.netlify.app oder deine eigene Domain. Solange du lokal arbeitest, füge auch localhost hinzu; liefere den Ordner mit einem beliebigen statischen Server aus, denn eine als file://-URL geöffnete Seite sendet keinen Origin.

Prüfen, ob es funktioniert

  1. Füge unter Allowed domains des Formulars die Domain deiner Site hinzu, plus localhost, während du entwickelst. Lass das Feld leer, um Posts von überall anzunehmen.
  2. Schalte im Browser JavaScript aus und melde dich an. Du landest auf thanks.html.
  3. Schalte es wieder ein und melde dich erneut an. Das Formular wird durch die Dankeszeile ersetzt, ohne die Seite zu verlassen.
  4. Setze Spam check auf Ikiform und melde dich noch einmal an. Die Seite wechselt zur Prüfung, dann zu thanks.html.
  5. Öffne Submissions im Dashboard. Die neue Einsendung ist da, mit jedem Feld, das du gesendet hast.
  6. Sende einen Post mit ausgefülltem Honeypot, _gotcha=bot. Er bekommt trotzdem eine Erfolgsantwort, aber die Einsendung wird als Spam gespeichert und löst weder E-Mail noch Webhook aus.

Stolperfallen

ProblemWas zu tun ist
Der Browser landet auf der Dankeseite von Ikiform statt auf deiner_redirect wird ignoriert, außer seine Domain steht unter Allowed domains und es ist eine https:-URL. Füge die Domain hinzu oder setze stattdessen Redirect after submit in den Einstellungen des Formulars.
403 origin_not_allowed, oder jeder Post landet im SpamDie Domain der Seite steht nicht unter Allowed domains des Formulars. Füge sie hinzu, plus localhost für lokale Tests. Subdomains einer eingetragenen Domain sind erlaubt.
Posts von einer als Datei geöffneten Seite werden abgelehnt oder als Spam markiertEine file://-Seite sendet keinen Origin. Liefere den Ordner mit einem lokalen statischen Server aus und füge localhost zu Allowed domains hinzu.
Die Dankeszeile erscheint nie, aber Anmeldungen kommen anDas Skript wurde nicht geladen: Prüfe den Pfad im src-Attribut oder ob der Host .js-Dateien ausliefert. Der einfache Post funktioniert weiterhin, deshalb kommen Anmeldungen an.
402 test_limit_reachedDer Workspace hat keinen Tarif und hat seine 50 kostenlosen localhost-Tests diesen Monat aufgebraucht. Wähle einen Tarif oder nutze Send test im Dashboard.

Die Doku zu HTML-Formularen listet jedes reservierte Feld auf, und die Wartelisten-Vorlage erzeugt eine Version mit Name und Rolle. Export der Liste und die Launch-E-Mail stehen in der Hauptanleitung.

Fragen

Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.