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

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

公開日 2026-10-25 · https://ikiform.com/ja/guides/contact-form-static-site/netlify

フォームに `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` で失敗します:

`ターミナル`:

```bash
curl -i -X POST -d "message=hello" https://your-site.netlify.app/contact/
```

`応答`:

```http
HTTP/2 404
server: Netlify
cache-status: "Netlify Edge"; fwd=method; fwd-status=404
content-length: 0
```

この `404` のせいで、実は存在するページを探し回ることになります。これは、検出されたフォームのどれも送信と一致しなかったという意味です。そもそも Netlify Forms を使うかどうかは別の問題で、[Ikiform と Netlify Forms の比較](https://ikiform.com/alternatives/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 の比較

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 は削除する`:

```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`:

```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`:

```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">` でファイルが選択されています。ファイルアップロードにはまだ対応していません。入力欄を削除するか、代わりにリンクを尋ねてください。 |

## よくある質問

### サイトを Netlify に置いたまま、別のフォームバックエンドを使えますか？

はい。Netlify が処理するのはビルド時に検出したフォームだけです。data-netlify がなく、action が別のエンドポイントを指すフォームは、他のページと同じく Netlify が配信し、そのエンドポイントに直接送信されます。

### Netlify でフォームが 404 になるのはなぜですか？

Netlify は、デプロイされた HTML で Netlify Forms がそのフォームを検出していない限り、静的ページへの POST に 404 を返します。フォームに Netlify Forms 用の印を付けるか、action を別のバックエンドに向けてください。

### あるフォームは Netlify Forms のまま、別のフォームで Ikiform を使えますか？

はい。検出は属性によってフォームごとに行われます。Netlify に処理させたいフォームには data-netlify を残し、form detection はオンのままにしてください。

### 切り替えると Netlify の過去の送信データは失われますか？

Netlify に残り、Ikiform には移りません。form detection をオフにする前にサイトの Forms ページからダウンロードし、記録として保管してください。

### フォームでファイルのアップロードを受け付けられますか？

まだできません。ファイルを含む送信は 422 files_not_supported で拒否されます。代わりに、共有ドライブの URL などファイルへのリンクを尋ねてください。

## その他のガイド

- [静的サイトのお問い合わせフォーム](https://ikiform.com/ja/guides/contact-form-static-site.md): サーバーなしで静的サイトに動くお問い合わせフォームを追加します。POST の送り先、スパム、メールでの返信、各ホストとフレームワーク向けのコードまで。
- [Vercel](https://ikiform.com/ja/guides/contact-form-static-site/vercel.md): Vercel の静的ページはフォームの POST に 405 Method Not Allowed を返します。その理由と、代わりにバックエンドへフォームを送る方法を説明します。
- [Cloudflare Pages](https://ikiform.com/ja/guides/contact-form-static-site/cloudflare-pages.md): Function を書かずに Cloudflare Pages にお問い合わせフォームを追加します。フォームバックエンドに送信し、_headers でヘッダーを設定し、自分の Turnstile キーを使います。
- [GitHub Pages](https://ikiform.com/ja/guides/contact-form-static-site/github-pages.md): GitHub Pages ではサーバーコードが動かず、フォームの POST は 405 Not Allowed になります。フォームバックエンドに送信し、プロジェクトサイトのリダイレクトを正しく設定しましょう。
- [Amazon S3](https://ikiform.com/ja/guides/contact-form-static-site/aws-s3.md): S3 のウェブサイトエンドポイントは平文 HTTP の GET と HEAD しか受け付けません。お問い合わせフォームはフォームバックエンドに送信し、リダイレクトは CloudFront で正しく扱います。
- [ポートフォリオサイト](https://ikiform.com/ja/guides/contact-form-static-site/portfolio-website.md): 初回の打ち合わせで聞くことを尋ねるポートフォリオのお問い合わせフォーム。案件、予算、時期、リンクを、サーバーなしで受け取り、受信トレイから返信できます。
