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

レシピ抽出で終わらせない — recipe-ai を生活運用ツールに重心シフトした設計

recipe-aiプロダクト設計SupabaseNext.jsBuild in Public

recipe-ai の h1 を「YouTube 料理動画から、レシピを抽出」から「動画から、今週の夕食へ」に変えた。

これは単なるコピー変更ではなく、プロダクトの重心を「抽出が主語」から「運用が主語」に動かす判断の結果だ。同時に「台所」ページを新設し、予定 → 買い物 → 在庫 → 消費のループを閉じるところまで実装した。

この記事でわかること:

  • 「抽出ツール」の訴求が弱くなる構造的な理由
  • 料理動画を保存して終わってしまう問題
  • cooking_plans と pantry_items の連動設計
  • 予定「作った」で関連材料を自動 consumed にする仕組み
  • LP 訴求を「運用」に寄せた結果の変化
  • C0 → C1 → C2 の食材検索の位置付け再定義

なぜ「抽出」を主役にしていたか

recipe-ai は元々、YouTube の料理動画から AI(Google Gemini)がレシピを抽出するのが中核機能だった。

「YouTube 料理動画 → AI → レシピ JSON」というパイプラインが動いたとき、これは十分な価値だと思った。特に字幕なしの動画でも Gemini が音声から材料・手順を構造化できる点は、既存のレシピ検索サービスに対する差別化として機能する。

だから LP の h1 は「YouTube 料理動画から、レシピを抽出」と、抽出を主語にして書いていた。

これは技術的には正しいが、ユーザーの行動から見ると弱い訴求だった。


保存して終わる問題

YouTube の料理動画を見るユーザーの典型行動はこうだ:

  1. 気になる動画を見つける
  2. 保存する / ブックマークする
  3. そのまま忘れる
  4. 1 に戻る

recipe-ai の抽出機能は 2 と 3 の間に入る。動画を保存するだけでなく、材料・手順まで構造化して保存する。これは保存の質を上げる機能だ。

でも「保存して忘れる」は本質的には解決しない。ユーザーが本当に欲しいのは「保存したレシピを料理して、冷蔵庫を使い切ること」であって、検索可能な保存データベースではない。

ここに気づいたとき、recipe-ai の重心を動かすべきだと判断した。


台所ループの設計

新設した /ja/kitchen ページは、3 つのセクションを統合している:

  1. 作る予定(cooking_plans): 今週作る予定のレシピ
  2. 買い物リスト(pantry_items の shopping 状態): まだ家にない材料
  3. 家にある材料(pantry_items の stocked 状態): 買って家にある材料

これらを 4 つの状態遷移で繋いでいる:

保存レシピ → 「作る予定に追加」 → cooking_plans に追加
                                  + 材料を pantry_items(status=shopping) に自動コピー

shopping → チェック → stocked(家にある)

stocked → 予定「作った」 → consumed(消費済み)

データモデル

-- 作る予定
CREATE TABLE cooking_plans (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES auth.users,
  recipe_id UUID REFERENCES recipes NULL,
  recipe_title TEXT,
  source_url TEXT,
  status TEXT CHECK (status IN ('planned', 'cooked', 'skipped')),
  added_at TIMESTAMPTZ DEFAULT NOW(),
  cooked_at TIMESTAMPTZ
);

-- 食材アイテム
CREATE TABLE pantry_items (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES auth.users,
  name TEXT NOT NULL,
  amount TEXT,
  status TEXT CHECK (status IN ('shopping', 'stocked', 'consumed')),
  source TEXT CHECK (source IN ('plan', 'manual')),
  plan_id UUID REFERENCES cooking_plans NULL,  -- 予定から来た材料は plan_id で紐付け
  added_at TIMESTAMPTZ DEFAULT NOW(),
  stocked_at TIMESTAMPTZ,
  consumed_at TIMESTAMPTZ
);

重要なのは plan_id だ。予定から自動生成された材料は、どの予定に紐づいているかを記録しておく。これで、予定を「作った」にした瞬間、紐づく材料を 全部自動で consumed にする ことができる。


「作った」一発で消費される仕組み

/api/plans/[id] の PATCH で、status を cooked に変更すると、以下の連動が走る:

// 予定を cooked に
await supabase
  .from('cooking_plans')
  .update({ status: 'cooked', cooked_at: new Date().toISOString() })
  .eq('id', planId);

// この予定から来た材料を全部 consumed に
await supabase
  .from('pantry_items')
  .update({ status: 'consumed', consumed_at: new Date().toISOString() })
  .eq('plan_id', planId);

これでユーザーは「作った」ボタン 1 回押すだけで、予定リストから 1 件消え、関連材料が在庫から消え、次の料理に進める。

手作業で「何をどれだけ消費したか」を記録する負担をゼロにするのが目的だ。料理後の作業ほど面倒なタイミングはないので、そこを自動化できると運用ループが回る。


Optimistic UI で即時反映

台所ページの全操作は Optimistic UI にした。「作る予定に追加」を押した瞬間、UI は即座に更新され、API は裏で実行される。失敗した場合だけロールバックする。

// 楽観的に追加
const tempPlan = { id: 'temp-' + Date.now(), ...newPlan };
setPlans((prev) => [...prev, tempPlan]);

try {
  const res = await fetch('/api/plans', { method: 'POST', body: JSON.stringify(newPlan) });
  const saved = await res.json();
  // 本 ID に差し替え
  setPlans((prev) => prev.map((p) => p.id === tempPlan.id ? saved : p));
} catch (e) {
  // ロールバック
  setPlans((prev) => prev.filter((p) => p.id !== tempPlan.id));
  toast.error('予定の追加に失敗しました');
}

手動追加は temp ID で即追加 → レスポンス後に本 ID に差し替え。これでクリックからの体感待ち時間がゼロになる。料理アプリは指がベトベトの状態で操作することがあるので、1 秒のラグも致命的だ。


LP 訴求の変更

重心が動いたら、LP のコピーも動かすべきだ。

Before:

  • h1: YouTube 料理動画から、レシピを抽出
  • サブ: URL を貼り付けるだけで、AI が材料・手順・コツを構造化します。

After:

  • h1: 動画から、今週の夕食へ
  • サブ: 料理名を検索、または YouTube URL を貼るだけで AI がレシピ化。保存すれば、つくる予定・買い物リスト・家にある材料まで自動でつながります。

変更のポイント:

  • 「抽出」という動詞を消した。抽出は手段であって、ユーザーが欲しい結果ではない
  • 「今週の夕食へ」という、ユーザーが得る未来の状態を主語にした
  • サブコピーで「料理名検索」と「YouTube URL」の両入口を明記した(訴求の裾野を広げる)

h1 は 12 字でモバイル 1 行に収まる。OGP 画像・metadata・Twitter card もすべて同じコピーに統一した。


食材検索の位置付けを再定義(C0 → C1 → C2)

6 AI 統合で「食材検索が本命」という合意は取ったが、それを「独立入口」として実装するか「既存検索の上位機能」として実装するかで設計が変わる。

ChatGPT 7 枚目の意見を情報出し直して引き出した 3 段階:

  • C0: 動画カードに「家にある材料で作れる」等の食材文脈を重ね合わせる(独立ページ不要)
  • C1: 検索結果の並び替え・フィルタ(家にある材料優先、買い足し少ない順)
  • C2: 独立した食材検索 MVP(C0/C1 で不足なら発動)

C0 から始めるのが筋だ。既存の動画発見体験(料理名検索 + おすすめ動画)を壊さずに、食材起点のニーズを重ねられる。

ただし実装上の注意がある: C0/C1 は extract_jobs キャッシュ(現在 348 本)の動画にしか食材文脈を出せない。YouTube Data API からの新規検索結果には適用不能なので、「キャッシュ内は C0/C1 適用、新規検索結果は抽出後に関連性表示」の二層設計が必要だ。


北極星指標を差し替えた

汎用ファネル(訪問 → 抽出 → ログイン → プレミアム)を見るのをやめて、「初回訪問から 7 日以内に、保存レシピ → cooking_plan 追加 → shopping/stocked 更新まで進んだユーザー比率」 を北極星にした。

これが「運用ループに乗ったユーザー」の比率を直接測る指標だ。抽出したかどうか、ログインしたかどうかは、運用までつながらないなら価値が限定的。

WITH user_timeline AS (
  SELECT u.id AS user_id, u.created_at AS signup_at,
    u.created_at + INTERVAL '7 days' AS window_end
  FROM auth.users u
  WHERE u.created_at < NOW() - INTERVAL '7 days'
),
user_activity AS (
  SELECT ut.user_id,
    EXISTS(SELECT 1 FROM recipes r WHERE r.user_id = ut.user_id AND r.created_at <= ut.window_end) AS saved,
    EXISTS(SELECT 1 FROM cooking_plans cp WHERE cp.user_id = ut.user_id AND cp.added_at <= ut.window_end) AS planned,
    EXISTS(SELECT 1 FROM pantry_items pi WHERE pi.user_id = ut.user_id AND pi.status IN ('shopping', 'stocked') AND pi.added_at <= ut.window_end) AS pantry
  FROM user_timeline ut
)
SELECT COUNT(*) AS cohort,
  COUNT(*) FILTER (WHERE saved AND planned AND pantry) AS activated,
  ROUND(100.0 * COUNT(*) FILTER (WHERE saved AND planned AND pantry) / NULLIF(COUNT(*), 0), 1) AS rate_pct
FROM user_activity;

既存テーブルだけで算出可能。Amplitude や PostHog を入れる前に、Supabase SQL で書けるかを先に試すべきだった。


まとめ

  • 「抽出ツール」では、ユーザーが本当に欲しい「冷蔵庫を使い切る」に届かない
  • 動画レシピ → 予定 → 買い物 → 在庫 → 消費 の生活運用ループで、保存して終わる問題を解消
  • cooking_plans と pantry_items を plan_id で連動させ、予定「作った」で自動消費
  • LP の h1 を「動画から、今週の夕食へ」に変更し、運用を主語にする
  • 食材検索は独立入口ではなく既存検索の上位機能として C0 → C1 → C2 で段階実装
  • 北極星を汎用ファネルから運用ループ activation 率に差し替え、既存テーブルだけで算出

recipe-ai は今後、この台所ループを中心に磨き込んでいく。動画レシピを生活で回せるツールを目指す。

recipe-ai を試す(無料)

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

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