ニュースレター登録フォーム

ニュースレター登録フォーム2 分で読めます

サーバーなしで、静的サイトにメール登録フォームを置く

Hugo、Jekyll、Eleventy、Astro の静的サイトにニュースレター登録フォームを追加し、メールツールの API キーをページに入れず、確認はツールで行います。

Preet SutharIkiform 創業者

静的サイトには送信先となる自分のサーバーがないので、フォームは読者のブラウザーから直接 Ikiform に送信します。メールツールの API キーはサイトに一切入れないでください。ブラウザーが知っているのは公開されている Ikiform のエンドポイントだけで、別の場所でホストしたリレーが購読者を追加します。フォームはパーシャルやインクルードとして一度だけ書き、設置場所ごとに異なる source を渡し、再読み込みなしの版には小さなスクリプトを 1 つ追加します。

ここが違う

静的サイトとは、ホスト上のファイルです。GitHub Pages、Netlify、Cloudflare Pages、S3 バケットなどです。読者がフォームを送信しても、自分で書いたものがサーバーで動くことはありません。そのため、ページからメールツールの API を呼び出すというよくある近道は使えません。ブラウザーで実行されるものは、誰でも読めるからです。

メールツールの API キーが、購読者 1 人の追加だけに限定されていることはまれです。購読者を追加できるキーは、たいていリスト全体の閲覧、エクスポート、削除もできます。静的サイトの JavaScript に入れると全訪問者に配布され、開発者ツールを開いた人なら誰でも手に入ります。

静的サイトで、メールツールの API キーをどこに置くかページにキーリレーにキー静的ページJavaScript にAPI キーメールツールのAPI誰でも呼び出せる自分のリスト誰でも読める静的ページ公開エンドポイントのみIkiformスパムチェック、署名付きWebhookリレーAPI キーを保持メールツール購読者を追加キー送信署名付きキー静的サイトで、メールツールの API キーをどこに置くかページにキーリレーにキー静的ページJavaScript に API キーメールツールの API誰でも呼び出せる自分のリスト誰でも読める静的ページ公開エンドポイントのみIkiformスパムチェック、署名付き WebhookリレーAPI キーを保持メールツール購読者を追加キー送信署名付きキー
  1. キーがページにあると、すべての訪問者がそれをダウンロードし、誰でもそのキーでメールツールの API を呼び出せます。
  2. リレーを使えば、ページが知っているのは Ikiform のエンドポイントだけです。これはフォームの送信しか受け付けません。
  3. Ikiform がスパムを除外し、署名付きの Webhook をリレーに送ります。
  4. 静的サイトの外でホストされたリレーが API キーを保持し、購読者を追加します。
フォームのエンドポイントは公開しても安全です。API キーは決して公開してはいけません。

フォームは一度だけ書く

ニュースレターのフォームは、フッター、各記事の末尾、ニュースレターのページなど、いくつものテンプレートに登場します。source の値を受け取るパーシャルにすれば、設置場所ごとに自分自身を記録できます。マークアップはどのジェネレーターでも同じで、変わるのは source のテンプレート構文だけです。

<form data-newsletter action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <input name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-label="Email" />
  <label><input name="consent" type="checkbox" value="true" required /> Email me the newsletter. I can unsubscribe at any time.</label>
  <input type="hidden" name="source" value="{{ .source | default "footer" }}" />
  <input type="hidden" name="_redirect" value="https://example.com/subscribed/" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Subscribe</button>
  <p role="status" hidden></p>
</form>

次に、設置場所の名前を付けてテンプレートに入れます。

{{ partial "newsletter.html" (dict "source" "post") }}
  • Astro では <Newsletter source="post" /> を使います。静的な Astro のビルドでは Actions を実行できないので、このコンポーネントも他と同じく直接 Ikiform に送信します。
  • Ikiform のフォームのフィールド定義に、source を最大長の短いテキストとして追加します。フィールドを定義している場合、定義にない入力欄は unknown_field で拒否されます。
  • _redirect は、フォームの Allowed domains のいずれかにある https: の URL でなければなりません。受信トレイを確認するよう伝える 1 行を載せた /subscribed/ ページを作ってください。

ページ上のすべてのフォームに 1 つのスクリプト

記事のページには、記事の後とフッターの 2 つのニュースレターフォームがあることがあります。このスクリプトは data-newsletter の付いたすべてのフォームを処理し、1 つが成功するとすべてを置き換えるので、少し下でもう一度購読を求められることはありません。ベースレイアウトから一度だけ読み込んでください。

static/newsletter.js
for (const form of document.querySelectorAll("form[data-newsletter]")) {
  const status = form.querySelector('[role="status"]');
  const button = form.querySelector("button");

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    button.disabled = true;
    status.hidden = false;
    status.textContent = "Subscribing";
    try {
      const response = await fetch(form.action, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(form),
      });
      const result = await response.json();
      if (result.ok) {
        for (const other of document.querySelectorAll("form[data-newsletter]")) {
          other.replaceChildren(Object.assign(document.createElement("p"), {
            textContent: "Almost done. Check your inbox for a confirmation link.",
          }));
        }
        return;
      }
      if (response.status === 429 && response.headers.has("Retry-After")) {
        status.textContent = "Too many tries from this network. Wait a minute and try again.";
      } else {
        const errors = result.error.fields ?? {};
        status.textContent = errors.email?.[0] ?? errors.consent?.[0] ?? result.error.message;
      }
    } catch {
      form.submit();
      return;
    }
    button.disabled = false;
  });
}
  • 厳しいネットワークフィルターの内側などで fetch が失敗した場合、スクリプトは form.submit() にフォールバックします。これは通常の方法で送信し、/subscribed/ に着地します。
  • 429 には専用のメッセージを表示します。同じオフィスやキャンパスのネットワークにいる読者は IP を共有しているので、訪問者ごとの制限にまとめて達することがあります。
  • フォームは JavaScript がオフでも動きます。スクリプトは応答の表示方法を変えるだけです。

JavaScript がオフの読者は /subscribed/ に着地するので、そのページでスクリプトが他の人にしていることを代わりに行いましょう。確認メールが送られていることを伝え、探すべき送信者名を示し、迷惑メールフォルダーにも触れ、ブログへのリンクを置きます。ジェネレーターで他のページと同じように作る普通のページで、あとでメールツールの確認リンクの遷移先にすることもできます。

リレーをどこに置くか

リレーには、コードを実行できる公開の HTTPS URL が必要です。静的ホストによって、提供されている場合とされていない場合があります。

静的ホストリレーの選択肢
GitHub Pages, S3関数なし。カスタムドメインの Cloudflare Worker か、自動化ツールを使います
Netlifyサイトのドメインの Netlify Function、または上記のいずれか
Cloudflare Pagesカスタムドメインの別の Worker。Ikiform は pages.dev と workers.dev の URL を拒否します
Vercel自分のドメインの関数。静的な出力を API ルートと並べて置けます

各メールツール用のリレーのコードは、Kit、MailerLite、beehiiv のページにあります。コードを一切書かないなら、Webhook を Zapier か n8n に向け、そのメールツール用のアクションを使ってください。URL のルールは Webhook のドキュメントにあります。

静的サイトには、別ドメインのデプロイプレビューがあることがよくあります。Allowed domains にないドメインからの送信はスパムとしてマークされるか拒否されるので、テスト中はプレビューのドメインを追加するか、本番のドメインと localhost でテストしてください。

サイトが Content Security Policy を送っている場合

多くの静的ホストでは、Netlify や Cloudflare Pages の _headers のように、ファイルからヘッダーを設定できます。Content-Security-Policy を送っている場合、ポリシーが Ikiform を許可していなければブラウザーはフォームをブロックします。form-action はプレーンな送信を、connect-src はスクリプトの fetch をカバーします。

_headers
Content-Security-Policy: default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com

ブロックされた送信はブラウザーのコンソールに CSP エラーとして表示され、Ikiform には届かないので、ダッシュボードにも何も表示されません。

動作を確認する

  1. 本番のドメインと localhost を Allowed domains に追加します。
  2. サイトをビルドして記事のページを開きます。両方のフォームがあり、ページのソースではそれぞれ別の source の値を持っています。
  3. 記事のフォームから自分のアドレスで購読します。両方のフォームが、受信トレイを確認するよう伝える 1 行に変わります。
  4. JavaScript をオフにしてフッターから購読します。/subscribed/ に着地します。
  5. ダッシュボードで Submissions を開きます。2 件の登録の source は post と footer になっています。
  6. ビルドしたサイトでメールツールの API キーを検索します。見つかりません。

ハマりどころ

問題対処法
ダッシュボードに何もなく、コンソールに CSP エラーがあるContent-Security-Policy の form-action と connect-src に https://ikiform.com を追加してください。
source で unknown_field の 422フォームのフィールド定義に source を追加するか、非表示の入力欄を削除してください。
403 origin_not_allowed が返る、またはすべての送信がスパムになるページのドメインがフォームの Allowed domains にありません。ドメインと、ローカルテスト用の localhost を追加してください。登録したドメインのサブドメインは許可されます。
ブラウザーが自分のページではなく Ikiform のサンクスページに移動する_redirect は、ドメインが Allowed domains にあり、https: の URL でないと無視されます。ドメインを追加するか、代わりにフォーム設定で Redirect after submit を設定してください。

よくある質問

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