静的サイトのお問い合わせフォーム

静的サイトのお問い合わせフォーム2 分で読めます

Netlify Forms を使わずに Netlify にお問い合わせフォームを追加する

Netlify Forms を使わずに、Netlify のサイトでお問い合わせフォームを運用します。ビルド時の検出なし、React からの JSON 送信、デプロイプレビュー用の別フォーム。

Preet SutharIkiform 創業者

フォームに data-netlify を付けず、action を Ikiform のエンドポイントに向けます。Netlify はページを配信するだけで、送信には一切関わりません。React や Vue のフォームに hidden の HTML のコピーは不要で、JSON で送信できます。プレビューのホストは Allowed domains でサイトと一致しないので、netlify.toml でデプロイプレビュー用に別のフォーム ID を設定してください。

Netlify ならではの違い

Netlify は、主要な静的ホストの中で唯一フォーム機能を内蔵しています。Netlify Forms はビルドのたびにデプロイされた HTML を読み、data-netlify か netlify の付いたフォームを見つけて、そのフォームについては自分のサイトへの送信を受け付けます。それ以外の静的ページへの送信は失敗し、Netlify では 405 ではなく 404 で失敗します:

ターミナル
curl -i -X POST -d "message=hello" https://your-site.netlify.app/contact/
応答
HTTP/2 404
server: Netlify
cache-status: "Netlify Edge"; fwd=method; fwd-status=404
content-length: 0

この 404 のせいで、実は存在するページを探し回ることになります。これは、検出されたフォームのどれも送信と一致しなかったという意味です。そもそも Netlify Forms を使うかどうかは別の問題で、Ikiform と Netlify Forms の比較で率直に答えています。サイトがずっと Netlify にあり、フォームがプレーンな HTML なら、Netlify Forms のほうが手間はかかりません。このページはそれ以外の場合のためのものです。

あなたの状況Netlify Forms が邪魔になる理由
フォームを React、Vue、Svelte で描画しているビルドは静的な HTML を解析するので、クライアントで描画するフォームには Netlify が見つけられる hidden の複製が必要
fetch から JSON を送信したいNetlify Forms は urlencoded と multipart のボディしか受け付けない
サイトを別のホストに移すかもしれないフォームは自分のドメインに送信するので、Netlify がそのドメインを配信している間しか動かない
メッセージを、検証できる Webhook に届けたいIkiform は各配信に Standard Webhooks のヘッダーで署名し、リトライする
同じ Netlify サイトでの Netlify Forms と Ikiform の比較Netlify Forms の場合Ikiform の場合ビルドNetlify が HTML を走査data-netlifyあり?自分のサイトへ POSTNetlify が保存POST で 404フォームは無視ビルド走査は不要ikiform.com へPOSTHTML か JSONIkiform保存、メール、Webhook見つかった見つからない同じ Netlify サイトでの Netlify Forms と Ikiform の比較Netlify Forms の場合Ikiform の場合ビルドNetlify が HTML を走査data-netlifyあり?自分のサイトへ POSTNetlify が保存POST で404フォームは無視ビルド走査は不要ikiform.com へPOSTHTML か JSONIkiform保存、メール、Webhook見つかった見つからない
  1. Netlify Forms では、Netlify がデプロイごとに HTML を走査し、data-netlify の付いたフォームを探します。
  2. 見つかったフォームは自分のサイトへの送信を受け付け、Netlify がそれを保存します。
  3. hidden のコピーなしで JavaScript が描画したフォームなど、見つからなかったフォームは、どの送信も 404 になります。
  4. Ikiform なら、ビルド時に何かを見つけてもらう必要はありません。
  5. フォームは HTML か JSON で ikiform.com に直接送信し、Ikiform が保存してメールで通知します。
両方を 1 つのサイトに共存させられます。data-netlify の付いたフォームは Netlify に、action が Ikiform のエンドポイントのフォームは Ikiform に送られます。

フォームを Netlify Forms から外す

  • フォームから data-netlify か netlify、netlify-honeypot、data-netlify-recaptcha を削除し、hidden の form-name 入力欄も削除します。form-name を残すと、普通のフィールドとして保存されます。
  • 下のような、JavaScript フォームの hidden の静的コピーを削除します。Netlify がフォームを検出するためだけに必要だったものです。
  • action(または fetch の URL)を Ikiform のエンドポイントに設定します。以前の action="/thanks" は、フォームの Redirect after submit 設定に移します。
  • 先に Netlify の Forms ページから過去の送信データをダウンロードしてください。自動では移行されません。
index.html は削除する
<form name="contact" netlify hidden>
  <input name="name" />
  <input name="email" />
  <textarea name="message"></textarea>
</form>

サイトの他のフォームがまだ Netlify Forms を使っているなら、form detection はオンのままにしてください。Netlify のドキュメントによれば、検出をオフにするのは Netlify Forms を使わないサイトだけが対象です。どこからも使わなくなったらオフにすれば、デプロイごとの走査を省けます。

Netlify のスパム設定の代わりになるもの

Netlify は送信に Akismet を実行し、netlify-honeypot と reCAPTCHA を提供しています。Ikiform では、フォームの Honeypot field を bot-field など今使っている名前に設定すれば、ハニーポットはそのまま機能します。スパムスコアは設定なしですべての送信に対して実行され、Spam check を Ikiform にすれば、キー不要の Cloudflare Turnstile が reCAPTCHA の代わりになります。切り替えるときは reCAPTCHA の div を削除してください。

JSON を送信する React のフォーム

Netlify Forms がなければ、クライアントで描画するフォームに複製は不要で、JSON も送れます。JSON なら型が保たれ、先に値をトリムすることもできます。このフォームは、ページをビルドした Netlify のコンテキストも記録します:

src/ContactForm.tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = `https://ikiform.com/f/${import.meta.env.VITE_IKIFORM_FORM_ID}`;

interface Message {
  name: string;
  email: string;
  message: string;
  context: string;
}

export function ContactForm() {
  const [sent, setSent] = useState(false);
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const body: Message & Record<string, string> = {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
      context: import.meta.env.VITE_CONTEXT ?? "local",
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
    setError("");
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify(body),
    }).catch(() => null);
    const result = await response?.json().catch(() => null);
    if (result?.ok) return setSent(true);
    const fields: Record<string, string[]> = result?.error?.fields ?? {};
    setError(Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.");
  }

  if (sent) return <p role="status">Got it. I'll answer from my own inbox, usually the same day.</p>;

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} minLength={10} required />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button>Send</button>
    </form>
  );
}
  • Content-Type: application/json は Ikiform に JSON として解析させ、Accept: application/json は JSON での応答を求めます。
  • VITE_CONTEXT は名前の一例です。Netlify はビルド中に CONTEXT を設定するので、ビルドコマンドで Vite に渡すか、このフィールドを省いてください。
  • ハニーポットは JSON のキーとして送ります。入力欄が非表示でもボディに含めておけば、入力したボットは引き続き自らスパムだと名乗ることになります。

デプロイプレビューには専用のフォームを

デプロイプレビューは deploy-preview-12--your-site.netlify.app のようなホストに置かれます。これは your-site.netlify.app のサブドメインではないので Allowed domains で一致せず、プレビューからの送信はスパムになります。netlify.toml でプレビューとブランチデプロイに専用のフォームを割り当て、同じファイルで Content-Security-Policy ヘッダーを設定してください:

netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[context.production.environment]
  VITE_IKIFORM_FORM_ID = "frm_xxxxxxxxxxxxxxxx"

[context.deploy-preview.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[context.branch-deploy.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"

プレビュー用フォームの Allowed domains は空のままにするか、テスト中だけ正確なプレビューのホストを追加します。本番用フォームには自分のドメインと your-site.netlify.app を設定します。netlify.toml のヘッダーはすべてのデプロイコンテキストに適用されます。_headers ファイルを使う場合は、公開ディレクトリに含まれるようにしてください。

Allowed domains に netlify.app だけを追加してはいけません。他人のサイトにあるあなたのフォームのコピーも含め、すべての Netlify サイトが一致してしまいます。

動作を確認する

  1. デプロイプレビューを開いて送信し、メッセージがプレビュー用フォームに届くことを確認します。
  2. マージしてから本番で送信します。本番用フォームに届き、Netlify の Forms ページに新しいものが表示されないことを確認します。
  3. 通知の受信者それぞれの受信トレイを確認します。各自にフィールド入りのメールが届き、フォームに email フィールドがあれば Reply の宛先は送信者になります。
  4. ハニーポットを埋めて _gotcha=bot で送信します。成功の応答は返りますが、送信データはスパムとして保存され、メールも Webhook も送られません。
  5. 何も届かない場合は、ダッシュボードのステータスフィルターを Spam に切り替えてください。スパムにも本物の送信と同じ成功の応答が返ります。

ハマりどころ

問題対処
送信すると 404 になる送信が自分のサイトに届いています。キャッシュされたビルドに古い action が残っているか、fetch の呼び出しがまだ / に送信しています。公開ディレクトリで form-name を検索してください。
送信データがまだ Netlify に表示されるページ上のいずれかのフォームが data-netlify か netlify 付きで自分のサイトに送信しているため、Netlify が検出と保存を続けています。属性を削除し、action を Ikiform に設定して再デプロイしてください。
プレビューでだけ Ikiform から 404 not_found が返るデプロイプレビューのコンテキストに VITE_IKIFORM_FORM_ID がないため、URL の末尾が undefined になっています。[context.deploy-preview.environment] に追加してください。
403 origin_not_allowed が返るか、すべての送信がスパムになるページのドメインがフォームの Allowed domains にありません。ドメインと、ローカルのテスト用に localhost を追加してください。登録済みドメインのサブドメインは許可されます。
422 files_not_supportedフォームの <input type="file"> でファイルが選択されています。ファイルアップロードにはまだ対応していません。入力欄を削除するか、代わりにリンクを尋ねてください。

よくある質問

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