# Vercel でホストしたお問い合わせフォームの 405 エラーを直す

> Vercel の静的ページはフォームの POST に 405 Method Not Allowed を返します。その理由と、代わりにバックエンドへフォームを送る方法を説明します。

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

Vercel は静的ビルドをファイルとして配信するので、ファイルへの `POST` は、そのパスを処理する関数がないため空のボディの `405` になります。フォームの `action` を Ikiform のエンドポイントに向けるか、送信を受け付ける Vercel Function を書いてください。Ikiform を使う場合は、Vercel の環境ごとにフォーム ID を設定し、プレビューはテスト用フォームに、本番は本物のフォームに送信されるようにします。

## Vercel が 405 を返す理由

Vercel は各リクエストを静的ファイルか関数に振り分けます。静的サイト、Vite のビルド、`output: "export"` の Next.js アプリにはファイルしかありません。`/contact` への `GET` はファイルを返しますが、同じパスへの `POST` には行き先がないので、あなたのコードが動く前に Vercel が拒否します:

`ターミナル`:

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

`応答`:

```http
HTTP/2 405
server: Vercel
content-disposition: inline
x-vercel-cache: HIT
content-length: 0
```

応答にボディがないので、ブラウザーには空白のページか独自のエラー画面が表示されます。原因になる構成は 3 つあります:

| あなたのコード | Vercel で 405 になる理由 |
| --- | --- |
| `action` のない `<form method="post">` | フォームが置かれているページ、つまり静的ファイルに送信している |
| Vite やプレーンな HTML のプロジェクトでの `action="/api/contact"` | プロジェクトのルートの `api/` フォルダーにファイルがないので、そのパスの関数が存在しない |
| 静的エクスポートでの Next.js の Server Action や Route Handler | `output: "export"` は Server Actions も、リクエストを読む Route Handler もサポートしない。`next dev` はエラーを報告し、ビルドにはそれらを動かすサーバーがない |

図: Vercel がフォームの POST をどう扱うか

1. フォームが Vercel のデプロイ上のパスに送信します。
2. Vercel はそのパスを処理する関数を探します。
3. 関数を書いていればそれが実行されます。静的ビルドには関数がないので、Vercel は空のボディで 405 を返します。
4. action が Ikiform を指していれば、送信は Vercel に届きません。
5. Ikiform がメッセージを保存してメールで通知し、ブラウザーを Vercel サイト上の静的ページにリダイレクトします。

Vercel が壊れているわけではありません。静的なデプロイには POST を処理するコードがなく、Vercel 側でそれを追加する方法は関数しかないのです。

つまり、関数を追加するか、送信を別の場所に送るかのどちらかです。`api/contact.ts` の Vercel Function でも動きますが、メール API、スパム除外、レート制限が必要になり、それを自分で保守することになります。送信を Ikiform に送れば、デプロイは静的なままです。HTML の修正は属性 1 つだけです:

`変更前`:

```html
<form method="post">
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button>Send</button>
</form>
```

`変更後`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
```

### 関数を自分で書きたい場合

自分のコードしか届かない場所にメッセージを送る必要があるなら、関数は妥当な選択です。始める前に、必要なものを数えておきましょう:

- メールプロバイダーのアカウント、認証済みの送信ドメイン、Vercel の環境変数に入れた API キー。
- サーバー側のバリデーション。ブラウザーの `required` 属性は便利なだけで、チェックにはなりません。
- スパム除外とレート制限。なければ、インターネット中のボットからのメールが届くお問い合わせフォームになります。
- メールプロバイダーが停止しているときにメッセージを保管する場所と、再送する方法。

フォームバックエンドなら、この一覧をまとめて引き受けます。それでも自分のコードを挟みたいなら、フォームの送信先は Ikiform のままにして、関数で署名付き Webhook を受け取ってください。ハンドラーは[自分のサーバー](https://ikiform.com/blog/form-submissions-to-your-own-server.md)で紹介しています。

## Vercel の環境ごとにフォーム ID を分ける

Vercel ではプルリクエストごとにプレビューのデプロイが作られます。プレビューが本番のフォームに送信すると、テストのメッセージがすべて本物のメッセージと並んで受信トレイに届きます。プレビュー用に 2 つ目の Ikiform フォームを作り、Vercel の環境変数で切り替えましょう。**Settings › Environment Variables** で、同じ名前を 2 回追加します:

`Vercel の環境変数`:

```text
VITE_IKIFORM_FORM_ID   Production   frm_xxxxxxxxxxxxxxxx
VITE_IKIFORM_FORM_ID   Preview      frm_yyyyyyyyyyyyyyyy
```

Vite はビルド時に `index.html` の `%NAME%` プレースホルダーを `VITE_` 変数で置き換えるので、プレーンな HTML フォームでも JavaScript なしで使えます。`VITE_VERCEL_ENV` は Vite のプロジェクトで Vercel が設定する変数で、メッセージがどのデプロイから来たかを記録します:

`index.html`:

```html
<form id="contact" action="https://ikiform.com/f/%VITE_IKIFORM_FORM_ID%" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />

  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" minlength="10" required></textarea>

  <input type="hidden" name="build" value="%VITE_VERCEL_ENV%" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button type="submit">Send message</button>
</form>
```

- 静的サイトでは、Vercel はビルド時に環境変数を読みます。変更したら再デプロイしてください。そうしないと HTML に古い値が残ります。
- 変数が存在しないとき、Vite はプレースホルダーをそのまま残すので、ID がないと `%VITE_IKIFORM_FORM_ID%` のまま公開され、すべての送信がフォームに届く前に `400 Bad Request` で失敗します。下のガードでそれを検出します。
- フォーム ID はどちらにせよ公開されます。ブラウザーに届くのは `VITE_` 変数だけなので、そこにシークレットを入れてはいけません。

`src/contact.js`:

```js
const form = document.querySelector("#contact");

if (form && form.action.includes("%VITE_")) {
  console.error("VITE_IKIFORM_FORM_ID is not set for this Vercel environment");
  form.querySelector("button").disabled = true;
}
```

`output: "export"` の Next.js を使っていますか？その場合はクライアントコンポーネントで `process.env.NEXT_PUBLIC_IKIFORM_FORM_ID` を読んでください。静的エクスポート用のフォームは[メインガイド](https://ikiform.com/ja/guides/contact-form-static-site.md#form)にあります。Next.js アプリが Vercel のサーバーで動いているなら、Server Action からシークレットキーで送信できます。[Next.js ガイド](https://ikiform.com/ja/docs/guides/nextjs.md#server-action)を参照してください。

## ヘッダーとクリーン URL

サイトで Content-Security-Policy を設定しているなら、`vercel.json` で設定して Ikiform を含めてください。`cleanUrls` を使うと `thanks.html` が `/thanks` で配信され、リダイレクト先がすっきりします:

`vercel.json`:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "cleanUrls": true,
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"
        }
      ]
    }
  ]
}
```

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

`your-project-git-branch-team.vercel.app` のようなプレビューの URL は `your-project.vercel.app` のサブドメインではないので、**Allowed domains** で一致しません。そこからの送信は `origin_mismatch` でスパムになります。これもプレビュー用のフォームを分ける理由の 1 つです。プレビュー用フォームの許可ドメインは空のままにするか、テストするプレビューのホストを追加してください。

> Vercel Authentication をオンにして、本番ドメイン以外のすべてのデプロイを対象にする Standard Protection を選んでいれば、プレビューの URL は Vercel のログインの後ろに置かれます。アクセスできるのはチームだけなので、許可ドメインのないプレビュー用フォームが一般に公開されることはありません。それを当てにする前に **Settings › Deployment Protection** を確認してください。プレビューからの送信は `localhost` のテストではないので、本番の送信データと同じくプランの使用量にカウントされます。

## 動作を確認する

1. 修正前に本番の URL に対して上の `curl` を実行し、`405` を確認します。修正後は、デプロイされた HTML のフォームの `action` が `https://ikiform.com/f/` で始まります。
2. プレビューのデプロイを開いて送信し、メッセージが本番ではなくプレビュー用のフォームに届くことを確認します。
3. 本番のドメインで送信し、`build` フィールドが `production` になっていることを確認します。
4. 通知の受信者それぞれの受信トレイを確認します。各自にフィールド入りのメールが届き、フォームに `email` フィールドがあれば **Reply** の宛先は送信者になります。
5. 何も届かない場合は、ダッシュボードのステータスフィルターを **Spam** に切り替えてください。スパムにも本物の送信と同じ成功の応答が返ります。

## ハマりどころ

| 問題 | 対処 |
| --- | --- |
| フォームを変更しても `405` のまま | テストしているデプロイが変更前にビルドされたものか、JavaScript のハンドラーが `fetch` で自分のパスに送信しています。ビルドされたファイルで `/api/contact` を検索してください。 |
| すべての送信が `400 Bad Request` になる、または Ikiform から `404 not_found` が返る | フォーム ID がビルドに届いていないか、その環境用の ID が間違っています。その環境に変数があることを確認して、再デプロイしてください。 |
| プレビューは動くのに、本番の送信がスパムになる | 本番用フォームの **Allowed domains** に本番のドメインがありません。`your-project.vercel.app` に加えて独自ドメインも追加してください。 |
| 自分のページではなく Ikiform のサンクスページに移動する | `_redirect` は、ドメインが **Allowed domains** にあり、かつ `https:` の URL でない限り無視されます。ドメインを追加するか、代わりにフォーム設定で **Redirect after submit** を設定してください。 |

## よくある質問

### Vercel でフォームを送信すると 405 Method Not Allowed になるのはなぜですか？

フォームが静的ファイルしかないパスに送信しているからです。Vercel は GET リクエストにはファイルを返しますが、そのパスに関数がなければ POST を 405 で拒否します。フォームの送信先をフォームバックエンドにするか、Vercel Function を追加してください。

### 静的エクスポートで Next.js の Server Action を使えますか？

使えません。output: export では、実行するサーバーがないため Server Actions はサポートされません。クライアントコンポーネントからフォームバックエンドに送信するか、静的エクスポートをやめてデプロイし、Vercel で Action を実行してください。

### プレビューのデプロイは本番と同じフォームを使うべきですか？

使わないほうがよいでしょう。Preview 環境の変数でプレビュー専用のフォーム ID を渡せば、テストのメッセージが受信トレイや Webhook に届きません。

### お問い合わせフォームに vercel.json は必要ですか？

不要です。フォームはなくても動きます。vercel.json を追加するのは、ikiform.com を許可する Content-Security-Policy を設定するときか、cleanUrls をオンにしてサンクスページの URL を整えたいときだけです。

### Ikiform は無料ですか？

いいえ、無料プランはありません。テストは無料で、支払いの前に localhost から月 50 件のテスト送信ができます。本番運用するには、月 $6 で 500 件からの Pro を選ぶか、1 回払いで 10,000 件 $49 または 50,000 件 $149 を購入できます。クレジットに有効期限はありません。

## その他のガイド

- [静的サイトのお問い合わせフォーム](https://ikiform.com/ja/guides/contact-form-static-site.md): サーバーなしで静的サイトに動くお問い合わせフォームを追加します。POST の送り先、スパム、メールでの返信、各ホストとフレームワーク向けのコードまで。
- [Netlify](https://ikiform.com/ja/guides/contact-form-static-site/netlify.md): Netlify Forms を使わずに、Netlify のサイトでお問い合わせフォームを運用します。ビルド時の検出なし、React からの JSON 送信、デプロイプレビュー用の別フォーム。
- [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): 初回の打ち合わせで聞くことを尋ねるポートフォリオのお問い合わせフォーム。案件、予算、時期、リンクを、サーバーなしで受け取り、受信トレイから返信できます。
