ウェブサイトやアプリのフィードバックフォーム

ウェブサイトやアプリのフィードバックフォーム2 分で読めます

自分のサイトで NPS アンケートを実施する:メールのリンクからスコアまで

メールに 0〜10 のスコアを載せ、スコアに応じて変わる追加質問を自分のページで尋ね、スクリプトで NPS を計算する NPS アンケートを実施します。

Preet SutharIkiform 創業者

アンケートメールに 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 にはホストされたフォームページがありません。
  • メールのリンクは機械にクリックされます。 多くのメールセキュリティスキャナーは、メッセージ内のリンクをすべて開いて確認します。リンクを開いただけでスコアが記録されると、スキャナーからのスコアが集まってしまいます。ページは本人が送信を押したときにだけ記録します。
NPS のスコア帯ごとに追加質問が変わり、結果に反映される仕組み0 から 6最も失望した点は何ですか?批判者差し引く7 か 8何があれば 10 になりますか?中立者合計にだけ数える9 か 10最も価値を感じている点は何ですか?推奨者加えるスコア追加質問数え方NPS のスコア帯ごとに追加質問が変わり、結果に反映される仕組み0 から 6最も失望した点は何ですか?批判者差し引く7 か 8何があれば10になりますか?中立者合計にだけ数える9 か 10最も価値を感じている点は何ですか?推奨者加えるスコア追加質問数え方
  1. 0〜6 のスコアでは何に失望したかを尋ね、批判者として数えます。
  2. 7 か 8 では何があれば 10 になるかを尋ね、合計にだけ数えます。
  3. 9 か 10 では最も価値を感じている点を尋ね、推奨者として数えます。
  4. NPS は推奨者の割合から批判者の割合を引いたもので、−100 から 100 の値になります。
役に立つ答えがスコアによって変わるので、質問もスコアに合わせて変えます。批判者の理由は直すべき問題で、推奨者の理由はホームページに使える言葉です。

フィールド

プレビュー

Ikiform のバリデーターをブラウザーで実行します。何も送信されません。

スコアに 11 や 7.5 を試してみてください。Ikiform はどちらも拒否します。wave がない場合も拒否されるので、すべての回答がいずれかのアンケートの回に属します。
フィールド理由
scorenumber、0〜10、整数のみ。数値として保存
reason追加質問。スコアだけでも数えられるので任意
respondent人ごとに生成して送る不透明なトークン。自分のデータベースで顧客に結び付けます
wave2026-q4 のような回。日付で絞り込まずに四半期を比較できます

顧客のメールアドレスやアカウント ID をリンクに入れないでください。URL はブラウザーの履歴、プロキシのログ、転送されたメールに残ります。16 文字以上のランダムなトークンなら、誰が回答したかは自分にだけわかり、他の誰にも何も伝わりません。

アンケートメール

リンクの並びはメールツールの差し込みタグで作るか、メールを送るコードで生成します。この関数は回答者 1 人分の HTML を返します。

nps-email.js
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 で送信します。ページ上でスコアを変えると、質問もそれに合わせて変わります。

nps.html
<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 にシークレットキーを入れて実行してください。

nps.mjs
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 時間以内のまったく同じ再送信は、スパムスコアを上げるだけです。重複はリストを管理している場所か、エクスポートするときに取り除いてください。

動作を確認する

  1. /nps?score=3&r=test_respondent_0001&wave=2026-q4 を開きます。3 が選択され、質問は「What disappointed you most?」になっています。
  2. 9 をクリックします。質問が「What do you value most about it?」に変わります。
  3. 同じリンクを curl で開きます。何も送信されていないので、Submissions には何も増えません。
  4. 送信を押します。回答の score は数値の 9 になっています。
  5. 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_limited1 人の訪問者がフォームのレート制限を超えて送信しました。Standard では訪問者ごとに 1 分あたり 5 件までです。Retry-After まで待つか、共有の IP 1 つから送信するフォームには Relaxed か Custom を選んでください。

よくある質問

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