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

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

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

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

Preet SutharIkiform 創業者

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

Vercel が 405 を返す理由

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

ターミナル
curl -i -X POST -d "message=hello" https://your-project.vercel.app/contact
応答
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 Handleroutput: "export" は Server Actions も、リクエストを読む Route Handler もサポートしない。next dev はエラーを報告し、ビルドにはそれらを動かすサーバーがない
Vercel がフォームの POST をどう扱うかaction が Vercel サイトを指すaction が Ikiform を指すPOST/contactフォームからVercelのルーティングファイルか関数か関数はある?自分の関数書いた場合405 でボディは空静的ファイルikiform.comへ POST同じフォームIkiform保存してメール通知サンクスページVercel上の静的ファイルはいいいえ303Vercel がフォームの POST をどう扱うかaction が Vercel サイトを指すaction が Ikiform を指すPOST /contactフォームからVercel のルーティングファイルか関数か関数はある?自分の関数書いた場合405でボディは空静的ファイルikiform.com へPOST同じフォームIkiform保存してメール通知サンクスページVercel 上の静的ファイルはい303いいえ
  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 つだけです:

変更前
<form method="post">
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button>Send</button>
</form>
変更後
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">

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

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

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

フォームバックエンドなら、この一覧をまとめて引き受けます。それでも自分のコードを挟みたいなら、フォームの送信先は Ikiform のままにして、関数で署名付き Webhook を受け取ってください。ハンドラーは自分のサーバーで紹介しています。

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

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

Vercel の環境変数
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
<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
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 を読んでください。静的エクスポート用のフォームはメインガイドにあります。Next.js アプリが Vercel のサーバーで動いているなら、Server Action からシークレットキーで送信できます。Next.js ガイドを参照してください。

ヘッダーとクリーン URL

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

vercel.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 を設定してください。

よくある質問

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