新しい SaaS のウェイトリスト

新しい SaaS のウェイトリスト2 分で読めます

プレーンな HTML でランディングページにウェイトリストフォームを追加する方法

ビルド不要でプレーンな HTML だけのウェイトリストページ。JavaScript がオフでも動き、読み込めばその場でお礼を表示し、必要なときはフォールバックします。

Preet SutharIkiform 創業者

メールアドレスを Ikiform のエンドポイントに送信してサンクスページにリダイレクトする普通の HTML フォームを書けば、JavaScript なしでどのホストでも動きます。そのうえで、同じフォームを fetch で送信してその場でお礼を表示する小さなスクリプトを追加し、人間確認がオンのときなどスクリプトで完了できない場合は通常の送信に戻します。

ウェイトリストのページに、フレームワークもビルドもサーバーも要りません。4 つの静的ファイルとフォームバックエンドがあれば十分で、ビルダーが書き出すどんなページよりも速く読み込まれます。このページでは全体を 1 層ずつ組み立て、各層が単独でも動くようにします。

ビルドなしで大切なこと

  • フォームそのものが連携です。 action を持つ素の <form> だけで Ikiform とつながります。パッケージも API キーもコンパイルも要りません。
  • JavaScript は必須ではなく、上乗せです。 スクリプトがブロックされても、遅くても、壊れていても、ページは登録を受け付けるべきです。以下の各層は前の層に機能を足し、失敗したら前の層に戻ります。
  • スクリプトで訪問者を行き詰まらせてはいけません。 fetch が失敗したり、fetch では実行できない人間確認を Ikiform が求めたりしたら、スクリプトはフォームをブラウザーに返し、通常の方法で送信させます。
  • ホスティングはどこでも構いません。 GitHub Pages、Netlify、Cloudflare Pages、S3 バケット、共有ホスティングのどれでも、これらのファイルをそのまま配信できます。
層追加されるものない場合
HTML フォームメールアドレスを送信し、thanks.html にリダイレクト何も動かないので、最初に作る
CSSライトモードとダークモードで読みやすいページ簡素だが動くフォーム
スクリプトその場でのお礼、フィールドエラー、ページ遷移なし通常の送信とリダイレクト
プレーンな HTML のウェイトリストページで登録がたどる 3 つの経路JavaScript なしスクリプトありスクリプトで完了できないフォーム送信urlencodedIkiform303 リダイレクトthanks.html自分のページfetchAccept:application/jsonIkiform201 かエラーその場のメッセージ同じページfetchネットワークエラーか確認が必要form.submit()通常の送信確認ページ、次にthanks.html_redirectJSONフォールバックプレーンな HTML のウェイトリストページで登録がたどる 3 つの経路JavaScript なしスクリプトありスクリプトで完了できないフォーム送信urlencodedIkiform303 リダイレクトthanks.html自分のページfetchAccept: application/jsonIkiform201 かエラーその場のメッセージ同じページfetchネットワークエラーか確認が必要form.submit()通常の送信確認ページ、次に thanks.html_redirectJSONフォールバック
  1. JavaScript がオフなら、ブラウザーがフォームを送信し、Ikiform が _redirect の URL に 303 でリダイレクトし、訪問者には thanks.html が表示されます。
  2. スクリプトが読み込まれていれば、fetch で送信して JSON を要求します。Ikiform は 201 かエラーを返し、スクリプトが同じページに結果を表示します。
  3. fetch が失敗したり、Ikiform が challenge_required を返したりした場合、スクリプトは form.submit() を呼びます。
  4. その通常の送信で、必要なら Ikiform の確認ページを経て、thanks.html に到着します。
どの経路でも最後に登録が保存されます。スクリプトが変えるのは訪問者への見え方だけです。

ファイル

ページ

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 は、通常の送信の後にブラウザーを thanks.html に移動させます。フォームの Allowed domains にある https: の URL でしか機能しないので、そこにドメインを追加してください。フォーム設定の Redirect after submit を使えば、hidden の入力欄なしで同じことができます。
  • _gotcha 入力欄はハニーポットです。人には見えず、すべての入力欄を埋めるボットはスパムとして保存されます。
  • aria-live="polite" により、スクリーンリーダーはフォーカスを移さずにスクリプトのメッセージを読み上げます。
  • script タグに defer を付けているので、フォームができてから実行され、ページの描画を妨げません。

機能の上乗せ

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 を付けると Ikiform がリダイレクトではなく JSON で応答するので、スクリプトがその場に結果を表示できます。
  • 冪等性キーはページの読み込みごとに 1 回作られます。ダブルクリックや、応答が遅いときのリトライでも、登録は 1 件だけ保存されます。
  • challenge_required は、フォームのスパムチェックが Ikiform に設定されていて、fetch がトークンを送らなかったことを意味します。form.submit() は通常の方法で送信するので、Ikiform の確認ページ、続いてサンクスページが表示されます。submit イベントは再び発火しないので、ループにはなりません。
  • ネットワークエラーでも同じフォールバックを使います。Ikiform に到達できなかった場合、通常の送信ではブラウザー自身のエラーが表示されますが、止まったままのボタンよりは正直です。
  • 429 では Retry-After を読み、どれだけ待てばよいかを表示します。

サンクスページ

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>

スタイル

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; }
}
ページが example.com 以外のドメインにある場合は、_redirect と戻るリンクを更新してください。Ikiform はたどれない _redirect を無視し、代わりに自身がホストするサンクスページを表示するので、不一致があっても登録が失われることはありません。

デプロイとテスト

4 つのファイルを 1 つのフォルダーに入れ、任意の静的ホストにアップロードします。次に、username.github.io、your-site.netlify.app、自分のドメインなど、ホストのドメインを Allowed domains に追加します。ローカルで作業する間は localhost も追加し、任意の静的サーバーでフォルダーを配信してください。file:// の URL で開いたページはオリジンを送らないためです。

動作を確認する

  1. フォームの Allowed domains に、自分のサイトのドメインと、開発中は localhost を追加します。空にしておくと、どこからの送信も受け付けます。
  2. ブラウザーで JavaScript をオフにして登録します。thanks.html に移動します。
  3. オンに戻してもう一度登録します。ページを離れずに、フォームがお礼の一文に置き換わります。
  4. Spam check を Ikiform に設定して、もう一度登録します。ページは確認に移り、その後 thanks.html に移動します。
  5. ダッシュボードで Submissions を開きます。送ったすべてのフィールドを含む新しい送信データがあります。
  6. ハニーポットに値を入れて _gotcha=bot で送信します。成功の応答は返りますが、送信データはスパムとして保存され、メールも Webhook も送られません。

ハマりどころ

問題対処法
ブラウザーが自分のページではなく Ikiform のサンクスページに移動する_redirect は、ドメインが Allowed domains にあり、https: の URL でない限り無視されます。ドメインを追加するか、代わりにフォームの設定で Redirect after submit を設定してください。
403 origin_not_allowed が返るか、すべての送信がスパムになるページのドメインがフォームの Allowed domains に入っていません。ドメインと、ローカルテスト用に localhost を追加してください。登録したドメインのサブドメインも許可されます。
ファイルとして開いたページからの送信が拒否される、またはスパムになるfile:// のページはオリジンを送りません。ローカルの静的サーバーでフォルダーを配信し、localhost を Allowed domains に追加してください。
登録は届くのに、お礼の一文が表示されないスクリプトが読み込まれていません。src 属性のパスや、ホストが .js ファイルを配信しているかを確認してください。通常の送信は動き続けるので、登録は届きます。
402 test_limit_reachedワークスペースにプランがなく、今月の無料の localhost テスト 50 件を使い切っています。プランを選ぶか、ダッシュボードの Send test を使ってください。

予約済みフィールドの一覧は HTML フォームのドキュメントに、名前と役割付きの版はウェイトリストのテンプレートにあります。リストのエクスポートとローンチメールについてはメインガイドを参照してください。

よくある質問

手順で詰まったときや、あなたの状況に合うガイドがほしいときはメールでどうぞ: hello@ikiform.com