ヘッドレスCMSなしで完全スタックアプリを構築する: Next.js + Supabase + Claude

ここ数年、私はヘッドレスCMSプラットフォームでウェブサイトを構築してきた。Contentful、Sanity、Strapi──名前を挙げればほぼすべて統合してきた。しかし2025年後半のどこかで、あるパターンに気づき始めた。増加するプロジェクトの数では、CMSに手を出していなかったのだ。代わりに、Vercel上のNext.js、データと認証用のSupabase、そして必要な限りはClaude APIを使ったフルスタックアプリを出荷していた。CMSベンダーなし。コンテンツモデリングUIなし。月額シート費用なし。

これは反骨精神からではない。ヘッドレスCMSが適切な呼び出しである絶対的なプロジェクトがある──Social Animalではそれらをたくさん構築している(ヘッドレスCMS開発の実績をご覧ください)。しかし、特定のクラスのアプリケーションにおいて、このベンダーフリースタックは単に実行可能なだけでなく、より優れている。この仕組み、使用時期、ゼロからの設定方法を詳しく説明しましょう。

目次

開発者がCMSを手放す理由

CMSが提供するものについて正直に考えよう: 非技術者がコンテンツを編集するためのUI、構造化データレイヤー、おそらくメディア管理、およびそれをすべてフェッチするAPI。マーケティングチームが毎日ブログ投稿を公開しているときは、これは本当に価値がある。

しかし、2026年に繰り返し見ているのはこれだ:

  • SaaSプロダクト──「コンテンツ」は編集コピーではなくユーザー生成データ
  • 内部ツール──チームは技術的で、データベースを直接編集するか軽量な管理パネルを使う
  • AI先制アプリケーション──コンテンツが即座に生成、要約、または変換される
  • スタートアップ──CMSに月$300-500を正当化できず、ユーザーは3人

これらのプロジェクトでは、CMSはオーバーヘッドだ。使わないコンテンツモデリング機能に金を払い、基本的には派手なデータベースラッパーであるサービスのAPIキーを管理し、物事を同期させるウェブフック複雑性に対処している。

代わりに何をするか? データレイヤー全体を所有する。SupabaseはPostgres(proprietary content storeではなく実データベース)、auth、ファイルストレージ、リアルタイムサブスクリプションを提供する。Claudeはインテリジェンスレイヤーを処理する。Next.jsとVercelが他のすべてを処理する。

スタック概览

レイヤー テクノロジー 役割 2026年価格設定(開始)
フロントエンド & API Next.js 15 (App Router) UI、サーバーコンポーネント、ルートハンドラ 無料(オープンソース)
ホスティング & エッジ Vercel デプロイメント、CDN、サーバーレス機能 無料ティア / $20/月 Pro
データベース & 認証 Supabase Postgres、Row Level Security、Auth、Storage 無料ティア / $25/月 Pro
AI層 Claude API (Anthropic) コンテンツ生成、要約、分類 トークン従量課金(Sonnet 4: 100万トークンあたり$3/$15)
管理UI カスタム(React + Supabase) チームのコンテンツ管理 $0(自分で構築)

中程度のトラフィックを持つ本番アプリの総コスト: $45-100/月。CMS単体で月$99-500を実行しホスティングも支払う前という典型的なヘッドレスCMSセットアップと比較してください。

Vercel上でNext.jsプロジェクトをセットアップする

Node.js 20+とVercelアカウントがあると仮定している。Next.jsが初めての場合、チームはNext.js開発機能ページで広範に記述している。

npx create-next-app@latest my-app --typescript --tailwind --app --src-dir
cd my-app

App RouterのNext.js 15は基盤だ。デフォルトではサーバーコンポーネントを使用している。つまり、ほとんどのデータ取得はサーバーで発生する──exposed API キーなし、初期コンテンツのクライアント側ローディングスピナーなし。

このスタック用の典型的なプロジェクト構造は:

src/
├── app/
│   ├── (public)/           # マーケティングページ、ブログ
│   ├── (dashboard)/        # 認証された管理エリア
│   │   ├── layout.tsx      # Auth チェックラッパー
│   │   ├── posts/
│   │   ├── media/
│   │   └── settings/
│   ├── api/
│   │   ├── ai/             # Claude API ルート
│   │   └── webhooks/       # Supabase realtime フック
│   └── layout.tsx
├── lib/
│   ├── supabase/
│   │   ├── client.ts       # ブラウザクライアント
│   │   ├── server.ts       # サーバークライアント
│   │   └── admin.ts        # サービスロールクライアント
│   ├── claude.ts           # Anthropic SDK ラッパー
│   └── utils.ts
├── components/
└── types/

環境変数

.env.localにはこれらが必要だ:

NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-key
ANTHROPIC_API_KEY=sk-ant-...

Vercelへのデプロイメント

GitHubにプッシュ。Vercelでリポジトリを接続。環境変数を追加。完了。これについて長く述べるつもりはない──VercelのNext.jsデプロイメント用DXは業界最高で、おそらくすでに仕組みがわかっている。

注目する価値がある一つの点: フィーチャーフラグまたは設定が再デプロイなしで更新される場合は、VercelのEdge Configを使用する。小さいことだがそれでも別の単一SaaSツールが置き換わる。

バックエンドとしてのSupabase: 認証、データベース、ストレージ

ここで魔法が起きる。Supabaseは単なる「PostgresではあるFirebase」ではない──完全にバックエンドプラットフォームが、実際に所有している。データは標準PostgreSQLデータベースに生存する。去りたい場合はpg_dumpして立ち去る。proprietary CMSでそれをやってみる。

データベーススキーマ

コンテンツ駆動型アプリ(通常、CMSに手を出すような種類)を構築していると言おう。記事、メディア、基本的な分類法を処理するスキーマはここだ:

-- UUID生成を有効化
create extension if not exists "uuid-ossp";

-- コンテンツテーブル(CMSコンテンツモデルの置き換え)
create table public.posts (
  id uuid default uuid_generate_v4() primary key,
  title text not null,
  slug text unique not null,
  body text, -- Markdown コンテンツ
  excerpt text,
  status text default 'draft' check (status in ('draft', 'published', 'archived')),
  author_id uuid references auth.users(id),
  featured_image text, -- Supabase Storage パス
  metadata jsonb default '{}', -- 柔軟なフィールド、マイグレーション不要
  published_at timestamptz,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- タグ / 分類法
create table public.tags (
  id uuid default uuid_generate_v4() primary key,
  name text unique not null,
  slug text unique not null
);

create table public.post_tags (
  post_id uuid references public.posts(id) on delete cascade,
  tag_id uuid references public.tags(id) on delete cascade,
  primary key (post_id, tag_id)
);

-- Row Level Security
alter table public.posts enable row level security;

-- 誰でも公開された投稿を読める
create policy "Public can read published posts"
  on public.posts for select
  using (status = 'published');

-- 認証されたユーザーは自分の投稿を管理できる
create policy "Authors can manage own posts"
  on public.posts for all
  using (auth.uid() = author_id);

そのmetadata jsonb列が鍵だ。それはマイグレーション実行の必要性なしでCMSのカスタムフィールドと同じ柔軟性を提供する。SEO説明が必要? metadata->>'seo_description'。Open Graph画像オーバーライドが必要? metadata->>'og_image'。必要な場所ではスキーマレス、整合性が必要な場所では構造化。

認証セットアップ

Supabase Authはすべてを処理する。メール/パスワード、マジックリンク、GoogleおよびGitHubとのOAuth──すべて組み込み。

// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          )
        },
      },
    }
  )
}

ファイルストレージ

Supabase StorageはCMSが持っていたメディアライブラリを置き換える。mediaという名前のバケットを作成、ポリシーを設定、自動CDN URLを持つS3互換ファイルストアがある。

// ファイルをアップロード
const { data, error } = await supabase.storage
  .from('media')
  .upload(`posts/${slug}/${file.name}`, file, {
    cacheControl: '3600',
    upsert: false,
  })

// 公開URLを取得
const { data: { publicUrl } } = supabase.storage
  .from('media')
  .getPublicUrl(`posts/${slug}/${file.name}`)

コンテンツインテリジェンスのためのClaude API統合

これは2026年スタックが従来のウェブ開発から最も大きく異なる場所だ。Claude APIはただのチャットボットではない──CMSプラグインおよびサードパーティサービスの完全なカテゴリを置き換えることができるインテリジェンスレイヤーだ。

本番環境で使用しているもの:

  • SEOメタデータの自動生成──投稿コンテンツから
  • コンテンツ要約──エクスサートとソーシャルカード用
  • コンテンツ分類とオートタグ付け
  • スマート検索──キーワードだけでなく意図を理解する
  • ドラフトアシスタンス──コンテンツ著者用

Anthropic SDKのセットアップ

npm install @anthropic-ai/sdk
// lib/claude.ts
import Anthropic from '@anthropic-ai/sdk'

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!,
})

export async function generateSEOMetadata(content: string, title: string) {
  const message = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    messages: [
      {
        role: 'user',
        content: `この記事のタイトルとコンテンツが与えられた場合、SEOメタデータを生成してください。

タイトル: ${title}
コンテンツ: ${content.slice(0, 3000)}

JSONのみで応答:
{
  "seo_title": "主要キーワードを含む50-60文字タイトル",
  "seo_description": "120-160文字のメタディスクリプション",
  "excerpt": "ソーシャルシェア用の1-2文のフック",
  "suggested_tags": ["tag1", "tag2", "tag3"]
}`,
      },
    ],
  })

  const text = message.content[0].type === 'text' ? message.content[0].text : ''
  return JSON.parse(text)
}

export async function classifyContent(content: string, existingTags: string[]) {
  const message = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 256,
    messages: [
      {
        role: 'user',
        content: `このコンテンツを既存リストから最も関連のあるタグに分類する。何も該当しない場合は最大2つの新しいタグを提案してください。

既存タグ: ${existingTags.join(', ')}

コンテンツ: ${content.slice(0, 2000)}

JSON で応答: { "tags": ["tag1", "tag2"], "new_tags": ["maybe-new"] }`,
      },
    ],
  })

  const text = message.content[0].type === 'text' ? message.content[0].text : ''
  return JSON.parse(text)
}

AI機能用APIルート

// app/api/ai/seo/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { generateSEOMetadata } from '@/lib/claude'

export async function POST(request: NextRequest) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { content, title } = await request.json()
  
  try {
    const metadata = await generateSEOMetadata(content, title)
    return NextResponse.json(metadata)
  } catch (error) {
    return NextResponse.json(
      { error: 'AI generation failed' },
      { status: 500 }
    )
  }
}

コストはここでは無視できる。典型的なSEOメタデータ生成呼び出しは、おそらく4,000の入力トークンと200の出力トークンを使用する。Claude Sonnet 4の価格が大約$3/100万入力トークンと$15/100万出力トークンで、それは呼び出しあたり約$0.015だ。1,000記事のメタデータを$15で生成できた。

カスタム管理インターフェースの構築

これは人々を神経質にする部分だ。「CMSがないなら、非技術者はどうやってコンテンツを編集する?」

シンプルな管理UIを構築する。そして2026年では、「シンプル」は実際にシンプルだ。基本的な投稿エディタコンポーネント:

// app/(dashboard)/posts/[id]/editor.tsx
'use client'

import { useState } from 'react'
import { createBrowserClient } from '@supabase/ssr'

export function PostEditor({ post }: { post: Post }) {
  const [title, setTitle] = useState(post.title)
  const [body, setBody] = useState(post.body || '')
  const [saving, setSaving] = useState(false)
  
  const supabase = createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )

  async function save() {
    setSaving(true)
    const { error } = await supabase
      .from('posts')
      .update({
        title,
        body,
        updated_at: new Date().toISOString(),
      })
      .eq('id', post.id)

    setSaving(false)
    if (error) alert('Save failed: ' + error.message)
  }

  async function generateSEO() {
    const res = await fetch('/api/ai/seo', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, content: body }),
    })
    const metadata = await res.json()
    // 生成されたメタデータを投稿に適用
    await supabase
      .from('posts')
      .update({ metadata, excerpt: metadata.excerpt })
      .eq('id', post.id)
  }

  return (
    <div className="max-w-4xl mx-auto p-6">
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        className="text-3xl font-bold w-full mb-4 border-b pb-2"
      />
      <textarea
        value={body}
        onChange={(e) => setBody(e.target.value)}
        className="w-full h-96 font-mono text-sm p-4 border rounded"
      />
      <div className="flex gap-4 mt-4">
        <button onClick={save} disabled={saving}
          className="px-4 py-2 bg-blue-600 text-white rounded">
          {saving ? 'Saving...' : 'Save Draft'}
        </button>
        <button onClick={generateSEO}
          className="px-4 py-2 bg-purple-600 text-white rounded">
          ✨ Generate SEO Metadata
        </button>
      </div>
    </div>
  )
}

はい、これはシンプルなtextareaだ。実際のプロジェクトでは、Tiptap、MDXEditor、またはBlockNoteのようなものと交換してリッチ編集を行う。要点は: 管理インターフェースは自分のコードだ。すべてのピクセル、すべてのワークフロー、すべてのパーミッションを制御する。CMS UI制限と戦う必要はない。

より複雑なプロジェクトでは、Refine または AdminJS を管理パネルフレームワークとして検討し、直接Supabaseにプラグインする。数週間の時間が節約される。

実コスト: このスタックが実際に費やすもの

月間100Kページビュー数を実行するコンテンツ重量サイトの具体的な数字をここに示す:

サービス ティア 月額料金 提供内容
Vercel Pro $20 1TB 帯域幅、1000 GB-hr サーバーレス
Supabase Pro $25 8GB データベース、250GB 帯域幅、100K auth ユーザー
Claude API 従量課金 $10-30 ~500万トークン/月(SEO生成、要約、検索)
ドメイン 年間 ~$1 .com ドメイン
合計 $56-76/月

これを典型的なヘッドレスCMSスタックと比較する:

サービス ティア 月額料金
Contentful Team $300
Vercel Pro $20
Algolia (search) Build $50
Auth0 (auth) Essentials $35
合計 $405/月

それは5-6倍のコスト差だ。Supabaseスタックはより少ない柔軟性ではなく、より多くの柔軟性を提供する。

CMSをまだ使うべき時

私はこれについて明確な目を保ちたい。すべてのプロジェクトで CMSを削除してはいけない。ヘッドレスCMSはまだ以下の場合にはより良い選択:

  • 大きな編集チームは構造化ワークフローが必要(承認チェーン、スケジュール、基本RBAC以上の役割)
  • コンテンツがプロダクト──パブリッシャー、メディア企業、数百のコントリビューターがいるドキュメンテーションサイト
  • ビジュアル編集が必要──一部のCMSプラットフォームはライブプレビューおよびビジュアルビルダーを提供し、複製に数ヶ月かかるだろう
  • マルチチャネル配信──同じコンテンツがウェブサイト、モバイルアプリ、デジタルサイネージ、メールにフィードする場合、CMSの構造化コンテンツモデルは価値を獲得する
  • スケールでの地域化──ContentfulとSanityのようなCMSプラットフォームはしっかりしたi18nワークフローを持つ

Social Animalでは依然多数のヘッドレスCMSプロジェクトを構築している。それがプロジェクトに必要な場合は、CMSについて話し合いましょう。しかしそれが必要ない成長するカテゴリのアプリについては、それに金を払うのを止める。

本番環境デプロイメントチェックリスト

このスタックを本番環境に配布する前に、このリストを実行する:

  • Row Level Security ポリシーはすべてのテーブルに対してテスト済み(Supabaseのポリシーシミュレータはここで役立つ)
  • レート制限はClaude API ルート上(Vercel の@vercel/edgeレート制限またはupstash/ratelimitを使用)
  • 入力検証はすべてのAPI ルート上(Zodは友達)
  • エラーバウンダリーはReactツリー内(Claudeは時々タイムアウト)
  • キャッシング戦略──Next.js 内でunstable_cacheまたはレートrevalidateTagを使用
  • モニタリング──パフォーマンス用 Vercel Analytics、データベースメトリクス用 Supabase ダッシュボード、API使用法用 Anthropic コンソール
  • バックアップ戦略──Supabase Pro には日次バックアップが含まれるが、論理レプリケーションまたはpg_dumpcronも設定
  • コンテンツセキュリティポリシーヘッダーはnext.config.jsで設定済み
  • 画像最適化──Next.js <Image>コンポーネントをSupabase Storage URLで使用

FAQ

Supabaseは本当にヘッドレスCMSを置き換えることができますか?

多くの用途ではそうだ。Supabaseはスキーマから自動生成されたREST およびGraphQL APIを持つPostgreSQLデータベース、ファイルストレージ、認証、リアルタイムサブスクリプションを提供する。箱から出てそれが提供しないのは洗練されたコンテンツ編集UIだ──自分で構築するか、Refineのようなツールを使用する必要がある。チームが技術的または小規模であれば、このトレードオフは絶対に価値がある。

典型的なウェブサイトのClaude APIのコストはどのくらいですか?

SEOメタデータ生成、コンテンツ要約、基本的な分類にClaudeを使用するコンテンツサイトについて、中程度の使用法(数百のAI操作)で月$10-30の支出を期待する。2026年のClaude Sonnet 4価格は大約100万入力トークンあたり$3、100万出力トークンあたり$15に位置する。単一のSEOメタデータ生成呼び出しは大約$0.01-0.02の費用。

このスタックはエンタープライズアプリケーションに適していますか?

エンタープライズの定義によって決まる。VercelとSupabaseは両方ともSLA、SOC 2準拠、および専任サポートを持つエンタープライズティアを提供する。スタックは高トラフィックをよく処理する──Vercel上のNext.jsは自動的にスケール、Supabase Proは接続プーリングと読み取りレプリカをサポート。コンプライアンス重い業界では、データをユーザーのインフラストラクチャに保つためにSupabaseの自社ホストオプションが必要。

CMSなしのコンテンツプレビューとドラフトワークフローをどうしますか?

自分で構築する。Next.js ドラフトモードと投稿テーブルのstatus列を組み合わせると、ドラフト/公開されたワークフローが得られる。プレビュー用に、ステータスに関係なく投稿をフェッチする認証ルートを作成する。それはCMS ダッシュボードで設定プレビューURLと比較して約50行のコード。

CMSなしでリッチテキスト編集をどう処理しますか?

最新のリッチテキストエディタライブラリを使用する。Tiptap(ProseMirrorに構築)は2026年で最も人気のある選択だ──協調編集、カスタムブロック、スラッシュコマンド、マークダウンショートカットをサポート。BlockNoteはNotionのようなUI持つ別の堅い選択肢。出力をHTML、マークダウン、またはJSON としてSupabasebody列に保存。

ヘッドレスCMSからこのスタックに移行できますか?

確実に。ほとんどのヘッドレスCMSプラットフォームはエクスポートAPIを持つ。CMSからコンテンツをプルしてSupabaseテーブルに挿入するマイグレーションスクリプトを記述する。複数のクライアントに対してこのマイグレーションを実行した──ContentfulおよびSanityからSupabaseバックアップセットアップに移動。最難関部分は通常CMSの proprietary リッチテキスト形式を標準 HTML またはマークダウンにマッピング。

Supabaseが何か起きた場合どうなりますか?

Supabaseは2025-2026で堅いアップタイムを持つが、いかなるサービスも完璧ではない。データは標準PostgreSQL内に生存しているため、オプションがある: 読み取りレプリカを設定、S3で自動バックアップを保持、またはスタンバイインスタンスさえ実行。自社ホストティアでは、インフラストラクチャを完全に制御する。これは実際はCMSベンダーに依存するより弾力的だ──Contentfulが停止を持つ場合、「別のContentfulに切り替える」ことはできない。

ブログまたはマーケティングサイトにこのスタックを使用する必要がありますか?

開発者の個人ブログまたはスタートアップのマーケティングサイトの場合、このスタックは完璧だ。完全な制御、最小コスト、CMSで高価なプラグインが必要なAI搭載機能を得る。週に20以上の記事を複雑な承認ワークフローで公開する大きなマーケティングチームの場合、おそらく適切なCMSを望むだろう。それはツールをチームに一致させることについて。プロジェクトがどのアプローチに合うかについて不確かな場合、価格ページを確認するか連絡して迅速なコンサルテーションを取得。