なぜFreemiumモデルが必要なのか?
もしあなたが3,999円のコースをネットで販売し、顧客が試し見る機会も与えられない場合、彼らが購入する確率はゼロに近いでしょう。 これが今やすべてのストリーミングプラットフォームやSaaSサービスがFreemium(Free + Premium)モデルを採用している理由です。 まず高品質な無料コンテンツの一部を提供して信頼を構築し、ユーザーが「無料でもこんなに価値があるなら、有料はもっとすごいに違いない」と感じた時、彼らは喜んで魔法のカード(クレジットカード)を取り出すのです。
Vibe Tutorでは、この「無料試読」と「有料ブロック」を自動判別するコンテンツファイアウォールをどのように実装しているのでしょうか?
ファイアウォールの第一防衛線:ルート分類
まず、システムに「ユーザーが現在どのタイプのコースを見ているのか」を認識させる必要があります。
src/app/courses/[...slug]/page.tsxでは、URLのslug配列を解析して判別します。
const courseId = slug[0]; // 例: "vibe-tutor-web" または "line-bot-basics"
const chapterSlug = slug.length > 1 ? slug[1] : null; // 例: "01-intro"
次に、コード内でコースの分類マトリックスを定義します:
// 常に無料体験可能なコース
const freeCourses = ["line-bot-basics", "gas-sheet-automation", "react-tailwind-portfolio", "github-vercel-deploy", "postgres-sql-basics"];
// 入門基礎コース (最初の2章を無料開放)
const beginnerCourses = ["coding-101", "github-basics", "langchain-rag-basics", "nextjs-app-router", "api-postman-basics", "python-data-analysis"];
これらの分類を使って、柔軟な判断ロジックを記述できます:
let isFreePreview = false;
if (freeCourses.includes(courseId)) {
// 1. 完全無料プロジェクト、直接許可
isFreePreview = true;
} else if (chapterSlug) {
if (beginnerCourses.includes(courseId)) {
// 2. 入門コース、ファイル名が01-または02-で始まる場合許可
if (chapterSlug.startsWith("01-") || chapterSlug.startsWith("02-")) {
isFreePreview = true;
}
} else {
// 3. 商業ハイエンドプロジェクト、非常に貴重なため第01章のみ無料試読
if (chapterSlug.startsWith("01-")) {
isFreePreview = true;
}
}
}
このisFreePreview変数により、「どの章が無料で閲覧可能か」という問題は解決されます。しかし、章が無料でない場合、ユーザーの「データベース権限」をさらに確認する必要があります。
ファイアウォールの第二防衛線:データベース権限検証
isFreePreviewがfalseの場合、それは有料記事であることを意味します。
この時、システムはすぐにアクセスを拒否してはいけません。ユーザーがすでに購入済みかもしれないからです。Supabaseデータベースを確認する必要があります。
let hasAccess = isFreePreview;
if (!hasAccess) {
// 1. ログインユーザー情報を取得
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (user) {
// 2. 当該ユーザーの全購入記録を照会
const { data: purchases } = await supabase
.from("vt_purchases")
.select("item_id")
.eq("user_id", user.id);
if (purchases) {
const itemIds = purchases.map(p => p.item_id);
// 3. 最終権限判断
if (
itemIds.includes("all-courses") || // 旧版全開放VIPタグ
itemIds.includes("vip-all-access") || // 究極の9,999VIPパス
itemIds.includes(courseId) || // 単一コーステキストライセンス
itemIds.includes(`${courseId}-source`) // 単一コースソースコード含むライセンス
) {
hasAccess = true; // 権限承認、許可!
}
}
}
}
注目すべきは、このコードがServer Componentで実行される点です。これは次のことを意味します:
- 絶対的な安全性: ハッカーがブラウザ側のJavaScript変数(例えば
hasAccessをtrueに変更)を改ざんしても記事を盗み見ることはできません。サーバーが権限なしと判断した場合、HTML自体が準備されないからです。 - 遅延なし: データベース照会はサーバー側で瞬時に完了し、結果が直接返されるため、ユーザーは煩わしい「読み込み中(Loading spinner)」を見ることはありません。
視覚的・心理学的トリック:ぼかし効果(Blur Effect)
hasAccessがfalseの場合、最も単純な方法は「支払いが必要です」と書かれた白い画面を表示することです。しかし、これは退屈で販売にもつながりません。
私たちはより誘惑的な方法を採用しました:見えるけど触れられない状態を作るのです。
記事はレンダリングされますが、外側のコンテナにTailwindのぼかし効果と高さ制限を加えます:
<article className={`prose max-w-none ${!hasAccess ? 'max-h-[250px] overflow-hidden blur-[6px] select-none pointer-events-none opacity-40' : ''}`}>
<div dangerouslySetInnerHTML={{ __html: contentHtml }} />
</article>
これにより、画面上には記事の最初の数段落が影のように表示され、徐々にぼやけていきます。 そして、記事の上部に大きく美しい「完全なチュートリアルコンテンツをアンロック」というプロンプトボックスを重ねます:
{!hasAccess && (
<div className="absolute top-0 left-0 w-full h-[600px] flex flex-col items-center pt-[100px] px-6">
<div className="glass p-8 rounded-3xl border border-primary/20 ...">
<Lock className="w-8 h-8 text-primary" />
<h3 className="text-2xl font-bold">完全なチュートリアルコンテンツをアンロック</h3>
<p>この章は有料コンテンツです。プロジェクトに参加すると5000字以上の深い分析がアンロックされます...</p>
<Link href="/pricing" className="...">有料プランを確認</Link>
</div>
</div>
)}
この「欲擒故縦」のデザインは、心理学の**損失回避(Loss Aversion)と好奇心ギャップ(Curiosity Gap)**を利用しています。ユーザーはすでにアウトラインを見ているのに、最も興味深い部分でぼやけてしまうと、「有料プランを確認」をクリックする確率が大幅に上昇するのです!
堅牢なコンテンツファイアウォールの構築方法を学んだら、次はユーザーアカウント作成の方法を見ていきましょう。次の章では、Supabase認証の神秘のベールを剥がしていきます。