マーケティングサイトの大規模リデザイン時に47ページを真夜中にちょうどライブにする必要があったことがあるなら、コンテンツステージングが「あると便利」な機能ではなく、スムーズなローンチと午後11時58分の混乱したSlackスレッドの違いであることを知っているはずです。しかし、ここが問題です。コンテンツステージングとスケジュール公開を提供するほとんどのCMSプラットフォームには、大きな、費用のかかる、ロックイン形の条件が付いています。

Social Animalでクライアントのためにコンテンツパイプラインを構築する過去2年間で、私はヘッドレスCMSプラットフォーム、オープンソースツール、カスタムステージングワークフローの組み合わせを使用してきました。学んだことは、プロフェッショナルグレードのコンテンツリリースを得るために、コンテンツ操作全体を単一のベンダーに任せる必要はないということです。オープンインフラストラクチャでより優れたものを構築できます。

この記事は、ベンダー管理のコンテンツステージング(SanityのContent Releasesなど)とSupabaseフィーチャーフラグなどのツールで独自に構築すること間の実際のトレードオフをまとめ、その後、両者の最良の部分を組み合わせる方法を示しています。

目次

2025年のコンテンツステージングが実際に意味すること

コンテンツステージングは「公開前にプレビュー」以上に進化しました。モダンなヘッドレスアーキテクチャでは、コンテンツステージングは複数のコンテンツソース全体の変更をオーケストレーションし、プレビュー環境での視覚的一貫性を保証し、コンテンツのバッチをアトミックにリリースすることを意味します。つまり、すべてが同時にライブになるか、何もライブにならないかのいずれかです。

Social AnimalのヘッドレスCMS開発プラクティスを通じて構築するサイトのための典型的なコンテンツリリースは次のようなものです:

  • 複数のドキュメント変更: 同時に公開する必要がある10~50個のコンテンツドキュメント
  • 相互参照の整合性: 新しいカテゴリを参照する新しいページ、新しい著者を参照する新しいカテゴリ
  • プレビュー環境: エディターはリリース前に、ステージングコンテンツがどのように見えるかを正確に確認する必要があります
  • スケジュール公開: コンテンツは特定の時刻、多くの場合マーケティングキャンペーンに関連付けられた時刻にライブになります
  • ロールバック機能: 何か壊れた場合、リリース全体を元に戻す必要があり、個別の部分ではなく

古いWordPressアプローチは、各投稿を「下書き」に設定して、一括公開することでした。ブログ投稿ではうまく機能します。ランディングページ、ドキュメント、価格表、機能比較全体で製品ローンチを調整している場合は、壊滅的に失敗します。

コンテンツステージングの3つのレベル

すべてのプロジェクトが同じレベルの複雑さを必要としているわけではありません:

レベル1: ドキュメントごとの下書き/公開。 すべてのCMSにこれがあります。コンテンツが独立しているエディトリアルワークフローに最適です。

レベル2: グループ化されたリリース。 複数のドキュメントが一緒にステージングされ、アトミックに公開されます。これがSanity Content ReleasesおよびSimilar機能が提供するものです。

レベル3: 環境ベースのステージング。 フィーチャーフラグがどのコンテンツバージョンがアクティブかを制御するフルプレビュー環境。これはオープンインフラストラクチャが本当に輝く場所です。

ほとんどのチームはレベル2が必要だと思いますが、実際にはレベル3が必要です。理由は以下の通りです: レベル2は分離されたコンテンツ変更を処理しますが、実際のローンチにはコード変更、デザイン変更、そしてコンテンツ変更が一緒に起こります。レベル3は3つすべてを調整できるようにします。

コンテンツリリースのベンダーロックインの問題

何かについて直接言わせてください。CMSベンダーがコンテンツステージングをプラットフォームに構築するとき、彼らはその心の善さからそうしているのではありません。それは堀です。エディターチームがベンダー固有のリリース管理に依存すると、CMSプラットフォームを切り替えることは、そのワークフロー全体をゼロから再構築することを意味します。

これはいくつかの方法で現れます:

価格設定レバレッジ。 コンテンツリリースはほぼ常にプレミアム機能です。Sanityはそれらをグロースプランの背後にゲートします。Contentfulはそれらをプレミアム層に置きます。チームがそれらに依存すると、ベンダーは価格を上げるときに、あなたがどこにも行かないことを知っています。

ワークフロー結合。 エディターはベンダー固有のUIとメンタルモデルを学びます。開発者はリリース管理のベンダー固有のAPIに対する統合を書きます。CI/CDパイプラインはベンダー固有のウェブフックを持っています。これのすべてを解き放つことは3~6か月のプロジェクトです。

カスタマイズが限定的。 ベンダー実装は決定を下します。2つの異なるCMSインスタンスにまたがるリリースが必要な場合はどうなりますか?LaunchDarklyのフィーチャーフラグロールアウトにコンテンツリリースをリンクする必要がある場合はどうなりますか?ベンダーが想像したものと一致しない承認ワークフローが必要な場合はどうなりますか?

私はベンダー管理のリリースが常に間違っていると言っているわけではありません。小規模チームにシンプルなニーズがある場合は、正しい選択になる可能性があります。しかし、何を交換しているかについて目を開けて進むべきです。

Sanity Content Releases: 得られるものと費用

Sanityはコンテンツリリース(初期反復では以前「Spaces」と呼ばれていました)を、ドキュメント変更を一緒に公開できる名前付きリリースにグループ化する方法として導入しました。何がうまくいっているかについて公平にしましょう。

Sanity Content Releasesが実際に行うこと

  • 複数のドキュメント変更を含む名前付き「リリース」を作成する
  • 公開前にコンテキスト内のすべての変更をプレビューする
  • 単一のアクションですべての変更をアトミックに公開する
  • 将来の公開のためにリリースをスケジュールする
  • リリース履歴を表示して、(場合によっては)元に戻す

開発者の経験は堅牢です。Sanityのperspectiveパラメーターを使用して、特定のリリースパースペクティブのコンテンツをクエリします:

// Sanityの特定のリリースからコンテンツをクエリする
import { createClient } from '@sanity/client'

const client = createClient({
  projectId: 'your-project',
  dataset: 'production',
  apiVersion: '2025-01-01',
  useCdn: false,
})

// 'summer-launch'リリースに表示されるドキュメントを取得する
const results = await client.fetch(
  `*[_type == "landingPage"]`,
  {},
  { perspective: 'release.summer-launch' }
)

コスト現実

2025年中盤現在、Sanityのコンテンツリリース料金にはグロースプラン以上が必要です:

  • 無料プラン: コンテンツリリースなし
  • グロースプラン: $15/ユーザー/月 -- 基本的なコンテンツリリースを含む
  • エンタープライズ: カスタム価格設定 -- 高度なスケジューリング、承認ワークフローを含む

8人のエディターチームの場合、グループ化されたリリースを取得するだけで、年間最低$1,440を見ています。これはAPIオーバーチャージの前に、ステージングワークフローが生成する追加のプレビュークエリです。

高いですか?本来的には違います。しかし、それはチームサイズでスケールするスケール済みコストであり、月が経つにつれてSanityエコシステムにより深くロックします。

Supabaseでコンテンツ用フィーチャーフラグを構築

ここが興味深くなります。オープンソースFirebase代替のSupabase -- は、ベンダーソリューションに匹敵するコンテンツステージングシステムを構築するためのプリミティブを提供します。そして、それはオープンインフラストラクチャ(自分でホストできます)であるため、ロックインはありません。

コアアイデア: Supabaseをコンテンツフィーチャーフラグシステムとして使用し、CMSとフロントエンド間に配置します。コンテンツはCMSに最終形式で存在しますが、Supabaseはそのコンテンツのどのバージョンが表示されるかを制御します。

このためにSupabaseを使用する理由

  • Row Level Security (RLS): ユーザーコンテキスト(プレビュー対本番)に基づいて、どのコンテンツバージョンが表示されるかを制御するポリシーを作成できます
  • リアルタイムサブスクリプション: エディターはステージング変更がプレビュー環境に即座に反映されるのを見ることができます
  • エッジ関数: カスタムリリースロジックをユーザーの近くにデプロイします
  • 自己ホスト可能: Supabase Cloudから移動する必要が生じた場合、スタック全体を自分で実行できます
  • PostgreSQLの下: ステージングメタデータは実データベースに存在し、所有権のあるシステムではありません

基本的なアーキテクチャ

-- Supabaseのコンテンツリリース管理
CREATE TABLE content_releases (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'staged', 'published', 'rolled_back')),
  scheduled_at TIMESTAMPTZ,
  published_at TIMESTAMPTZ,
  created_by UUID REFERENCES auth.users(id),
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE release_items (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  release_id UUID REFERENCES content_releases(id) ON DELETE CASCADE,
  cms_document_id TEXT NOT NULL,  -- Sanity/Contentful/whateverへの参照
  cms_type TEXT NOT NULL,
  content_snapshot JSONB,  -- ステージング時のコンテンツのスナップショット
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE feature_flags (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  key TEXT UNIQUE NOT NULL,
  enabled BOOLEAN DEFAULT false,
  release_id UUID REFERENCES content_releases(id),
  metadata JSONB DEFAULT '{}',
  updated_at TIMESTAMPTZ DEFAULT now()
);

これにより、CMSベンダーから完全に独立したリリース管理システムが得られます。来年Sanityをコンテンツフルに入れ替えますか? リリース管理は変わりません。

直接対比: Supabaseフィーチャーフラグ対Sanity Content Releases

これらのアプローチを正直に比較しましょう:

要因 Sanity Content Releases Supabaseフィーチャーフラグ
セットアップ時間 数分(組み込み) 数日(カスタムビルド)
月額費用(8エディター) 約$120/月(グロースプラン) 約$25/月(Supabase Pro)
ベンダーロックイン 高い(Sanity固有) 低い(PostgreSQL + オープンソース)
プレビュー体験 優れている(ネイティブStudio) 良い(カスタムプレビューが必要)
クロスCMSリリース いいえ(Sanityのみ) はい(CMS不知論的)
コード+コンテンツリリース いいえ はい(デプロイメントフラグと連携)
スケジューリング 組み込み(Growth+) カスタム(エッジ関数+cron)
ロールバック 部分的 完全(ロジックを制御)
エディターUX ポリッシュド 実装による
自己ホスト可能 いいえ はい
承認ワークフロー エンタープライズのみ カスタム(必要なものを構築)

トレードオフは明確です: Sanityはポリッシュされた、既成のエクスペリエンスを提供します。Supabaseはフレキシビリティと独立を提供しますが、エディターインターフェースを自分で構築する必要があります。

アーキテクチャ: オープンインフラストラクチャコンテンツステージングパイプライン

これは、ベンダー依存性のない深刻なコンテンツステージングが必要なクライアントのために定着したアーキテクチャです。Next.js開発プロジェクトAstroビルドでこのパターンを頻繁に使用します。

フロー

  1. コンテンツオーサリング は任意のヘッドレスCMS(Sanity、Contentful、Strapi -- 重要ではありません)で行われます
  2. CMSウェブフック はコンテンツ変更時に起動し、メタデータをSupabaseにプッシュします
  3. Supabaseはリリースグループを保存 -- どのコンテンツ変更がどのリリースに属しているか
  4. プレビュー環境 はSupabaseにクエリして、どのコンテンツバージョンを表示するかを決定します
  5. リリーストリガー (手動、スケジュール済み、またはAPI駆動)Supabaseのフィーチャーフラグを反転します
  6. ISR/オンデマンド再検証 Next.jsまたはAstroで影響を受けるページを再構築します
  7. ロールバック はフィーチャーフラグを元に戻し、別の再検証をトリガーします

キーインサイト

CMSはリリースについて知る必要がありません。コンテンツはCMS内の公開状態または下書き状態に存在するだけです。Supabaseはトラフィックコントローラーとして機能し、フロントエンドが何らかのコンテンツバージョンをレンダリングするかを決定します。

このデカップリングは非常に強力です。次のことができます:

  • Sanityの無料プランを使用して、コンテンツリリースを取得する
  • 複数のCMSインスタンス間でリリースを調整する
  • コンテンツリリースをコード展開に同じフィーチャーフラグシステムを介して結びつける
  • CMSベンダーを切り替えずにリリースパイプラインを再構築する

実装ガイド

このシステムのコアを構築しましょう。フロントエンドフレームワークとしてNext.jsを使用します。これはほとんどのクライアントが使用しているためですが、このパターンはどのフレームワークでも機能します。

ステップ1: Supabaseリリースマネージャー

// lib/releases.ts
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
)

export async function createRelease(name: string) {
  const { data, error } = await supabase
    .from('content_releases')
    .insert({ name, status: 'draft' })
    .select()
    .single()

  if (error) throw error
  return data
}

export async function addToRelease(
  releaseId: string,
  cmsDocumentId: string,
  cmsType: string,
  contentSnapshot: Record<string, unknown>
) {
  const { error } = await supabase
    .from('release_items')
    .insert({
      release_id: releaseId,
      cms_document_id: cmsDocumentId,
      cms_type: cmsType,
      content_snapshot: contentSnapshot,
    })

  if (error) throw error
}

export async function publishRelease(releaseId: string) {
  // アトミックに公開: リリースステータスを更新し、フィーチャーフラグを有効にする
  const { error: releaseError } = await supabase
    .from('content_releases')
    .update({ status: 'published', published_at: new Date().toISOString() })
    .eq('id', releaseId)

  if (releaseError) throw releaseError

  const { error: flagError } = await supabase
    .from('feature_flags')
    .update({ enabled: true, updated_at: new Date().toISOString() })
    .eq('release_id', releaseId)

  if (flagError) throw flagError

  // 影響を受けたページの再検証をトリガー
  await triggerRevalidation(releaseId)
}

ステップ2: コンテンツ解決ミドルウェア

ここが魔法が起こるところです。データ取得層はSupabaseフィーチャーフラグをチェックして、提供するコンテンツのバージョンを決定します:

// lib/content-resolver.ts
import { createClient as createSanityClient } from '@sanity/client'
import { createClient as createSupabaseClient } from '@supabase/supabase-js'

const sanity = createSanityClient({
  projectId: process.env.SANITY_PROJECT_ID!,
  dataset: 'production',
  apiVersion: '2025-01-01',
  useCdn: true,
})

const supabase = createSupabaseClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!
)

export async function resolveContent(
  documentId: string,
  isPreview: boolean = false,
  previewReleaseId?: string
) {
  // このドキュメントを含むアクティブなリリースがあるかをチェック
  let releaseContent = null

  if (isPreview && previewReleaseId) {
    // プレビューモードでは、特定のリリースからステージングコンテンツを表示
    const { data } = await supabase
      .from('release_items')
      .select('content_snapshot')
      .eq('release_id', previewReleaseId)
      .eq('cms_document_id', documentId)
      .single()

    releaseContent = data?.content_snapshot
  } else {
    // 本番では、このドキュメントをオーバーライドする公開リリースがあるかをチェック
    const { data } = await supabase
      .from('release_items')
      .select('content_snapshot, content_releases!inner(status)')
      .eq('cms_document_id', documentId)
      .eq('content_releases.status', 'published')
      .order('created_at', { ascending: false })
      .limit(1)
      .single()

    releaseContent = data?.content_snapshot
  }

  if (releaseContent) {
    return releaseContent
  }

  // 標準CMSコンテンツにフォールバック
  return sanity.fetch(`*[_id == $id][0]`, { id: documentId })
}

ステップ3: Supabaseエッジ関数によるスケジュール公開

// supabase/functions/publish-scheduled/index.ts
import { createClient } from '@supabase/supabase-js'

Deno.serve(async () => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  // 現在以前のスケジュール済みで、公開されていないリリースを検索
  const { data: dueReleases } = await supabase
    .from('content_releases')
    .select('id, name')
    .eq('status', 'staged')
    .lte('scheduled_at', new Date().toISOString())

  if (!dueReleases?.length) {
    return new Response(JSON.stringify({ published: 0 }), {
      headers: { 'Content-Type': 'application/json' },
    })
  }

  for (const release of dueReleases) {
    await supabase
      .from('content_releases')
      .update({ status: 'published', published_at: new Date().toISOString() })
      .eq('id', release.id)

    await supabase
      .from('feature_flags')
      .update({ enabled: true })
      .eq('release_id', release.id)

    console.log(`Published release: ${release.name}`)
  }

  return new Response(
    JSON.stringify({ published: dueReleases.length }),
    { headers: { 'Content-Type': 'application/json' } }
  )
})

これをSupabaseクロンジョブとして毎分実行するように設定すると、スケジュール公開ができます。

ステップ4: プレビュールート

Next.js App Routerで:

// app/api/preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const releaseId = searchParams.get('release')
  const slug = searchParams.get('slug') || '/'

  if (!releaseId) {
    return new Response('Missing release parameter', { status: 400 })
  }

  const draft = await draftMode()
  draft.enable()

  // リリースIDをクッキーに格納してコンテンツリゾルバーで使用
  const response = redirect(slug)
  response.headers.set(
    'Set-Cookie',
    `preview-release=${releaseId}; Path=/; HttpOnly; SameSite=Lax`
  )

  return response
}

これで、/api/preview?release=summer-launch&slug=/productsを訪問して、任意のリリースをプレビューできます。そのリリースがライブになったときに、サイトがどのように見えるかを正確に確認します。

どのアプローチをいつ使用するか

私はワンサイズフィットオールの回答を信じていません。誠実な推奨は次のとおりです:

以下の場合にSanity Content Releasesを使用:

  • チームが小さい(5人以下のエディター)
  • 長期的にSanityにコミットしている
  • コンテンツリリースはシンプル(クロスシステム調整不要)
  • 開発者のバンド幅がカスタムツール構築に足りない
  • 予算が主な懸念ではない

以下の場合にSupabaseフィーチャーフラグで構築:

  • コンテンツとコードのリリースを一緒に調整する必要がある
  • 複数のCMSプラットフォームを使用しているか、将来的に切り替える予定
  • チームは特定のワークフロー要件を持っている(ベンダーツールがサポートしていない)
  • リリース管理インフラストラクチャを自分でホストしたい
  • 長期コストと独立がセットアップ速度よりも重要

以下の場合にハイブリッドアプローチを使用:

  • Sanityの編集エクスペリエンスはほしいが、クロスシステムリリース調整が必要
  • CMS間で移行しており、その移行を生き残るリリース管理が必要
  • コンテンツと特定のアプリケーション動作に影響する細粒度のフィーチャーフラグが必要

ハイブリッドアプローチは、実はヘッドレスCMS従事でほとんどの場合推奨しているものです。個別のドキュメント用にCMS内のネイティブドラフト/公開を使用し、調整されたリリースとフィーチャーフラギング用にSupabaseを上に層にします。

FAQ

ヘッドレスCMSのコンテンツステージングが実際に意味することは?

コンテンツステージングは、ライブになる前にコンテンツ変更を準備、プレビュー、グループ化するプロセスです。ヘッドレスアーキテクチャでは、複数のAPI駆動型コンテンツソース間のドラフトコンテンツ管理と、公開されたコンテンツがどのように見えるかをプレビュー環境が正確に反映するようにすることを意味します。シンプルなドラフト/公開切り替え以上です -- 実際のステージングは関連する変更をアトミックに公開するリリースへのグループ化を伴います。

Supabaseは本当に、支払われたCMS機能を置き換えるのに無料に十分ですか?

Supabaseの無料プランは500MBのデータベースストレージ、50,000月額アクティブユーザー、500,000エッジ関数呼び出しを提供します。コンテンツステージングメタデータ(単にリリースグループとフィーチャーフラグです -- コンテンツそのものではない)の場合、ほとんどのチームにはそれ以上です。Pro計画(月$25)は多くの大きな操作をカバーします。CMS プレミアム機能をユーザーごとに支払うことと比較すると、3~4人のエディター以上のチームでは数学が機能します。

このアプローチをContentful、Strapi、または他のCMSプラットフォームで使用できますか?

完全にはい。それが全体的なポイントです。Supabaseはユーザーのセットの独立した層として座っているため、CMSとフロントエンド間で、コンテンツがどこから来るかは気にしません。Sanity、Contentful、Hygraph、さらにはWordPressをコンテンツソースとして実装しています。コンテンツリゾルバーミドルウェアは、特定のCMSから取得する方法を知る必要があります。

フィーチャーフラグアプローチでロールバックを処理する方法は?

実は、ロールバックはベンダー管理のリリースよりもフィーチャーフラグの方がシンプルです。フィーチャーフラグを無効に反転し、影響を受けたページの再検証をトリガーします。完了です。Supabaseに保存されたコンテンツスナップショットはロールバックポイントとして機能します。対照的に、ベンダー管理のロールバックは多くの場合、以前のドキュメントバージョンを個別に再公開する必要があります。

これは静的サイトジェネレータとISRで動作しますか?

はい、特に静的サイトジェネレータとISR(Incremental Static Regeneration)ではうまく機能します。Next.jsをISRで、またはAstroのハイブリッドレンダリングを使用すると、リリースが公開されるときにオンデマンド再検証をトリガーします。再検証APIはコンテンツリゾルバーを呼び出し、これは現在、新しく公開されたコンテンツを返し、影響を受けたページが再生成されます。Next.js開発サービスAstro開発サービスを使用するクライアント向けのアプローチを詳細に文書化しています。

リリース管理のエディターUIを構築するにはどうすればよいですか?

いくつかのオプションがあります。最も速いのは、Supabaseの自動生成されたREST APIとShadcn/UIなどのReactコンポーネントライブラリを使用して簡単な管理パネルを構築することです。使用可能な何かを構築するのに2~3日かかります。より多くのポーランドの場合、Supabaseスタジオをカスタムプラグインで拡張して、Supabaseリリース管理APIに話しかけることができます。また、Retoolまたは同様の内部ツールビルダーを使用して、数時間でリリースダッシュボードを作成するチームも見ています。

独自のコンテンツステージングシステムを構築するリスクは何ですか?

最大のリスクは保守負荷です。ベンダー管理機能はバグ修正、パフォーマンス改善、新しい機能を自動的に取得します。独自に構築する場合、それのすべてを所有します。第2のリスクは、エッジケースです -- 2つのリリースが同じドキュメントを変更するときのコンフリクト解決、または特定のコード変更に依存するリリースの処理など。これらは解決可能な問題ですが、思慮深いエンジニアリングが必要です。チームがその容量を持っていない場合、ベンダーソリューションはより安全です。カスタムステージングパイプラインの設計に関する支援が必要な場合は、お問い合わせください

20以上のエディターでのスケール時の価格比較は?

Sanity Growthの20人のエディター($15/ユーザー/月)では、年間$3,600を支払っているだけで、コンテンツリリースを含むプランを取得しています。Supabase Pro(月$25、年$300)に加えて、エッジ関数と余分なデータベース使用量に月$50、あなたは大体年間$900にいます。ギャップはエンタープライズスケールでさらに広がります。しかし、カスタムシステムを構築および保守する開発コスト(通常、初期段階で40~80時間、月2~4時間の継続的)を考慮するのを忘れないでください。特定の状況のコストの内訳については、価格設定ページを確認してください。