メインコンテンツへスキップ
← 記事一覧に戻る
·開発·27 min read

Claude Code で 0→MVP を1日で作る全記録 — recipe-ai Build in Public

recipe-aiClaude Code個人開発Build in PublicMVP

YouTube の料理動画を見ながら「この材料、何グラムだっけ」と巻き戻す。あの無駄な時間を AI で消したい。そう思い立ってから、MVP が本番に乗るまで約1日。Claude Code に CLAUDE.md を食わせ、scaffold から Stripe 課金まで一気に走り切った全記録を公開する。

recipe-ai は個人開発のプロダクトであると同時に、自分が作った LaunchKit(SaaS スターター)の実証でもある。「本当にこのスターターで速く作れるのか」を自分自身で証明する Dogfooding プロジェクトだ。結果として、着工から2日目にはデプロイ完了、月額コスト ¥0 で放置モードに入った。

この記事でわかること:

  • CLAUDE.md / AGENTS.md を事前に設計しておくと、AI への指示出しがどれだけ変わるか
  • YouTube 料理動画からレシピを自動抽出する仕組みの全体像
  • Supabase Auth + RLS を使った認証・データ分離の実装手順
  • Cooking Mode(Wake Lock API)や材料スケーリングなどの UX 機能
  • Stripe Payment Links でチップ型課金を組み込む方法
  • 無料インフラでタイムアウト問題が発生し、Cloudflare Workers に移行するまでの経緯
  • 「判断の速さ」が開発スピードの本質である理由

何を作ったのか: recipe-ai

recipe-ai は、YouTube 料理動画の URL を貼るだけで、AI がレシピ(材料・手順・コツ)を自動抽出してくれる Web アプリだ。

技術スタック:

レイヤー技術コスト
フロントエンドNext.js 16 + Tailwind v4 + shadcn/ui¥0
認証 + DBSupabase(Auth, PostgreSQL)¥0(Free プラン)
AI 抽出Gemini 2.5 Flash(マルチモーダル)¥0(Free Tier)
API サーバーCloudflare Workers¥0(Free プラン)
ホスティングVercel¥0(Hobby プラン)
課金Stripe Payment Links(チップ型)3.6% / 件のみ

月額固定費: ¥0

完成した機能一覧:

  • YouTube URL からレシピ自動抽出(映像+音声解析、字幕不要)
  • 4つの Gemini モデルから選択可能(精度 vs 速度)
  • Structured Output / Freeform の切り替え
  • レシピ保存(Supabase + RLS)
  • Cooking Mode(画面消灯防止 + ステップ送り)
  • 材料スケーリング(0.5x / 1x / 2x / 3x)
  • マークダウンコピー
  • 多言語対応基盤(next-intl、現在は日本語のみ)
  • Stripe 3段階チップ(¥100 / ¥500 / ¥1,000)
  • Tally フィードバックフォーム
  • 特定商取引法に基づく表記ページ

なぜ1日で作れたのか(概要)

3つの要因がある。

1. CLAUDE.md と AGENTS.md の事前設計

Claude Code に「何を作るか」「どう作るか」「何をやってはいけないか」を最初に全部書いておく。これをやるかやらないかで、AI とのやり取り回数が劇的に変わる。曖昧な指示の往復がなくなる。

2. LaunchKit スターターの流用

自分が作った SaaS スターター(KOBO)の構成をそのまま使った。Supabase Auth のクライアント設計、RLS のパターン、Tailwind + shadcn/ui のセットアップ、i18n の設定——全て既に動いているコードがある。ゼロから書く部分が大幅に減った。

3. コスト最優先という判断基準

「月額固定費 ¥0 で運用できるか」。この一本の軸で全ての技術選定を判断した。Vercel Hobby か Pro か迷わない。Supabase Free か有料か迷わない。AI モデルは無料枠があるかで選ぶ。判断に迷う時間がゼロになると、手が止まらない。

有料パートでは、Hour 0 から Hour 9 まで、実際の作業内容をコード付きで時系列に公開する。


recipe-ai を試す(無料)

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

Hour 0: CLAUDE.md と AGENTS.md の設計(最重要)

MVP を1日で作れた最大の理由はコーディングの速さではない。事前設計の密度だ。

Claude Code を起動する前に、2つのファイルを手で書いた。

CLAUDE.md: プロジェクト定義

CLAUDE.md はプロジェクトのルートに置くファイルで、Claude Code が最初に読み込む。ここに「何を作るか」と「何をやってはいけないか」を明確に書く。

recipe-ai の CLAUDE.md はこうなっている:

# recipe-ai (LaunchKit 実証兼用)

YouTube 料理動画の URL からレシピ(材料・手順・コツ)を
AI 自動抽出する MVP。
LaunchKit(KOBO)スターターを使った実証案件、
Build in Public 素材も兼ねる。

## 技術スタック
- Next.js 16 (App Router) + TypeScript
- Supabase (Auth, DB)
- Tailwind v4 + shadcn/ui

## 重要ルール
- Phase 1 スコープ: 字幕付き動画限定、認証あり、保存あり、課金なし
- 角丸禁止・Editorial 系デザイン
- 60 分超の動画はエラー
- Supabase クライアント生成は lib/supabase/ の既存ユーティリティを使う

ポイントは「Phase 1 スコープ」の定義だ。Claude Code は指示がなければ機能を足し続ける。「字幕付き動画限定」「課金なし」と明記しておくことで、Whisper 音声処理や Stripe 連携を勝手に実装しようとするのを防ぐ。

CLAUDE.md を書くのに使った時間は約30分。この30分が、後続の全作業で「それはスコープ外」「その技術は使わない」と Claude Code に何十回も説明する時間を消した。事前設計に30分かけることで、実装中の手戻りが激減する。

AGENTS.md: 技術的な罠を潰す

AGENTS.md はより技術的な詳細を書くファイルだ。特に Next.js 16 の破壊的変更について、事前に Claude Code に伝えておく必要がある。

## Next.js 16 注意点
- middleware.ts は廃止 → proxy.ts に名前変更必須
- エクスポート関数も proxy または default

## Supabase クライアント(重要)
新規にクライアントを作成しないこと。以下の既存ユーティリティを使う:
| ファイル | 用途 |
|---------|------|
| lib/supabase/client.ts | ブラウザ用 createClient() |
| lib/supabase/server.ts | サーバー用 createClient() |
| lib/supabase/middleware.ts | proxy.ts 用(updateSession) |

## デザイン規約
- 角丸禁止
- stone カラーパレット中心
- アクションボタンは黒地白文字(bg-stone-900)

Next.js 16 では middleware.tsproxy.ts にリネームされた。これを AGENTS.md に書いておかないと、Claude Code は古い知識で middleware.ts を生成し、動かないコードでハマる。

同様に、Supabase クライアントの「新規作成禁止」も重要だ。Claude Code は指示がなければ独自の createServerClient 呼び出しを書く。既存ユーティリティのパスを明示しておくことで、コードの一貫性が保たれる。


Hour 1-2: scaffold + 基本 UI

LaunchKit スターターからの流用

ゼロから npx create-next-app するのではなく、自作の LaunchKit(KOBO)の構成を流用した。具体的には:

  • lib/supabase/ ディレクトリ(client.ts, server.ts, middleware.ts)をそのままコピー
  • proxy.ts の認証保護パターンを流用
  • Tailwind v4 + shadcn/ui の設定をコピー
  • next-intl の i18n 設定を流用

この時点で「認証付きの Next.js 16 アプリの骨格」が既に動いている。あとはドメイン固有のロジックを足すだけだ。

proxy.ts の設計

Next.js 16 の proxy(旧 middleware)で、i18n ルーティングと認証保護を同時に処理する:

// proxy.ts
import createMiddleware from "next-intl/middleware";
import { updateSession } from "@/lib/supabase/middleware";
import { type NextRequest, NextResponse } from "next/server";
import { routing } from "./i18n/routing";

const intlMiddleware = createMiddleware(routing);
const protectedRoutes = ["/recipes"];

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const pathnameWithoutLocale = pathname.replace(/^\/ja/, "");

  const isProtected = protectedRoutes.some((route) =>
    pathnameWithoutLocale.startsWith(route)
  );

  if (isProtected) {
    const { supabaseResponse, user } = await updateSession(request);
    if (!user) {
      return NextResponse.redirect(new URL(`/ja/login`, request.url));
    }
    return supabaseResponse;
  }

  return intlMiddleware(request);
}

/recipes(保存済みレシピ一覧)へのアクセスだけ認証を要求し、トップページは誰でもアクセスできる。未認証ユーザーもレシピ抽出は試せる設計にした。

Tailwind + shadcn/ui

shadcn/ui のコンポーネントは必要になったものだけ npx shadcn add で追加する方針。最初に入れたのは Button と Input だけ。

デザイン規約として「角丸禁止」「stone カラーパレット」「アクションボタンは黒地白文字」を AGENTS.md で指定しているので、Claude Code が生成するコンポーネントは全てこのルールに従う。手動で CSS を直す必要がない。


Hour 3-4: コア機能 -- Gemini API 連携

ここが recipe-ai の心臓部だ。

YouTube URL パース

まず、様々な形式の YouTube URL から videoId を抽出するユーティリティ:

// lib/youtube.ts
export function extractVideoId(url: string): string | null {
  try {
    const u = new URL(url);
    if (u.hostname === "youtu.be") {
      const id = u.pathname.slice(1);
      return /^[\w-]{11}$/.test(id) ? id : null;
    }
    if (
      u.hostname.endsWith("youtube.com") ||
      u.hostname.endsWith("youtube-nocookie.com")
    ) {
      const v = u.searchParams.get("v");
      if (v && /^[\w-]{11}$/.test(v)) return v;
      const m = u.pathname.match(/^\/(shorts|embed)\/([\w-]{11})/);
      if (m) return m[2];
    }
    return null;
  } catch {
    return null;
  }
}

youtube.com/watch?v=youtu.be//shorts//embed/youtube-nocookie.com の全形式に対応。正規表現で videoId の11文字フォーマットを検証する。

Gemini 2.5 Flash でレシピ抽出

当初は Claude Haiku 4.5 を使う予定だった。しかし開発途中で Gemini 2.5 Flash に切り替えた。理由は「YouTube URL を直接渡せる」マルチモーダル API があったからだ。字幕テキストを事前に取得する必要がなくなり、コードが大幅にシンプルになった。

// lib/recipe.ts(核心部分)
const SYSTEM_PROMPT = `あなたは日本語の料理動画から
レシピを構造化抽出する専門家です。
入力された YouTube 動画(映像+音声)から、
レシピ情報を JSON で抽出してください。

各レシピの必須項目:
- title: 料理名(簡潔に、最大40文字)
- ingredients: 材料の配列
  [{ name: "材料名", amount: "分量(任意)" }]
- steps: 手順の配列
  (番号は付けない、自然な日本語で)

ルール:
- 料理動画でない場合は is_recipe: false, recipes: []
- 1動画に複数レシピが登場する場合は全て抽出する
- 推測で材料や手順を追加しない`;

export async function extractRecipes(
  youtubeUrl: string,
  opts: ExtractOptions = {}
): Promise<ExtractResult> {
  const ai = getClient();
  const model: ModelId = opts.model ?? "gemini-2.5-flash";

  const response = await ai.models.generateContent({
    model,
    contents: [
      {
        role: "user",
        parts: [
          {
            fileData: {
              fileUri: youtubeUrl,
              mimeType: "video/*",
            },
          },
          { text: promptText },
        ],
      },
    ],
    config: {
      systemInstruction: SYSTEM_PROMPT,
      temperature: 0.2,
    },
  });

  // JSON パース + バリデーション
  // ...
}

fileData に YouTube URL をそのまま渡す。Gemini が動画を視聴し、映像と音声の両方からレシピを抽出する。字幕がない動画でも動く。

Recipe 型の定義

TypeScript の型で、LLM の出力を構造化する:

// types/recipe.ts
export type Ingredient = {
  name: string;
  amount?: string;
};

export type Recipe = {
  title: string;
  servings?: string;
  time?: string;
  difficulty?: "easy" | "medium" | "hard";
  ingredients: Ingredient[];
  steps: string[];
  tips?: string[];
  sourceUrl: string;
  sourceVideoTitle?: string;
};

export type ExtractErrorCode =
  | "INVALID_URL"
  | "VIDEO_NOT_FOUND"
  | "NO_TRANSCRIPT"
  | "TOO_LONG"
  | "NOT_RECIPE"
  | "API_ERROR";

ExtractErrorCode をユニオン型で定義しておくと、フロントエンド側でエラーメッセージの出し分けが楽になる。Claude Code にこの型を認識させておけば、API Route のエラーハンドリングも型安全に生成してくれる。

AI モデルの選定は「使ってみないとわからない」が本音だ。Claude Haiku で始めて、途中で Gemini Flash に乗り換えた。切り替えのコストは Recipe 型を共通にしておいたおかげで最小限だった。型を先に決めておくと、モデル差し替えが怖くなくなる。


Hour 5-6: 認証 + DB

Supabase Auth

LaunchKit から流用した Supabase クライアントをそのまま使う。recipe-ai 固有のコードはほぼない。Magic Link 認証で、ユーザーはメールアドレスを入れるだけ。

重要なのは、recipe-ai は KOBO(LaunchKit)と 同じ Supabase プロジェクト を共用している点だ。プロジェクトを分けると Free プラン枠を2つ消費する。コスト最優先の方針で、1プロジェクトを複数アプリで共用する設計を選んだ。

recipes テーブル + RLS

テーブル定義はシンプル。レシピデータは jsonb カラムに丸ごと格納する:

-- supabase/migrations/001_recipes.sql
create table if not exists recipes (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users(id)
    on delete cascade,
  source_url text not null,
  source_video_title text,
  data jsonb not null,
  created_at timestamptz not null default now()
);

create index if not exists recipes_user_id_created_at_idx
  on recipes (user_id, created_at desc);

alter table recipes enable row level security;

-- 自分のレシピだけ CRUD できる
create policy "users select own recipes" on recipes
  for select using (auth.uid() = user_id);

create policy "users insert own recipes" on recipes
  for insert with check (auth.uid() = user_id);

create policy "users update own recipes" on recipes
  for update using (auth.uid() = user_id);

create policy "users delete own recipes" on recipes
  for delete using (auth.uid() = user_id);

RLS(Row Level Security)で auth.uid() = user_id を全操作に適用。KOBO の他のテーブルとは完全に分離される。Supabase の RLS は「書き忘れたら全公開」なので、migration ファイルに必ず含めておく。

保存機能の UX 設計

未ログインユーザーでもレシピ抽出は使える。保存ボタンを押したときに初めてログインを要求する設計にした。

// HomeClient.tsx(保存処理の抜粋)
async function onSave(index: number) {
  const recipe = recipes[index];
  if (!isLoggedIn) {
    // レシピを sessionStorage に退避してからログインへ
    sessionStorage.setItem(
      STORAGE_KEY,
      JSON.stringify(recipes)
    );
    setSaveStates((prev) =>
      prev.map((s, i) =>
        i === index ? "loginRequired" : s
      )
    );
    return;
  }
  // ログイン済み → Supabase に保存
  const supabase = createClient();
  const { data: { user } } = await supabase.auth.getUser();
  await supabase.from("recipes").insert({
    user_id: user.id,
    source_url: recipe.sourceUrl,
    source_video_title: recipe.sourceVideoTitle ?? null,
    data: recipe,
  });
}

sessionStorage にレシピを退避しておき、ログイン完了後に復元する。ログインのためにページ遷移しても、抽出結果が消えない。この「未ログインでも使える → 保存時にログイン」のパターンは、フリーミアムアプリの定番だ。


Hour 7-8: UX 仕上げ

Cooking Mode(Wake Lock API)

料理中にスマホの画面が消えるのは致命的だ。Wake Lock API を使って画面を点灯し続ける Cooking Mode を実装した。

Cooking Mode では:

  • 画面消灯を防止(navigator.wakeLock.request("screen")
  • 手順をステップごとに大きく表示
  • スワイプまたはボタンで次のステップに進む
  • 材料リストに戻れる

Wake Lock API はブラウザの可視状態が変わると自動的に解除される。タブを切り替えたときに再取得するイベントリスナーも必要になる。

材料スケーリング

「2人分」のレシピを4人分にしたい。材料の分量を 0.5x / 1x / 2x / 3x で切り替えられるスケーリング機能を実装した。

// lib/scale.ts
export const SCALE_OPTIONS = [0.5, 1, 2, 3] as const;
export type Scale = (typeof SCALE_OPTIONS)[number];

export function scaleIngredients(
  ingredients: Ingredient[],
  scale: Scale
): Ingredient[] {
  if (scale === 1) return ingredients;
  return ingredients.map((ing) => ({
    name: ing.name,
    amount: ing.amount
      ? scaleAmount(ing.amount, scale)
      : undefined,
  }));
}

分量の文字列(「大さじ2」「200g」「1/2 個」)から数値を抽出し、倍率をかけて戻す。日本語の料理用語(大さじ、小さじ、カップ、合)に対応する必要があるため、正規表現のパターンマッチが地味に面倒だった。

エラーハンドリング

ExtractErrorCode の各コードに対して、ユーザーにわかりやすいメッセージを出す:

エラーコードユーザーメッセージ
INVALID_URL正しい YouTube URL を入力してください
VIDEO_NOT_FOUND動画が見つかりません(非公開・削除済みの可能性)
NO_TRANSCRIPTこの動画から字幕を取得できませんでした
TOO_LONG60分を超える動画は処理できません
NOT_RECIPE料理動画ではないようです
API_ERRORサーバーエラーが発生しました

エラーメッセージは messages/ja.json で管理し、next-intl で表示する。将来の英語対応に備えた設計だ。


Hour 9: デプロイ + 課金

Vercel デプロイ

Vercel にリポジトリを接続して npm run build が通ることを確認。環境変数を Vercel ダッシュボードに設定:

  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_ANON_KEY
  • GEMINI_API_KEY

Supabase 側で Site URL と Redirect URL を本番ドメインに変更。これで認証フローが本番環境でも動く。

Stripe Payment Links(3段階チップ)

recipe-ai はサブスクリプション課金ではなく、チップモデルを採用した。理由:

  • MVP の段階でユーザーに月額課金を求めるのは早すぎる
  • チップなら「気に入ったら払ってね」で心理的ハードルが低い
  • Stripe Payment Links なら、コード実装ゼロで決済ページが作れる

Stripe Dashboard で3つの Payment Link を作成:

チップ金額用途
コーヒー1杯¥100気軽な応援
ランチ代¥500標準的な支援
開発応援¥1,000手厚い応援

Payment Link の URL をフッターに配置するだけ。API 連携もコードも不要。これが「コスト最優先」の設計思想の典型例だ。完璧な課金システムを作ろうとすると、Webhook、購入記録、アクセス制御……と膨大な実装が必要になる。チップモデルなら、リンクを貼るだけで終わる。

特定商取引法に基づく表記

日本で決済を扱う以上、特商法ページは必須。静的ページとして /tokushoho に配置した。

Tally フィードバックフォーム

ユーザーからのフィードバックを受け取るために Tally(無料のフォームサービス)を埋め込んだ。Slack 連携で通知が飛ぶように設定。Google Forms でもいいが、Tally の方がデザインが馴染む。

Stripe Payment Links は「最小限の課金実装」として非常に優秀。コードゼロで決済ページが完成する。MVP 段階では、Webhook や購入記録の実装に時間を使うべきではない。まず「お金を受け取れる状態」にすることが最優先。精密な課金管理は、売上が立ってから考えればいい。


翌日: インフラ移行

タイムアウト問題発生

デプロイ翌日に問題が起きた。

Vercel Hobby プランは API Route のタイムアウトが 60秒。Gemini 2.5 Flash が長い料理動画(20分超)を処理すると、レスポンスに 90秒以上かかることがある。60秒で強制切断される。

3段階移行: Vercel → Edge Function → Cloudflare Workers

第1段階: Vercel Hobby(60秒制限)

  • 短い動画は動くが、15分超の動画で頻繁にタイムアウト
  • Pro プラン(月 $20)にすればタイムアウトが300秒に伸びるが、コスト最優先の方針に反する

第2段階: Supabase Edge Function(150秒制限)

  • 抽出 API だけを Supabase Edge Function に移行
  • ほとんどの動画は150秒以内に処理完了……と思ったら、34分の動画が 151.9秒 で 546 エラー。1.9秒の超過で全滅

第3段階: Cloudflare Workers(タイムアウトなし)

  • Workers の Free プランはタイムアウト制限がない(Cloudflare 公式 Docs によれば CPU 時間 10ms 制限はあるが、外部 API 待ちは含まれない)
  • 抽出 API を Workers に移行して解決

最終的なアーキテクチャ:

  • フロント + 静的ページ: Vercel(Next.js 16)
  • 抽出 API: Cloudflare Workers
  • 認証 + DB: Supabase

フロントから Workers の API を呼ぶだけなので、CORS 設定を追加するだけで移行完了。この移行の詳細は別記事で書いた。

放置モード突入

Cloudflare Workers 移行後、安定稼働を確認して「放置モード」に入った。月額コスト ¥0、ユーザーが来れば Gemini API の Free Tier で処理される。売上が立てば Stripe チップで入金。立たなくても損失ゼロ。

これが「不死戦略」の実践だ。固定費がゼロなら、プロダクトが死ぬことはない。


振り返り: 速さの秘訣は「判断の速さ」

0 から MVP まで約1日。翌日のインフラ移行を含めても2日。速かった理由を整理する。

1. コスト最優先という軸があると迷わない

技術選定で迷う時間がゼロだった。「無料か?」「無料枠で足りるか?」——この質問に Yes/No で答えるだけ。Vercel Pro か Hobby か。Supabase Free か Pro か。全て即決。

迷っている時間が最も高コストなリソースだ。判断基準を1つに絞ることで、迷いが消える。

2. CLAUDE.md 事前設計で AI に任せる範囲が明確になる

Claude Code は万能ではない。だが、「何を作るか」「何を使うか」「何をやらないか」を事前に定義しておけば、その範囲内では驚くほど的確にコードを生成する。

AGENTS.md で Next.js 16 の破壊的変更を伝えておく。デザイン規約を明示しておく。既存ユーティリティのパスを指定しておく。これだけで、生成コードのレビュー負荷が激減する。

3. 完璧を求めない(MVP は Minimum)

MVP は Minimum Viable Product。最小限の実行可能なプロダクト。最小限とは:

  • 認証がなくてもレシピ抽出は使える
  • 課金はチップ型で、コード実装ゼロ
  • 対応言語は日本語のみ
  • エラーハンドリングは最低限
  • デザインはシステムフォント + stone カラー

「あとで足せるものは全部あとで」。この割り切りが、1日でデプロイまで到達できた最大の理由だ。

個人開発の MVP で重要なのは、完璧なプロダクトを作ることではない。「世の中に出す」ことだ。出してみないと、誰が使うのか、何が足りないのか、そもそも需要があるのかがわからない。

recipe-ai は今、放置モードで稼働している。月額コスト ¥0。フィードバックが来たら改善する。来なくても損失ゼロ。この状態を2日で作れたのは、Claude Code のおかげでもあるが、それ以上に「判断の速さ」のおかげだ。

Next Step

次に読むならこの導線です

すべての記事を見る
有料で次へ進む¥1,000

【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード

Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude 自動化」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。

次の実験記録も追う

Claude Code × 個人開発の実験ログ、失敗、判断変更をまとめて追いたい人向けに、月次でLab Freeを届けます。

masatoman のメルマガ — 毎週月曜の朝に 1 通

masatoman.net で今週公開した記事の中から 1 本を、読者目線で深掘りした手紙が届きます。「自分も同じことやってる」「ここで詰まってた」が見つかる予告編。

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