# 新しい SaaS のウェイトリストを作る方法

> 自分のランディングページにローンチ前のウェイトリストを作ります。フォーム、スパム対策、重複、確認メール、ローンチ日のきれいなエクスポートまで扱います。

公開日 2026-10-25 · https://ikiform.com/ja/guides/waitlist

ランディングページにプレーンな HTML フォームを置き、訪問者のメールアドレスをフォームバックエンドに送信すれば、ローンチ前にサーバーを動かす必要はありません。登録は 1 件ずつ Webhook でローンチ時に使うメールツールに転送し、確認メールはそのツールに送らせます。ローンチ日にはリストを CSV でエクスポートし、メールアドレスで重複を取り除いて、ローンチのメールを 1 通送ります。

ウェイトリストは、SaaS のアイデアを試すいちばん安上がりな方法です。まだ存在しないもののために見知らぬ人がメールアドレスを残してくれるなら、作る理由があり、リリース時に知らせる相手のリストも手に入ります。このガイドでは、データベースもサーバーも使わずに自分のランディングページにウェイトリストを作り、ローンチ日に備えます。

## ウェイトリストの仕組み

どのウェイトリストも 3 つの部分でできています。ページ上のフォーム、登録を 1 件ずつ保存する場所、そしてリストにメールを送るツールです。ここではフォームの送信先を Ikiform にします。Ikiform は送信をスパムチェックして保存し、あなたにメールで通知し、署名付き Webhook を送ります。小さなリレーか自動化ツールが、そのアドレスをメールツールに渡します。

図: ウェイトリストの登録がランディングページからメールツールに届くまで

1. 訪問者がランディングページでメールアドレスを入力すると、フォームが Ikiform のエンドポイントに送信します。
2. Ikiform はハニーポット、スパムスコア、レート制限をチェックしてから登録を保存します。
3. 登録はダッシュボードに表示され、そこで検索したり、リストを CSV でエクスポートしたりできます。
4. Ikiform はあなたとチームにメールを送ります。登録した本人にメールを送ることはありません。
5. Ikiform が署名付きの submission.created Webhook を送ります。
6. リレーか自動化ツールがアドレスをメールツールに追加し、メールツールが確認メールと、のちにローンチのメールを送ります。

初日の時点ではメールツールはなくてもかまいません。何週間も集めてから CSV をインポートしてもよく、Webhook を使えば最初から両方を同期できます。

## ウェイトリストを運用する 4 つの方法

唯一の正解となるツールはありません。この表では、よくある 4 つの構成を、得意なことと後々のコストで比べます。

| 方法 | 得意なこと | トレードオフ |
| --- | --- | --- |
| LaunchList や Waitlister などのウェイトリスト SaaS | 紹介リンク、リーダーボード、順番待ちの順位が最初から使えます。まだサイトがなくてもホストされたページがあります。 | リストは他社のプロダクトの中にあります。料金を払うかスタイルを変えない限り、ウィジェットは相手の見た目のままです。料金はたいてい登録数に応じて増えます。 |
| [Typeform](https://ikiform.com/alternatives/typeform) や [Tally](https://ikiform.com/alternatives/tally) などのフォームビルダーの埋め込み | 洗練された 1 問ずつのフローを、コードなしで作れます。 | ページに馴染まない iframe で、独自のスクリプトを読み込みます。ブランド表示、回答数の上限、Webhook はプランしだいです。 |
| 自前のデータベースと API ルート | データと、紹介コードなどのロジックを完全に制御できます。 | スパム対策、レート制限、通知、エクスポートを自分で作る必要があり、ランディングページが静的でもサーバーが必要です。 |
| Ikiform などのフォームバックエンド | どのサイトでも自分の HTML を使えます。スパムフィルタリング、保存、メール通知、署名付き Webhook、CSV エクスポートがサーバーなしで使えます。 | 紹介のリーダーボードはなく、登録した本人へのメールもありません。それらはメールツールが担います。 |

紹介の仕組みが計画のすべてなら、ウェイトリスト SaaS を選びましょう。フォームをサイトの他の部分と同じ見た目にし、リストを持ち運べるようにして、ローンチ後にどのみち使うツールからメールを送りたいなら、フォームバックエンドを選びましょう。このガイドの残りではフォームバックエンドを使います。

## フォーム

最初はメールアドレスだけを尋ねましょう。フィールドが増えるたびに登録は減ります。質問は、すでに登録してくれた人にあとからメールで聞けます。下でスタックを選んでください。選んだスタックは、このガイドのすべてのコードサンプルに引き継がれます。

**HTML**

登録フォームを表示したい場所にこのフォームを貼り付けます。通常の方法で送信してリダイレクトするので、JavaScript がなくても動きます。

`index.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" placeholder="you@company.com" autocomplete="email" required />
  <input type="hidden" name="ref" value="homepage" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Join the waitlist</button>
</form>
```

訪問者をページにとどめるには、フォームのあとにこのスクリプトを追加します。`fetch` で送信し、フォームをお礼の一文に差し替え、サーバーが返すフィールドエラーを表示します。

`waitlist.js`:

```js
const form = document.querySelector('form[action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx"]');
const status = document.createElement("p");
status.setAttribute("role", "alert");
form.append(status);

const labels = {
  email: "Email",
};

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button");
  button.disabled = true;
  status.textContent = "";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      form.replaceWith("You're on the list. We'll email you at launch.");
      return;
    }
    const fields = Object.entries(result.error.fields ?? {});
    status.textContent = fields.length
      ? fields.map(([id, messages]) => `${labels[id] ?? id}: ${messages[0]}`).join(" ")
      : result.error.message;
  } catch {
    status.textContent = "Couldn't reach the server. Try again.";
  }
  button.disabled = false;
});
```

HTML でさらに詳しく知るには [プレーンな HTML でランディングページにウェイトリストフォームを追加する方法](https://ikiform.com/ja/guides/waitlist/html.md) を読んでください。

**Next.js**

Server Action がシークレットキーを付けて登録を転送し、`useActionState` がページを再読み込みせずに結果を表示します。JavaScript がまだ読み込まれていなくても、フォームは送信できます。

`app/waitlist/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface JoinState {
  ok: boolean;
  message: string;
  email?: string;
}

export async function joinWaitlist(_previous: JoinState, formData: FormData): Promise<JoinState> {
  const email = String(formData.get("email") ?? "").trim();
  if (!email.includes("@")) return { ok: false, message: "Enter the email we should send your invite to.", email };

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      Origin: (await headers()).get("origin") ?? "",
    },
    body: JSON.stringify({
      email,
      ref: String(formData.get("ref") ?? "homepage"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
  return {
    ok: false,
    message: result?.error?.fields?.email?.[0] ?? result?.error?.message ?? "Couldn't join the list. Try again.",
    email,
  };
}
```

`app/waitlist/waitlist-form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type JoinState, joinWaitlist } from "./actions";

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

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

  if (state.ok) return <p role="status">{state.message}</p>;

  return (
    <form action={action}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required defaultValue={state.email} placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.message ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
    </form>
  );
}
```

Next.js でさらに詳しく知るには [サーバーアクションで Next.js のウェイトリストを作る方法](https://ikiform.com/ja/guides/waitlist/nextjs.md) を読んでください。

**Astro**

`accept: "form"` を指定した Astro Action が Zod でメールアドレスを検証して転送します。フォームはクライアントの JavaScript なしで送信するので、ページはオンデマンドでレンダリングする必要があります。アダプターを追加し、`prerender = false` のままにしてください。

`src/actions/index.ts`:

```ts
import { ActionError, defineAction } from "astro:actions";
import { getSecret } from "astro:env/server";
import { z } from "astro/zod";

export const server = {
  joinWaitlist: defineAction({
    accept: "form",
    input: z.object({
      email: z.email(),
      ref: z.string().default("homepage"),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          Authorization: `Bearer ${getSecret("IKIFORM_SECRET_KEY")}`,
          ...(origin ? { Origin: origin } : {}),
        },
        body: JSON.stringify(input),
      });
      const result = await response.json();
      if (!result.ok) {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: result.error.fields?.email?.[0] ?? result.error.message,
        });
      }
      return { joined: true };
    },
  }),
};
```

`src/pages/index.astro`:

```astro
---
import { actions, isInputError } from "astro:actions";

export const prerender = false;

const result = Astro.getActionResult(actions.joinWaitlist);
---

{result?.data?.joined ? (
  <p role="status">You're on the list. We'll email you on launch day.</p>
) : (
  <form method="POST" action={actions.joinWaitlist}>
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required placeholder="you@company.com" />
    <input type="hidden" name="ref" value="homepage" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && (
      <p role="alert">{isInputError(result.error) ? "Enter a valid email address." : result.error.message}</p>
    )}
    <button>Join the waitlist</button>
  </form>
)}
```

Astro でさらに詳しく知るには [Astro でウェイトリストを作る](https://ikiform.com/ja/guides/waitlist/astro.md) を読んでください。

**TanStack Start**

`createServerFn` の POST 関数が `FormData` を受け取り、`.validator()` でチェックしてサーバーから転送します。ルートは `useServerFn` を通じてそれを呼び出します。

`src/routes/index.tsx`:

```tsx
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";

const joinWaitlist = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    const email = String(data.get("email") ?? "").trim();
    if (!email.includes("@")) throw new Error("Enter the email we should send your invite to.");
    return {
      email,
      ref: String(data.get("ref") ?? "homepage"),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }) => {
    const origin = getRequestHeader("origin");
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify(data),
    });
    const result = await response.json();
    if (result.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
    return { ok: false, message: String(result.error.fields?.email?.[0] ?? result.error.message) };
  });

export const Route = createFileRoute("/")({ component: Home });

function Home() {
  const join = useServerFn(joinWaitlist);
  const [state, setState] = useState<{ ok: boolean; message: string } | null>(null);
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setPending(true);
    try {
      setState(await join({ data }));
    } catch (error) {
      setState({ ok: false, message: error instanceof Error ? error.message : "Couldn't join the list. Try again." });
    } finally {
      setPending(false);
    }
  }

  if (state?.ok) return <p role="status">{state.message}</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state?.message ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
    </form>
  );
}
```

TanStack Start でさらに詳しく知るには [TanStack Start でウェイトリストを作る](https://ikiform.com/ja/guides/waitlist/tanstack-start.md) を読んでください。

**React Router**

ルートの `action` が送信を転送し、`actionData` が結果をコンポーネントに返します。`data()` を使うと、エラー時に Ikiform のステータスコードがそのまま保たれます。

`app/routes/home.tsx`:

```tsx
import { data, Form, useNavigation } from "react-router";
import type { Route } from "./+types/home";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email") ?? "").trim();

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      Origin: request.headers.get("origin") ?? "",
    },
    body: JSON.stringify({
      email,
      ref: String(formData.get("ref") ?? "homepage"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
  return data(
    { ok: false, message: String(result.error.fields?.email?.[0] ?? result.error.message) },
    { status: response.status },
  );
}

export default function Home({ actionData }: Route.ComponentProps) {
  const navigation = useNavigation();
  const pending = navigation.state === "submitting";

  if (actionData?.ok) return <p role="status">{actionData.message}</p>;

  return (
    <Form method="post">
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {actionData?.message ? <p role="alert">{actionData.message}</p> : null}
      <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
    </Form>
  );
}
```

React Router でさらに詳しく知るには [React Router でウェイトリストを作る](https://ikiform.com/ja/guides/waitlist/react-router.md) を読んでください。

**SvelteKit**

デフォルトのフォームアクションが送信を転送し、エラー時にはメッセージ付きで `fail()` を返します。`use:enhance` は再読み込みなしで送信し、入力したメールアドレスを入力欄に残します。SvelteKit 3 では、`$app/env/private` から読めるように、`src/env.ts` で `defineEnvVars` を使って `IKIFORM_SECRET_KEY` を宣言してください。

`src/routes/+page.server.ts`:

```ts
import { fail } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { Actions } from "./$types";

export const actions = {
  default: async ({ request, fetch, url }) => {
    const data = await request.formData();
    const email = String(data.get("email") ?? "").trim();
    if (!email.includes("@")) return fail(400, { email, message: "Enter the email we should send your invite to." });

    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
        Origin: request.headers.get("origin") ?? url.origin,
      },
      body: JSON.stringify({
        email,
        ref: String(data.get("ref") ?? "homepage"),
        _gotcha: String(data.get("_gotcha") ?? ""),
      }),
    });
    const result = await response.json();
    if (!result.ok) {
      return fail(response.status, {
        email,
        message: String(result.error.fields?.email?.[0] ?? result.error.message),
      });
    }
    return { joined: true };
  },
} satisfies Actions;
```

`src/routes/+page.svelte`:

```svelte
<script lang="ts">
  import { enhance } from "$app/forms";
  import type { PageProps } from "./$types";

  let { form }: PageProps = $props();
  let pending = $state(false);
</script>

{#if form?.joined}
  <p role="status">You're on the list. We'll email you on launch day.</p>
{:else}
  <form
    method="POST"
    use:enhance={() => {
      pending = true;
      return async ({ update }) => {
        await update();
        pending = false;
      };
    }}
  >
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required value={form?.email ?? ""} placeholder="you@company.com" />
    <input type="hidden" name="ref" value="homepage" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if form?.message}
      <p role="alert">{form.message}</p>
    {/if}
    <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
  </form>
{/if}
```

SvelteKit でさらに詳しく知るには [SvelteKit でウェイトリストを作る](https://ikiform.com/ja/guides/waitlist/sveltekit.md) を読んでください。

**Nuxt**

サーバールートがシークレットキーをブラウザーから隠します。環境変数に `NUXT_IKIFORM_SECRET_KEY` を設定すると、Nuxt がそこから `runtimeConfig.ikiformSecretKey` を埋めます。

`server/api/waitlist.post.ts`:

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ email?: string; ref?: string; _gotcha?: string }>(event);
  const config = useRuntimeConfig(event);
  const origin = getRequestHeader(event, "origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.ikiformSecretKey}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({
      email: body.email?.trim() ?? "",
      ref: body.ref ?? "homepage",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: String(result.error.fields?.email?.[0] ?? result.error.message),
    });
  }
  return { joined: true };
});
```

`nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  runtimeConfig: {
    ikiformSecretKey: "",
  },
});
```

`app/components/WaitlistForm.vue`:

```vue
<script setup lang="ts">
const email = ref("");
const trap = ref("");
const status = ref<"idle" | "sending" | "joined">("idle");
const error = ref("");

async function join() {
  status.value = "sending";
  error.value = "";
  try {
    await $fetch("/api/waitlist", {
      method: "POST",
      body: { email: email.value, ref: "homepage", _gotcha: trap.value },
    });
    status.value = "joined";
  } catch (failure) {
    error.value = (failure as { data?: { message?: string } }).data?.message ?? "Couldn't join the list. Try again.";
    status.value = "idle";
  }
}
</script>

<template>
  <p v-if="status === 'joined'" role="status">You're on the list. We'll email you on launch day.</p>
  <form v-else @submit.prevent="join">
    <label for="email">Email</label>
    <input id="email" v-model="email" name="email" type="email" required placeholder="you@company.com" />
    <input v-model="trap" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Joining" : "Join the waitlist" }}</button>
  </form>
</template>
```

Nuxt でさらに詳しく知るには [Nuxt でウェイトリストを作る](https://ikiform.com/ja/guides/waitlist/nuxt.md) を読んでください。

**React**

シングルページアプリには自前のサーバーがないので、フォームはブラウザーから直接 Ikiform に送信します。このコードには絶対にシークレットキーを入れないでください。代わりに **Allowed domains**（許可ドメイン）がフォームを守ります。

`src/WaitlistForm.tsx`:

```tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

export function WaitlistForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "joined">("idle");
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    setStatus("sending");
    setError("");
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
      const result = await response.json();
      if (result.ok) {
        setStatus("joined");
        return;
      }
      setError(result.error.fields?.email?.[0] ?? result.error.message);
    } catch {
      setError("Couldn't reach the server. Try again.");
    }
    setStatus("idle");
  }

  if (status === "joined") return <p role="status">You're on the list. We'll email you on launch day.</p>;

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Joining" : "Join the waitlist"}</button>
    </form>
  );
}
```

**Vue**

同じブラウザー側の送信を Vue の単一ファイルコンポーネントで書いたものです。フォームは `action` を保持しているので、スクリプトの読み込みに失敗しても動きます。

`src/components/WaitlistForm.vue`:

```vue
<script setup lang="ts">
import { ref } from "vue";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

const status = ref<"idle" | "sending" | "joined">("idle");
const error = ref("");

async function join(event: SubmitEvent) {
  const body = new FormData(event.target as HTMLFormElement);
  status.value = "sending";
  error.value = "";
  try {
    const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      status.value = "joined";
      return;
    }
    error.value = result.error.fields?.email?.[0] ?? result.error.message;
  } catch {
    error.value = "Couldn't reach the server. Try again.";
  }
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'joined'" role="status">You're on the list. We'll email you on launch day.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="join">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required placeholder="you@company.com" />
    <input type="hidden" name="ref" value="homepage" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Joining" : "Join the waitlist" }}</button>
  </form>
</template>
```

- エンドポイントはフォームの URL です。`frm_xxxxxxxxxxxxxxxx` をダッシュボードの ID に置き換えてください。
- `name="email"` がフィールド ID です。`email` のままにしてください。Ikiform はこれを通知メールの返信先アドレスに使い、使い捨てメールのドメインかどうかもチェックします。
- 非表示の `_gotcha` 入力欄がハニーポットです。人には見えません。すべての入力欄を埋めるボットは自らスパムだと示すことになり、サーバー版はこの値に手を加えずにそのまま渡します。
- 非表示の `ref` 入力欄は、登録がどこから来たかを記録します。ページやキャンペーンごとに値を変えてください。
- **Fields** タブでフィールドを定義していると、定義にない入力欄は `422` と `unknown_field` エラーで拒否されます。そのため、下のように `email` に加えて `ref` も登録してください。
- プレーンな HTML の送信は、成功後にブラウザーをリダイレクトします。フォームの設定で **Redirect after submit** に自分のサンクスページを設定するか、設定しなければ Ikiform のホストされたページが表示されます。

`フィールド`:

```json
{
  "version": 1,
  "title": "Waitlist",
  "fields": [
    { "id": "email", "type": "email", "label": "Email", "required": true, "placeholder": "you@company.com" },
    { "id": "ref", "type": "text", "label": "Source", "validation": { "max_length": 40 } }
  ]
}
```

> サーバー側のバージョンはシークレットキーを付けて送信するので、Ikiform は **Allowed domains** とその人間確認をスキップします。その場合、すべての送信がサーバーの IP から届くため、訪問者ごとに 1 分あたり 5 件という Standard の制限が全訪問者の合計に適用されてしまいます。サーバー経由で送信するフォームには Relaxed か Custom の[レート制限](https://ikiform.com/ja/docs/rate-limits.md)を選び、`IKIFORM_SECRET_KEY` はサーバー専用の環境変数に置いてください。

サーバー版はブラウザーの `Origin` ヘッダーを引き継ぎます。Ikiform は無料の `localhost` テストをこのヘッダーで判別しますが、サーバー自身の `fetch` はこれを送らないからです。

ドキュメントのフレームワーク別ガイドではさらに詳しく説明しています: [Next.js](https://ikiform.com/ja/docs/guides/nextjs.md)、[Astro](https://ikiform.com/ja/docs/guides/astro.md)、[SvelteKit](https://ikiform.com/ja/docs/guides/sveltekit.md)、[Nuxt](https://ikiform.com/ja/docs/guides/nuxt.md)、[React](https://ikiform.com/ja/docs/guides/react.md)、[Vue](https://ikiform.com/ja/docs/guides/vue.md)、[プレーンな HTML](https://ikiform.com/ja/docs/html.md)。[SDK](https://ikiform.com/ja/docs/sdk.md) はブラウザーからのリクエストを `submit()` としてまとめており、[ウェイトリストのフォームテンプレート](https://ikiform.com/forms/waitlist)では 4 フィールド版を生成できます。

## 尋ねる価値のあるフィールド

必要なフィールドはメールアドレスだけです。ローンチ前にリストを分類したいなら、追加するのは多くても 2 つまでにしましょう。下の 4 フィールド版を試してみてください。Ikiform のバリデーターをブラウザーで実行し、保存される内容か、返ってくる `422` を表示します。

フォーム: Waitlist

- **メールアドレス** (`email`, email, 必須)
- **名前** (`name`, text)
- **お仕事は何ですか？** (`role`, select)
- **どこで知りましたか？** (`source`, text)
- **流入元** (`ref`, text)

[ウェイトリストのテンプレート](https://ikiform.com/forms/waitlist)のフィールドに `ref` を加えたものです。これは見える入力欄ではなく、ページが非表示の入力欄で埋めます。フォームの **Fields** タブに貼り付けると、Ikiform がサーバー側でこれを適用します。

| フィールド | 尋ねる理由 | 注意点 |
| --- | --- | --- |
| `email` | リストそのもの | 必須。スマートフォンで適切なキーボードが出るよう `type="email"` を使います |
| `name` | 個人宛てのローンチメール | 任意。名だけで十分です |
| `role` | 創業者を先に招待したり、エンジニアには別のメールを書いたりできます | `select` なら、絞り込みに使いやすいきれいな回答になります |
| `source` | お金をかける前に、どのチャネルが効くかがわかります | 自由記述は正直ですが散らかります。非表示の `ref` 入力欄のほうがきれいです。下を参照してください |
| `ref` | 尋ねずにページやキャンペーンごとの登録数を数えられます | 非表示の入力欄。任意の `text` で、`max_length` は 40 |

尋ねずに登録の流入元を追跡するには、ページごとかキャンペーンごとに非表示の入力欄を追加します。`_` で始まる名前は Ikiform への指示で保存されないので、`ref` のような普通の名前を使ってください:

`非表示の入力欄`:

```html
<input type="hidden" name="ref" value="homepage" />
<input type="hidden" name="_redirect" value="https://example.com/thanks" />
```

各送信データにはページの Origin とリファラーも記録されるので、`ref` がなくても登録がどのページから来たかがわかります。`_redirect` 入力欄はブラウザーを自分のサンクスページに移動させます。これは、フォームの **Allowed domains** のいずれかにある `https:` の URL でのみ機能します。

> ニュースレターなど、ローンチのお知らせ以外も送るなら、約束する内容をそのままラベルにした必須の同意チェックボックスを追加しましょう。Ikiform は送信データのタイムスタンプとともに `true` を保存するので、その人がいつ同意したかが記録されます。[ニュースレターのテンプレート](https://ikiform.com/forms/newsletter-signup)にはこれが含まれています。

## ボットをリストに入れない

メールアドレスの入力欄がある公開フォームには、数日のうちにボットが寄ってきます。偽の登録は数字を水増しするだけでなく、ローンチのメールがスパムトラップに届いて送信者の評価を下げてしまいます。Ikiform は 4 つの防御を重ねており、最初の 2 つは設定なしで有効です。

| 防御 | 防げるもの | 設定 |
| --- | --- | --- |
| ハニーポット | すべての入力欄を埋める単純なボット | 上のフォームにある非表示の `_gotcha` 入力欄 |
| スパムスコア | 使い捨てアドレス（+2）、同一内容の繰り返し（+3）、リンク、既知のスパムフレーズ。5 に達した登録はスパムとして保存されます | 不要 |
| レート制限 | ループで送信するスクリプト | デフォルトで有効: Standard で、訪問者ごとに 1 分あたり 5 件、フォーム全体で 1 分あたり 300 件 |
| 人間確認 | 本物のブラウザーを動かすボット | **Spam check** を **Ikiform** に設定します。キーの管理なしで Cloudflare Turnstile が動きます |

スパムは通常の成功の応答で保存されるので、ボットは何も学べません。スパムはメールも Webhook も送らず、使用量にもカウントされません。最初の 1 か月は週に 1 回、ダッシュボードの **Spam** フィルターを確認しましょう。本物の登録が入っていたらスパムではないとマークすると、その時点で Webhook が送られます。

**Allowed domains** にも自分のドメインを追加しましょう。他のサイトからの送信はスパムとしてマークされるか、**Reject** を選んだ場合は `403 origin_not_allowed` で拒否されます。人間確認をオンにしても、HTML フォームは変更不要です。`fetch` 版は、先に `@ikiform/sdk` の `challenge()` でトークンを取得する必要があります。その 4 行は[スパム対策のドキュメント](https://ikiform.com/ja/docs/spam.md#human-check)にあります。

> ローンチ時の送信はまとめて押し寄せます。大きな場所でページを紹介すると、個々の訪問者の制限より先に、フォーム全体の 1 分あたり 300 件の制限に引っかかることがあります。前日に Custom のレート制限で引き上げておきましょう。オフィスやカンファレンスでは全員が 1 つの IP を共有するので、そうした日には訪問者ごとに 1 分あたり 20 件の Relaxed が合っています。

## 同じ人が 2 回登録する

人は別のタブから、2 台目のデバイスから、あるいは 1 週間後に忘れて、もう一度登録します。Ikiform は受け付けた送信をすべて保存し、メールアドレスで重複をまとめたり拒否したりはしません。重複した場合は次のようになります:

| ケース | 結果 |
| --- | --- |
| 同じメールアドレスで、フィールドが異なるか 24 時間以上あと | 通常の送信データがもう 1 件 |
| 24 時間以内に同じフィールドをもう一度 | スパムスコアに +3。5 未満なので通常の送信データがもう 1 件 |
| 使い捨てメールドメインからの同一内容の繰り返し | +3 と +2 で 5 に達し、スパムとして保存 |

そのため、ある程度の重複は想定し、リストの最終的な置き場所で取り除きましょう。手間の少ない順に 3 つの方法があります:

1. **メールツールで。** ほとんどのメールツールは連絡先をアドレスで管理するので、同じアドレスを 2 回追加しても連絡先が 2 件できるのではなく、1 件が更新されます。
2. **エクスポート時に。** どこかにインポートする前に、CSV から重複したメールアドレスを取り除きます。方法を下で 2 つ紹介します。
3. **リレーで。** アドレスを小文字にして前後の空白を除き、一意制約か upsert で挿入すれば、重複しても何も起きません。

ダブルクリックで 2 行できること自体を防ぎたいですか？ `fetch` 版で、ページを読み込むごとに[冪等性キー](https://ikiform.com/ja/docs/frameworks.md#idempotency)を送ってください。これで誤って繰り返される送信は防げますが、本人が意図して 2 回登録するのは防げません。

## 登録の確認

Ikiform はフォームに入力した本人にメールを送りません。通知メールはあなたとチームだけに届くので、自動返信、確認メール、ダブルオプトインはありません。それらは [Webhook](https://ikiform.com/ja/docs/webhooks.md) をきっかけにメールツールから送ってください。これは意図的な設計です。ローンチのメールを送るツールが確認メールも送れば、配信停止やバウンスを 1 か所で管理できます。

Ikiform とメールツールをつなぐ方法は 2 つあります:

- **コードなし。** 自動化ツールを指す Webhook エンドポイントを追加し、そのツールのメールツール用アクションを使います。[Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md)、[Make](https://ikiform.com/blog/form-submissions-to-make.md)、[n8n](https://ikiform.com/blog/form-submissions-to-n8n.md) を参照してください。
- **小さなリレー。** 任意の Node ホスト上のルートハンドラーで署名を検証し、メールツールの API を呼び出します。下の例は Next.js のルートですが、同じコードが Cloudflare Worker でも動きます。

`app/api/waitlist/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    if (event.type !== "submission.created") return new Response(null, { status: 204 });
    const email = String(event.data.fields.email ?? "").trim().toLowerCase();
    if (!email) return new Response(null, { status: 204 });
    const response = await fetch(process.env.LIST_API_URL!, {
      method: "POST",
      headers: {
        authorization: `Bearer ${process.env.LIST_API_KEY}`,
        "content-type": "application/json",
      },
      body: JSON.stringify({
        email,
        name: event.data.fields.name ?? null,
        tags: ["waitlist", String(event.data.fields.role ?? "unknown")],
      }),
    });
    if (response.status === 409) return new Response(null, { status: 204 });
    if (!response.ok) return new Response(`List answered ${response.status}`, { status: 502 });
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

- `verifyWebhook` が署名とタイムスタンプをチェックするので、他人がこの URL からアドレスを追加することはできません。
- `fetch` をメールツールの連絡先追加の呼び出しに置き換えてください。たいていはメールアドレスとリストまたはタグを受け取り、そのツールでダブルオプトインがオンなら独自の確認メールを送ります。
- `204` を返すと、配信が成功したことを Ikiform に伝えます。`502` を返すと Ikiform は同じ `webhook-id` でリトライするので、メールツールが少しの間停止しても登録は失われません。

ダッシュボードの **Webhooks** でエンドポイントを追加し、その `whsec_` シークレットを `IKIFORM_WEBHOOK_SECRET` にコピーします。1 つのワークスペースにはエンドポイントを 3 つまで作れます。リトライとペイロードについては [Webhook のドキュメント](https://ikiform.com/ja/docs/webhooks.md)で、各配信を 1 回だけ保存する版については[自分のサーバー](https://ikiform.com/blog/form-submissions-to-your-own-server.md)で説明しています。

## ローンチ日

図: 最初の登録からローンチ日までのウェイトリスト

1. Ikiform に送信するフォームを載せたページを公開します。
2. 毎週ページを共有し、新しい登録とその回答を読みます。
3. ローンチの 1 週間前に、リストを CSV でエクスポートし、重複を取り除いて、役割ごとに分けます。
4. ローンチ日に、メールツールからリストに 1 通のメールを送ります。

作業のほとんどは 1 週間前のエクスポートです。ローンチのメール自体は短くてかまいません。

### リストをエクスポートする

ダッシュボードで **Submissions** を開き、ウェイトリストのフォームを選んで、ステータスを **Received**、期間を **All time** にし、**Export CSV** を選びます。列は `id`、`created_at`、`form`、`status` で、そのあとにフィールドごとの列が 1 つずつ続きます。ファイルは BOM 付きの UTF-8 なので、Excel でもアクセント記号などが崩れません。1 回のエクスポートは最大 100,000 行で、それを超える場合は [REST API](https://ikiform.com/ja/docs/guides/export.md#api) でページングしてください。

Google スプレッドシートで重複を取り除くには、CSV をインポートし、メールアドレスが E 列にある状態で、空のシートに次の数式を入れます:

`Google スプレッドシート`:

```text
=SORTN(A2:F, 9^9, 2, LOWER(TRIM(E2:E)), TRUE)
```

大文字小文字と空白を無視してアドレスごとに 1 行を残し、メールアドレス順に並べます。あるいは、エクスポートしたファイルのあるフォルダーで Node を使って次を実行します。各アドレスの最初の行を残し、値の中にカンマがないことを前提にしていますが、メールアドレスだけのフォームならその前提は成り立ちます:

`dedupe.mjs`:

```js
import { readFileSync, writeFileSync } from "node:fs";

const [header = "", ...rows] = readFileSync("submissions.csv", "utf8").replace(/^\uFEFF/, "").trim().split(/\r?\n/);
const columns = header.split(",");
const emailAt = columns.indexOf("email");
const seen = new Set();
const unique = rows.filter((row) => {
  const email = row.split(",")[emailAt]?.trim().toLowerCase();
  if (!email || seen.has(email)) return false;
  seen.add(email);
  return true;
});

writeFileSync("waitlist.csv", [header, ...unique].join("\n"));
console.log(`${rows.length} rows, ${unique.length} unique emails`);
```

### ローンチのメールを送る

自分の受信箱からではなく、メールツールから送りましょう。個人の受信箱から何百ものアドレスに一斉送信すると、メールプロバイダーにはスパムに見えます。また、送った瞬間から配信停止の処理が必要になります。短くまとめましょう:

`ローンチのメール`:

```text
Subject: Acme is live. You're in.

Hi Ada,

You joined the Acme waitlist in the spring. Today it's open.

Sign in here: https://acme.example/start

You get the first 30 days free because you waited.
Reply to this email if anything breaks. It comes to me.

Ada, founder of Acme
```

- 何が公開されたかを伝え、登録ページかログインページに直接リンクします。
- 長めのトライアルや割引など、待ってくれたことへの特典を用意し、1 回だけ伝えます。
- 個人の名前で送ります。返信は、手に入る中で最高の初期フィードバックです。
- `role` を集めたなら、創業者とエンジニアで書き出しを変えましょう。それを集める理由はそこにあります。

> アドレスを確認せずに何か月も集めていたなら、まず一部にだけ送り、バウンス率を見てください。タイプミスや無効なアドレスだらけのリストは、プロダクトのメールを送るドメインの評価を下げます。

## 状況に合わせて選ぶ

上の手順はどこでも使えます。以下のページでは、特定のローンチやサイトビルダーで変わる点を扱います。

- [Product Hunt ローンチリスト](https://ikiform.com/ja/guides/waitlist/product-hunt.md): Product Hunt の数週間前からローンチリストを集め、登録をチャネルごとにタグ付けし、太平洋時間のリセットに合わせてローンチ日のメールを 2 回に分けて送ります。
- [選考付きのベータ登録](https://ikiform.com/ja/guides/waitlist/beta-signup.md): サーバーが必須を強制する選考質問付きのベータ応募フォームと、応募者を採点してリストから各コホートを選ぶスクリプトを紹介します。
- [モバイルアプリのウェイトリスト](https://ikiform.com/ja/guides/waitlist/mobile-app.md): 使っているスマホを尋ね、iPhone のテスターを TestFlight に送り、Play のクローズドテストを埋める、iOS・Android アプリ向けの公開前ウェイトリストです。
- [Next.js のウェイトリスト](https://ikiform.com/ja/guides/waitlist/nextjs.md): Next.js App Router のウェイトリスト。シークレットキーで登録を転送するサーバーアクション、useActionState、楽観的なサンクス表示、訪問者ごとの制限を扱います。
- [Framer のウェイトリスト](https://ikiform.com/ja/guides/waitlist/framer.md): Framer でウェイトリストの登録を集める 2 つの方法。Ikiform への Webhook を使う Framer のフォームか、訪問者ごとの制限とスパムチェックが残るコードコンポーネントです。
- [Webflow のウェイトリスト](https://ikiform.com/ja/guides/waitlist/webflow.md): Webflow の Form Block の送信先を Ikiform にし、非表示フィールドで登録に流入元のタグを付け、短いスクリプトですべてのフォームで Webflow の成功表示を残します。
- [Carrd のウェイトリスト](https://ikiform.com/ja/guides/waitlist/carrd.md): Carrd のページでウェイトリストの登録を集める方法。Pro Plus では UTM を記録する Send to URL フォーム、Pro Standard ではハニーポット付きの埋め込み HTML フォームを使います。
- [プレーンな HTML のウェイトリスト](https://ikiform.com/ja/guides/waitlist/html.md): ビルド不要でプレーンな HTML だけのウェイトリストページ。JavaScript がオフでも動き、読み込めばその場でお礼を表示し、必要なときはフォールバックします。
- [Astro のウェイトリスト](https://ikiform.com/ja/guides/waitlist/astro.md): 静的なランディングページに Astro のウェイトリストを。Action が登録を転送し、スクリプトがそれを呼び、オンデマンドの 1 ページが JavaScript なしの送信を受け止めます。
- [TanStack Start のウェイトリスト](https://ikiform.com/ja/guides/waitlist/tanstack-start.md): TanStack Start のウェイトリスト。1 つのサーバールートが通常のフォーム送信と fetch の両方に応え、validateSearch が流入元と結果を URL で運びます。
- [React Router のウェイトリスト](https://ikiform.com/ja/guides/waitlist/react-router.md): 全ページのフッターに置く React Router のウェイトリスト。fetcher が 1 つのルート Action に送信し、登録ごとにページを記録し、JavaScript なしでも動きます。
- [SvelteKit のウェイトリスト](https://ikiform.com/ja/guides/waitlist/sveltekit.md): SvelteKit 3 のウェイトリスト。フックが最初の ?ref= を Cookie に保存し、名前付きのフォーム Action が登録を転送し、再訪者には登録済みと表示します。
- [Nuxt のウェイトリスト](https://ikiform.com/ja/guides/waitlist/nuxt.md): プリレンダーしたランディングページに Nuxt のウェイトリストを。1 つの Nitro ルートが $fetch と通常のフォーム送信を受け、Zod で確認し、JavaScript なしならリダイレクトします。

## 動作を確認する

ページを共有する前に、これを一通り確認してください。Ikiform に無料プランはありません。テストは無料です。プランのないワークスペースでも、`localhost` からのテスト送信を月 50 件まで受け付けます。本番運用するには、500 件で月 $6 からの Pro を選ぶか、買い切りにしてください。10,000 件で $49、または 50,000 件で $149 で、クレジットに有効期限はありません。[料金](https://ikiform.com/ja/pricing)を参照してください。

1. フォームの **Allowed domains** に、自分のサイトのドメインと、開発中は `localhost` を追加します。空にしておくと、どこからの送信も受け付けます。
2. ページでフォームに入力して送信します。サンクスページか、Ikiform のホストされたページに移動します。
3. ダッシュボードで **Submissions** を開きます。送ったすべてのフィールドを含む新しい送信データがあります。
4. 通知の受信者それぞれの受信箱を確認します。各自にフィールドを載せたメールが届き、フォームに `email` フィールドがあれば **Reply** は送信者宛てになります。
5. Webhook を追加した場合は、**Webhooks** でエンドポイントを開きます。配信は `succeeded` と表示され、サーバーの応答も確認できます。
6. ハニーポットに値を入れて `_gotcha=bot` で送信します。成功の応答は返りますが、送信データはスパムとして保存され、メールも Webhook も送られません。
7. 何も届かない場合は、ダッシュボードのステータスフィルターを **Spam** に切り替えてください。スパムにも本物の送信データと同じ成功の応答が返ります。
8. 同じアドレスで 2 回登録し、メールツールの連絡先が 1 件のままであることを確認します。

## トラブルシューティング

| 問題 | 対処法 |
| --- | --- |
| `403 origin_not_allowed` が返るか、すべての送信がスパムになる | ページのドメインがフォームの **Allowed domains** に入っていません。ドメインと、ローカルテスト用に `localhost` を追加してください。登録したドメインのサブドメインも許可されます。 |
| `429 rate_limited` | 1 人の訪問者が、フォームのレート制限を超えて送信しました。Standard では訪問者ごとに 1 分あたり 5 件までです。`Retry-After` まで待つか、共有の 1 つの IP から送信するフォームには Relaxed か Custom を選んでください。 |
| `403 challenge_required` | フォームのスパムチェックが **Ikiform** に設定されていて、`fetch` の送信にトークンがありませんでした。送信のたびに先に `@ikiform/sdk` の `challenge(formId)` でトークンを取得するか、チェックをオフにしてください。 |
| 送信データはダッシュボードにあるのに、メールも Webhook も届かない | スパムとして保存されています。スパムは何も送りません。**Spam** フィルターで開いて理由を確認し、スパムではないとマークすると、メールと Webhook が送られます。 |
| ブラウザーが自分のページではなく Ikiform のサンクスページに移動する | `_redirect` は、ドメインが **Allowed domains** にあり、`https:` の URL でない限り無視されます。ドメインを追加するか、代わりにフォームの設定で **Redirect after submit** を設定してください。 |
| 同じメールアドレスが 2 回表示される | 想定どおりの動作です。Ikiform はメールアドレスで重複を除去しません。エクスポート時に重複を取り除くか、リレーやメールツールでメールアドレスをキーに upsert してください。 |
| 登録は Ikiform に届くのに、メールツールには届かない | **Webhooks** でエンドポイントを開き、配信ログを読んでください。`400` ならシークレットが間違っています。ツールのステータス付きの `502` なら、ツールが連絡先を拒否しています。 |
| `402 test_limit_reached` | ワークスペースにプランがなく、今月の無料の `localhost` テスト 50 件を使い切っています。プランを選ぶか、ダッシュボードの **Send test** を使ってください。 |

## よくある質問

### ウェイトリストのページを作るのにバックエンドは必要ですか？

自前のサーバーは不要です。フォームには送信先が必要で、それがフォームバックエンドです。フォームの action を Ikiform のエンドポイントに向ければ、Ikiform が登録を 1 件ずつ保存、フィルタリング、転送します。ランディングページは静的なままでかまいません。

### Ikiform はウェイトリストに登録した人に確認メールを送りますか？

いいえ。Ikiform がメールを送るのはあなたとチームで、登録した本人ではないので、確認メールやダブルオプトインのメールはありません。登録を 1 件ずつ Webhook でメールツールに送り、そちらでダブルオプトインをオンにしてください。

### ウェイトリストのフォームで偽の登録を防ぐには？

非表示の _gotcha ハニーポットを残し、デフォルトのレート制限をオンのままにし、Allowed domains に自分のドメインを追加して、スパムチェックを Ikiform に設定すれば、キーの管理なしで Cloudflare Turnstile のチェックが入ります。使い捨てアドレスもスパムスコアを上げます。

### 同じメールアドレスが 2 回保存されることはありますか？

あります。Ikiform は受け付けた送信をすべて保存し、メールアドレスで重複をまとめません。リストの置き場所で重複を除去してください。メールツール、データベースの一意な列、またはインポート前の CSV です。

### 紹介プログラムや順番待ちの順位を追加できますか？

Ikiform だけではできません。紹介リンクやリーダーボードはありません。紹介コードを非表示のフィールドで渡して自分で数えるか、紹介がローンチの中心なら、紹介を軸にしたウェイトリスト製品を使ってください。

### Ikiform は無料ですか？

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

## その他のガイド

- [Product Hunt ローンチリスト](https://ikiform.com/ja/guides/waitlist/product-hunt.md): Product Hunt の数週間前からローンチリストを集め、登録をチャネルごとにタグ付けし、太平洋時間のリセットに合わせてローンチ日のメールを 2 回に分けて送ります。
- [選考付きのベータ登録](https://ikiform.com/ja/guides/waitlist/beta-signup.md): サーバーが必須を強制する選考質問付きのベータ応募フォームと、応募者を採点してリストから各コホートを選ぶスクリプトを紹介します。
- [モバイルアプリのウェイトリスト](https://ikiform.com/ja/guides/waitlist/mobile-app.md): 使っているスマホを尋ね、iPhone のテスターを TestFlight に送り、Play のクローズドテストを埋める、iOS・Android アプリ向けの公開前ウェイトリストです。
- [Next.js のウェイトリスト](https://ikiform.com/ja/guides/waitlist/nextjs.md): Next.js App Router のウェイトリスト。シークレットキーで登録を転送するサーバーアクション、useActionState、楽観的なサンクス表示、訪問者ごとの制限を扱います。
- [Framer のウェイトリスト](https://ikiform.com/ja/guides/waitlist/framer.md): Framer でウェイトリストの登録を集める 2 つの方法。Ikiform への Webhook を使う Framer のフォームか、訪問者ごとの制限とスパムチェックが残るコードコンポーネントです。
- [Webflow のウェイトリスト](https://ikiform.com/ja/guides/waitlist/webflow.md): Webflow の Form Block の送信先を Ikiform にし、非表示フィールドで登録に流入元のタグを付け、短いスクリプトですべてのフォームで Webflow の成功表示を残します。
- [Carrd のウェイトリスト](https://ikiform.com/ja/guides/waitlist/carrd.md): Carrd のページでウェイトリストの登録を集める方法。Pro Plus では UTM を記録する Send to URL フォーム、Pro Standard ではハニーポット付きの埋め込み HTML フォームを使います。
- [プレーンな HTML のウェイトリスト](https://ikiform.com/ja/guides/waitlist/html.md): ビルド不要でプレーンな HTML だけのウェイトリストページ。JavaScript がオフでも動き、読み込めばその場でお礼を表示し、必要なときはフォールバックします。
- [Astro のウェイトリスト](https://ikiform.com/ja/guides/waitlist/astro.md): 静的なランディングページに Astro のウェイトリストを。Action が登録を転送し、スクリプトがそれを呼び、オンデマンドの 1 ページが JavaScript なしの送信を受け止めます。
- [TanStack Start のウェイトリスト](https://ikiform.com/ja/guides/waitlist/tanstack-start.md): TanStack Start のウェイトリスト。1 つのサーバールートが通常のフォーム送信と fetch の両方に応え、validateSearch が流入元と結果を URL で運びます。
- [React Router のウェイトリスト](https://ikiform.com/ja/guides/waitlist/react-router.md): 全ページのフッターに置く React Router のウェイトリスト。fetcher が 1 つのルート Action に送信し、登録ごとにページを記録し、JavaScript なしでも動きます。
- [SvelteKit のウェイトリスト](https://ikiform.com/ja/guides/waitlist/sveltekit.md): SvelteKit 3 のウェイトリスト。フックが最初の ?ref= を Cookie に保存し、名前付きのフォーム Action が登録を転送し、再訪者には登録済みと表示します。
- [Nuxt のウェイトリスト](https://ikiform.com/ja/guides/waitlist/nuxt.md): プリレンダーしたランディングページに Nuxt のウェイトリストを。1 つの Nitro ルートが $fetch と通常のフォーム送信を受け、Zod で確認し、JavaScript なしならリダイレクトします。
- [静的サイトのお問い合わせフォーム](https://ikiform.com/ja/guides/contact-form-static-site.md): サーバーなしで静的サイトに動くお問い合わせフォームを追加します。POST の送り先、スパム、メールでの返信、各ホストとフレームワーク向けのコードまで。
- [フォームをメールに送る](https://ikiform.com/ja/guides/send-form-to-email.md): メールサーバーなしで、すべての送信データを受信トレイに届けます。mailto: と PHP の mail() がうまくいかない理由と、スパムを除外して返信もできる構成を解説します。
- [お問い合わせフォームのスパムを止める](https://ikiform.com/ja/guides/spam-protection.md): 本物の訪問者に負担をかけない層でお問い合わせフォームのスパムを止めます。ハニーポット、時間、内容のスコア、レート制限、必要なときだけの人間確認です。
