新しい SaaS のウェイトリスト2 分で読めます
Framer のサイトにウェイトリストを追加する方法
Framer でウェイトリストの登録を集める 2 つの方法。Ikiform への Webhook を使う Framer のフォームか、訪問者ごとの制限とスパムチェックが残るコードコンポーネントです。
Framer のフォームは外部の URL に送信できませんが、各送信データを Webhook に送れるので、ノーコードなら Ikiform のエンドポイントを Webhook にした Framer のフォームを使います。一般公開のローンチには、代わりに小さなコードコンポーネントを使ってください。ブラウザーから直接送信されるので、Ikiform の訪問者ごとのレート制限、許可ドメイン、ハニーポットがすべて機能し続けます。
ページ自体は午後だけで作れるので、ローンチ前のページの多くは Framer で作られています。わかりにくいのはウェイトリストのフォームです。Framer のフォームブロックは action を変更できないため、実際のトラフィックが来ると挙動が異なる 2 つの方法から選ぶことになります。
Framer ならではの違い
- カスタムの action がありません。 Framer のフォームは、Framer 独自のストレージ、メール、Webhook のいずれかの送信先に送ります。コードなしで外部のバックエンドに届ける方法は Webhook だけです。
- Webhook は訪問者ではなく Framer のサーバーから送られます。 Ikiform に見えるのは Framer の IP アドレスで、
Originヘッダーはありません。そのため、許可ドメイン、訪問者ごとのレート制限、人間確認という 3 つの保護が無効になります。 - リトライ。 Framer は
2xx以外をすべて失敗とみなし、最大 5 回リトライします。リダイレクトはたどりません。Ikiform は JSON の送信に201を返すので、保存された登録は 1 回目で成功になります。 - コードコンポーネントは本物の React です。 公開サイト上の訪問者のブラウザーで動くので、その中のフォームは他の Web ページと同じように送信します。
- Webhook の経路では、訪問者が Framer のフォームを送信し、Framer のサーバーがフィールドを JSON として Ikiform に転送します。
- Ikiform からは Framer のサーバーが送信者に見えるので、訪問者を区別したり、フォームがどのサイトにあるかを確認したりできません。
- コードコンポーネントでは、訪問者のブラウザーが Ikiform に直接送信します。
- Ikiform には訪問者ごとの IP とあなたのドメインが見えるので、すべての保護が適用されます。
経路を選ぶ
| Item | Webhook 付きの Framer フォーム | コードコンポーネント |
|---|---|---|
| 設定 | 右サイドバーの 2 つの項目 | コードファイル 1 つを 1 回貼り付け |
| スタイル | Framer のフォームのスタイルと状態 | 編集できるインラインスタイルか独自のクラス |
| 訪問者ごとのレート制限 | いいえ。すべての送信が Framer の IP を共有 | はい、それぞれ 1 分あたり 5 件 |
| 許可ドメイン | 空のままにする | Framer のドメインと独自ドメインを設定 |
| ハニーポット | いいえ | はい |
| 人間確認 | オフ | オフ(fetch で送信するため) |
| 流入元の記録 | 不要 | URL の ?ref=、またはインスタンスごとの既定値 |
数百人に共有するページなら Webhook の経路で十分です。どこか大きな場所にページを投稿する予定なら、コードコンポーネントを使ってください。Framer のサーバー経由でボットの登録が一気に来ると、止められるのはスパムスコアだけです。
経路 1: Webhook 付きの Framer のフォーム
- ページにフォームを追加し、Email の入力欄を 1 つ置きます。入力欄の設定で名前を
emailにします。Framer は各入力欄の名前を JSON のキーとして送り、Ikiform はemailを返信先と使い捨てドメインのチェックに使います。 - フォームを選択します。Send To の横で Add… › Webhook を選び、エンドポイント
https://ikiform.com/f/frm_xxxxxxxxxxxxxxxxを貼り付けます。署名シークレットは任意です。Ikiform は Framer の署名を確認しません。 - Ikiform では Allowed domains(許可ドメイン)を空のままにし、スパムチェックをオフにします。どちらかがオンだと、Framer からの送信には origin がなくブラウザーでの確認も実行できないため、すべての送信がスパム判定されるか拒否されます。
- レート制限を Relaxed に、ローンチ当日は Custom に設定します。Standard では Framer からの送信がすべて 1 人の訪問者として数えられ、フォーム全体で 1 分あたり 300 件が上限になります。
- フォームの成功時の表示を Framer でスタイリングします。Ikiform が
201を返したときに表示されます。
経路 2: コードコンポーネント
Assets パネルで Code を開き、WaitlistSignup という名前のコードファイルを作成して、これを貼り付けます。ページにドラッグし、プロパティパネルで Form ID を設定します。
import { addPropertyControls, ControlType } from "framer";
import { type CSSProperties, type FormEvent, useRef, useState } from "react";
const box: CSSProperties = {
font: "inherit",
padding: "12px 14px",
borderRadius: 10,
border: "1px solid rgba(0, 0, 0, 0.15)",
};
interface Props {
formId: string;
buttonLabel: string;
joinedText: string;
source: string;
}
export default function WaitlistSignup({ formId, buttonLabel, joinedText, source }: Props) {
const [status, setStatus] = useState<"idle" | "sending" | "joined">("idle");
const [error, setError] = useState("");
const key = useRef("");
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const body = new FormData(event.currentTarget);
const ref = new URLSearchParams(window.location.search).get("ref");
body.set("ref", (ref || source).slice(0, 40));
key.current ||= crypto.randomUUID();
setStatus("sending");
setError("");
try {
const response = await fetch(`https://ikiform.com/f/${formId}`, {
method: "POST",
headers: { Accept: "application/json", "Idempotency-Key": key.current },
body,
});
const result = await response.json();
if (result.ok) return setStatus("joined");
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" style={{ margin: 0 }}>{joinedText}</p>;
return (
<form onSubmit={onSubmit} style={{ display: "flex", flexWrap: "wrap", gap: 8, width: "100%" }}>
<input
name="email"
type="email"
placeholder="you@company.com"
aria-label="Email"
autoComplete="email"
required
style={{ ...box, flex: "1 1 220px" }}
/>
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
<button type="submit" disabled={status === "sending"} style={{ ...box, cursor: "pointer" }}>
{status === "sending" ? "Joining" : buttonLabel}
</button>
{error ? <p role="alert" style={{ flexBasis: "100%", margin: 0 }}>{error}</p> : null}
</form>
);
}
addPropertyControls(WaitlistSignup, {
formId: { type: ControlType.String, title: "Form ID", defaultValue: "frm_xxxxxxxxxxxxxxxx" },
buttonLabel: { type: ControlType.String, title: "Button", defaultValue: "Join the waitlist" },
joinedText: { type: ControlType.String, title: "Joined text", defaultValue: "You're on the list." },
source: { type: ControlType.String, title: "Source", defaultValue: "framer" },
});- プロパティパネルには、ボタンのラベル、登録完了時のテキスト、既定の Source が表示されます。ヒーローに
hero、フッターにfooterのインスタンスを 1 つずつ置けば、どちらがコンバージョンにつながるかがわかります。 - ページの URL の
?ref=は既定値より優先されるので、キャンペーンのリンクには自動でタグが付きます。 - 冪等性キーは最初の送信時に作られ、同じ訪問中のリトライでは再利用されるので、スマートフォンでダブルタップしても保存される登録は 1 件です。
windowは送信ハンドラーの中でだけ読みます。Framer はまずサーバーでコンポーネントをレンダリングするので、レンダリング中にwindowに触れるとそれが壊れます。
コンポーネント用の Ikiform の設定
メールアドレスを必須にし、ref を許可するために、フォームの Fields タブで次を追加します:
{
"version": 1,
"title": "Waitlist",
"fields": [
{ "id": "email", "type": "email", "label": "Email", "required": true },
{ "id": "ref", "type": "text", "label": "Source", "validation": { "max_length": 40 } }
]
}- Allowed domains に
your-project.framer.appと独自ドメインを追加します。framer.appだけを追加してはいけません。すべての Framer サイトが一致してしまいます。 - スパムチェックはオフのままにします。コンポーネントは
fetchで送信し、トークンを取得しないので、チェックがオンのフォームは403 challenge_requiredを返します。 - レート制限は Standard のままにします。これで訪問者がそれぞれ個別に数えられます。
動作を確認する
- サイトを公開します。Webhook もコードコンポーネントも、エディターのプレビューからではなく、公開されたサイトからだけ送信します。
framer.appドメインでフォームを送信し、独自ドメインでももう一度送信します。- ダッシュボードで Submissions を開きます。送ったすべてのフィールドを含む新しい送信データがあります。
- コンポーネントの場合は、
?ref=test付きでページを開き、登録のrefがtestになっていることを確認します。 - 通知の受信者それぞれの受信箱を確認します。各自にフィールドを載せたメールが届き、フォームに
emailフィールドがあれば Reply は送信者宛てになります。 - 何も届かない場合は、ダッシュボードのステータスフィルターを Spam に切り替えてください。スパムにも本物の送信データと同じ成功の応答が返ります。
ハマりどころ
| 問題 | 対処法 |
|---|---|
Webhook 経由の登録がすべて origin_mismatch でスパムに入る | Allowed domains が設定されていますが、Framer のサーバーからの送信には origin がありません。このフォームでは設定を空にするか、コードコンポーネントに切り替えてください。 |
| Framer が Webhook の配信失敗を報告する | Ikiform が送信を拒否しています。スパムチェックがオンで 403 challenge_required になっているか、入力欄の名前が email でないため 422 になっています。原因を直してください。リトライしても同じように失敗します。 |
429 rate_limited | 1 人の訪問者が、フォームのレート制限を超えて送信しました。Standard では訪問者ごとに 1 分あたり 5 件までです。Retry-After まで待つか、共有の 1 つの IP から送信するフォームには Relaxed か Custom を選んでください。 |
コンポーネントに Unknown field "ref" と表示される | フィールド定義に ref がありません。上の定義のように追加してください。 |
403 origin_not_allowed が返るか、すべての送信がスパムになる | ページのドメインがフォームの Allowed domains に入っていません。ドメインと、ローカルテスト用に localhost を追加してください。登録したドメインのサブドメインも許可されます。 |
お問い合わせフォーム版を含む Framer の設定の全体は、Framer のドキュメントガイドにあります。リストのエクスポートとローンチメールの送信はメインガイドで説明しています。
よくある質問
フォームの action としてはできません。Framer のフォームは Framer 独自の送信先に送り、その 1 つが任意の https URL への Webhook です。Ikiform にはそれを使うか、コードコンポーネントで独自のフォームをレンダリングしてください。
Webhook の経路では、Framer は Origin ヘッダーなしで自社のサーバーから送信するので、Allowed domains を設定したフォームではすべての送信が origin_mismatch になります。Webhook を使うフォームでは Allowed domains を空にしてください。
Framer はフォームの送信数を無制限としているので、Webhook 経由の登録が Framer の上限に達することはありません。コードコンポーネントの経路では Framer のフォームをまったく使いません。
いいえ、無料プランはありません。テストは無料で、支払う前に localhost からテスト送信を月 50 件まで送れます。本番運用には、500 件で月 $6 からの Pro か、買い切りで 10,000 件 $49、または 50,000 件 $149 を選べます。クレジットに有効期限はありません。
手順で詰まったときや、あなたの状況に合うガイドがほしいときはメールでどうぞ: hello@ikiform.com