フォーム ID

Next.js

Next.js App Router のクライアントコンポーネントから、またはシークレットキーを使う Server Action から送信します。

Next.js から送信するには、よい方法が 2 つあります。クライアントコンポーネントでブラウザーから送信する方法は、レート制限に訪問者ごとの IP が使われ、どのスパムチェックとも組み合わせられます。Server Action から送信する方法は、JavaScript なしでもフォームが動きます。

ItemクライアントコンポーネントServer Action
JavaScript なしで動くいいえはい
レート制限のカウント単位訪問者ごとサーバーの IP
Ikiform の人間確認SDK の challenge を使うシークレットキーでスキップ
許可ドメインブラウザーのオリジンと照合シークレットキーでスキップ
シークレットキーが必要いいえはい

SDK をインストールする

ターミナル
npm install @ikiform/sdk

SDK は任意です。以下の内容はすべて fetch でも動きます。JavaScript と fetch を参照してください。

クライアントコンポーネントから送信する

app/contact/contact-form.tsx
"use client";

import { submit } from "@ikiform/sdk";
import { useState } from "react";

type Status = "idle" | "sending" | "sent" | "error";

export function ContactForm() {
  const [status, setStatus] = useState<Status>("idle");
  const [errors, setErrors] = useState<Record<string, string[]>>({});
  const [message, setMessage] = useState("");

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus("sending");
    const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(event.currentTarget));
    if (result.ok) return setStatus("sent");
    setErrors(result.error.fields ?? {});
    setMessage(result.error.message);
    setStatus("error");
  }

  if (status === "sent") return <p>Thanks, we got it.</p>;

  return (
    <form onSubmit={onSubmit} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required aria-invalid={Boolean(errors.email)} />
      {errors.email ? <p>{errors.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {status === "error" && !Object.keys(errors).length ? <p role="alert">{message}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send"}</button>
    </form>
  );
}
  • submit は例外を投げません。{ ok: true, id } または { ok: false, error, status, requestId } に解決されます。
  • フォームにフィールドが定義されている場合、error.fields はフィールド ID をメッセージに対応づけます。
  • このコンポーネントは、app/contact/page.tsx など、任意のサーバーコンポーネントのページから描画します。

Ikiform の人間確認を使う場合

フォームのスパムチェックが Ikiform の場合は、送信の直前に毎回トークンを取得します。challenge は小さな ikiform.com のフレームをマウントし、Turnstile がクリックを必要としない限り表示されません。

contact-form.tsx
const data = new FormData(event.currentTarget);
const turnstileToken = await challenge("frm_xxxxxxxxxxxxxxxx");
const result = await submit("frm_xxxxxxxxxxxxxxxx", data, { turnstileToken });

import { challenge, submit } from "@ikiform/sdk"; でインポートします。トークンは 1 回限りなので、送信ごとに 1 回呼び出してください。

Server Action から送信する

ダッシュボードの API keys で submissions:write スコープを持つシークレットキーを作成し、環境変数として保存します。ブラウザーには絶対に公開しないでください。NEXT_PUBLIC_ を付けてはいけません。

.env.local
IKIFORM_SECRET_KEY=ik_sk_your_secret_key
app/contact/actions.ts
"use server";

export interface ContactState {
  ok: boolean;
  message: string;
  fields?: Record<string, string[]>;
}

export async function sendContact(_previous: ContactState, formData: FormData): Promise<ContactState> {
  const body = new URLSearchParams();
  for (const [name, value] of formData) {
    if (typeof value === "string" && !name.startsWith("$ACTION")) body.append(name, value);
  }
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
    },
    body,
  });
  const result = await response.json();
  if (result.ok) return { ok: true, message: "Thanks, we got it." };
  return { ok: false, message: result.error.message, fields: result.error.fields };
}
app/contact/contact-form.tsx
"use client";

import { useActionState } from "react";
import { type ContactState, sendContact } from "./actions";

const initial: ContactState = { ok: false, message: "" };

export function ContactForm() {
  const [state, action, pending] = useActionState(sendContact, initial);

  if (state.ok) return <p>{state.message}</p>;

  return (
    <form action={action}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      {state.fields?.email ? <p>{state.fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" />
      {state.message && !state.fields ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send"}</button>
    </form>
  );
}
サーバーから送られる送信はすべてサーバーの IP アドレスから届くため、Ikiform の訪問者ごとのレート制限は全訪問者をまとめてカウントします。標準のプリセットは IP とフォームごとに 1 分あたり 5 件までなので、アクセスの多いサイトでは上限に達することがあります。可能ならブラウザーから送信するか、サーバーを経由するフォームでは緩めまたはカスタムのレート制限を選んでください。

SDK の submit は Server Action でも使えますが、Authorization ヘッダーを送らないため、許可ドメインや Ikiform の人間確認があるフォームでは、送信が拒否されるかフラグが付きます。例のとおり、シークレットキーを付けた fetch を使ってください。

Route Handler で Webhook を受け取る

Next.js アプリで送信ごとに処理を行うには、Route Handler を追加します。Webhook を受け取るを参照してください。