ウェブサイトやアプリのフィードバックフォーム2 分で読めます
自分のサイトで NPS アンケートを実施する:メールのリンクからスコアまで
メールに 0〜10 のスコアを載せ、スコアに応じて変わる追加質問を自分のページで尋ね、スクリプトで NPS を計算する NPS アンケートを実施します。
アンケートメールに 0〜10 の 11 個のリンクを載せ、それぞれ自分のサイトのページを、スコアと不透明な回答者トークンを URL に付けて指すようにします。ページはスコアをあらかじめ選択し、スコアに応じた追加質問を尋ね、本人が送信を押すまで何も記録しません。スコアは 0〜10 の数値フィールドに保存し、NPS は 9 と 10 の割合から 0〜6 の割合を引いて計算します。
NPS はどこが違うのか
NPS は、固定のスケールで 1 つだけ質問します。私たちを薦める可能性はどのくらいか、0〜10 で。次の 3 点が、ピラーページの 1〜5 の評価と異なります。
- スケールは 0 から始まります。 Ikiform の
ratingフィールドは 1 から最大 10 までなので、0 を保持できません。NPS ではmin: 0、max: 10、integer: trueのnumberフィールドを使い、数値として保存します。 - たいていはメールで送ります。 メールクライアントはフォームの送信やスクリプトの実行を確実には行えないので、メールにはリンクを載せ、フォームは自分のサイトに置きます。Ikiform にはホストされたフォームページがありません。
- メールのリンクは機械にクリックされます。 多くのメールセキュリティスキャナーは、メッセージ内のリンクをすべて開いて確認します。リンクを開いただけでスコアが記録されると、スキャナーからのスコアが集まってしまいます。ページは本人が送信を押したときにだけ記録します。
- 0〜6 のスコアでは何に失望したかを尋ね、批判者として数えます。
- 7 か 8 では何があれば 10 になるかを尋ね、合計にだけ数えます。
- 9 か 10 では最も価値を感じている点を尋ね、推奨者として数えます。
- NPS は推奨者の割合から批判者の割合を引いたもので、−100 から 100 の値になります。
フィールド
プレビュー
Ikiform のバリデーターをブラウザーで実行します。何も送信されません。
wave がない場合も拒否されるので、すべての回答がいずれかのアンケートの回に属します。| フィールド | 理由 |
|---|---|
score | number、0〜10、整数のみ。数値として保存 |
reason | 追加質問。スコアだけでも数えられるので任意 |
respondent | 人ごとに生成して送る不透明なトークン。自分のデータベースで顧客に結び付けます |
wave | 2026-q4 のような回。日付で絞り込まずに四半期を比較できます |
顧客のメールアドレスやアカウント ID をリンクに入れないでください。URL はブラウザーの履歴、プロキシのログ、転送されたメールに残ります。16 文字以上のランダムなトークンなら、誰が回答したかは自分にだけわかり、他の誰にも何も伝わりません。
アンケートメール
リンクの並びはメールツールの差し込みタグで作るか、メールを送るコードで生成します。この関数は回答者 1 人分の HTML を返します。
function npsRow(respondent, wave) {
const cells = Array.from({ length: 11 }, (_, score) => {
const link = new URL("https://example.com/nps");
link.searchParams.set("score", String(score));
link.searchParams.set("r", respondent);
link.searchParams.set("wave", wave);
return `<td><a href="${link}" style="display:block;padding:8px 10px">${score}</a></td>`;
});
return `<table role="presentation"><tr>${cells.join("")}</tr></table>
<p>0 is not at all likely, 10 is extremely likely.</p>`;
}メールは質問と数字の並びだけにしてください。前置きが長いとスマートフォンではスケールが画面の下に押し出されます。回答の大半は、スクロールせずに数字をクリックする人から届きます。
ランディングページ
これをサイトの /nps に置きます。0〜10 のスケールを描画し、リンクのスコアをあらかじめ選択し、そのスコアに合った追加質問を設定し、本人が送信を押したときにだけ fetch で送信します。ページ上でスコアを変えると、質問もそれに合わせて変わります。
<form id="nps" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<fieldset>
<legend>How likely are you to recommend us to a friend or colleague?</legend>
<div id="nps-scale"></div>
</fieldset>
<label for="reason" id="reason-label">What's the main reason for your score?</label>
<textarea id="reason" name="reason" rows="4" maxlength="4000"></textarea>
<input type="hidden" name="respondent" />
<input type="hidden" name="wave" />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Send</button>
<p id="nps-status" role="status"></p>
</form>
<script>
const QUESTIONS = [
[6, "What disappointed you most?"],
[8, "What would make it a 10 for you?"],
[10, "What do you value most about it?"],
];
const params = new URLSearchParams(location.search);
const form = document.getElementById("nps");
const label = document.getElementById("reason-label");
const scale = document.getElementById("nps-scale");
for (let score = 0; score <= 10; score += 1) {
scale.insertAdjacentHTML(
"beforeend",
`<label><input type="radio" name="score" value="${score}" required /> ${score}</label>`,
);
}
function askFor(score) {
label.textContent = QUESTIONS.find(([top]) => score <= top)[1];
}
const preset = Number(params.get("score"));
if (params.has("score") && Number.isInteger(preset) && preset >= 0 && preset <= 10) {
form.elements.score.value = String(preset);
askFor(preset);
}
form.elements.respondent.value = params.get("r") ?? "";
form.elements.wave.value = params.get("wave") ?? "";
scale.addEventListener("change", (event) => askFor(Number(event.target.value)));
form.addEventListener("submit", async (event) => {
event.preventDefault();
const status = document.getElementById("nps-status");
status.textContent = "Sending";
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
}).catch(() => null);
const result = await response?.json().catch(() => null);
if (result?.ok) {
form.replaceChildren(Object.assign(document.createElement("p"), { textContent: "Thanks for answering." }));
return;
}
status.textContent = result?.error?.fields?.score?.[0] ?? result?.error?.message ?? "Couldn't send it. Try again.";
});
</script>/nps を直接開いた人には、何も選択されていないスケールが表示されます。デフォルトを設定しない限り、その回答は wave がないため拒否され、迷い込んだ訪問が数字に混ざりません。スコアを計算する
SDK で回ごとのすべての回答を読み、回答者ごとに最新の回答だけを残して、式を当てはめます。IKIFORM_SECRET_KEY にシークレットキーを入れて実行してください。
import { createClient } from "@ikiform/sdk";
const WAVE = process.argv[2] ?? "2026-q4";
const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY });
const latest = new Map();
for await (const submission of ikiform.submissions.iterate({ formId: "frm_xxxxxxxxxxxxxxxx", status: "received" })) {
const { score, respondent, wave } = submission.fields;
if (wave !== WAVE) continue;
const key = respondent || submission.id;
const seen = latest.get(key);
if (!seen || seen.at < submission.created_at) latest.set(key, { score, at: submission.created_at });
}
const scores = [...latest.values()].map((answer) => answer.score);
const promoters = scores.filter((value) => value >= 9).length;
const detractors = scores.filter((value) => value <= 6).length;
const nps = Math.round(((promoters - detractors) / scores.length) * 100);
console.log(`${WAVE}: ${scores.length} people, ${promoters} promoters, ${detractors} detractors, NPS ${nps}`);気が変わって 2 つ目の数字をクリックする人がいるので、スクリプトは回答者ごとに回答を 1 つだけ残します。Ikiform はメールアドレスで重複した送信をまとめたり拒否したりしません。24 時間以内のまったく同じ再送信は、スパムスコアを上げるだけです。重複はリストを管理している場所か、エクスポートするときに取り除いてください。
動作を確認する
/nps?score=3&r=test_respondent_0001&wave=2026-q4を開きます。3 が選択され、質問は「What disappointed you most?」になっています。- 9 をクリックします。質問が「What do you value most about it?」に変わります。
- 同じリンクを
curlで開きます。何も送信されていないので、Submissions には何も増えません。 - 送信を押します。回答の
scoreは数値の 9 になっています。 node nps.mjs 2026-q4を実行します。1 人、推奨者 1 人と表示されます。
ハマりどころ
- 同じ顧客にアンケートを送りすぎないでください。四半期に 1 回などの間隔を決め、回答者のリストは自分の側で管理します。
- サンプルが少ないと値が大きく振れます。回答が 20 件なら、1 人で NPS が 5 ポイント動きます。スコアの横に人数も載せましょう。
- 低いスコアには返信する価値があります。ピラーページの低評価アラートのような Webhook のリレーで、
score <= 6を条件に批判者をチームのチャットに送ってください。 - アンケートのフォームテンプレートは、長めのアンケートの中でスコアを尋ねます。単発の調査には向いていますが、NPS の質問を単独で尋ねるほうが回答は集まります。
- Ikiform はフォームを入力した本人にはメールを送りません。通知メールが届くのはあなたとチームだけなので、自動返信、確認メール、ダブルオプトインはありません。それらは Webhook をきっかけに、自分のメールツールから送ってください。
トラブルシューティング
| 問題 | 対処法 |
|---|---|
Must be at most 10 付きの 422、または整数のエラー | スコアがスケール以外の場所から来ています。たとえば手で書き換えた URL です。ページがリンクから受け付けるのは 0〜10 だけです。メールテンプレートのリンクを確認してください。 |
wave を示す 422 | リンクに wave が含まれていないか、2026-q4 のような形式の値に一致していません。メールテンプレートを直すか、ページのスクリプトでデフォルトを設定してください。 |
403 origin_not_allowed が返る、またはすべての送信がスパムになる | ページのドメインがフォームの Allowed domains にありません。ドメインと、ローカルテスト用の localhost を追加してください。登録したドメインのサブドメインは許可されます。 |
429 rate_limited | 1 人の訪問者がフォームのレート制限を超えて送信しました。Standard では訪問者ごとに 1 分あたり 5 件までです。Retry-After まで待つか、共有の IP 1 つから送信するフォームには Relaxed か Custom を選んでください。 |
よくある質問
0〜10 の 11 個のリンクをメールに入れ、それぞれがスコアを URL に含めて自分のサイトのページを開くようにします。ページはそのスコアをあらかじめ選択し、追加の質問を尋ね、本人が送信を押したときに回答を記録します。
いいえ。評価は 1 から最大 10 までで、NPS には 0 が必要です。min 0、max 10、integer true の数値フィールドを使ってください。
9 か 10 の回答の割合から 0〜6 の回答の割合を引きます。7 と 8 は合計に含めるだけで、それ以外では無視します。結果は −100 から 100 になります。
メールのセキュリティスキャナーは、受信したメールのリンクを開いて確認します。クリック時に記録すると、そうした訪問も回答として数えてしまいます。送信を押したときだけ記録すれば、数字は正確なままです。
手順で詰まったときや、あなたの状況に合うガイドがほしいときはメールでどうぞ: hello@ikiform.com