メインコンテンツへスキップ
← 記事一覧に戻る
·運営·14 min read

個人開発AIアプリのフィードバックループ設計 — Tally × Slack × Build in Public

recipe-aiTallySlackフィードバック個人開発

個人開発で最も見落とされるのが、ユーザーからのフィードバック収集だ。作って公開して終わり。ユーザーが何に困っているか、何を求めているかを知らないまま、開発者の思い込みで機能を積み上げていく。

筆者が開発中の recipe-ai(YouTube 料理動画からレシピを AI 抽出するアプリ)では、MVP の段階からフィードバックループを組み込んだ。Tally でフォームを作り、Webhook で自前の API に飛ばし、Slack に通知する。かかるコストはゼロ円。この記事では、その全体設計と実装コードを公開する。

この記事でわかること:

  • ゼロ円で構築できるフィードバック収集の全体設計
  • Tally フォームの iframe 埋め込みと設計のコツ
  • Webhook 受信 → Slack Block Kit 通知の Next.js API Route 実装
  • Build in Public とフィードバックの相乗効果

なぜフィードバックループが必要か

個人開発者にとって、ユーザーの声は最も安い市場調査だ。広告を出す前に、目の前にいるユーザーが何を考えているかを聞くほうがはるかに効率がいい。

Build in Public の本質は「開発の過程を公開すること」ではない。ユーザーとの対話だ。フィードバックを受け取り、改善し、その過程をまた公開する。このループが回り始めると、プロダクトとコンテンツの両方が同時に育つ。

問題は、フィードバック収集の仕組みが面倒に見えることだ。Google Forms で作ってスプレッドシートに溜めるのが定番だが、通知が来ないので見忘れる。SaaS のフィードバックツールを導入すると月額費用がかかる。

必要なのは、ユーザーが送った瞬間に開発者の手元に届く仕組みを、ゼロ円で作ることだ。

全体設計: Tally → Webhook → Slack

今回の構成はシンプルな 3 段階のパイプラインになっている。

ユーザー
  |
  v
[Tally フォーム(iframe 埋め込み)]
  |  フォーム送信
  v
[Tally Webhook]
  |  POST リクエスト
  v
[recipe-ai API Route: /api/tally-webhook]
  |  整形 + 転送
  v
[Slack Incoming Webhook]
  |  Block Kit メッセージ
  v
開発者の Slack チャンネル

各ツールの役割は明確に分かれている。

  • Tally: フォーム UI の提供。Webhook 送信機能が無料プランで使える
  • Next.js API Route: Tally の payload を Slack 用に整形する中継地点
  • Slack Incoming Webhook: リッチな通知の受け口

コスト内訳は以下の通り。

ツールプラン月額
TallyFree0 円
SlackFree0 円
Next.js API RouteVercel Hobby0 円
合計0 円

なぜ Tally から直接 Slack に送らないのか。Tally の Slack 連携はプレーンテキストしか送れない。API Route を挟むことで Slack Block Kit を使ったリッチな通知が可能になり、将来的にデータベースへの保存やフィルタリングも追加できる。

Tally フォームの設置

recipe-ai ではフィードバックページ(/feedback)に Tally フォームを iframe で埋め込んでいる。

// app/[locale]/feedback/page.tsx(抜粋)
const TALLY_EMBED_URL = process.env.NEXT_PUBLIC_TALLY_EMBED_URL ?? "";

// フォーム表示部分
{TALLY_EMBED_URL ? (
  <>
    <iframe
      src={TALLY_EMBED_URL}
      title="ご要望・お問い合わせフォーム"
      width="100%"
      height="720"
      loading="lazy"
      className="block bg-transparent"
      data-tally-src={TALLY_EMBED_URL}
    />
    <Script
      src="https://tally.so/widgets/embed.js"
      strategy="lazyOnload"
    />
  </>
) : (
  <div className="border border-stone-300 bg-white p-6 text-sm text-stone-600">
    フォーム準備中です。しばらくお待ちください。
  </div>
)}

ポイントは 3 つ。

1. 環境変数で URL を管理する。 NEXT_PUBLIC_TALLY_EMBED_URL にフォームの Embed URL を入れる。ローカル開発時はフォームなしでもページが壊れないよう、フォールバック UI を出す。

2. loading="lazy" で初期表示を軽くする。 Tally の iframe はページ下部にあることが多いので、ビューポートに入るまで読み込まない。

3. tally.so/widgets/embed.jslazyOnload で読む。 自動リサイズのために必要だが、ページの初期表示をブロックしない。

フォーム設計のコツ

フォームの項目は最小限にする。recipe-ai では以下の 3 項目に絞っている。

  • 種別(バグ報告 / 機能要望 / 感想 / その他)-- 選択式
  • 内容 -- 自由記述
  • メールアドレス -- 任意。返信希望の場合のみ

項目が多いほど離脱率は上がる。MVP の段階で聞くべきことは「何に困っているか」「何が欲しいか」の 2 点だけだ。

Webhook → Slack 通知の実装

Tally のフォーム設定画面で Webhook URL を登録すると、送信のたびに JSON payload が POST される。recipe-ai ではこれを受け取って Slack に転送する API Route を実装している。

以下が app/api/tally-webhook/route.ts の全コードだ。

import { NextResponse } from "next/server";

type TallyField = {
  key: string;
  label: string;
  type: string;
  value: string | string[] | number | boolean | null;
  options?: { id: string; text: string }[];
};

type TallyPayload = {
  eventId?: string;
  eventType?: string;
  createdAt?: string;
  data?: {
    formId?: string;
    formName?: string;
    fields?: TallyField[];
  };
};

function formatFieldValue(f: TallyField): string {
  const v = f.value;
  if (v == null || v === "") return "(未入力)";
  if (Array.isArray(v)) {
    // Multiple choice は option ID の配列で来るので label に変換
    if (f.options && f.options.length > 0) {
      const labels = v
        .map((id) => f.options?.find((o) => o.id === id)?.text ?? String(id))
        .filter(Boolean);
      return labels.join(", ");
    }
    return v.join(", ");
  }
  return String(v);
}

export async function POST(request: Request) {
  const slackUrl = process.env.SLACK_WEBHOOK_URL;
  if (!slackUrl) {
    console.error("[tally-webhook] SLACK_WEBHOOK_URL is not set");
    return NextResponse.json(
      { ok: false, error: "not_configured" },
      { status: 500 }
    );
  }

  let payload: TallyPayload;
  try {
    payload = (await request.json()) as TallyPayload;
  } catch {
    return NextResponse.json(
      { ok: false, error: "invalid_json" },
      { status: 400 }
    );
  }

  const formName = payload.data?.formName ?? "Tally Form";
  const fields = payload.data?.fields ?? [];
  const created = payload.createdAt ?? new Date().toISOString();

  const fieldLines = fields
    .map((f) => `*${f.label}*: ${formatFieldValue(f)}`)
    .join("\n");

  const slackPayload = {
    text: `新しいフィードバックが届きました(${formName})`,
    blocks: [
      {
        type: "header",
        text: { type: "plain_text", text: `${formName} に新着` },
      },
      {
        type: "section",
        text: { type: "mrkdwn", text: fieldLines || "(フィールドなし)" },
      },
      {
        type: "context",
        elements: [
          {
            type: "mrkdwn",
            text: `送信時刻: ${new Date(created).toLocaleString("ja-JP", {
              timeZone: "Asia/Tokyo",
            })}`,
          },
        ],
      },
    ],
  };

  try {
    const res = await fetch(slackUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(slackPayload),
    });
    if (!res.ok) {
      const body = await res.text();
      console.error("[tally-webhook] Slack error:", res.status, body);
      return NextResponse.json(
        { ok: false, error: "slack_error", status: res.status },
        { status: 502 }
      );
    }
  } catch (e) {
    console.error("[tally-webhook] fetch failed:", e);
    return NextResponse.json(
      { ok: false, error: "fetch_failed" },
      { status: 502 }
    );
  }

  return NextResponse.json({ ok: true });
}

コードの要点

型定義で Tally の payload を安全に受け取る。 TallyField には valuestring | string[] | number | boolean | null と複数の型がありえる。選択式フィールドは option ID の配列として送られてくるため、options からラベルに変換する処理が必要だ。

Slack Block Kit で構造化する。 header ブロックでフォーム名、section ブロックで各フィールドの内容、context ブロックで送信時刻を表示する。text フィールドのフォールバックテキストも設定しているので、通知が表示できない環境でも内容が読める。

エラーハンドリングは 3 段階。 環境変数未設定(500)、JSON パース失敗(400)、Slack 送信失敗(502)をそれぞれ分けて返す。Tally はリトライ機能があるので、5xx を返せば再送してくれる。

環境変数の設定

必要な環境変数は 2 つだけ。

# Slack Incoming Webhook の URL
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../xxx

# Tally の署名検証用(任意)
TALLY_SIGNING_SECRET=your_signing_secret

Slack Incoming Webhook は、Slack のワークスペース設定 → アプリ → Incoming Webhooks から取得する。チャンネルを指定して Webhook URL を発行するだけだ。

X シェアボタンによる拡散導線

recipe-ai のトップページにはフィードバックページへのリンクに加え、ナビゲーションに「ご要望」リンクを設置している。

<Link
  href="/ja/feedback"
  className="text-stone-700 hover:text-stone-900 px-2 py-1 hidden sm:inline"
>
  ご要望
</Link>

SNS シェアも間接的なフィードバックシグナルとして機能する。ユーザーがアプリを X でシェアしてくれること自体が「使えた」「面白かった」という肯定的なフィードバックだ。

シェアボタンを設置する際のポイントは、プリフィルテキストの設計にある。ユーザーにゼロから文章を書かせない。アプリ名と URL が入った状態で X の投稿画面を開く。ハッシュタグも入れておくと、後からエゴサーチで拾える。

https://twitter.com/intent/tweet?text=recipe-ai で料理動画からレシピを抽出してみた&url=https://recipe-ai.example.com&hashtags=recipe_ai,個人開発

フィードバックフォームとシェアボタンは役割が違う。フォームは「困っていること」「欲しいもの」を拾う。シェアは「満足度」と「認知拡大」を同時に取る。両方あることで、ネガティブとポジティブの両面からユーザーの声が集まる。

Build in Public との相乗効果

フィードバックループと Build in Public は、組み合わせると掛け算になる。

フィードバックをコンテンツに変換できる。 「バグ報告が来た → 原因を調べた → 修正した」という流れは、そのまま技術記事のネタになる。ユーザーの声が起点なので、需要があることが最初から担保されている。

ユーザーの声が次のユーザーを呼ぶ。 「このフィードバックを受けて改善しました」と X で報告すると、他の潜在ユーザーに「自分の声も聞いてもらえそうだ」という印象を与える。個人開発の強みは、大企業にはできないスピード感のある対話だ。

改善の証拠が蓄積される。 Slack に通知が溜まっていくこと自体が、プロダクトが使われている証拠になる。Build in Public で「フィードバック N 件いただきました」と報告できる。数字は信頼を生む。

具体的な運用フローを整理すると、こうなる。

  1. ユーザーが Tally フォームからフィードバックを送信
  2. Slack に即座に通知が届く
  3. 内容を確認し、対応可能なものは即日修正
  4. 修正内容を X で報告(Build in Public)
  5. その報告を見た別のユーザーが使い始める
  6. 新しいユーザーからまたフィードバックが届く

このループが回り始めると、開発のモチベーションも維持しやすい。自分のためだけに作っているのではなく、使ってくれている人がいるという実感がある。

まとめ

Tally(無料)+ Next.js API Route + Slack(無料)で、ゼロ円のフィードバックループが作れる。MVP の段階でこの仕組みを入れておくと、Build in Public のコンテンツ素材とプロダクト改善の両方が同時に手に入る。作って終わりにしない仕組みを、最初から組み込もう。

recipe-ai を試す(無料)

この記事で作っている YouTube 料理レシピ抽出アプリ本体。動画 URL を貼るだけで AI がレシピに変換。月 5 本まで無料。

関連記事

課金ゼロで始めるAIアプリ収益化

Stripe Payment Links とチップモデルの実装

Claude Code で 0→MVP を1日で作る全記録

recipe-ai Build in Public の全工程を公開

Claude Crew Lab Free — 毎月の実験記録をメールで

Claude Code × 個人開発のリアルな事故・発見・SaaS アイデアを毎月第1月曜にお届け。登録で「収益化チェックリスト 15 項目」を無料プレゼント。

Lab Free 登録(月1回・無料)

この記事が役に立ったらシェア