レシピ抽出で終わらせない — recipe-ai を生活運用ツールに重心シフトした設計
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 に戻る
recipe-ai の抽出機能は 2 と 3 の間に入る。動画を保存するだけでなく、材料・手順まで構造化して保存する。これは保存の質を上げる機能だ。
でも「保存して忘れる」は本質的には解決しない。ユーザーが本当に欲しいのは「保存したレシピを料理して、冷蔵庫を使い切ること」であって、検索可能な保存データベースではない。
ここに気づいたとき、recipe-ai の重心を動かすべきだと判断した。
台所ループの設計
新設した /ja/kitchen ページは、3 つのセクションを統合している:
- 作る予定(cooking_plans): 今週作る予定のレシピ
- 買い物リスト(pantry_items の
shopping状態): まだ家にない材料 - 家にある材料(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 は今後、この台所ループを中心に磨き込んでいく。動画レシピを生活で回せるツールを目指す。
この記事で作っている YouTube 料理レシピ抽出アプリ本体。動画 URL を貼るだけで AI がレシピに変換。月 5 本まで無料。
この記事が役に立ったらシェア