ガイド

ガイド4 分で読めます

バックエンドなしで静的サイトにお問い合わせフォームを追加する

サーバーなしで静的サイトに動くお問い合わせフォームを追加します。POST の送り先、スパム、メールでの返信、各ホストとフレームワーク向けのコードまで。

Preet SutharIkiform 創業者

静的ホストはファイルを配信するだけで、フォームの POST はたいてい 405 か 404 で拒否するので、フォームは別の場所に送信する必要があります。フォームの action を Ikiform などのフォームバックエンドに向ければ、メッセージごとに保存し、スパムを除外し、送信者を reply-to にしたメールで通知します。サイトは静的なままで、サーバーのコードも、ページ内のシークレットキーも、デプロイ方法の変更も要りません。

静的サイトとは、ホストがファイルとして配信する HTML、CSS、JavaScript のフォルダーです。だから速く、安く、壊れにくいのです。そしてそれこそが、お問い合わせフォームが真っ先に動かなくなる理由でもあります。フォームの送信は誰かが受け取るべきリクエストですが、静的ホストには待ち受けているものが何もありません。

静的ホストでフォームが失敗する理由

action のない <form method="post"> は、そのフォームがあるページ自身に送信します。静的ホストではそれはファイルで、ファイルが応答するのは GET と HEAD だけです。2026-10-25 に主要な各ホストで公開中の静的ページに POST を送ってみましたが、どれも何も保存しませんでした:

ホスト静的ページへの POST の結果
GitHub Pages小さな HTML ページで 405 Not Allowed
Vercel空のボディで 405
Netlify404。サイトに 404 ページがあればそれを表示。ただし Netlify Forms がデプロイ時にそのフォームを検出した場合を除く
Cloudflare Pages空のボディで 405
Amazon S3 のウェブサイトエンドポイント405 Method Not Allowed、コードは MethodNotAllowed

解決策はどこでも同じで、送信を受け付ける URL に送ることです。その URL は、自分で書いてホストするコードでも、ホストの機能でも、フォームバックエンドでも構いません。このガイドではこの先、サイトを静的なまま保てるフォームバックエンドを使います。

修正の前後で、お問い合わせフォームの送信がどこへ行くかaction が自分のサイトを指すaction が Ikiform を指すお問い合わせページ静的な HTML静的ホストファイルを配信、GET に応答405 か 404メッセージが消えるお問い合わせページ同じ HTML、新しい actionIkiform確認、保存、メールサンクスページ自分のサイトに戻るPOSTPOST303修正の前後で、お問い合わせフォームの送信がどこへ行くかaction が自分のサイトを指すaction が Ikiform を指すお問い合わせページ静的な HTML静的ホストファイルを配信、GET に応答405 か 404メッセージが消えるお問い合わせページ同じ HTML、新しい actionIkiform確認、保存、メールサンクスページ自分のサイトに戻るPOSTPOST303
  1. action がないか、自分のサイトのパスを指す action だと、ブラウザーは静的ホストに送信します。
  2. ホストには POST に対して実行するコードがないので、405 か 404 を返し、メッセージは失われます。
  3. action を Ikiform のエンドポイントに向けると、送信はそれを受け付けるサーバーに届きます。
  4. Ikiform はメッセージのスパムチェックを行い、保存して、メールで通知します。
  5. Ikiform は 303 リダイレクトで応答し、ブラウザーは自分のサイトのサンクスページを読み込みます。
変わるのはフォームの action だけです。ページ、ホスト、デプロイはそのままです。

各メッセージに起きること

Ikiform はフォームの送信先のエンドポイントです。ページをホストしたりフォームをレンダリングしたりはせず、どちらも自分で持ちます。送信ごとにスパムチェックを行い、メッセージを保存し、あなたが読む場所に届けます。

お問い合わせフォームの 1 件のメッセージを Ikiform がどう扱うかお問い合わせフォームどの静的ホストでもIkiformスパムチェック、保存通知メール返信は送信者へダッシュボード検索、スパムフィルター、CSV署名付き WebhookSlack、Discord、自分のコード送信お問い合わせフォームの 1 件のメッセージを Ikiform がどう扱うかお問い合わせフォームどの静的ホストでもIkiformスパムチェック、保存通知メール返信は送信者へダッシュボード検索、スパムフィルター、CSV署名付き WebhookSlack、Discord、自分のコード送信
  1. 訪問者がお問い合わせフォームに入力し、フォームが Ikiform のエンドポイントに送信されます。
  2. Ikiform はハニーポット、レート制限、スパムスコアを確認してから、メッセージを保存します。
  3. 通知の受信者それぞれに、reply-to を送信者のアドレスにしたメールが届きます。
  4. メッセージはダッシュボードに表示され、検索、スパムの復元、CSV のエクスポートができます。
  5. Webhook を追加すると、Ikiform は署名付きのコピーを Slack、Discord、自動化ツール、自分のコードに送ります。
ほとんどのお問い合わせフォームはメールで十分です。いつも作業している場所にメッセージを届けたいときに Webhook を追加してください。

静的サイトのフォームを受け取る 5 つの方法

方法得意なことトレードオフ
mailto: リンクまたは action="mailto:..."設定は一切不要。アクセスの少ない個人ページなら十分です。訪問者のメールアプリを開きます(設定されていれば)。何も保存もフィルターもされず、アドレスが HTML に載るのでスクレイパーに拾われます。
Netlify Forms などのホストのフォーム機能属性 1 つで済み、別のアカウントは不要。たいていホスティングのプランに含まれています。そのホストでしか動きません。ホストを移るとフォームを作り直すことになり、JavaScript でレンダリングするフォームには回避策が必要です。
自分で書くサーバーレス関数バリデーション、保存、送るメールを完全に制御できます。バックエンドを抱えることになります。メール API のキー、スパム対策、レート制限、リトライ、ログ。関数の形式はホストごとに異なります。
Google Forms、Tally、Typeform などのホスト型フォームの埋め込みコード不要。長いフォームや複数ステップのフォームに向いています。iframe か別サイトへのリンクになり、相手の見た目とスクリプトがページに入ります。
Ikiform、Formspree、Basin、Web3Forms などのフォームバックエンドどのホストでも自分の HTML を使えます。サーバーなしで保存、スパム対策、メール通知ができます。アカウントと請求がもう 1 つ増え、フォームのデータはそのサービスに置かれます。

Netlify を使っていて移る予定がないなら、手間の少なさで Netlify Forms にかなうものはありません。バックエンドを自分で書きたいなら、関数は週末のプロジェクトにちょうどよい題材です。ホストやフレームワークを変えてもフォームを動かし続けたいなら、フォームバックエンドを使ってください。各バックエンドは比較ページで並べて比べられます。

フォーム

以下のサンプルはすべてブラウザーから送信します。静的デプロイには送信元になるサーバーがないからです。スタックを選んでください。選択はこのガイドのすべてのコードサンプルに引き継がれます。静的サイトジェネレーターには個別のセットアップページがあります: Jekyll、Hugo、Eleventy、Gatsby。

手書きのサイトなら、連携はこれで全部です。フォームは通常の方法で送信するので JavaScript がオフでも動き、送信に成功するとブラウザーは thanks.html に移動します。

contact.html
<form id="contact" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" maxlength="120" required />
  <p class="field-error" data-error-for="name" hidden></p>

  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <p class="field-error" data-error-for="email" hidden></p>

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" minlength="10" maxlength="5000" required></textarea>
  <p class="field-error" data-error-for="message" hidden></p>

  <input type="hidden" name="_redirect" value="https://example.com/thanks.html" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button type="submit">Send message</button>
  <p id="contact-status" role="status"></p>
</form>
<p id="contact-sent" role="status" hidden>Thanks. Your message is on its way, and I'll reply by email.</p>

<script src="/contact.js" defer></script>

スクリプトは任意です。訪問者をページにとどめ、各フィールドのエラーを入力欄の下に表示し、失敗したときも入力したメッセージを残します。

contact.js
const form = document.querySelector("#contact");
const status = document.querySelector("#contact-status");
const done = document.querySelector("#contact-sent");
const button = form.querySelector("button");

function showErrors(fields = {}) {
  for (const slot of form.querySelectorAll("[data-error-for]")) {
    const name = slot.dataset.errorFor;
    const message = fields[name]?.[0] ?? "";
    slot.textContent = message;
    slot.hidden = !message;
    form.elements[name]?.setAttribute("aria-invalid", message ? "true" : "false");
  }
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  button.disabled = true;
  status.textContent = "Sending";
  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) {
      form.hidden = true;
      done.hidden = false;
      return;
    }
    showErrors(result.error.fields);
    status.textContent = result.error.message;
  } catch {
    status.textContent = "Couldn't send. Check your connection and try again.";
  }
  button.disabled = false;
});
  • frm_xxxxxxxxxxxxxxxx をダッシュボードにある自分のフォームの ID に置き換えてください。ID はもともと公開される前提のもので、デプロイした瞬間に HTML に含まれます。静的サイトにシークレットの API キーを置くことは絶対にしないでください。
  • メールアドレスのフィールド名は email のままにしてください。Ikiform はこれを通知メールの reply-to に設定し、使い捨てメールのドメインと照合します。
  • 非表示の _gotcha 入力欄はハニーポットです。人には見えず、すべての入力欄を埋めるボットは自らスパムだと名乗ることになります。
  • JavaScript 版は Accept: application/json を送るので、Ikiform は成功すると 201 を、失敗すると各フィールドと問題を示す error.fields オブジェクト付きの 422 を返します。

本番でフレームワークがサーバー上で動くなら、代わりにそこから送信して、エンドポイントをページに出さないこともできます。サーバー版は Next.js、SvelteKit、Nuxt の各ガイドに、シークレットキーについてはサーバーから送信するにあります。

サーバー側の版はシークレットキー付きで送信するので、Ikiform は Allowed domains と人間確認をスキップします。その場合はすべての送信がサーバーの IP から来るため、訪問者ごとに 1 分あたり 5 件という Standard の制限が、全訪問者をまとめて数えることになります。サーバー経由で送信するフォームには Relaxed か Custom のレート制限を選び、IKIFORM_SECRET_KEY はサーバー専用の環境変数に置いてください。

お問い合わせフォームのフィールド

名前、メールアドレス、メッセージがあれば、ほとんどのお問い合わせフォームは足ります。メッセージの種類によって対応する人が違うなら、件名の選択欄を追加する価値があります。下のフォームを試してみてください。ブラウザーで Ikiform のバリデーターを実行し、保存される内容か、返ってくる 422 を表示します。

プレビュー

Ikiform のバリデーターをブラウザーで実行します。何も送信されません。

お問い合わせフォームのテンプレートと同じ定義です。フォームの Fields タブに貼り付けると、HTML がどうであれ Ikiform がサーバー側でこれを適用します。
フィールド理由注意点
name返信での呼びかけに使う名前欄のリンクは強いスパムのサインなので、ID は name のままにする
email返信の宛先必須。type="email" でスマートフォンに適切なキーボードが出る
topic営業とサポートを適切な担当者に振り分ける選択欄は一覧にある値しか受け付けないので、安心して絞り込みに使える
messageフォームの目的そのもの最小 10 文字にすると一言だけの送信を防げる

電話番号、会社の規模など、最初の返信で使わないものは省いてください。フィールドを増やすほどメッセージは減ります。フィールド定義がなければ、Ikiform はフォームが送る名前付きの入力欄をすべて保存します。定義があれば、合わない送信を拒否します。

ファイルのアップロードにはまだ対応していません。ファイル付きの送信には 422 files_not_supported が返ります。

受信トレイから返信する

通知メールは、フォームごとに確認済みの受信者最大 10 人に届きます。フォームに email フィールドがあればメールの reply-to が送信者になるので、メールアプリで Reply(返信)を押せばそのまま相手に返信できます。ダッシュボードからアドレスをコピーする必要はありません。

通知はフォームごとに 1 時間あたり 50 通までで、それを超えると 1 時間ごとのダイジェストになるので、スパムの波で受信トレイが埋もれることはありません。スパムと判定されたメッセージはメールを送りません。_subject は通知メールの件名を変えません。Webhook と API には meta.subject として渡されます。

Ikiform はフォームを入力した本人にはメールを送りません。通知メールが届くのはあなたとチームだけなので、自動返信、確認メール、ダブルオプトインはありません。それらは Webhook をきっかけに、自分のメールツールから送ってください。

メッセージをメールの代わりに、またはメールに加えてチームのチャットで受け取りたいですか? Webhook を追加して、Slack、Discord、Telegram の手順に従ってください。

送信後に訪問者に表示されるもの

プレーンな HTML の送信はリダイレクトで終わります。Ikiform はブラウザーからの送信が成功すると 303 See Other で応答し、ブラウザーは移動先を GET で読み込みます。これなら静的ホストも喜んで配信します。

プレーンな HTML フォームの 1 回の送信の裏にある 3 つのリクエストブラウザー訪問者が送信を押すikiform.com/f/…メッセージを保存自分の/thanks.html静的ファイルPOST303 の後に GETプレーンな HTML フォームの 1 回の送信の裏にある 3 つのリクエストブラウザー訪問者が送信を押すikiform.com/f/…メッセージを保存自分の /thanks.html静的ファイルPOST303 の後に GET
  1. ブラウザーがフォームのフィールドを Ikiform のエンドポイントに送信します。
  2. Ikiform はメッセージを保存し、Location ヘッダー付きの 303 See Other で応答します。
  3. ブラウザーはリダイレクトに従ってサンクスページを GET し、静的ホストはそれを他のファイルと同じように配信します。
リダイレクト先がなければ、手順 3 では自分のページではなく Ikiform がホストするサンクスページが読み込まれます。

Ikiform は次の順番で移動先を選びます:

  • フォーム設定の Redirect after submit。設定できるのはチームだけなので、http: でも https: でも任意の URL を使えます。サイトが 1 つならこれがいちばん簡単です。
  • フォーム内の _redirect フィールド。フォームの Allowed domains にある https: の URL の場合に限ります。1 つのフォームを複数のサイトや言語で使うときに使います。
  • どちらもなければ、Ikiform がホストするサンクスページ。許可された https: のページからの送信なら、サイトへ戻るリンクが付きます。

サンクスページは実在するファイルにし、検索結果には出ないようにしてください。いつ返信するかを書いておけば、同じ人から 2 通目のメールが来るのを防げます。

thanks.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="robots" content="noindex" />
    <title>Message sent</title>
  </head>
  <body>
    <h1>Thanks, your message is on its way</h1>
    <p>I read every message and reply within two working days, from the address on this site.</p>
    <p><a href="/">Back to the home page</a></p>
  </body>
</html>

上の JavaScript 版はリダイレクトを使いません。Accept: application/json 付きの送信には JSON が返り、ページが自分で成功のメッセージを表示します。

静的なフォームのスパム

エンドポイントはページのソースに載っていて、ボットはページのソースを読みます。公開から数日でスパムが来ると思っておいてください。Ikiform は防御を何層にも重ねているので、設定ゼロから始められます:

防御防げるもの設定
ハニーポットすべての入力欄を埋めるボット非表示の _gotcha 入力欄
スパムスコア名前欄のリンク(+3)、多数のリンク(+2)、既知のスパム表現(+2)、使い捨てアドレス。5 に達したメッセージはスパムとして保存なし
レート制限ループで送信し続けるスクリプトデフォルトでオン: Standard、訪問者ごとに 1 分あたり 5 件
許可ドメイン他のサイトから送信される、フォームのコピーフォーム設定にドメインを追加
人間確認本物のブラウザーを操作するボットSpam check を Ikiform にすると、キー不要で Cloudflare Turnstile を使える

スパムには本物のメッセージと同じ成功の応答が返るのでボットは何も学べず、メールも Webhook も送られません。お問い合わせフォームで 1 人が数分のうちに 2 回以上送信することはまれなので、公開後は訪問者ごとに 10 分あたり 2 件の Strict が合っています。各層の説明はスパムのドキュメントにあり、人間確認を使うには JavaScript 版で SDK の challenge() を 1 回呼ぶ必要があります。

許可ドメインとプレビューデプロイ

Allowed domains は、ドメインとそのサブドメインに一致します。静的ホストでは思ったより多くのドメインを使います。本番のドメイン、ホスト自身のサブドメイン、そしてブランチやプルリクエストごとのプレビュー用アドレスです。プレビューの中には、サイトのホスト用アドレスのサブドメインで一致するものもあれば、そうでなく個別の登録が必要なものもあります。

ホスト追加するものプレビュー
GitHub Pagesusername.github.io と独自ドメインデフォルトではプレビューなし
Vercelyour-project.vercel.app と自分のドメインブランチの URL は一致しない。テスト中は正確なホストを追加する
Netlifyyour-site.netlify.app と自分のドメインdeploy-preview-12--your-site.netlify.app は一致しない
Cloudflare Pagesyour-project.pages.dev と自分のドメインabc123.your-project.pages.dev は最初から一致する

github.io、vercel.app、netlify.app のような共有ホストだけを追加することは絶対にしないでください。他人のサイトも含め、そのホスト上のすべてのサイトが一致してしまいます。

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

静的サイトのテンプレートの多くには厳しい Content-Security-Policy が含まれています。フォームが動くかどうかを決めるディレクティブは 2 つです。プレーンな HTML の送信には form-action、fetch 版には connect-src です。どちらかが欠けていると、リクエストはページを出る前にブラウザーにブロックされ、手がかりはコンソールのメッセージだけになります。

レスポンスヘッダー
Content-Security-Policy: default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com

form-action には 'self' を残してください。ブラウザーによっては、サンクスページへのリダイレクトもこれと照合するためです。ヘッダーを置く場所はホストによって異なります。Netlify と Cloudflare Pages は _headers、Vercel は vercel.json、GitHub Pages は <meta> タグ、S3 なら前段の CloudFront のレスポンスヘッダーポリシーです。それぞれの方法は下のホスト別ページにあります。

デプロイ前にテストする

ファイルをダブルクリックするのではなく、ローカルサーバー経由でページを開いてください。file:// で開いたページは Origin: null を送り、どの許可ドメインにも一致せず、ローカルのテストとしても扱われません。次のどれでも現在のフォルダーを配信できます:

ターミナル
npx serve .

python3 -m http.server 8000

テスト中は localhost を Allowed domains に追加してください。localhost からの送信はテストとして扱われ、プランのないワークスペースでも月 50 件まで使えるので、支払いの前にフォーム全体を作り上げられます。

状況に合わせて選ぶ

上のフォームはどの静的ホストでも動きます。ホストごとに失敗のしかた、プレビュー用ドメイン、ヘッダーファイルが異なります。それらは次のページで説明しています。

動作を確認する

本番のドメインで 1 回、ホストがプレビューを作るならプレビューでも 1 回、通して確認してください。Ikiform に無料プランはありません。テストは無料で、プランのないワークスペースでも localhost からのテスト送信を月 50 件まで受け付けます。本番運用するには、月 $6 で 500 件からの Pro を選ぶか、1 回払いで 10,000 件 $49 または 50,000 件 $149 のパックを購入します。クレジットに有効期限はありません。料金を参照してください。

  1. フォームの Allowed domains にサイトのドメインを追加し、開発中は localhost も追加します。空のままにすると、どこからの送信でも受け付けます。
  2. ローカルサーバーから送信し、Submissions にメッセージが表示されることを確認します。
  3. デプロイしてから、JavaScript をオフにして本番のサイトから送信します。自分のサンクスページに移動するはずです。
  4. JavaScript をオンに戻し、メッセージを空にして送信します。エラーはホストされたエラーページではなく、そのページ上に表示されるはずです。
  5. 通知の受信者それぞれの受信トレイを確認します。各自にフィールド入りのメールが届き、フォームに email フィールドがあれば Reply の宛先は送信者になります。
  6. ハニーポットを埋めて _gotcha=bot で送信します。成功の応答は返りますが、送信データはスパムとして保存され、メールも Webhook も送られません。
  7. 何も届かない場合は、ダッシュボードのステータスフィルターを Spam に切り替えてください。スパムにも本物の送信と同じ成功の応答が返ります。

トラブルシューティング

問題対処
送信を押した直後に 405 Method Not Allowed か 404フォームが自分のサイトに送信しています。ソースではなく、デプロイ後の HTML の action 属性を確認してください。ビルドの処理やフレームワークの <Form> コンポーネントが置き換えている可能性があります。
何も起きず、コンソールに Content-Security-Policy の記述があるプレーンな送信のために form-action に、fetch のために connect-src に https://ikiform.com を追加してください。
403 origin_not_allowed が返るか、すべての送信がスパムになるページのドメインがフォームの Allowed domains にありません。ドメインと、ローカルのテスト用に localhost を追加してください。登録済みドメインのサブドメインは許可されます。
ローカルのテストが 402 plan_required で拒否されるページが file:// の URL で開かれたため、送信に使えるオリジンがなく、本番扱いになりました。ローカルサーバーでフォルダーを配信し、localhost からテストしてください。
自分のページではなく Ikiform のサンクスページに移動する_redirect は、ドメインが Allowed domains にあり、かつ https: の URL でない限り無視されます。ドメインを追加するか、代わりにフォーム設定で Redirect after submit を設定してください。
422 validation_failed送信がフォームのフィールド定義に合っていません。error.fields に各フィールドと理由が示されます。入力欄の name をフィールド ID に合わせてください。
送信データはダッシュボードにあるのに、メールも Webhook も届かないスパムとして保存されています。スパムは何も送りません。Spam フィルターで開いて理由を確認し、スパムではないとマークすると、メールと Webhook が送られます。
429 rate_limited1 人の訪問者がフォームのレート制限を超えて送信しました。Standard では訪問者ごとに 1 分あたり 5 件までです。Retry-After の時間だけ待つか、1 つの共有 IP から送信するフォームには Relaxed か Custom を選んでください。

よくある質問

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