mcpField
C
catalog-kit

catalog-kit

github.com/officexappマーケティング・グロース080 回インストール26 週間前に更新
リポジトリ

catalog-kit について

Catalog Kit は、AI エージェントを通じてマーケティングカタログ、ランディングページ、ファネルを作成・運用するツールキットです。スキーマ編集、公開、テスト、分析、ローカル開発に対応します。

主な機能

  1. 01JSON スキーマからカタログを作成
  2. 02サブドメインや独自ドメインで公開
  3. 03加重 A/B テストを実施
  4. 04コンバージョンと訪問者の行動を追跡
  5. 05CLI でローカルプレビューと検証
  6. 06フォーム、決済、カスタムコンポーネントに対応

活用シーン

  1. 01リード獲得フォームや商品カタログの作成
  2. 02ファネルのバリエーションをトラフィック分割で比較
  3. 03訪問者の離脱とコンバージョンの確認
  4. 04カタログのローカル開発と検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/officexapp/catalog-kit --skill catalog-kit

インストール後、Claude Code で/catalog-kitを入力して読み込まれたか確認します。

同梱ファイルREADME.md

詳細

Catalog Kit

AI エージェントを通じて、マーケティングカタログ、ランディングページ、複数ステップのファネルを直接作成・管理できます。60 種類以上のコンポーネントタイプを使ってカタログを作成し、すぐに公開し、重み付けされたバリエーションで A/B テストを実施し、コンバージョン分析をリアルタイムで確認できます。

OfficeX にインストール: officex.app/store/en/app/catalog-kit

できること

  • カタログを作成 — JSON スキーマからリード獲得フォーム、商品カタログ、複数ステップのファネルを構築します
  • すぐに公開 — カタログはサブドメイン(SUBDOMAIN.catalogkit.cc)またはカスタムドメインで公開できます
  • 分析を確認 — 訪問者数、コンバージョン、ページの離脱、フィールドの入力完了、参照元、収益を確認できます
  • A/B テストを実施 — 重み付けされたバリアントでトラフィックを分割し、最も高い成果を上げるものを見つけます
  • AI によるバリアント振り分けと事前入力 — 自然言語のヒントを使って、訪問者を最適なカタログバリアントに自動で振り分け、条件に合ったフォームフィールドに事前入力します
  • サンドボックス編集 — カタログを複製して公開中のバージョンに影響を与えずに安全に変更し、準備ができたら昇格させます
  • 要素インスペクター — Shift+Alt を押しながら要素(ページコンポーネント、上部ナビゲーションバー、カートボタン、カートドロワー、カート項目、画面下部の固定バー、チェックアウトページ、ポップアップ)にカーソルを合わせて調査し、AI エージェント用に正確な pageId/componentId 参照をコピーします
  • 訪問者のジャーニーを表示 — 各訪問者が行った操作を手順ごとに正確に追跡します
  • アクセスを管理 — チームメンバーやインテグレーション用の API キーを作成します
  • メディアホスティングをお任せ — 画像や動画は保存、圧縮され、CDN 経由で配信されます。独自の S3 バケットを用意する必要はありません
  • 画像をアップロード(無料) — WebP への自動圧縮、サムネイル生成、CDN 配信をクレジット消費なしで利用できます
  • 動画をアップロード — アダプティブストリーミング用に HLS へ自動トランスコードし、CDN 経由で配信します
  • ファイルをアップロード・ダウンロード — ダウンロード可能なファイル(PDF、ZIP、ドキュメント)を S3 でホストし、CDN 経由で配信します。クレジットは 50MB ごとに消費されます
  • カートとチェックアウト — ページのオファーを収集し、Stripe Checkout に送信する組み込みカート UI(フローティングボタン+スライド式ドロワー)を利用できます。カスタムカート HTML は不要です
  • エージェント API — AI エージェントは、ステートフルなセッション API を介してカタログフォームにヘッドレスで入力できます。サーバー側の検証と段階的な情報開示に対応しています
  • 設定としての TypeScript — カタログを完全な型安全性を備えた .ts ファイルとして作成し、CLI 経由でプッシュできます
  • ローカルプレビュー — catalogs catalog dev my-catalog.ts でローカルプレビューを起動できます。ホットリロード、表示条件、デバッグパネル、検証オーバーレイに対応しています
  • 開発ツールバー — カタログのスラッグ、スキーマのバージョンバッジ(v1.0)、ページグラフ、要素インスペクターの切り替え、デバッグモードの切り替え、キャッシュの消去ボタン、Stripe のステータス、イベントビューアー、本番環境へのリンクを表示する固定ツールバーが上部に表示されます。フローティングピルに最小化でき(クリックすると再展開)、状態は sessionStorage を介して再読み込み後も保持されます
  • ローカルファイル参照 — スキーマで ./images/hero.png を使用すると、プッシュ時に CDN へ自動アップロードされます
  • ローカルで検証 — catalogs catalog validate my-catalog.ts でルーティング、コンポーネント ID、孤立ページ、予約済みページ ID などを確認できます。トークンは不要です
  • カタログのひな形を作成 — catalogs catalog init でテンプレート(quiz-funnel、lead-capture、product-catalog、blank)から新しいカタログを作成します
  • リモートとの差分を確認 — catalogs catalog diff my-catalog.ts でデプロイ済みバージョンとの構造上の変更を表示します
  • ブラウザーで開く — catalogs catalog open my-slug で公開済みカタログの URL を既定のブラウザーで開きます
  • ローカル Stripe チェックアウト — .env に STRIPE_SECRET_KEY=sk_test_... と STRIPE_PUBLISHABLE_KEY=pk_test_... を追加すると、開発サーバーで実際の Stripe チェックアウトセッションとインラインのカード入力欄をローカルに作成できます。本番環境と完全に同等の機能を提供します。メタデータテンプレート、client_reference_id、reuse_payment_method(保存済みの支払い方法)、クーポンコード、すべての stripe_overrides に対応しています。キーがマシンの外に出ることはありません
  • ローカル開発イベント — ページビュー、フィールドの変更、チェックアウトイベントがターミナルと、AI エージェントが購読できる SSE エンドポイント(/__dev_events_stream)にストリーミングされます。本番環境と同様に POST /events、POST /events/batch、/e/batch を受け付けます。本番環境を一切汚染しません
  • ローカル API の同等性 — 開発サーバーは GET /public/catalogs/dev-user/:slug(カタログを JSON として取得)と POST /routing/variant(キーワード照合による決定論的なバリアント振り分け)を公開し、エージェントがローカルで同じ API をテストできます
  • カスタム JavaScript — <script> タグを含む html コンポーネントと window.CatalogKit API ブリッジを使って、カスタムのクライアントサイドロジックを挿入します
  • カスタム HTML コンポーネント — type: "html" コンポーネントを使って、任意の HTML/CSS/JS をカタログ内にレンダリングします
  • カスタム React コンポーネント — React コンポーネントを window.__catalogkit_components に登録して、完全にカスタマイズ可能なインタラクティブ UI を構築します

スクリプトとカスタムロジック — 概要

Catalog Kit には、カスタムロジック向けの2 つのスクリプトシステムがあります。それぞれの使いどころを理解することが重要です。

1. CatalogKit グローバル API(window.CatalogKit)— html コンポーネント内のインライン <script>

こちらが主要なスクリプトシステムです。 任意のページに <script> タグを含む html コンポーネントを追加します。このスクリプトは、window.CatalogKit.get() を介してカタログのランタイムにフルアクセスできます。

{
  "id": "my_script",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\nkit.on('beforenext:my_page', async (e) => {\n  // custom logic here\n});\n</script>"
  }
}

基本パターン: type: "html" → <script> タグ → window.CatalogKit.get() → kit.on(event, callback)

用途: サーバー側の検証、動的ルーティング、条件付き UI、API 呼び出し、タイマー、価格計算ツール、送信のインターセプト、カスタム DOM。

主な API メソッド:

  • kit.getField(id) / kit.setField(id, value) — フォームフィールドの読み取り/書き込み
  • kit.on('beforenext:pageId', async (e) => { ... }) — ナビゲーションをインターセプトし、ブロックするには e.preventDefault() を呼び出します
  • kit.setValidationError(id, msg) — フィールドにインラインエラーを表示
  • kit.setButtonLoading(bool) / kit.setButtonDisabled(bool) — 続行ボタンを制御
  • kit.setComponentProp(id, prop, value) — 任意のコンポーネントプロパティを動的に変更(非表示、ラベルの変更、選択肢の入れ替え)
  • kit.getVar(key) / kit.setVar(key, value) — スクリプト変数(リアクティブで、テンプレート内では {{var:key}} として利用可能)
  • kit.getGlobal(key) / kit.setGlobal(key, value) — ページ間で共有されるグローバル変数(ナビゲーション後も保持)

避けるべき重大な間違い: window.CatalogKit はインスタンスではなく、レジストリです。必ず最初に .get() を呼び出してください。

// ✅ CORRECT
const kit = window.CatalogKit.get();
kit.on('pageenter', () => { ... });

// ❌ WRONG — will throw "is not a function"
window.CatalogKit.on('pageenter', () => { ... });
2. TypeScript ページフック(on_enter、on_change、beforenext)— .ts カタログファイル内

TypeScript でカタログを作成する場合、ページにフックを実際の関数として直接記述できます。プッシュ時に CLI がこれらを文字列にシリアライズします。

pages: {
  checkout: {
    title: "Checkout",
    components: [...],
    hooks: {
      on_enter: (ctx) => {
        ctx.setButtonDisabled(true);
        ctx.fetch("https://api.example.com/check")
          .then(r => r.json())
          .then(data => { ctx.setField("status", data.status); ctx.setButtonDisabled(false); });
      },
      on_change: async (ctx) => {
        // ctx.field_id, ctx.field_value available
      },
    },
  },
}

用途: TypeScript カタログでのページレベルのライフサイクルフック。ctx オブジェクトには setField、setButtonDisabled、setButtonLoading、setValidationError、setProp、fetch、field_id、field_value が含まれます。

どちらを使うべきですか?
シナリオ使用するもの
ナビゲーション前のサーバー側検証CatalogKit の beforenext スクリプト
API からの動的なドロップダウン選択肢CatalogKit の pageenter スクリプト
コンポーネントの条件付き表示/非表示CatalogKit の fieldchange スクリプト
価格計算ツールまたはライブウィジェットDOM 操作を伴う CatalogKit スクリプト
バックエンドへの送信のインターセプトCatalogKit の submit スクリプト
TS カタログでのシンプルなページ入場/変更フックTypeScript ページフック
カスタム React コンポーネントwindow.__catalogkit_components に登録
HTML での動的データ表示テンプレート補間:{{field_id}}、{{var:key}}、{{global:key}}({{checkboxId.optionValue.inputId}} のようなドット区切りのネストされたキーに対応)
カスタム HTML と表示

html コンポーネントには 2 つの用途があります。表示される HTML/CSS をレンダリングすると同時に、非表示のスクリプトを実行できます。

{
  "id": "price_display",
  "type": "html",
  "props": {
    "content": "<div style='text-align:center;font-size:32px;font-weight:bold;'>{{var:price}}</div>\n<script>\nconst kit = window.CatalogKit.get();\nkit.on('fieldchange:quantity', () => {\n  kit.setVar('price', '$' + (Number(kit.getField('quantity')) * 29));\n});\n</script>"
  }
}

完全な API リファレンス、イベント一覧、および 11 個のクックブック例は、以下のCatalogKit グローバル APIセクションにあります。


はじめに

OfficeX に Catalog Kit をインストールすると、認証情報が自動的に発行されます。ダッシュボードで登録し、Settings から API キーを作成することもできます。

# Your API key (created from Settings page or received on install)
export CATALOG_KIT_TOKEN="cfk_..."

本番環境の API URL は https://api.catalogkit.cc です。CLI とすべての SDK の例でデフォルトとしてハードコードされています。API URL の環境変数を設定する必要はありません。CLI と REST API はどちらもこの URL を自動的に使用します。

認証

すべてのリクエストで、API キーを Bearer トークンとして渡します。

curl -H "Authorization: Bearer cfk_..." \
  https://api.catalogkit.cc/api/v1/catalogs

OfficeX 経由でインストールした場合は、インストール時の認証情報も使用できます。

TOKEN=$(echo -n "${OFFICEX_INSTALL_ID}:${OFFICEX_INSTALL_SECRET}" | base64)
curl -H "Authorization: Bearer $TOKEN" \
  https://api.catalogkit.cc/api/v1/catalogs

現在のユーザー

認証中のユーザーを確認します。メールアドレス、サブドメイン、カスタムドメイン、クレジット、認証情報が返されます。

GET https://api.catalogkit.cc/api/v1/me

レスポンス(200):

{
  "ok": true,
  "data": {
    "user_id": "usr_abc123",
    "email": "[email protected]",
    "username": "yourname",
    "subdomain": "1e6b7940",
    "custom_domain": "shop.example.com",
    "catalog_url": "https://shop.example.com",
    "stripe_key_set": true,
    "stripe_key_last4": "4x7K",
    "credits": 850,
    "officex_connected": true,
    "auth_method": "api_key",
    "api_key_id": "kx9f2",
    "is_superadmin": false,
    "created_at": "2025-01-15T10:30:00.000Z"
  }
}

CLI では catalogs whoami を実行すると、同じ情報が人が読みやすい形式で表示されます。


設定

設定を取得する
GET https://api.catalogkit.cc/api/v1/settings

レスポンス(200):

{
  "ok": true,
  "data": {
    "user_id": "usr_abc123",
    "subdomain": "my-brand",
    "custom_domain": "shop.example.com",
    "stripe_key_set": true,
    "stripe_key_last4": "4x7K"
  }
}
設定を更新する
PUT https://api.catalogkit.cc/api/v1/settings
{
  "subdomain": "my-brand",
  "custom_domain": "shop.example.com",
  "stripe_secret_key": "rk_live_..."
}

すべてのフィールドは任意です。変更したいものだけを含めてください。

フィールド説明
subdomain2〜32 文字の小文字の英数字とハイフン。カタログは <subdomain>.catalogkit.cc で提供されます。一意である必要があります。
custom_domainカタログを独自のドメインから提供します。DNS の設定(CNAME と TXT の確認レコード)が必要です。
stripe_secret_key決済を受け付けるための Stripe シークレットキーまたは制限付きキーです。削除するには null を設定します。

レスポンス(200):

{
  "ok": true,
  "data": {
    "user_id": "usr_abc123",
    "subdomain": "my-brand",
    "custom_domain": "shop.example.com",
    "stripe_key_set": true,
    "stripe_key_last4": "4x7K"
  }
}

サブドメインを変更すると、すべてのカタログ URL とリダイレクト URL が自動的に更新されます。


カスタムドメイン

<subdomain>.catalogkit.cc の代わりに、独自のドメイン(例:shop.example.com)からカタログを提供できます。SSL 証明書は自動的に発行されます。DNS レコードを追加するだけで、あとはシステムが処理します。

仕組み
  1. POST /api/v1/domains を呼び出して、ドメインを指定します
  2. API が SSL 証明書(ACM)を発行し、追加する 3 つの DNS レコードを返します
  3. DNS プロバイダーで DNS レコードを追加します
  4. ステータスが active になるまで、POST /api/v1/domains/refresh を呼び出して確認します
  5. https://your-domain.com/slug でカタログを公開できます

システムは CloudFront 上の統合 SAN 証明書を使用します。1 つのディストリビューションで、すべてのカスタムドメインを提供します。ドメインごとのインフラストラクチャは不要です。SSL 証明書は無料です(AWS ACM)。

ステップ 1 — ドメインを発行する
curl -X POST https://api.catalogkit.cc/api/v1/domains \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "shop.example.com" }'

レスポンス(201):

{
  "ok": true,
  "data": {
    "domain": "shop.example.com",
    "status": "pending_validation",
    "cloudfront_attached": false,
    "validation_cname_name": "_abc123.shop.example.com.",
    "validation_cname_value": "_def456.acm-validations.aws.",
    "error": null,
    "dns_records": [
      {
        "type": "CNAME",
        "name": "shop.example.com",
        "value": "catalogkit.cc",
        "purpose": "Route traffic to Catalog Kit"
      },
      {
        "type": "TXT",
        "name": "_cf-verify.shop.example.com",
        "value": "cf-domain-verify=usr_abc123",
        "purpose": "Verify domain ownership"
      },
      {
        "type": "CNAME",
        "name": "_abc123.shop.example.com.",
        "value": "_def456.acm-validations.aws.",
        "purpose": "SSL certificate validation (ACM)"
      }
    ],
    "created_at": "2025-01-15T10:30:00.000Z",
    "updated_at": "2025-01-15T10:30:00.000Z"
  }
}
ステップ 2 — DNS レコードを追加する

DNS プロバイダーで、dns_records 配列にある 3 つのレコードをすべて追加します。

#種類名前/ホスト値/ターゲット用途
1CNAMEshop.example.comcatalogkit.ccトラフィックをルーティングする
2TXT_cf-verify.shop.example.comcf-domain-verify=usr_abc123所有権の証明
3CNAME_abc123.shop.example.com._def456.acm-validations.aws.SSL 証明書の検証

レコード #3 は発行処理ごとに異なります。正確な値は API レスポンスで確認してください。

ステップ 3 — 有効になるまで確認する

DNS レコードを追加したら、refresh を呼び出して発行処理を進めます。

curl -X POST https://api.catalogkit.cc/api/v1/domains/refresh \
  -H "Authorization: Bearer YOUR_API_KEY"

ステータスの推移: pending_validation → issued → active

  • pending_validation — ACM が DNS 経由で SSL 証明書を検証するのを待っています。30 秒ごとに確認してください。
  • issued — SSL 証明書の準備ができ、CloudFront にアタッチ中です。もう一度確認してください。
  • active — 完了です。ドメインが HTTPS で公開されています。
  • failed — error フィールドを確認してください。通常は DNS の設定ミスが原因です。
ステップ 4 — 動作を確認する

ステータスが active になったら、カタログが公開されています。

https://shop.example.com/my-catalog-slug
https://shop.example.com/my-catalog-slug/variant-slug
https://shop.example.com/my-catalog-slug?hint="interested in premium plan"
https://shop.example.com/dashboard
ドメインのステータスを確認する
GET https://api.catalogkit.cc/api/v1/domains/status

現在の発行状態、DNS レコード、エラーがあればその内容を返します。

カスタムドメインを削除する
curl -X DELETE https://api.catalogkit.cc/api/v1/domains \
  -H "Authorization: Bearer YOUR_API_KEY"

CloudFront からドメインを削除し、SSL 証明書を削除して、アカウントの custom_domain をクリアします。

API でカスタムドメインを使用する

API が user_id を受け付ける箇所では、代わりに domain を使用できます。

# Fetch a catalog by custom domain
GET https://api.catalogkit.cc/public/catalogs/by-domain/my-catalog?domain=shop.example.com

# Route a visitor to the best variant (GET)
GET https://api.catalogkit.cc/public/route-variant?domain=shop.example.com&slug=my-catalog&hint="enterprise buyer"

# Route a visitor to the best variant (POST)
curl -X POST https://api.catalogkit.cc/public/route-variant \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "shop.example.com",
    "slug": "my-catalog",
    "hint": "enterprise buyer"
  }'
AI エージェントのワークフロー

カスタムドメインをプログラムから接続する AI エージェント向け:

1. POST /api/v1/domains { "domain": "shop.example.com" }
2. Parse dns_records from response
3. Add all 3 DNS records via your DNS provider's API
4. Loop: POST /api/v1/domains/refresh every 30s
5. When status === "active" → done
6. If status === "failed" → read error, fix DNS, POST /api/v1/domains to retry
Cloudflare ユーザー向け

DNS を Cloudflare で管理している場合は、すべての CNAME レコードを 「DNS only」(オレンジではなく灰色の雲アイコン)に設定してください。Cloudflare のオレンジ色の雲プロキシは SSL を終端し、ACM の検証と CloudFront の証明書提供の両方に干渉します。

トラブルシューティング
問題原因対処方法
pending_validation のまま進まないACM 検証用の CNAME が追加されていない、または反映されていないdig CNAME _abc123.shop.example.com +short でレコード #3 を確認します
No account found for domainトラフィック用 CNAME は機能していますが、アカウントにドメインが設定されていませんPOST /domains が成功したことを確認します
ステータスが failedDNS の設定ミス、または Cloudflare プロキシが有効ですerror フィールドを確認し、DNS を修正してから、再試行のために POST /domains を呼び出します
ERR_NAME_NOT_RESOLVEDトラフィック用 CNAME が追加されていませんレコード #1(shop.example.com → catalogkit.cc)を追加します
active の後に SSL エラーが発生するCloudFront の反映に時間がかかっていますCloudFront のデプロイに 5〜10 分ほど待ちます
制限事項
  • アカウントごとにカスタムドメインは1つまでです
  • ACM が統合証明書でサポートする SAN は最大100個です。CloudFront ディストリビューションあたり約95個のカスタムドメインに対応できる十分な数です
  • エイペックスドメイン(サブドメインのプレフィックスがない example.com など)には、CNAME フラットニングまたは ALIAS レコードが必要です。Cloudflare、Route53、DNSimple はサポートしていますが、従来型の DNS ではサポートされていません
  • DNS レコードを追加してから SSL のプロビジョニングが完了するまで、通常2〜10分かかります
  • カスタムドメインの名前解決では、キャッシュされていないリクエストごとに DynamoDB の検索が1回追加されます(インメモリに60秒間キャッシュされます)

カタログの管理

カタログの一覧表示
GET https://api.catalogkit.cc/api/v1/catalogs

クエリパラメーター: limit(デフォルトは50、最大200)、cursor(不透明なページネーショントークン)、include_sandboxes(サンドボックスカタログを含める場合は true)

多数のカタログがあるアカウントでは、カーソルベースのページネーションに対応しています。続きの結果がある場合、レスポンスには cursor 値が含まれます。次のページを取得するには、これを ?cursor=... として渡してください。cursor が null の場合、最後のページに到達しています。

レスポンス:

{
  "ok": true,
  "data": [
    {
      "catalog_id": "01HXY...",
      "slug": "my-funnel",
      "name": "My Funnel",
      "status": "published",
      "visibility": "public",
      "version": "1.0.3",
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    }
  ],
  "cursor": "eyJwayI6Ik..."
}
カタログの作成
POST https://api.catalogkit.cc/api/v1/catalogs
{
  "slug": "spring-sale",
  "name": "Spring Sale Landing Page",
  "schema": { ... },
  "status": "published",
  "visibility": "public",
  "version": "1.0.0-initial"
}
  • slug — URL で使いやすい名前(小文字、ハイフン区切り)。カタログは設定済みのドメインで公開されます
  • status — "published"(公開)または "draft"(非表示)。デフォルトは "published" です
  • visibility — "public"(一覧に掲載)または "unlisted"(リンクを知っている人のみ)。デフォルトは "unlisted" です
  • version — Semver 形式の文字列(例:"1.0.0" または "1.0.0-launch")。省略した場合、デフォルトは "1.0.0" です。レスポンスには常に含まれます。

レスポンス(201):

{
  "ok": true,
  "data": {
    "catalog_id": "01HXY...",
    "slug": "spring-sale",
    "name": "Spring Sale Landing Page",
    "status": "published",
    "visibility": "public",
    "version": "1.0.0-initial",
    "url": "https://SUBDOMAIN.catalogkit.cc/spring-sale"
  }
}
カタログの表示
GET https://api.catalogkit.cc/api/v1/catalogs/:id

スキーマを含むカタログ全体を返します。

カタログの更新
PUT https://api.catalogkit.cc/api/v1/catalogs/:id

すべてのフィールドは任意です。変更する項目のみ送信してください。

{
  "name": "Updated Name",
  "schema": { ... },
  "status": "draft",
  "visibility": "public",
  "slug": "new-slug",
  "old_slug_action": "redirect",
  "version": "2.0.0-redesign"
}

スラッグを変更する場合、old_slug_action で古い URL の扱いを指定します。

  • "redirect"(デフォルト)— 古い URL から新しい URL にリダイレクトします
  • "release" — 古い URL を再利用できるようにします

バージョン管理: 更新のたびにパッチバージョンが自動的に上がります(例:1.0.0 → 1.0.1)。ラベル付きの特定のバージョンを設定するには、version を明示的に渡してください(例:"2.0.0-black-friday")。サーバーはレスポンスで常に新しいバージョンを返します。

カタログの削除
DELETE https://api.catalogkit.cc/api/v1/catalogs/:id
キャッシュの無効化

公開カタログの URL では、キャッシュを制御するために _cb クエリパラメーターを使用できます。

パラメーター動作
?_cb=latestデータベースから常に最新の情報を取得し、すべてのキャッシュをスキップします。公開後、変更をすぐに確認する際に使用してください。
?_cb=1.0.3キャッシュされたバージョンが 1.0.3 と一致しない場合にのみキャッシュをスキップします。既知のバージョンを指定するのに効率的です。
(パラメーターなし)通常のキャッシュを使用します(新鮮なデータを30秒間保持し、その後60秒間 stale-while-revalidate)。訪問者のパフォーマンスに最適です。

例: カタログを更新した後、変更が公開されていることを確認します。

GET https://SUBDOMAIN.catalogkit.cc/spring-sale?_cb=latest

公開カタログのレスポンスには常に version が含まれるため、どのバージョンが提供されているかを確認できます。

{
  "ok": true,
  "data": {
    "catalog_id": "01HXY...",
    "version": "1.0.4-spring-update",
    "schema": { ... }
  }
}

アナリティクスと結果

すべてのアナリティクスエンドポイントには認証が必要です。アナリティクスの呼び出し1回につき、1クレジットが必要です。イベント追跡(訪問者のアクティビティ)は無料です。

概要指標
GET https://api.catalogkit.cc/api/v1/analytics/catalogs/:id

クエリパラメーター: start、end(ISO 日付。例:2024-01-01)

集計指標として、ユニーク訪問者数、ページビュー総数、フォーム送信数、コンバージョン率、ページごとの閲覧数、バリアント別の内訳、参照元、チェックアウトの統計、売上を返します。

時系列(日時別の傾向)
GET https://api.catalogkit.cc/api/v1/analytics/catalogs/:id/timeseries

クエリパラメーター(必須): start、end(ISO 日付)、interval(day または hour)

{
  "ok": true,
  "data": [
    { "date": "2024-01-01", "page_views": 150, "sessions": 80, "form_submits": 25, "checkout_errors": 2, "checkout_completes": 5, "revenue_cents": 4900 }
  ]
}
離脱分析

訪問者がファネルのどこで離脱したかを確認できます。

GET https://api.catalogkit.cc/api/v1/analytics/catalogs/:id/dropoff

クエリパラメーター: start、end(ISO 日付)

{
  "ok": true,
  "data": {
    "total_visitors": 500,
    "pages": [
      { "page_id": "intro", "visitors": 500, "drop_off_rate": 0 },
      { "page_id": "questions", "visitors": 350, "drop_off_rate": 30 }
    ],
    "fields": [
      { "field_id": "questions/email", "completions": 300, "completion_rate": 85.7 }
    ]
  }
}
回答の分布

訪問者が各質問またはフォームフィールドにどのように回答したかを確認できます。

GET https://api.catalogkit.cc/api/v1/analytics/catalogs/:id/responses

クエリパラメーター: start、end、page_id、component_id(すべて任意)

{
  "ok": true,
  "data": {
    "components": {
      "questions/q1": {
        "total_responses": 200,
        "distribution": {
          "Option A": { "count": 112, "percent": 56 },
          "Option B": { "count": 28, "percent": 14 },
          "Option C": { "count": 60, "percent": 30 }
        }
      }
    }
  }
}
生イベント

フィルタリングを使って、個々の訪問者イベントを確認します。

GET https://api.catalogkit.cc/api/v1/analytics/catalogs/:id/events

クエリパラメーター: start、end、cursor、limit(デフォルトは100、最大5000)、event_type、page_id、component_id、variant_slug、utm_source、utm_medium、utm_campaign、referrer

レスポンスにはページネーション用のcursorが含まれます(処理が完了するとnull)。

訪問者のジャーニー

カタログ内での1人の訪問者の全行程を追跡します。

GET https://api.catalogkit.cc/api/v1/analytics/tracers/:tracerId

すべてのイベントが時系列順に返され、概要としてイベント総数、最初と最後のアクセス日時、閲覧したページ、送信の有無が含まれます。


エージェント API(ヘッドレスフォーム送信)

AI エージェントは、ブラウザーを使わずにプログラムからカタログを操作できます。エージェント API はステートフルなセッションを提供し、サーバー側の検証と段階的なフィールド開示を行いながら、カタログをページごとに進めます。

セッションを開始する
POST https://api.catalogkit.cc/agent/v1/sessions
{
  "session_id": "my-unique-session-id",
  "catalog_slug": "enterprise-demo",
  "user_id": "usr_abc123"
}

レスポンス:

{
  "ok": true,
  "data": {
    "session_id": "my-unique-session-id",
    "status": "active",
    "step": {
      "page_id": "qualification",
      "title": "Tell us about your company",
      "agent_context": "Qualify the lead before showing pricing",
      "fields": [
        {
          "id": "company_name",
          "type": "short_text",
          "label": "Company Name",
          "required": true,
          "agent_hint": "The legal entity name"
        },
        {
          "id": "industry",
          "type": "dropdown",
          "label": "Industry",
          "required": true,
          "options": [
            { "value": "saas", "heading": "SaaS" },
            { "value": "healthcare", "heading": "Healthcare" }
          ]
        }
      ],
      "disclosed": [
        { "type": "heading", "text": "Enterprise Demo Request" }
      ]
    }
  }
}
次のステップに進む
POST https://api.catalogkit.cc/agent/v1/sessions/{session_id}/advance
{
  "page_id": "qualification",
  "answers": {
    "company_name": "Acme Corp",
    "industry": "healthcare"
  }
}

レスポンス(次のステップ):

{
  "ok": true,
  "data": {
    "step": {
      "page_id": "pricing",
      "title": "Choose your plan",
      "fields": [
        {
          "id": "selected_plan",
          "type": "dropdown",
          "label": "Plan",
          "required": true,
          "options": [
            { "value": "pro", "heading": "Pro - $99/mo" },
            { "value": "enterprise", "heading": "Enterprise - Custom" }
          ]
        }
      ]
    }
  }
}

レスポンス(完了):

{
  "ok": true,
  "data": {
    "complete": true,
    "form_state": { "company_name": "Acme Corp", "industry": "healthcare", "selected_plan": "pro" }
  }
}
セッションのステータスを確認する
GET https://api.catalogkit.cc/agent/v1/sessions/{session_id}
主な動作
  • 冪等性: 既存のsession_idで再開すると、現在のステップが返されます(重複したセッションは作成されません)。
  • サーバーによる制御: 現在のページのフィールドのみが返されます。条件が満たされるまで、価格設定や後続ページは表示されません。
  • 検証: 回答はサーバー側で検証されます(必須フィールド、メールアドレスの形式、数値の範囲など)。
  • 段階的な処理: 条件付きフィールドとルーティングはブラウザー UI と同じように機能します。サーバーが表示条件とルーティング条件を評価します。
  • セッションの有効期限: 1時間操作がないとセッションの有効期限が切れます(次のステップに進むたびにTTLが更新されます)。
エージェント向けのスキーマフィールド

カタログの作成時に、AI エージェントとのやり取りを改善するため、次の任意フィールドを追加します。

  • カタログルートのagent — { enabled: true, description: "What this catalog does" }
  • ページのagent_context — エージェントにページの目的を説明します
  • コンポーネントのagent_hint — フィールドの意味をエージェントに説明します

重み付きバリアントによる A/B テスト

スキーマに重み付きバリアントを追加して、カタログの異なるバージョンをテストします。重み付きランダムルーティングにはvariant_routing: "random"、AI ベースのルーティングには"hint"、両方には"hybrid"を設定します。

{
  "schema": {
    "variant_routing": "random",
    "variants": [
      { "id": "v1", "slug": "control", "weight": 50, "description": "Original" },
      { "id": "v2", "slug": "new-headline", "weight": 50, "description": "New headline" },
      { "id": "v3", "slug": "old-test", "weight": 50, "enabled": false, "description": "Paused variant" }
    ]
  }
}

target_slugを持つバリアントは、訪問者を別のカタログにルーティングします。target_slugを持たないバリアントは、同じカタログ内でパーソナライズのヒントを適用します。バリアントを削除せず、すべてのルーティング(hint、random、hybrid)から除外するには、enabled: falseを設定します。パフォーマンスの低い A/B テストのグループを一時停止する場合に便利です。無効なバリアントでも直接 URL にアクセス(/slug/variant-slug)できます。既存のリンクが切れることはありません。


パーソナライズのアーキテクチャ:バリアントと動的オファーの違い

Q:?hint=やその他のシグナルに基づいて、訪問者ごとに表示するオファーを動的に選べますか?

Catalog Kit は、静的なバリアント+動的ルーティングモデルを採用しています。AI は?hint=を使って、表示するバリアントを選びますが、各バリアントで定義されるページとオファーの順序は固定され、事前に決められています。これは意図的な設計です。その理由は次のとおりです。

仕組み
  1. 訪問者がアクセスし、?hint="startup founder looking for analytics"(または任意の自然言語)を指定します
  2. AI ルーティング(400ミリ秒未満)がヒントとバリアントの説明を読み取り、最適なバリアントを選びます
  3. バリアントが有効化され、訪問者には特定のページ、オファー、コピーで構成された、完全に事前定義済みのファネルが表示されます
  4. __variantsによるパーソナライズ — バリアント内では、個々のコンポーネントのプロパティ(見出し、画像、説明)をヒントの値に基づいてさらに調整できます
なぜ完全に動的なオファー選択を行わないのですか?
懸念事項静的なバリアントが優れている理由
価格の整合性各バリアントは既知の Stripe の価格 ID に対応しています。動的に組み立てると、誤った価格や互換性のないバンドルが表示されるリスクがあります。
監査可能性「この訪問者にオファー X が表示されたのはなぜか?」に答えられます。バリアント B に該当し、オファー 1→2→3 が表示されたからです。LLM の調査は必要ありません。
コンプライアンスOfficeX の顧客はクレジットのレート制限を設定します。予測可能なオファーの順序により、支出を監査・管理できます。
パフォーマンスバリアントの振り分けに使う LLM 呼び出しは 1 回で済み、高速です(約 400ms)。ページごとのオファー選択を追加すると、レイテンシーとコストが倍増します。
テスト各バリアントは、既知のコンバージョン指標を使って個別にテストできます。動的なオファーでは、A/B 分析の統計的な信頼性が損なわれます。
代わりにできること
  • バリアントを増やす — AI ヒントによる振り分けを使った 4~8 個のバリアントで、ほとんどのパーソナライズのニーズに対応できます。それぞれ、完全にテスト可能なファネルです。
  • コンポーネントの props で __variants を使う — 1 つのカタログ構造内で、ヒントの値に応じて見出し、画像、コピー、さらにはオファーのタイトルや価格も変更できます。
  • target_slug を使う — バリアントを別々のカタログに振り分けることで、まったく異なるカタログ構造を A/B テストできます。
  • 条件付きルーティングエッジを使う — url_param、field、hint、score、video の条件に基づいて、訪問者を別のページに振り分けます。
考え方

次のように考えてください。AI は進む経路を選びますが、各経路はその場で生成するものではなく、あらかじめ舗装された道です。 これにより、動的コンテンツによるパーソナライズのメリットと、静的ファネルの信頼性・測定可能性を両立できます。

?hint="enterprise CTO"
    → AI routes to variant "enterprise"
        → Page 1: enterprise headline + ROI calculator offer
        → Page 2: annual pricing offer (predetermined Stripe price)
        → Page 3: enterprise checkout

?hint="solo freelancer"
    → AI routes to variant "freelancer"
        → Page 1: freelancer headline + quick-start offer
        → Page 2: monthly pricing offer (different Stripe price)
        → Page 3: simple checkout

どちらの経路もカタログスキーマで完全に定義されています。AI はどちらが適切かを選ぶだけです。


スキーマのイントロスペクション

カタログ内のすべてのページとコンポーネントのマップを取得します。分析をクエリする前に構造を把握するのに便利です。

GET https://api.catalogkit.cc/api/v1/catalogs/:id/schema/ids
{
  "pages": {
    "landing": { "title": "Get Started", "index": 0 },
    "details": { "title": "Your Details", "index": 1 }
  },
  "components": {
    "landing/email": { "type": "email", "label": "Your Email", "required": true },
    "landing/company": { "type": "short_text", "label": "Company Name" }
  },
  "routing_entry": "landing"
}

API キー

チームメンバーやインテグレーション用の API キーを管理します。

  • POST /api/v1/api-keys — キーを作成します(ロール:reader、editor、admin、custom)。シークレットは一度だけ返されるため、安全に保管してください。
  • GET /api/v1/api-keys — すべてのキーを一覧表示します(シークレットは伏せられます)。内部で自動的にページネーションを行い、すべてのキーを返します。
  • DELETE /api/v1/api-keys/:keyId — キーを無効にします。
  • POST /api/v1/api-keys/:keyId/rotate — キーをローテーションします。古いキーを無効にし、同じ設定で新しいキーを作成します。

メディアホスティング

Catalog Kit にはマネージドメディアストレージが含まれているため、独自の S3 バケットや CDN を用意する必要はありません。API 経由で画像や動画をアップロードすれば、ストレージ、圧縮、トランスコーディング、CDN 配信を自動で処理します。返されるすべてのメディア URL は本番環境ですぐに使える形式で、カタログコンポーネントの src 値として直接使用できます。


画像

画像をアップロードすると、高速に読み込めるよう自動的に WebP に圧縮されます。画像のアップロードは無料です(クレジットは消費されません)。圧縮はバックグラウンドの Lambda で自動的に行われ、ファイルは CDN 経由で配信されます。

画像をアップロードする
POST https://api.catalogkit.cc/api/v1/images/upload
{
  "filename": "hero-banner.png",
  "content_type": "image/png",
  "size_bytes": 2500000,
  "no_compress": false
}

レスポンス(201):

{
  "ok": true,
  "data": {
    "image_id": "01ABC...",
    "upload_url": "https://s3.amazonaws.com/...",
    "original_url": "https://cdn.../media/images/original/...",
    "compressed_url": "https://cdn.../media/images/compressed/...webp",
    "thumbnail_url": "https://cdn.../media/images/compressed/...thumb.webp",
    "no_compress": false
  }
}

署名付きの upload_url に PUT リクエストでファイルをアップロードします。Content-Type ヘッダーには、アップロードリクエストで指定した content_type(例:image/png)を必ず設定してください。署名付き URL はそのコンテンツタイプに対して署名されています。ヘッダーが一致しない場合、S3 に拒否されます。また、ヘッダーを省略すると、ブラウザーで画像がブロックされる可能性があります(ORB / CORB エラー)。

PUT <upload_url>
Content-Type: image/png   ← must match the content_type from the upload request
<binary image body>

圧縮は自動的に行われます。画像コンポーネントの src には compressed_url を使用してください。

圧縮状況を確認する
GET https://api.catalogkit.cc/api/v1/images/:imageId/status
画像を一覧表示する
GET https://api.catalogkit.cc/api/v1/images

クエリパラメーター: limit(デフォルトは 50、最大 200)、cursor(不透明なページネーショントークン)

カーソルベースのページネーションに対応しています。次のページを取得するには、前のレスポンスの cursor を渡します。結果がこれ以上ない場合、cursor は null になります。

圧縮を無効にする

アップロードリクエストで "no_compress": true を設定します。元の URL がそのまま使用されます。

圧縮の詳細
  • 出力形式: WebP(最高の圧縮率、あらゆるブラウザーで利用可能)
  • 最大サイズ: 幅 2048px(アスペクト比を維持し、拡大はしません)
  • サムネイル: 幅 400px、品質 70
  • 対応入力形式: JPEG、PNG、GIF、WebP、TIFF、BMP、AVIF、HEIC/HEIF
  • 料金: 無料(クレジットは消費されません)
  • オリジナル: 1 年後に自動削除されます(圧縮版は保持されます)

動画

動画コンテンツを管理対象のメディアバケットにアップロードすると、アダプティブストリーミング用の HLS トランスコードが自動的に行われます。動画は CDN 経由で配信されるため、外部ホスティングは不要です。

  • POST /api/v1/videos/upload — 署名付きアップロード URL を取得します(100MB ごとにクレジットを消費します)
  • POST /api/v1/videos/:videoId/transcode — HLS トランスコードを開始します(推定分数ごとにクレジットを消費します)
  • GET /api/v1/videos/:videoId/status — トランスコードの進行状況を確認し、再生 URL を取得します

ファイル

管理対象の S3 ストレージにダウンロード可能なファイル(PDF、ZIP、ドキュメントなど)をアップロードしてホストし、CDN 経由で配信できます。ファイルはユーザーごとに分けて管理され、料金は 50MB あたり 1 クレジット(最低 1 クレジット)です。ファイルは 1 年間保持されます。

ファイルをアップロードする
POST https://api.catalogkit.cc/api/v1/files/upload
{
  "filename": "pricing-guide.pdf",
  "content_type": "application/pdf",
  "size_bytes": 5000000
}

レスポンス(201):

{
  "ok": true,
  "data": {
    "file_id": "01ABC...",
    "upload_url": "https://s3.amazonaws.com/...",
    "cdn_url": "https://cdn.../media/files/...",
    "filename": "pricing-guide.pdf",
    "size_bytes": 5000000,
    "credits_charged": 1
  }
}

署名付きの upload_url を使用してファイルをアップロードします(ファイル本体を含む PUT リクエスト)。file_download 表示コンポーネントの src には cdn_url を使用します。

ダウンロード URL を取得する
GET https://api.catalogkit.cc/api/v1/files/:fileId/download

ブラウザーでのダウンロード用に Content-Disposition: attachment が設定された、署名付きダウンロード URL(有効期限 1 時間)を返します。

ファイルを一覧表示する
GET https://api.catalogkit.cc/api/v1/files

クエリパラメーター: limit(デフォルト 50、最大 200)、cursor(不透明なページネーショントークン)

カーソルベースのページネーションに対応しています。次のページを取得するには、前のレスポンスの cursor を渡します。これ以上結果がない場合、cursor は null になります。

ファイルダウンロードコンポーネント

file_download 表示コンポーネントを使うと、カタログにダウンロードボタンを表示できます。

{
  "id": "download_guide",
  "type": "file_download",
  "props": {
    "src": "https://cdn.../media/files/user123/fileId/pricing-guide.pdf",
    "filename": "Pricing Guide.pdf",
    "size_bytes": 5000000,
    "button_text": "Download",
    "style": "primary",
    "description": "Complete pricing breakdown"
  }
}

プロパティ:src(必須)、filename(必須)、size_bytes、button_text、style("primary" | "secondary" | "outline" | "ghost")、description、icon。

モバイルでフォームの入力内容が失われないよう、ダウンロードは新しいタブで開きます。


Webhook

カタログに webhook_url が設定されている場合、Catalog Kit は耐久性のあるキューと再試行機能を使って、訪問者のイベントを転送します。配信は 少なくとも 1 回行われます。

  • form_submit、funnel_complete、checkout_error、checkout_complete、lead_captured などの重要なイベントは、可能な限り速やかに個別に配信されます
  • page_view、field_change などの重要度の低い分析イベントや、その他の大量に発生するインタラクションイベントは、エンドポイントへの集中を防ぐため、まとめてバッチペイロードとして送信される場合があります
  • すべてのイベントには重複排除用の event_id(ULID)が含まれ、追跡対象のイベントには、人が読めるページ/コンポーネントのコンテキストを含む schema_ref が含まれます

単一イベントの配信例:

{
  "delivery_type": "event",
  "event_id": "01HV...",
  "event": "form_submit",
  "catalog_slug": "ai-agency-bundle",
  "user_id": "usr_abc123",
  "page_id": "thank_you",
  "form_state": { "email": "[email protected]", "plan": "pro" },
  "timestamp": "2026-03-13T10:30:00.000Z"
}

大量イベントのバッチ配信例:

{
  "delivery_type": "events.batch",
  "catalog_slug": "ai-agency-bundle",
  "user_id": "usr_abc123",
  "event_count": 3,
  "batched_at": "2026-03-13T10:30:05.000Z",
  "events": [
    { "delivery_type": "event", "event_id": "01HV...", "event": "page_view", "page_id": "intro", "timestamp": "2026-03-13T10:30:00.000Z" },
    { "delivery_type": "event", "event_id": "01HW...", "event": "field_change", "component_id": "email", "timestamp": "2026-03-13T10:30:01.000Z" },
    { "delivery_type": "event", "event_id": "01HX...", "event": "field_complete", "component_id": "email", "timestamp": "2026-03-13T10:30:02.000Z" }
  ]
}

重要: webhook_url は受動的なイベント転送専用です。イベントの発生後に非同期で配信され、ページ遷移をブロックすることはありません。エンドポイントでは配信が少なくとも 1 回行われることを前提とし、event_id による重複排除を行う必要があります。ユーザーが先へ進む前に、サーバー側でフォームデータを検証する必要がある場合(例:メールアドレスがすでに登録されているかの確認、ウォレットアドレスの検証、独自の適格性ロジックの実行)は、代わりに CatalogKit の beforenext スクリプトを使用してください。詳しくは、以下のサーバーサイドフォーム検証をご覧ください。


サーバー側のフォーム検証(一般的なパターン)

ユーザーが「続行」をクリックしても、プラットフォームがサーバーを自動的に呼び出すことはありません。クライアント側の検証(必須フィールド、形式チェック)は自動的に実行されますが、カスタムのサーバー側検証は、CatalogKit のスクリプティングブリッジを使用して開発者が接続する必要があります。

サーバーがエラー(例:HTTP 422 と拒否理由)を返しているのに、フロントエンドにエラーメッセージが表示されない場合は、次の方法で解決できます。

サーバー検証が必要なページに、インラインの <script> を含む html コンポーネントを追加します。

{
  "id": "server_validator",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\nkit.on('beforenext:YOUR_PAGE_ID', async (e) => {\n  kit.setButtonLoading(true);\n  try {\n    const res = await fetch('https://your-api.com/validate', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        email: kit.getField('email'),\n        name: kit.getField('name'),\n      }),\n    });\n    const data = await res.json();\n\n    if (!res.ok || data.status === 'rejected') {\n      // Show the server's error message on a specific field\n      kit.setValidationError('email', data.reason || data.error || 'Validation failed');\n      e.preventDefault(); // Block navigation — user stays on this page\n    }\n  } catch (err) {\n    kit.setValidationError('email', 'Network error — please try again');\n    e.preventDefault();\n  } finally {\n    kit.setButtonLoading(false);\n  }\n});\n</script>"
  }
}

主な要件:

  • e.preventDefault() — これがないと、検証に失敗してもページが遷移します
  • kit.setValidationError(componentId, message) — フィールドの上にエラーをインライン表示します。後から消去するには null を渡します
  • kit.setButtonLoading(true/false) — サーバー呼び出し中に「続行」ボタンにスピナーを表示します
  • スコープ付きイベント — 関連するページでのみ実行するには、beforenext だけでなく beforenext:YOUR_PAGE_ID を使用します

関連項目: CatalogKit グローバル API のセクションには、サーバーの応答に基づく動的ルーティング、リアルタイムのぼかし検証、条件付き UI など、より高度な例があります。


バリアント分析

各カタログには、catalog_id(例:catalog_550e8400-e29b-41d4-a716-446655440000)がタグとして自動的に追加されます。カタログのバリアント間で分析データを比較するには(例:A/B テストの場合)、ベースカタログの catalog_id を各バリアントの schema.tags に追加します。一致する tag_patterns をスコープとする API キーを使うと、タグが付いたすべてのバリアントの分析データをクエリできます。


カタログスキーマのリファレンス

カタログスキーマは、ファネル全体を JSON として定義します。最小限のリード獲得の例を示します。

{
  "slug": "lead-capture",
  "pages": [
    {
      "id": "landing",
      "title": "Get Started",
      "components": [
        { "id": "name", "type": "short_text", "label": "Your Name", "required": true },
        { "id": "email", "type": "email", "label": "Email", "required": true }
      ],
      "submit_label": "Submit"
    }
  ],
  "routing": { "entry": "landing", "edges": [] }
}

予約済みのページ ID: checkout、__checkout、__global、submitted、__submitted は、CatalogKit に組み込まれたチェックアウト、インスペクター、送信システム用に予約されているため、ページ ID として使用しないでください。一般に、__ で始まる ID も避けてください。CLI の catalog validate コマンドで確認できます。

テーマ

テーマオプションは settings.theme の下に設定します。

  • primary_color(必須)— ボタン、アクセント、アクティブ状態に使用する 16 進カラーコード
  • font — Google Font のファミリー名(例:"Inter")
  • font_size — 本文と入力欄の基本フォントサイズ(rem)。デフォルト:1(16px)。18px の場合は 1.125、20px の場合は 1.25 を使用します
  • mode — "light"(デフォルト)または "dark"
  • border_radius — 全体に適用する角丸の半径(px)
  • background_image — カバーページの背景画像の URL
  • background_color — ページの背景色を表す 16 進カラーコード
  • background_overlay — "dark"、"light"、"none"、または 0~1 の数値
SEO とソーシャルプレビュー

各カタログでは、ブラウザーのタブタイトル、ファビコン、メタディスクリプション、Open Graph 画像の SEO メタデータを個別に設定できます。カタログスキーマのトップレベル seo フィールドに設定します。

const catalog: CatalogSchema = {
  slug: "spring-sale",
  seo: {
    title: "Spring Sale 2025 — Get 40% Off",
    description: "Find your perfect plan in under 60 seconds. Limited time offer.",
    favicon: "https://cdn.example.com/favicon.png",
    og_image: "https://cdn.example.com/og-spring-sale.jpg",
  },
  settings: { /* ... */ },
  pages: { /* ... */ },
  routing: { /* ... */ },
};
フィールド型説明
seo.titlestringブラウザーのタブタイトルです。最初のページの title、次にカタログの slug が使用されます
seo.descriptionstring検索エンジンやリンクプレビュー(Slack、Twitter、Facebook など)向けのメタディスクリプションです
seo.faviconstringファビコン画像(ICO、PNG、SVG)の URL です。ブラウザーのデフォルトのファビコンを置き換えます
seo.og_imagestringソーシャルメディアのリンクプレビュー向け Open Graph 画像の URL です。推奨サイズ:1200×630px

すべてのフィールドは省略可能です。省略した場合:

  • タイトルは最初のページのタイトル、次にカタログの slug が使用されます
  • ファビコンはブラウザーのデフォルトが使用されます
  • 説明と OG 画像は設定されません

仕組み:

  • ブラウザーの場合: カタログの読み込み時に、document.title と <meta> / <link> 要素を介してタイトル、ファビコン、説明、OG タグが動的に挿入されます
  • ソーシャルメディアのクローラー(Facebook、Twitter、Slack、LinkedIn、WhatsApp、Telegram、Discord、Google、Bing)の場合:CloudFront がボットのユーザーエージェントを検出し、正しい <meta> タグをすべて含む軽量なページを返すサーバーサイド HTML エンドポイント(/meta/:slug)にリダイレクトします。JavaScript は不要です
コンポーネントの種類(全 61 種類)

入力(27): short_text、long_text、rich_text、email、phone、url、password、number、currency、date、datetime、time、date_range、dropdown、multiselect、multiple_choice、checkboxes、picture_choice、star_rating、slider、file_upload、signature、address、location、switch、checkbox、choice_matrix、ranking、opinion_scale

表示(16): heading、paragraph、banner、image、video、pdf_viewer、file_download、social_links、html、divider、faq、testimonial、pricing_card、timeline、iframe、modal、custom

レイアウト(3): section_collapse、table、subform

ページ機能: payment、captcha

コンポーネント単位の表示制御(すべてのコンポーネントの種類)

すべてのコンポーネント(表示および入力)は、表示を制御するために次のトップレベルフィールドをサポートしています。

フィールド型説明
hiddenbooleanコンポーネントを静的に非表示にします。表示コンポーネント(callout、image、banner など)を含むすべての種類で動作します
visibilityConditionGroupフォームの状態、URL パラメーター、またはヒントに基づいて動的に表示/非表示を切り替えます。すべての種類で動作します

単純なオン/オフには hidden: true を使います。条件ロジックには visibility を使います。どちらも表示コンポーネントと入力コンポーネントの両方で動作します。非表示のコンポーネントは検証の対象外です。

共通の入力プロパティ

すべての入力コンポーネントは、ラベル、ヘルプテキスト、検証用の基本プロパティをサポートしています。

プロパティ型説明
labelstring入力欄の上に表示されるメインラベルです
sublabelstringメインラベルの下に表示される小さな補足テキストです(別名:subheading)
descriptionstringサブラベルの下に表示される、薄いスタイルのヘルプテキストです
tooltipstringラベルの横に表示される情報アイコン(ⓘ)です。ホバーまたはタップすると説明ポップオーバーが表示されます
requiredbooleanフィールドを必須として指定します(赤いアスタリスクが表示されます)
placeholderstring入力欄内のプレースホルダーテキストです
hiddenbooleanUI でフィールドを非表示にします(レガシー機能です。代わりにコンポーネント単位の hidden を使用してください)
copyableboolean入力欄の横にクリップボードへコピーするアイコンを表示します。編集可能な入力欄(short_text、long_text、rich_text、email、phone、url、number、currency、date、datetime、time、password、dropdown、address)で動作します。フィールドに値が入るとアイコンが表示されます。

すべてのラベルプロパティを使った例:

{
  "id": "tg_username",
  "type": "short_text",
  "props": {
    "label": "Your Telegram Username",
    "sublabel": "We'll use this to add you to the team group",
    "tooltip": "Go to Telegram Settings > Username to find or set yours",
    "placeholder": "@username",
    "required": true
  }
}
テキスト入力のプロパティ(short_text と long_text)

short_text(単一行)と long_text(複数行のテキストエリア)は、上記の共通入力プロパティに加えて、次の追加プロパティを共有します。

プロパティ型デフォルト説明
min_lengthnumber—最小文字数
max_lengthnumber—最大文字数
default_valuestring—あらかじめ入力されたデフォルト値
disabledbooleanfalse入力をグレー表示にし、操作できないようにします
readonlybooleanfalseクリップボードにコピーするボタン付きの読み取り専用

long_text 専用プロパティ(テキストエリア):

プロパティ型デフォルト説明
rowsnumber4表示するテキスト行数(初期の高さを制御します)
resizestring"vertical"テキストエリアのサイズ変更をドラッグで行えるかどうか。選択肢:"vertical"、"horizontal"、"both"、"none"

例 — 6 行で、サイズ変更できないフィードバック用テキストエリア:

{
  "id": "feedback",
  "type": "long_text",
  "props": {
    "label": "Your Feedback",
    "sublabel": "Tell us what you think",
    "tooltip": "Be as detailed as you like",
    "placeholder": "Write your thoughts here...",
    "required": true,
    "rows": 6,
    "resize": "none",
    "max_length": 2000
  }
}
その他の選択肢(自由記述の「その他、ご記入ください」)

選択コンポーネント(multiple_choice、checkboxes、dropdown)では、訪問者が独自の回答を入力できる「その他」の項目を任意で追加できます。

プロパティ型デフォルト説明
other_optionbooleanfalse「その他」の選択肢を追加します。選択するとテキスト入力欄が表示されます。
other_labelstring"Other"「その他」ボタンのカスタムラベルです。
other_placeholderstring—自由記述入力欄のプレースホルダーです。
require_allbooleanfalse(checkboxes/multiple_choice)すべての選択肢を選択必須にします。required: true および require_all_fields と組み合わせると、すべての選択肢にチェックを入れ、すべての必須のネストされた入力欄に入力するまで、ボタンは無効のままになります。

値は __other__:<text> として保存されます。自由記述の代替入力を意図的に有効にする場合を除き、other_option: true は設定しないでください。そうしないと、予期しないテキストエリアが表示されます。

選択肢の見出しと小見出し

各選択肢では、主な表示テキストに heading を使用し、補足情報には任意で subheading を使用します。

{
  "options": [
    { "value": "solopreneur", "heading": "Solopreneur / Creator", "subheading": "I run my own business or personal brand" },
    { "value": "agency", "heading": "Agency / Freelancer", "subheading": "I build funnels and sites for clients" },
    { "value": "saas", "heading": "SaaS / Software" }
  ]
}
選択肢のリボン

multiple_choice と picture_choice の選択肢には、色をカスタマイズできるリボンバッジ(例:「おすすめ」、「お得」)を表示できます。

{
  "options": [
    { "value": "starter", "heading": "Starter", "subheading": "For individuals" },
    { "value": "pro", "heading": "Pro", "subheading": "For growing teams", "ribbon": "Recommended" },
    { "value": "enterprise", "heading": "Enterprise", "subheading": "Custom solutions", "ribbon": "Best Value", "ribbon_bg": "#10b981", "ribbon_color": "#fff" }
  ]
}
プロパティ型デフォルト説明
ribbonstring—選択肢に表示するバッジのテキスト
ribbon_bgstringテーマカラーリボンの背景色
ribbon_colorstring#fffリボンのテキスト色
無効な選択肢

multiple_choice、checkboxes、dropdown、picture_choice の各選択肢は、個別に disabled: true に設定できます。無効な選択肢は表示されますが選択できず、不透明度 50% と cursor-not-allowed で描画されます。今後の機能や「近日公開」のプランを示すのに便利です。

{
  "options": [
    { "value": "starter", "heading": "Starter — Free" },
    { "value": "pro", "heading": "Pro — $29/mo" },
    { "value": "enterprise", "heading": "Enterprise — Coming Soon", "disabled": true }
  ]
}
選択肢のメッセージ

multiple_choice と checkboxes の各選択肢には、選択時に表示するカスタム message を設定できます。クイズの採点とは独立して、状況に応じたヒント、個別のガイダンス、情報を伝えるコールアウトに使用します。

{
  "id": "team_size",
  "type": "multiple_choice",
  "props": {
    "label": "What's your team size?",
    "options": [
      { "value": "solo", "heading": "Just me", "message": "Great! Our solo plan is perfect for individual creators." },
      { "value": "small", "heading": "2-10 people", "message": "Nice! You'll love our team collaboration features." },
      { "value": "large", "heading": "10+ people", "message": "We recommend scheduling a demo for enterprise needs." }
    ]
  }
}

メッセージは、選択した選択肢の下に青い情報コールアウトとして表示されます。デフォルトでは、メッセージはクリックするとすぐに表示されます(option_message_trigger: "on_select")。訪問者が「続行」をクリックした後にのみメッセージを表示するには、option_message_trigger: "on_next" を設定します(クイズの reveal_on_select と同じ 2 段階のフローです)。

{
  "id": "goal",
  "type": "checkboxes",
  "props": {
    "label": "What are your goals?",
    "option_message_trigger": "on_next",
    "options": [
      { "value": "leads", "heading": "Generate leads", "message": "We'll set you up with our lead capture templates." },
      { "value": "sales", "heading": "Increase sales", "message": "Check out our checkout optimization guide after signup." },
      { "value": "brand", "heading": "Build brand awareness", "message": "Our content calendar tool is included free." }
    ]
  }
}
プロパティ型デフォルト説明
messagestring—選択時に表示する選択肢ごとのメッセージ(Option 上)
option_message_trigger"on_select" | "on_next""on_select"選択肢のメッセージを表示するタイミング(ChoiceProps 上)

注: 選択肢のメッセージは、クイズの option_messages とは別のものです。クイズの option_messages はクイズ設定で値をキーとして設定され、不正解の場合にのみ表示されます。選択肢の message フィールドは、クイズモードに関係なく、選択時に常に表示されます。

ピクチャー選択コンポーネント

画像カードを使ったビジュアルオプションピッカーです。各オプションには画像、見出し、値があります。単一選択と複数選択に対応しています。オプションには、補足情報を表示するための subheading を任意で含めることができます。

{
  "id": "platform",
  "type": "picture_choice",
  "props": {
    "label": "Select your platform",
    "required": true,
    "image_fit": "contain",
    "options": [
      { "heading": "X (Twitter)", "value": "twitter", "image": "https://example.com/x-logo.png" },
      { "heading": "LinkedIn", "subheading": "Best for B2B", "value": "linkedin", "image": "https://example.com/linkedin-logo.png" },
      { "heading": "Reddit", "value": "reddit", "image": "https://example.com/reddit-logo.png" }
    ]
  }
}
プロパティ型デフォルト説明
optionsarray[]{ heading, value, image } オブジェクトの配列です。image は URL です。補足テキストとして subheading を任意で指定できます
multiplebooleanfalse複数のオプションを選択できるようにします
image_fit"contain" / "cover""contain"カード内で画像をどのように表示するかを指定します。contain は余白を付けて画像全体を表示します(アスペクト比が異なる画像が混在する場合の安全なデフォルト設定です。ロゴ、アイコン、写真などに適しています)。cover はカード全体を埋めるように画像をトリミングします(すべての画像のアスペクト比が似ている場合にのみ使用してください)

image_fit の選び方: ロゴ、アイコン、またはサイズが異なる画像のセットには、デフォルトの "contain" を使用してください。すべての画像が完全に表示されます。すべての画像が、アスペクト比のそろった横長の写真またはイラストの場合にのみ、"cover" に切り替えてください。

画像コンポーネント

image 表示コンポーネントは、適切なデフォルト設定(幅いっぱい、cover フィット)で画像を表示します。CSS で設定を完全に上書きできます。

{
  "id": "hero_img",
  "type": "image",
  "props": {
    "src": "https://cdn.example.com/hero.webp",
    "alt": "Hero banner",
    "border_radius": 16,
    "link": "https://example.com"
  }
}
プロパティ型デフォルト説明
srcstring(必須)画像 URL。自動で最適化された WebP を使用するには、画像アップロード API の compressed_url を指定してください
altstring""アクセシビリティ用の代替テキスト
widthnumber/string—HTML の width 属性(レイアウトのずれを防ぐのに役立ちます)
heightnumber/string—HTML の height 属性(レイアウトのずれを防ぐのに役立ちます)
border_radiusnumber16px 単位の境界線の角丸半径
linkstring—クリック可能なリンクで画像を囲みます(新しいタブで開きます)
use_originalbooleanfalse圧縮版ではなく、元の(非圧縮)画像 URL を使用します
CSS による画像のカスタムスタイリング

デフォルトでは、画像は幅いっぱいに表示され、object-fit: cover が設定されます。サイズ、配置、またはレスポンシブ動作をカスタマイズするには、settings.custom_css を使用し、コンポーネントの id で画像を指定してください。

各画像コンポーネントのラッパー div には id="{componentId}" が設定されるため、#componentId img で <img> タグを指定できます。

例:最大幅とレスポンシブブレークポイントを設定した中央揃えの画像

{
  "settings": {
    "custom_css": "#hero_img img { max-width: 600px; margin: 0 auto; object-fit: contain; } @media (max-width: 768px) { #hero_img img { max-width: 100%; } }"
  },
  "pages": [
    {
      "id": "landing",
      "components": [
        { "id": "hero_img", "type": "image", "props": { "src": "https://cdn.example.com/hero.webp" } }
      ]
    }
  ]
}

例:モバイルとデスクトップでサイズが異なる画像を横に並べる

{
  "settings": {
    "custom_css": "#photo_a img, #photo_b img { object-fit: contain; height: 200px; } @media (min-width: 768px) { #photo_a img, #photo_b img { height: 350px; } }"
  },
  "pages": [
    {
      "id": "gallery",
      "components": [
        { "id": "photo_a", "type": "image", "width": "half", "props": { "src": "https://cdn.example.com/a.webp" } },
        { "id": "photo_b", "type": "image", "width": "half", "props": { "src": "https://cdn.example.com/b.webp" } }
      ]
    }
  ]
}

よく使用する CSS の上書き設定:

CSS プロパティ例用途
max-widthmax-width: 400px画像の幅を制限します(margin: 0 auto で中央揃え)
object-fitobject-fit: containトリミングせずに画像全体を表示します(デフォルトは cover)
heightheight: 300px高さを固定します(object-fit と組み合わせて使用)
border-radiusborder-radius: 50%境界線の角丸半径を CSS で上書きします(円形など)
opacityopacity: 0.8画像を半透明にします
filterfilter: grayscale(100%)CSS フィルターを適用します

ヒント: デフォルトの .ck-img クラスでは、width: 100%; height: auto; object-fit: cover が設定されています。#id img セレクターは詳細度が高いため、!important を使わずにこれらの設定を上書きできます。

見出しコンポーネント

heading 表示コンポーネントは、3 つのテキストレベルに対応しています。

{
  "id": "hero",
  "type": "heading",
  "props": {
    "micro_heading": "Welcome to the program",
    "text": "Heading Title",
    "subtitle": "Supporting text below the heading",
    "level": 1,
    "align": "left"
  }
}
プロパティ型デフォルト説明
textstring(必須)メインの見出しテキスト
level1–61HTML の見出しレベル(h1–h6)。サイズを指定します
micro_headingstring—見出しの上に表示する小さな大文字のテキスト
subtitlestring—見出しの下に表示する補足テキスト
align"left" / "center" / "right""left"テキストの配置

3 つすべてを積み重ねると、完全な見出しブロックになります。マイクロ見出し(小さな大文字)、メインの見出し(太字)、サブタイトル(細字)です。

ページアクションとCTAボタン

ページアクションボタン(およびデフォルトの送信/続行ボタン)では、コンバージョンを高めるために side_statement と reassurance のテキストを設定できます。

ページアクションの場合:

{
  "actions": [
    {
      "id": "cta",
      "label": "Get Started Now",
      "style": "primary",
      "side_statement": "No credit card required",
      "reassurance": "Cancel anytime. 30-day money back guarantee."
    }
  ]
}

デフォルトの送信ボタンの場合:

{
  "title": "Your Details",
  "submit_label": "Continue",
  "submit_side_statement": "Takes only 2 minutes",
  "submit_reassurance": "Your information is secure and never shared.",
  "components": [...]
}
プロパティ型説明
side_statementstringボタンの右側にインラインで表示されるテキスト
reassurancestringボタンの下に小さく控えめに表示されるテキスト
submit_side_statementstringside_statement と同じですが、デフォルトの送信ボタン(ページレベル)に適用されます
submit_reassurancestringreassurance と同じですが、デフォルトの送信ボタン(ページレベル)に適用されます
button_disabled_messagestring無効なボタンをクリックしたときに表示されるエラーメッセージ(デフォルト:"Please fill in all required fields")。require_all_fields またはスクリプトで無効化されたボタンと併用します
送信ボタンを非表示にする

入力コンポーネントがなく、明示的な submit_label も設定されていない場合、最後のページでは送信/続行ボタンが自動的に非表示になります。それ以外の場合は、次の方法があります。

方法手順
自動非表示(最後のページ、入力項目なし)ページに入力コンポーネントを追加しないだけで、ボタンは自動的に非表示になります
任意のページで強制的に非表示ページオブジェクトに "hide_navigation": true を設定します
戻るボタンのみを非表示ページオブジェクトに "hide_back": true を設定します
カスタムボタンに置き換えページで "actions" 配列を使用すると、デフォルトのボタンが完全に置き換わります
{
  "title": "Thank You!",
  "hide_navigation": true,
  "components": [
    { "id": "thanks", "type": "text", "props": { "content": "We'll be in touch soon." } }
  ]
}
埋め込みボタン

multiple_choice、checkboxes、timeline、およびチェックアウトのカート項目にインラインボタンを追加できます。ボタンは各選択肢やタイムライン項目の横に表示されます。「リンクを開いた後にチェックボックスをオンにする」といったパターンに便利です。カート項目では、サイドリンク用の button(例:「詳細を見る」)を設定できます。タイムライン項目では、説明の下ではなくタイトルとインライン(カードの右上)に表示する side_button も設定できます。

選択肢の場合(multiple_choice/checkboxes):

{
  "id": "checklist",
  "type": "checkboxes",
  "props": {
    "label": "Complete These Steps",
    "options": [
      {
        "value": "download",
        "label": "Download Telegram",
        "button": { "label": "Open Telegram", "url": "https://t.me/download", "style": "primary", "size": "sm" }
      },
      {
        "value": "message",
        "label": "Message Coach AI",
        "button": { "label": "Open Chat", "url": "https://t.me/coach_bot", "target": "_blank", "icon": "💬" }
      }
    ]
  }
}

タイムライン項目の場合:

{
  "id": "steps",
  "type": "timeline",
  "props": {
    "items": [
      {
        "title": "Open Setter Coach AI",
        "description": "Your AI assistant walks you through Day 1.",
        "button": { "label": "Open Chat", "url": "https://t.me/coach_bot", "style": "primary", "size": "sm" },
        "checkbox": true
      },
      {
        "title": "Join Call Center",
        "description": "Get access to the team channel.",
        "button": { "label": "Join Channel", "url": "https://t.me/channel", "style": "outline" },
        "side_button": { "label": "Preview", "url": "https://t.me/channel/preview", "style": "ghost", "size": "sm" },
        "checkbox": { "label": "Joined" }
      }
    ]
  }
}

チェックアウトのカート項目の場合(ページオファー経由):

カート項目には、価格の横にサイドリンクとして表示される任意の button を設定できます。「詳細を見る」や「詳しくはこちら」などのリンクに便利です。

{
  "offer": {
    "id": "growth-bundle",
    "title": "Growth Bundle",
    "price_display": "$49/mo",
    "stripe_price_id": "price_...",
    "button": { "label": "Details", "url": "https://example.com/growth", "style": "secondary", "size": "sm" }
  }
}

ボタンのプロパティ:

プロパティ型デフォルト説明
labelstring(必須)ボタンのテキスト
urlstring(必須)リンク先URL
target"_blank" / "_self""_blank"新しいタブまたは同じタブで開きます
size"sm" / "md" / "lg""sm"ボタンのサイズ
style"primary" / "secondary" / "outline" / "ghost""primary"表示スタイル(テーマカラーを使用)
iconstring—ラベルの前に表示する絵文字またはテキストアイコン

タイムラインのチェックボックス: シンプルな「完了」チェックボックスには checkbox: true を、カスタムラベルを設定するには checkbox: { "label": "参加済み" } を指定します。チェックボックスは見た目上のものであり(クライアント側で切り替わります)、フォームデータとしては記録されません。

事前入力モードと読み取り専用コピー

入力コンポーネントは、事前入力された値の表示方法を制御する prefill_mode プロパティに対応しています。

  • "editable"(デフォルト)— 事前入力された値を通常の編集可能な入力欄に表示します
  • "readonly" — 値を、スタイル付きの読み取り専用入力欄にクリップボードへのコピーボタンとともに表示します。ユーザーはクリップボードアイコンをクリックして値をコピーできます。生成されたコード、API キー、紹介リンクなど、ユーザーがコピーする必要はあっても編集すべきではない値の表示に便利です。
  • "hidden" — 事前入力された場合、コンポーネントを完全に非表示にします(データを目立たないように渡す場合に便利です)。重要: フィールドが非表示になるのは、事前入力(URL パラメーターまたはデフォルト値)で値を受け取った場合のみです。事前入力値が指定されていない場合、フィールドは通常の編集可能な入力欄として表示されます。このモードは、カタログ間でデータを目立たないように渡すためのものです。ユーザーが手動で入力することを想定したフィールドには使用しないでください。
{
  "id": "referral_code",
  "type": "short_text",
  "props": { "label": "Your Referral Code" },
  "prefill_mode": "readonly"
}

値を事前入力するには、コンポーネント ID に一致する URL パラメーターとして渡します。例:?referral_code=ABC123。読み取り専用入力欄にはクリップボードアイコンが表示されます。クリックすると値がコピーされ、チェックマークが短時間表示されます。

コピー可能(コピーアイコン付きの編集可能な入力欄)

編集可能なまま、ユーザーが簡単に値をコピーできるようにするには、props で copyable: true を使用します。

{
  "id": "generated_link",
  "type": "url",
  "props": { "label": "Your Share Link", "copyable": true }
}

入力欄に値が入ると、その横にコピーアイコンが表示されます。クリックすると現在の値がクリップボードにコピーされ、チェックマークが表示されます。readonly とは異なり、フィールドは引き続き完全に編集可能です。対応コンポーネント:short_text、long_text、rich_text、email、phone、url、number、currency、date、datetime、time、password、dropdown、address。

ページの自動スキップ

ページで auto_skip: true を設定すると、表示されているすべての入力フィールドに値がすでに入っている場合、そのページを自動的にスキップします。URL パラメーターやデフォルト値によってページに事前入力される複数ステップのファネルで便利です。訪問者はページを見ずに次のページへ進みます。

{
  "collect_info": {
    "title": "Your Details",
    "auto_skip": true,
    "components": [
      { "id": "email", "type": "email", "props": { "label": "Email", "required": true } },
      { "id": "name", "type": "short_text", "props": { "label": "Name", "required": true } }
    ]
  }
}

[email protected]&name=John(prefill_mappings でマッピング)を指定すると、このページは完全にスキップされます。ルールは次のとおりです。

  • ページに表示可能な入力欄が少なくとも1つあり、そのすべてに値が入っている場合にのみスキップします
  • 表示専用ページ(入力欄なし)は自動スキップされません
  • on_enter フックの実行後に処理されるため、フックで設定した値によってスキップ条件を満たすことができます
  • スキップされたページはブラウザーの履歴に表示されません(戻るボタンでそのページを飛ばします)
  • スキップされたページごとに page_auto_skipped アナリティクスイベントが発火します
自動スキップを使ったカタログの連携

よくあるパターンとして、2つのカタログを連携させる方法があります。たとえば、登録フォームからオンボーディングフローにリダイレクトし、収集したデータを引き継ぐことで、すでに回答済みのページをスキップできます。

ステップ 1:カタログ A — フォームの値を URL パラメーターとして渡してリダイレクトする

settings.completion.redirect_url と {{field_id}} テンプレートを使って、フォームデータを次のカタログに渡します。

{
  "settings": {
    "completion": {
      "redirect_url": "https://yoursubdomain.catalogkit.cc/onboarding?email={{comp_email}}&name={{comp_name}}&phone={{comp_phone}}",
      "redirect_delay": 0
    }
  }
}

ステップ 2:カタログ B — URL パラメーターをコンポーネント ID にマッピングし、自動スキップを有効にする

受け取り側のカタログで prefill_mappings を設定し、URL パラメーターから適切なフィールドに値を設定します。また、事前入力時に表示しないページでは auto_skip: true を設定します。

{
  "settings": {
    "url_params": {
      "prefill_mappings": {
        "email": "comp_email",
        "name": "comp_name",
        "phone": "comp_phone"
      }
    }
  },
  "pages": {
    "contact_info": {
      "title": "Your Contact Info",
      "auto_skip": true,
      "components": [
        { "id": "comp_email", "type": "email", "props": { "label": "Email", "required": true } },
        { "id": "comp_name", "type": "short_text", "props": { "label": "Name", "required": true } },
        { "id": "comp_phone", "type": "phone", "props": { "label": "Phone" } }
      ]
    },
    "preferences": {
      "title": "Your Preferences",
      "components": [...]
    }
  }
}

訪問者が [email protected]&name=John&phone=555 を使ってカタログ B にアクセスすると、contact_info ページは自動的にスキップされ、preferences に直接移動します。パラメーターが不足している場合は、一部の項目に値が事前入力された状態でページが表示されます。

必須フィールドへの入力が完了するまでボタンを無効化

現在のページに表示されている required フィールドに空欄があると、続行/送信ボタンは自動的に無効化されます。各フィールドに required: true を設定するだけでよく、ページレベルのフラグは必要ありません。

{
  "contact_info": {
    "title": "Your Details",
    "components": [
      { "id": "email", "type": "email", "props": { "label": "Email", "required": true } },
      { "id": "name", "type": "short_text", "props": { "label": "Name", "required": true } },
      { "id": "newsletter", "type": "checkbox", "props": { "label": "Subscribe to newsletter" } }
    ]
  }
}

この例では、email と name の両方に値が入力されるまで、ボタンは無効のままです。任意項目の newsletter チェックボックスは、ページ遷移を妨げません。

オプトアウト: 以前のクリック後に検証する動作(ボタンは有効のままで、クリック時にエラーを表示)に戻すには、ページに明示的に require_all_fields: false を設定します。

仕組み:

  • 表示中で、読み取り専用でも非表示でもない必須フィールドのみをチェックします
  • 表示条件に従います。必須フィールドが条件によって非表示になっている場合、入力を妨げません
  • 配列(複数選択、チェックボックス)にも対応し、value.length > 0 をチェックします
  • require_all プロパティ(checkboxes/multiple_choice): checkboxes または multiple_choice コンポーネントに true を設定すると、(1つだけでなく)すべての選択肢を選択する必要があります。選択状態にかかわらず、ネストされた必須入力もすべて検証されます。
  • ブール型フィールド(switch、checkbox)には truthy な値が必要です。必須の switch/checkbox は、検証を通過するためにオンにする必要があります。オフにすると、ボタンは再び無効になります。
  • インラインボタンと画面下部に固定表示されるバーの両方で機能します
  • チェックされたチェックボックスのネストされた入力は検証に含まれます(require_all: true の場合は、すべてのネストされた入力が対象です)
  • 形式の検証: アドレス型(solana_address、evm_address、bitcoin_address)は、値が入力されていても形式が無効な場合(有効な base58 Solana アドレスではない場合など)、ボタンを無効のままにします。トップレベルの入力とネストされた入力の両方に適用されます。
  • スクリプトの propOverrides に従います。スクリプトが ctx.setProp() を使ってコンポーネントの required、hidden、readonly を動的に設定すると、ボタンの状態はリアルタイムで更新されます
  • ボタンは無効時に、不透明度 50%、cursor-not-allowed で表示されます
スクリプトで制御するボタンの状態

より複雑なロジック(非同期検証、API チェックなど)には、スクリプトフックで setButtonDisabled() と setButtonLoading() を使用します。

{
  hooks: {
    on_enter: (ctx) => {
      // Disable button until an API call succeeds
      ctx.setButtonDisabled(true);
      ctx.setButtonLoading(true);

      ctx.fetch("https://api.example.com/check")
        .then(r => r.json())
        .then(data => {
          ctx.setField("status", data.status);
          ctx.setButtonDisabled(false);
          ctx.setButtonLoading(false);
        });
    }
  }
}

両方の方法を組み合わせることもできます。必須フィールドのチェックはシンプルなケースを自動的に処理し、スクリプトからの setButtonDisabled(true) で追加のブロック条件を設定できます。必須フィールドに未入力がある場合、またはスクリプトから setButtonDisabled(true) が呼び出された場合、ボタンは無効になります。

setButtonLoading(true) を指定すると、ボタンにスピナーアニメーションが表示されます。ユーザーに待機してもらう必要がある API 呼び出しなどの非同期処理に便利です。

setButtonDisabled と setButtonLoading はどちらも、ページ遷移時に自動的に false にリセットされます。

スクリプトで制御する検証エラー

setValidationError(componentId, message) を使用すると、スクリプトから任意のフィールドにカスタムエラーメッセージを表示できます。クリアするには null を渡します。

{
  hooks: {
    on_change: async (ctx) => {
      // Custom async validation or LLM-powered feedback
      const resp = await ctx.fetch("https://api.example.com/validate", {
        method: "POST",
        body: JSON.stringify({ answer: ctx.field_value }),
      });
      const data = await resp.json();
      if (!data.valid) {
        ctx.setValidationError(ctx.field_id, data.feedback); // e.g. "Almost! Think about X"
      } else {
        ctx.setValidationError(ctx.field_id, null); // Clear error
      }
    }
  }
}

これはクイズコンポーネントだけでなく、あらゆる入力タイプで機能します。on_change フックと組み合わせると、ユーザーの入力や選択に応じて REST API や LLM からリアルタイムのフィードバックを提供できます。

コンポーネントの幅(複数列レイアウト)

任意のコンポーネントに width プロパティを設定して、横並びのレイアウトを作成できます。幅が全幅未満の隣接するコンポーネントは、自動的にフレックス行にまとめられます。

値: "full"(デフォルト)、"half"、"third"、"two_thirds"

{
  "components": [
    { "id": "phone_img", "type": "image", "width": "half", "props": { "src": "https://example.com/phone.png" } },
    { "id": "phone_text", "type": "paragraph", "width": "half", "props": { "text": "**Your Phone**\n\nThis gig is 100% mobile-friendly." } },
    { "id": "leads_img", "type": "image", "width": "half", "props": { "src": "https://example.com/leads.png" } },
    { "id": "leads_text", "type": "paragraph", "width": "half", "props": { "text": "**Leads Vending Machine**\n\nGet your daily prospects." } }
  ]
}

モバイルではコンポーネントが縦に並び、デスクトップでは横に並びます。幅は自由に組み合わせられます。たとえば、サイドバーのレイアウトには "third" と "two_thirds" を組み合わせます。

複数ページのルーティング

回答に応じて、訪問者を異なるページに誘導できます。

{
  "routing": {
    "entry": "landing",
    "edges": [
      {
        "from": "landing",
        "to": "enterprise",
        "conditions": {
          "match": "all",
          "rules": [{ "field": "company_size", "operator": "greater_than", "value": 100 }]
        }
      },
      { "from": "landing", "to": "standard", "is_default": true }
    ]
  }
}

条件演算子: equals、not_equals、contains、not_contains、greater_than、less_than、greater_than_or_equal、less_than_or_equal、starts_with、ends_with、regex、in、not_in、is_empty、is_not_empty、between

クイズの採点

複数選択または入力コンポーネントにクイズの採点を追加します。

{
  "id": "q1",
  "type": "multiple_choice",
  "label": "What does CTA stand for?",
  "options": ["Click To Act", "Call To Action", "Create The Ad"],
  "quiz": {
    "correct_answer": "Call To Action",
    "points": 10,
    "explanation": "CTA = Call To Action",
    "wrong_message": "Not quite — CTA stands for Call To Action!",
    "correct_message": "You nailed it!",
    "option_messages": {
      "Click To Act": "Close, but 'Click To Act' isn't a standard marketing term.",
      "Create The Ad": "That's a common misconception — CTA is about the action, not the ad."
    }
  }
}
  • wrong_message — 回答が間違っている場合に表示するカスタムテキスト(デフォルト:「不正解です。」)
  • correct_message — 回答が正しい場合に表示するカスタムテキスト(デフォルト:「正解です!」)
  • option_messages — 選択肢の値をキーとして、特定の間違った選択肢が選ばれた場合に表示する選択肢ごとのメッセージ(その選択肢については wrong_message より優先されます)

採点では大文字と小文字を区別せず、型の不一致も許容します。たとえば、ユーザーが "call to action" を選択した場合も correct_answer: "Call To Action" と一致します。また、単一選択入力では correct_answer: ["c"](要素が1つの配列)も correct_answer: "c" と同様に機能します。

スコアに基づくルーティング:{ "score": "percent", "operator": "greater_than", "value": 80 }

インラインのクイズフィードバック(続行時に表示)

クイズ設定に reveal_on_select: true を追加すると、訪問者が「続行」をクリックしたときに正誤フィードバックを表示できます。

{
  "id": "q1",
  "type": "multiple_choice",
  "label": "What's the catch?",
  "options": [
    { "value": "a", "heading": "No Babysitting Policy" },
    { "value": "b", "heading": "Must show up consistently" },
    { "value": "c", "heading": "All of the Above" }
  ],
  "quiz": {
    "correct_answer": "c",
    "points": 10,
    "explanation": "All three are true — this program rewards effort.",
    "reveal_on_select": true
  }
}

reveal_on_select が true の場合、フローは次の2段階になります。

  1. 訪問者は自由に回答を選択します(選択肢はロックされません)。
  2. 「続行」をクリックすると、回答が表示されます。
    • 正解には緑色の枠線が付きます。
    • 間違った選択には赤色の枠線が付きます。
    • フィードバックバナーに correct_message/wrong_message が表示されます(option_messages[value] が設定されている場合は選択肢ごとのメッセージが表示されます)。
    • 解説テキストが表示されます(設定されている場合)。
    • 選択肢がロックされます。
    • 「回答を表示しました!上で結果を確認してから、[続行]をクリックしてください。」というバナーが表示されます。
    • 「続行」ボタンが表示されるように、ページが自動スクロールします。
  3. 訪問者がもう一度「続行」をクリックすると、次のページに進みます。

multiple_choice(単一選択)と checkboxes(複数選択)の両方のコンポーネントで機能します。デフォルトの動作(インラインフィードバックなし。代わりに後のページで reveal_answers を使用)にするには、reveal_on_select を省略するか、false に設定します。

回答の表示(結果ページ)

ページ設定に reveal_answers を追加すると、任意のページにクイズ結果の概要を表示できます。これは推奨される宣言的な方法で、スクリプトは不要です。

{
  "results": {
    "reveal_answers": {
      "from_pages": ["quiz_page_1", "quiz_page_2"],
      "show_score": true,
      "show_correct": true,
      "show_explanation": true
    }
  }
}

スコアの概要(例:「7 / 10」)と、正誤の表示、解説、間違った回答のメッセージを含む、問題ごとの内訳が自動的に表示されます。

カスタムの結果 UI(動的な見出しや、スコアに応じた紹介リンクなど)を使用する場合は、結果ページのスクリプトで kit.getQuizScores() を使用します。

{
  "id": "results_script",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\nkit.on('pageenter:results', () => {\n  const s = kit.getQuizScores();\n  document.querySelector('#score').textContent = s.total + ' / ' + s.max;\n});\n</script>"
  }
}
タイムライン

交互配置または片側配置のレイアウトで、縦型タイムラインを表示します。

{
  "id": "process",
  "type": "timeline",
  "props": {
    "variant": "alternating",
    "items": [
      { "title": "Step 1: Setup", "description": "Create your account", "icon": "🏠", "color": "#f59e0b" },
      { "title": "Step 2: Configure", "description": "Set up your campaign", "icon": "🔍", "color": "#ef4444" },
      { "title": "Step 3: Launch", "description": "Go live", "icon": "📅", "color": "#22c55e" }
    ]
  }
}

バリエーション: "default"(すべての項目を右側に配置)、"alternating"(デスクトップでは左右交互に配置し、モバイルでは縦に積み重ねます)。

各項目で使用できるプロパティ:title(必須)、description(省略可能、Markdown)、icon(色付きの円内に表示する絵文字)、image(円形画像の URL)、color(項目ごとの色。未指定の場合はテーマの色を使用)、button(説明の下に埋め込むボタン。埋め込みボタンを参照)、side_button(カード上部右側のタイトルと並べて表示する埋め込みボタン)、checkbox(インタラクティブなチェックボックス。true または { "label": "カスタム" })。

ファイルのアップロード

ドラッグ&ドロップで単一または複数のファイルをアップロードします。ファイル形式のフィルタリング、サイズ制限、複数ファイルモードに対応しています。

{
  "id": "resume",
  "type": "file_upload",
  "props": {
    "label": "Upload your resume",
    "accept": ".pdf,.doc,.docx",
    "max_size_mb": 10,
    "required": true
  }
}

複数ファイルの例:

{
  "id": "portfolio",
  "type": "file_upload",
  "props": {
    "label": "Upload portfolio images",
    "multiple": true,
    "accept": "image/*",
    "max_files": 5,
    "max_size_mb": 10
  }
}

プロパティ: multiple(ブール値、デフォルトは false)、accept(文字列。例:"image/*,.pdf")、max_files(数値、デフォルトは 10)、max_size_mb(数値、デフォルトは 25)。

パスワード

表示/非表示を切り替えられるボタン(目のアイコン)付きのパスワード入力です。デフォルトでは type="password" を使用し、ユーザーが目のアイコンをクリックすると type="text" に切り替わります。

{
  "id": "user_password",
  "type": "password",
  "props": {
    "label": "Create a password",
    "placeholder": "Enter password",
    "required": true
  }
}
署名

署名を記録するための、キャンバスベースの描画パッドです。値は base64 PNG のデータ URL として保存されます。リセット用の「クリア」ボタンが付属します。

{
  "id": "consent_signature",
  "type": "signature",
  "props": {
    "label": "Sign below to confirm",
    "required": true
  }
}
ウォレットアドレス入力

インライン検証に対応した、検証済みのウォレットアドレス入力タイプが3種類あります。

  • evm_address — Ethereum/EVM アドレス(0x + 40桁の16進数)
  • solana_address — Solana アドレス(32〜44文字のbase58)
  • bitcoin_address — Bitcoin アドレス(Legacy、P2SH、Bech32、Taproot)
{
  "id": "eth_wallet",
  "type": "evm_address",
  "props": { "label": "Your ETH Wallet", "required": true }
}
{
  "id": "sol_wallet",
  "type": "solana_address",
  "props": { "label": "Solana Wallet" }
}
{
  "id": "btc_wallet",
  "type": "bitcoin_address",
  "props": { "label": "Bitcoin Address" }
}

3種類すべて、リアルタイムの形式検証と視覚的なフィードバック(緑色のチェック/赤色のX)に対応した、等幅フォントのテキスト入力として表示されます。

お客様の声のサイズとリンク

testimonial コンポーネントは、レイアウトの密度に応じた size バリエーションに対応しています。

{
  "id": "review",
  "type": "testimonial",
  "props": {
    "text": "This changed everything for our team.",
    "author": "Jane Smith",
    "subtitle": "CEO at Acme Inc.",
    "avatar": "https://example.com/jane.jpg",
    "rating": 5,
    "link": "https://twitter.com/janesmith",
    "variant": "card",
    "size": "medium"
  }
}
プロパティ型デフォルト説明
textstring(必須)引用文
authorstring(必須)著者名
subtitlestring—役職、会社名、またはサブタイトルのテキスト(別名:role)
avatarstring—プロフィール画像のURL
ratingnumber (1-5)—星評価
linkstring—著者名をクリック可能なリンクにします
variant"card" / "quote" / "minimal""card"レイアウトスタイル
size"compact" / "medium" / "large""medium"パディング、テキストサイズ、アバターサイズを調整します
コールアウト

ヒント、警告、注記、その他の重要な情報を強調表示するコールアウトボックスです。6種類のプリセットスタイルに対応し、折りたたみ可能なモードも設定できます。

{
  "id": "important",
  "type": "callout",
  "props": {
    "style": "warning",
    "title": "Important Notice",
    "text": "Complete all steps within 48 hours to keep your spot."
  }
}

折りたたみ可能なコールアウト:

{
  "id": "faq-note",
  "type": "callout",
  "props": {
    "style": "tip",
    "title": "Pro Tip",
    "text": "You can use **markdown** in the body text.",
    "collapsible": true
  }
}
プロパティ型デフォルト説明
style"info" / "tip" / "warning" / "danger" / "note" / "success""info"ビジュアルプリセット(色 + デフォルトのアイコン)
titlestring—太字の見出しテキスト
textstring—本文テキスト(Markdown に対応)
iconstring—デフォルトのアイコン(絵文字)を上書きします
collapsiblebooleanfalse展開/折りたたみ可能な形式で表示します(title が必要です)
Iframe コンポーネント

外部URLをカタログに埋め込みます。src では {{field_id}} テンプレートに対応しており、訪問者がフィールドに入力すると動的にURLが更新されます。

{
  "id": "demo_embed",
  "type": "iframe",
  "props": {
    "src": "https://app.example.com/preview?email={{comp_email}}&plan={{comp_plan}}",
    "height": 500,
    "border_radius": 12,
    "title": "Live Preview"
  }
}
プロパティ型デフォルト説明
srcstring—埋め込むURLです。{{field_id}} テンプレートに対応しています(値はURLエンコードされます)
heightnumber | string400px単位またはCSS値で指定する高さ
widthstring"100%"CSSの幅の値
border_radiusnumber16px単位の角丸半径
sandboxstring"allow-scripts allow-same-origin allow-forms"iframe の sandbox 属性
allowstring""iframe の allow 属性(例:"camera; microphone")
borderbooleanfalseiframe の周囲に枠線を表示します
titlestring"Embedded content"アクセシビリティ用のタイトル

iframe のURLはリアクティブに再解決されます。訪問者が comp_email に入力すると、更新されたURLで iframe がすぐに再読み込みされます。

モーダル(情報ポップアップ)

スクロール可能なモーダルダイアログを開くボタンです。利用規約、プライバシーポリシー、詳細な製品情報など、ページが煩雑になるコンテンツに最適です。本文では、Markdown 形式の書式(太字、斜体、リンク、リスト)を使用できます。

基本的なモーダル(静的コンテンツのみ):

{
  "id": "terms_modal",
  "type": "modal",
  "props": {
    "button_label": "View Terms & Conditions",
    "button_style": "link",
    "title": "Terms & Conditions",
    "body": "## 1. Acceptance of Terms\n\nBy accessing and using this service, you accept and agree to be bound by the terms...\n\n## 2. Use License\n\n- Permission is granted to temporarily use this service\n- This is the grant of a license, not a transfer of title\n\n## 3. Disclaimer\n\nThe materials on this website are provided on an **as is** basis...",
    "max_width": "640px"
  }
}

入力フィールドを埋め込んだモーダル(内容を確認して署名するパターン):

モーダルの本文には、入力コンポーネントや表示コンポーネントを埋め込めます。confirm_sets_field を使用すると、ユーザーが確定したときにフォームフィールドの値を自動設定できます(例:署名後にチェックボックスを自動的にオンにする)。require_inputs を使用すると、必須の埋め込み入力がすべて入力されるまで確定ボタンを無効にできます。

{
  "id": "agreement_modal",
  "type": "modal",
  "props": {
    "button_label": "Read & Sign Agreement",
    "button_style": "outline",
    "title": "Service Agreement",
    "body": "Please read the following terms carefully and sign below to confirm your acceptance.",
    "components": [
      { "id": "agreement_text", "type": "paragraph", "props": { "text": "By signing below you agree to all terms and conditions..." } },
      { "id": "sig", "type": "signature", "label": "Your Signature", "required": true },
      { "id": "typed_name", "type": "short_text", "label": "Type your full name", "required": true, "placeholder": "John Doe" }
    ],
    "confirm_label": "I Agree",
    "confirm_sets_field": { "field_id": "terms_accepted", "value": true },
    "require_inputs": true
  }
}

埋め込みコンポーネントの値は、複合 ID modalComponentId.nestedInputId(例:agreement_modal.sig、agreement_modal.typed_name)で保存されます。

Prop型デフォルト説明
button_labelstring"View"トリガーボタンに表示するテキスト
button_style"primary" | "outline" | "ghost" | "link""primary"トリガーボタンの表示スタイル
titlestring—モーダルの上部に表示されるヘッダー
bodystring""スクロール可能なコンテンツ(Markdown 形式の太字、斜体、リンク、リストに対応)
max_widthstring"640px"モーダルダイアログの最大幅
componentsarray—モーダル本文内に表示される埋め込み入力/表示コンポーネント(チェックボックスのネストされた入力と同じ形式)
confirm_labelstring"Confirm" / "Close"フッターボタンのラベル。components がある場合は「Confirm」、ない場合は「Close」がデフォルトです
confirm_sets_field{ field_id, value }—確定時に、このフィールドに指定した値を設定します(例:チェックボックスを自動的にオンにする)
require_inputsbooleanfalse必須の埋め込み入力がすべて入力されるまで確定ボタンを無効にします

モーダルは、X ボタンをクリックする、Escape キーを押す、背景のオーバーレイをクリックする、またはフッターボタンをクリックすると閉じます。components がある場合は、確定ボタンの横にキャンセルボタンが表示されます。

カスタム React コンポーネント

組み込みのコンポーネントタイプでは実現できない、React の高度なインタラクティブ機能が必要なユーザー向けです。外部スクリプトから独自の React コンポーネントを読み込み、名前で参照します。

ステップ 1: コンポーネントを window.__catalogkit_components に登録するスクリプトタグを追加します。

{
  "settings": {
    "scripts": [
      { "src": "https://cdn.example.com/my-components.js", "position": "head" }
    ]
  }
}

ステップ 2: スクリプト内でコンポーネントを登録します。

// my-components.js
window.__catalogkit_components = window.__catalogkit_components || {};

window.__catalogkit_components.PriceCalculator = ({ formState, setField, themeColor, quantity }) => {
  const price = (quantity || 1) * 29.99;
  return React.createElement('div', {
    style: { padding: '16px', borderRadius: '12px', border: '1px solid #e5e7eb' }
  },
    React.createElement('p', { style: { fontSize: '24px', fontWeight: 'bold', color: themeColor } },
      '$' + price.toFixed(2)
    ),
    React.createElement('button', {
      onClick: () => setField('comp_price', price),
      style: { marginTop: '8px', padding: '8px 16px', backgroundColor: themeColor, color: 'white', borderRadius: '8px', border: 'none', cursor: 'pointer' }
    }, 'Lock in price')
  );
};

ステップ 3: カタログスキーマで参照します。

{
  "id": "price_calc",
  "type": "custom",
  "props": {
    "component": "PriceCalculator",
    "quantity": 3
  }
}

コンポーネントに渡される Props:

Prop説明
themeColorカタログのテーマカラー(16 進数文字列)
formStateすべてのフォームフィールド値の読み取り専用スナップショット
setField(componentId, value)任意のフォームフィールド値を設定
...propsスキーマから渡されるその他すべての Props(上記の quantity など)

重要な注意事項:

  • スクリプトはコンポーネントを window.__catalogkit_components に登録する必要があります。レンダラーはページ読み込み後、最大 5 秒間登録を確認します
  • コンポーネントはエラーバウンダリーでラップされます。コンポーネントでエラーが発生した場合、カタログがクラッシュする代わりに、わかりやすいエラーメッセージが表示されます
  • React はグローバルに利用できます(カタログですでに読み込まれています)。React.createElement を使用するか、JSX を独自にバンドルしてください
  • 組み込みコンポーネントと同様に、formState が変わるとコンポーネントが再レンダリングされます
  • TypeScript のカタログでも、type: "custom" は同じように機能します
タイムライン内のネストされた入力

タイムライン項目では、タイムラインカード内に入力フィールドを埋め込むための inputs 配列を使用できます。ネストされた入力は、インデントされた左罫線付きのパネルに表示されます。値は複合 ID timelineComponentId.inputId で保存されます。

{
  "id": "onboarding",
  "type": "timeline",
  "props": {
    "items": [
      {
        "title": "Set Your Availability",
        "description": "Choose when you're free to take calls.",
        "icon": "📅",
        "inputs": [
          { "id": "timezone", "type": "dropdown", "label": "Timezone", "props": { "options": ["EST", "CST", "PST"] } },
          { "id": "hours", "type": "short_text", "label": "Available hours", "placeholder": "e.g. 9am-5pm" }
        ]
      },
      {
        "title": "Upload ID",
        "description": "We need a photo ID for verification.",
        "icon": "🪪",
        "inputs": [
          { "id": "id_photo", "type": "file_upload", "label": "Photo ID", "props": { "accept": "image/*" } }
        ]
      }
    ]
  }
}
セクションカードとしてのチェックボックス

チェックボックスは、主要なセクションカードコンポーネントです。各オプションはカードとして機能し、切り替え行の下にネストされたサブコンポーネント(入力、表示コンテンツ、さらには別のチェックボックスも含む)を配置できます。切り替え部分とネストされたコンテンツは別々の DOM 領域に配置されているため、ネストされた入力をクリックしてもチェックボックスが誤って切り替わることはありません。 デフォルトの動作: ネストされた入力項目は常に表示されるため、ユーザーはチェックする前に必要な項目を確認できます。すべての required なネスト入力項目に値が入力されると、チェックボックスは自動的にチェックされます。つまり、ユーザーがフィールドに入力するだけで、チェックボックスは完了状態になります。

条件付きレンダリング: オプションに expand_on_select: true を設定すると、チェックボックスが手動でチェックされるまで、そのオプションのネスト入力項目は非表示になります(従来の動作)。これらのオプションでは自動チェックは無効になります。

オプションでは、value、heading、subheading、image(サムネイル)、button(サイドリンク)、expand_on_select(ブール値)、inputs(ネストされたサブコンポーネントの配列)をサポートしています。

値は複合 ID で保存されます:checkboxComponentId.optionValue.inputId。ネスト入力の default_value(input.props.default_value または input.default_value)は起動時に初期化されるため、初回のレンダリングから getAllFields() およびテンプレート補間({{checkboxId.optionValue.inputId}})を通じてネストされた値を利用できます。

スクリプトを作成する AI エージェントへの重要事項: ネスト入力の値には、複合 ID からのみアクセスできます。input.id 単体を使用すると、必ず undefined が返されます。たとえば、id: "tasks" のチェックボックスコンポーネント、value: "step_1" のオプション、id: "proof_url" のネスト入力がある場合:

  • 正しい例: kit.getField("tasks.step_1.proof_url")
  • 誤った例: kit.getField("proof_url") — undefined が返されます
  • 正しいイベントスコープ: kit.on("fieldchange:tasks.step_1.proof_url", ...)
  • 誤ったイベントスコープ: kit.on("fieldchange:proof_url", ...) — イベントは発火しません
{
  "id": "onboarding_tasks",
  "type": "checkboxes",
  "props": {
    "label": "Complete your onboarding",
    "options": [
      {
        "value": "gcash",
        "label": "Setup GCash USDC",
        "description": "Connect your crypto wallet for payouts",
        "button": { "label": "What is GCash?", "url": "https://example.com/gcash", "style": "ghost", "size": "sm" },
        "inputs": [
          { "id": "wallet", "type": "solana_address", "label": "Your GCash Solana USDC Address", "required": true },
          { "id": "note", "type": "paragraph", "props": { "text": "This is your Solana wallet address from GCash — **not** your GCash phone number." } }
        ]
      },
      {
        "value": "eth",
        "label": "Setup Ethereum Wallet",
        "image": "https://example.com/eth-icon.png",
        "inputs": [
          { "id": "eth-wallet", "type": "evm_address", "label": "Your ETH Address", "required": true }
        ]
      },
      {
        "value": "preferences",
        "heading": "Set Your Preferences",
        "expand_on_select": true,
        "inputs": [
          { "id": "group_size", "type": "dropdown", "label": "Preferred group size", "props": { "options": ["Small (3-5)", "Medium (6-10)", "Large (10+)"] } },
          { "id": "sub_tasks", "type": "checkboxes", "label": "Sub-tasks", "props": { "options": [
            { "value": "read_docs", "heading": "Read the documentation" },
            { "value": "watch_video", "heading": "Watch intro video", "button": { "label": "Watch", "url": "https://example.com/video", "style": "primary", "size": "sm" } }
          ] } }
        ]
      },
      { "value": "self_paced", "heading": "Self-Paced Learning" }
    ]
  }
}

サポートされるネスト項目のタイプ:

  • 入力タイプ: short_text、long_text、email、phone、url、password、number、dropdown、multiple_choice、checkboxes(ネスト可!)、switch、checkbox、star_rating、slider、opinion_scale、file_upload、signature、solana_address、evm_address、bitcoin_address
  • 表示タイプ: paragraph、heading、banner、image、divider、html、callout — 静的コンテンツとしてレンダリングされます(フォームの値は保存されません)

ネスト入力のプロパティ:

inputs 配列の各項目には、次のフィールドがあります。

プロパティタイプ説明
idstring(必須) ネスト入力の一意の識別子
typestring(必須) 入力タイプ(例:short_text、solana_address、paragraph)
labelstring入力項目の上に表示するラベル
placeholderstringプレースホルダーテキスト
requiredbooleanこのネスト入力を必須にします。ここ、または props.required 内に設定できます。どちらもサポートされています。
propsobject入力コンポーネントに渡す追加のプロパティ(例:{ "required": true, "sublabel": "...", "readonly": true })

AI エージェントへの重要事項: required は input.required(トップレベル)または input.props.required(props 内)のどちらにも設定できます。どちらも同じように動作します。例:{ "id": "wallet", "type": "solana_address", "required": true } は { "id": "wallet", "type": "solana_address", "props": { "required": true } } と同等です。

読み取り専用およびコピー可能なネスト入力: props 内に "readonly": true を設定すると、ネスト入力はクリップボードにコピーするボタン付きの読み取り専用フィールドとして表示されます。ユーザーがコピーする必要のある、事前入力済みのメッセージ、コード、リンクに最適です。"copyable": true を設定すると、入力項目を編集可能なままにして、横にコピー用ボタンを追加できます。サポートされるタイプ:short_text、long_text、rich_text、email、phone、url、number、currency、date、datetime、time、password、dropdown、address。例:{ "id": "msg", "type": "long_text", "label": "Message to send", "props": { "readonly": true } }。

オプションのプロパティ:

プロパティ型説明
valuestring(必須) オプションの一意の識別子
headingstring(必須) 主に表示されるテキスト
subheadingstring見出しの下に表示される補足テキスト
imagestringサムネイル画像の URL(角丸、32x32)
ribbonstringオプションに表示されるバッジテキスト(例:「おすすめ」、「最もお得」)。multiple_choice と picture_choice で機能します
ribbon_bgstringリボンの背景色(デフォルトはテーマカラー)
ribbon_colorstringリボンのテキスト色(デフォルトは白)
buttonEmbeddedButtonサイドリンクボタン(埋め込みボタンを参照)
messagestring選択時にオプションの下に表示されるカスタムメッセージ(オプションメッセージを参照)
inputsarrayネストされたサブコンポーネント — デフォルトでは常に表示され、必須入力が入力されると自動的にチェックされます
expand_on_selectbooleantrue の場合、チェックボックスがオンになった後にのみネストされた入力を表示します(自動チェックは行いません)。デフォルト:false
プログレスライン

ビューポート上部に細いプログレスラインを追加し(Fillout.com のように)、訪問者の進行に合わせて伸びるようにします。

{
  "settings": {
    "progress_line": {
      "enabled": true,
      "position": "top",
      "height": 4,
      "color": "#3b82f6"
    }
  }
}

オプション:

  • position:"top"(ビューポート上部に固定、デフォルト)または "below_topbar"(既存のトップバーの下)
  • height:高さ(ピクセル単位、デフォルト 4)
  • color:色の上書き(デフォルトはテーマの primary_color)

既存の progress_bar 設定とは独立しており、両方を併用できます。

ポップアップ

訪問者の行動に基づいてポップアップを表示します。

{
  "popups": [
    {
      "id": "exit-popup",
      "trigger": { "type": "exit_intent", "delay_ms": 3000 },
      "pages": ["landing"],
      "mode": "modal",
      "content": { "title": "Wait!", "body": "Get 10% off before you go" }
    }
  ]
}

トリガーの種類: exit_intent、scroll_depth、inactive、timed、page_count、custom、video_progress、video_chapter

ページ遷移

ページ間を移動する際のアニメーションスタイルとスクロール動作を制御します。

{
  "settings": {
    "page_transition": "slide-up",  // "slide-up" | "fade" | "slide-left" | "scale" | "none"
    "page_scroll": "instant"        // "instant" | "smooth"
  }
}

遷移: slide-up(デフォルト — フェード+上方向へのスライド)、fade(不透明度のみ)、slide-left(右からスライド、ウィザードのような表示)、scale(控えめなズームイン)、none(即時切り替え)。

スクロール: instant(デフォルト — ページ上部へジャンプ)または smooth(ページ上部へスムーズにスクロール)。

ファネル終了時の動作

完了画面は組み込みで用意されていません。 終了状態はエディターで設計します。最後の送信後も訪問者は現在のページにとどまり(ボタンは二重送信を防ぐため無効になります)。一般的なパターンは次のとおりです。

  1. 「ありがとうございました」ページをデザインする — テキスト、画像、リンクなどの表示専用コンポーネントを含む最後のページを追加します。入力フィールドがないため、送信ボタンは自動的に非表示になり、このページが自然な終了ページになります。
  2. 送信後にリダイレクトする — settings.completion.redirect_url を使用して、フォームの送信後に訪問者を外部 URL に移動します。
{
  "settings": {
    "completion": {
      "redirect_url": "https://example.com/thank-you?email={{email}}",
      "redirect_delay": 0
    }
  }
}
  1. ページアクションを使用する — 最後のページに「サイトにアクセス」、「共有」などのボタン用のカスタム actions を追加します。

自動非表示ルール: 最後のページ(次のページへのルートがないページ)に入力コンポーネントがなく、明示的な submit_label もない場合、送信ボタンは自動的に非表示になります。表示専用の終了ページにボタンを表示するには、submit_label を明示的に設定してください。

重要: 送信ボタンがクリックされると、form_submit イベントと funnel_complete イベントは引き続き通常どおり発生します。終了ページはそれに応じてデザインしてください。データを収集する必要がある場合は、ページに入力欄を配置すると送信ボタンが自動的に表示されます。

動的な動作(CatalogKit API)

カスタムのクライアントサイドロジックには、インラインの <script> タグから window.CatalogKit グローバル API を使用します。

<script>
  const kit = window.CatalogKit.get();
  kit.on('fieldchange:email', ({ value }) => {
    console.log('Email changed to', value);
  });
  kit.on('beforenext:pricing', async ({ preventDefault }) => {
    // Custom validation or API call
    const ok = await fetch('/validate', { method: 'POST', body: JSON.stringify(kit.getAllFields()) });
    if (!ok) preventDefault();
  });
</script>

利用可能なイベント:pageenter、pageexit、beforenext、submit、fieldchange — すべてスコープ指定に対応しています(例:fieldchange:email)。

利用可能なメソッド:getField、setField、getAllFields、setButtonLoading、setButtonDisabled、setValidationError、setComponentProp、goNext、goBack、goToPage。


CatalogKit Global API (window.CatalogKit)

window.CatalogKit で公開されている、実行中のカタログに対する完全な読み書きアクセスを提供するライブ JavaScript ブリッジです。通常の JavaScript であれば、html コンポーネント内のインライン <script> タグ、外部スクリプト、ブラウザーのコンソールから利用できます。カスタムロジック、サーバーサイド検証、条件付き UI、インタラクティブなウィジェットを構築するための推奨される方法です。

インスタンスへのアクセス

重要:必ず最初に .get() を呼び出してください。 window.CatalogKit はレジストリであり、インスタンスではありません。.get() のみを持ちます。すべての API メソッド(on、off、getField、setField など)は、.get() が返すインスタンスにあります。

// ✅ CORRECT — always use .get() to obtain an instance first
const kit = window.CatalogKit.get();           // most recently mounted catalog
const kit = window.CatalogKit.get('cat_abc');   // specific catalog by ID

// ❌ WRONG — will throw "is not a function"
window.CatalogKit.on('pageenter', ...);        // .on() does not exist on the registry
window.CatalogKit.getField('email');           // .getField() does not exist on the registry

複数フォームの分離: 同じページ上に複数のカタログがある場合、それぞれが独自の catalog_id で個別に登録されます。状態が相互に影響することはありません。特定のカタログを対象にするには、.get(id) を使用してください。

API リファレンス
メソッド説明
状態の読み取り
kit.getField(id)任意のフォームフィールドの現在値を取得します。ネストされたチェックボックス/複数選択入力の場合は、複合 ID を使用します:kit.getField('checkboxId.optionValue.inputId')
kit.getAllFields()すべてのフォーム値の読み取り専用コピーを取得します(checkboxId.optionValue.inputId のようなネストされた複合キーを含みます)
kit.getVar(key)スクリプト変数を取得します(テンプレートでは {{var:key}} としても使用できます)
kit.getAllVars()すべてのスクリプト変数の読み取り専用コピーを取得します
kit.getUrlParam(key)URL クエリパラメーターを取得します
kit.getAllUrlParams()すべての URL パラメーターの読み取り専用コピーを取得します
kit.getPageId()現在のページ ID
kit.getGlobal(key)グローバル(ページ間で共有される)値を取得します
kit.getQuizScores()クイズのスコアを取得します:{ total, max, percent, correct_count, question_count, answers[] }。各回答には { component_id, page_id, label, options, given_answer, correct_answer, is_correct, points_earned, points_possible, explanation, wrong_message } が含まれます
状態の書き込み
kit.setField(id, value)フィールド値を設定します。画面に即座に反映されます
kit.setVar(key, value)スクリプト変数を設定します(再レンダリングが発生し、テンプレートでは {{var:key}} として使用できます)
kit.setGlobal(key, value)グローバル値を設定します(ページ間で保持され、テンプレートでは {{global:key}} として使用できます。注:単独では再レンダリングは発生しません)
ボタンの制御
kit.setButtonLoading(bool)続行ボタンの読み込みスピナーを表示/非表示にします
kit.setButtonDisabled(bool)続行ボタンを有効/無効にします
kit.setValidationError(id, msg)フィールドにカスタムエラーを表示します(クリアするには null を指定します)
ナビゲーション
kit.goNext()次のページに進みます(検証とフックを実行します)
kit.goBack()前のページに戻ります
kit.goToPage(pageId)ID で指定した任意のページに直接移動します(現在のページを履歴に追加し、検証は行いません)
kit.__devForceGoToPage(pageId)開発専用:auto_skip を1サイクルだけバイパスしてページに移動します。開発用ツールバーの Pages グラフで使用します。本番環境では利用できません
コンポーネントのプロパティ
kit.setComponentProp(id, prop, value)実行時に任意のコンポーネントプロパティを上書きします(例:hidden、label、options)。表示用と入力用を問わず、すべてのコンポーネントタイプで使用できます
名前付きカート(kit.cart(name?) は CartHandle を返します)
kit.cart(name?)指定したカート名(デフォルト:"default")の CartHandle を返します。名前付きカートを使うと、複数のチェックアウトを含むアップセルファネルを作成できます
handle.add(item)CartItem をこのカートに追加します(offer_id で重複を除外します)。最低限、offer_id、page_id、title が必要です
handle.remove(offerId)offer_id でアイテムを削除します
handle.items()このカート内のアイテムを格納した読み取り専用配列を取得します
handle.setPaymentItems(items)このカートの Stripe 明細項目を上書きします。表示用アイテムに戻すには null を指定します。mode_override: "payment" を使用する場合、アイテムに amount_cents を含める必要があります
handle.paymentItems()Stripe の上書き配列を取得します。設定されていない場合は null を返します
handle.clear()このカート内のすべてのアイテムと支払いの上書きをクリアします
handle.moveTo(targetName, offerId?)1つのアイテム(offer_id で指定)またはすべてのアイテムを、別の名前付きカートに移動します
kit.getCartNames()空ではないカートの名前を一覧表示します
kit.clearAllCarts()すべての名前付きカートと支払いの上書きをクリアします
Stripe 顧客(複数チェックアウトでの再利用)
kit.getStripeCustomerId()以前のチェックアウト手順で使用した Stripe 顧客 ID を取得します。該当する ID がない場合は null を返します
kit.setStripeCustomerId(id)次のチェックアウトで使用する Stripe 顧客 ID を手動で設定します
カートドロワー(デフォルトのカート)
kit.openCart()プログラムからカートドロワーを開きます
kit.closeCart()カートドロワーを閉じます
kit.startCheckout(cartName?)指定したカートの組み込みチェックアウトページをプログラムから開始します(デフォルトは "default")。before_checkout を発火してからチェックアウトを表示します。カートのチェックアウトボタンを DOM 経由でクリックする代わりに使用してください
クライアント参照 ID(Stripe に client_reference_id として渡されます)
kit.setClientReferenceId(id)Stripe チェックアウトセッション用にカスタムの client_reference_id を設定します。クリアするには null を指定します。セッション作成の直前にカスタムロジックを実行できるよう、before_checkout リスナー内での使用が最適です
kit.getClientReferenceId()現在のクライアント参照 ID を取得します。設定されていない場合は null を返します
イベント
kit.on(event, callback)ライフサイクルイベントを購読します(下記の「イベント」セクションを参照)
kit.off(event, callback)購読を解除します
ユーティリティ
kit.fetchglobalThis.fetch のエイリアス
イベント — スコープ指定のライフサイクルフック

イベントは event または event:scope_id の形式です。スコープを指定しないリスナーは、すべてのページ/フィールドで発火します。スコープを指定したリスナーは、指定されたページまたはフィールドでのみ発火します。

イベントスコープペイロード非同期?説明
fieldchangeフィールド ID{ fieldId, value, prevValue }いいえフォームフィールドの値が変更されました。ネストされた checkbox/multiple_choice 入力の場合、スコープには入力 ID 単体ではなく複合 ID を使用する必要があります(例:fieldchange:checkboxId.optionValue.inputId)。
pageenterページ ID{ pageId }いいえページがアクティブになりました(画面遷移後)
pageexitページ ID{ pageId }はいページを離れる直前です(beforenext の後)
beforenextページ ID{ pageId, preventDefault(), setNextPage(id) }はい検証後、画面遷移前 — 遷移のブロックやリダイレクトが可能です
submitページ ID{ pageId, formState, preventDefault() }はい最終ページの送信 — ブロック可能です
cart_add—{ item, items }いいえオファーがカートに追加されました
cart_remove—{ offer_id, items }いいえオファーがカートから削除されました
cart_open—{ items }いいえカートドロワーが開きました
cart_close—{ items }いいえカートドロワーが閉じました
before_checkout—{ items, cartName, preventDefault() }はいチェックアウト前 — ブロックやリダイレクトが可能です。cartName はチェックアウト対象のカートです
checkout_error—{ error_message, error_code }いいえチェックアウトの決済に失敗しました(3DS、カードの拒否、ネットワークエラー、資金保留の拒否)。error_code は、利用可能な場合は Stripe のエラーコードです(例:"card_declined"、"authentication_required")

スコープ指定の例:

const kit = window.CatalogKit.get();

// Global — fires on every page
kit.on('beforenext', async (e) => { ... });

// Scoped — fires only on 'checkout' page
kit.on('beforenext:checkout', async (e) => { ... });

// Global — fires for any field
kit.on('fieldchange', (e) => { ... });

// Scoped — fires only when 'email' field changes
kit.on('fieldchange:email', (e) => { ... });

// Scoped — nested checkbox input (MUST use compound ID)
// Given: checkboxes id="tasks", option value="setup", nested input id="proof_url"
kit.on('fieldchange:tasks.setup.proof_url', (e) => { ... });
// ❌ WRONG: kit.on('fieldchange:proof_url', ...) — never fires

// Page-specific enter/exit
kit.on('pageenter:pricing', (e) => { ... });
kit.on('pageexit:checkout', (e) => { ... });

DOM イベント: blur、focus、click、input、keydown などの標準 DOM イベントには、DOM を直接使用してください。CatalogKit が扱うのは、DOM に相当するものがないライフサイクルイベントのみです(ページ遷移とフォーム状態の変更は、内部で React によって管理されます)。

// DOM events — use the DOM directly
document.getElementById('email').addEventListener('blur', () => {
  const kit = window.CatalogKit.get();
  // validate on blur...
});
html コンポーネントでの使用

html コンポーネントは、ブリッジを実用的にする次の 2 つの機能をサポートしています。

  1. テンプレート補間: HTML コンテンツ内の {{field_id}}、{{var:key}}、{{global:key}} は、それぞれ現在のフィールド値、スクリプト変数、グローバル値に置き換えられます。checkbox/multiple_choice の複合 ID では、ドット区切りのネストされたキーをサポートします(例:{{checkboxId.optionValue.inputId}})。波括弧内の空白も許容されます(例:{{ field_id }})。フィールドと変数は再レンダー時にリアクティブに更新されます。グローバル値はページ間で保持されますが、それ自体では再レンダーを発生させません(リアクティブに更新する必要がある場合は、setField または setVar の呼び出しと組み合わせてください)。
  2. インラインスクリプトの実行: html コンテンツ内の <script> タグは、レンダー後に自動実行され、window.CatalogKit に完全にアクセスできます。
{
  "id": "my_script",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n// your code here\n</script>"
  }
}
クックブック:一般的なパターン

以下の例は、インライン <script> タグを含む html コンポーネントを使用した実用的なパターンを示します。これらのコンポーネントは、カタログスキーマ内の該当ページに配置してください。

1. サーバーサイドのフォーム検証(beforenext + ローディング状態)

画面遷移を許可する前に、バックエンドに対してフォームデータを検証します。「続行」ボタンにローディングスピナーを表示し、インラインで検証エラーを表示します。

{
  "id": "checkout_validator",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\nkit.on('beforenext:checkout', async (e) => {\n  kit.setButtonLoading(true);\n\n  try {\n    const res = await fetch('https://api.myapp.com/validate-order', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        email: kit.getField('email'),\n        quantity: kit.getField('quantity'),\n      }),\n    });\n    const data = await res.json();\n\n    if (!data.ok) {\n      kit.setValidationError('email', data.error || 'Validation failed');\n      e.preventDefault();\n    }\n  } catch (err) {\n    kit.setValidationError('email', 'Network error — please try again');\n    e.preventDefault();\n  } finally {\n    kit.setButtonLoading(false);\n  }\n});\n</script>"
  }
}
2. ページ表示時の動的コンテンツ読み込み

ページの読み込み時にバックエンドからデータを取得し、ドロップダウンの選択肢を動的に設定します。

{
  "id": "plan_loader",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\nkit.on('pageenter:select_plan', async () => {\n  kit.setButtonLoading(true);\n  try {\n    const res = await fetch('https://api.myapp.com/plans');\n    const plans = await res.json();\n    kit.setComponentProp('plan_dropdown', 'options',\n      plans.map(p => ({ value: p.id, heading: p.name + ' — ' + p.price + '/mo' }))\n    );\n  } finally {\n    kit.setButtonLoading(false);\n  }\n});\n</script>"
  }
}
3. フィールド値に応じた条件付き UI

サーバーへの呼び出しなしで、ユーザーの選択に基づいてコンポーネントを表示または非表示にします。入力コンポーネントと表示コンポーネント(callout、banner、image など)のすべてのタイプで動作します。

{
  "id": "conditional_ui",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\nkit.on('fieldchange:account_type', (e) => {\n  const isBusiness = e.value === 'business';\n  // Hide/show input fields\n  kit.setComponentProp('company_name', 'hidden', !isBusiness);\n  kit.setComponentProp('tax_id', 'hidden', !isBusiness);\n  kit.setComponentProp('company_name', 'required', isBusiness);\n  // Hide/show display components (banners, callouts, images, etc.)\n  kit.setComponentProp('business_info_banner', 'hidden', !isBusiness);\n});\n</script>"
  }
}
4. blur 時のリアルタイムなメールアドレス検証(DOM イベント + CatalogKit)

blur/focus にはネイティブ DOM イベントを使用し、状態の読み取りとエラー表示には CatalogKit を使用します。

{
  "id": "email_validator",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\n// Wait for DOM to render the email input\nsetTimeout(() => {\n  const el = document.querySelector('[data-component-id=\"email\"] input');\n  if (!el) return;\n\n  el.addEventListener('blur', async () => {\n    const email = kit.getField('email');\n    if (!email) return;\n\n    const res = await fetch('https://api.myapp.com/check-email?email=' + encodeURIComponent(email));\n    const data = await res.json();\n\n    if (data.taken) {\n      kit.setValidationError('email', 'This email is already registered');\n    } else {\n      kit.setValidationError('email', null);\n    }\n  });\n}, 100);\n</script>"
  }
}
5. API レスポンスに基づく動的ルーティング

サーバーでの検証後、API レスポンスに基づいてユーザーを異なるページにルーティングします。

{
  "id": "smart_router",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\nkit.on('beforenext:qualification', async (e) => {\n  kit.setButtonLoading(true);\n  try {\n    const res = await fetch('https://api.myapp.com/qualify', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        revenue: kit.getField('annual_revenue'),\n        employees: kit.getField('team_size'),\n      }),\n    });\n    const { tier } = await res.json();\n    kit.setGlobal('qualified_tier', tier);\n\n    if (tier === 'enterprise') {\n      e.setNextPage('enterprise_onboarding');\n    } else if (tier === 'disqualified') {\n      kit.setValidationError('annual_revenue', 'We currently only serve businesses with $100k+ revenue');\n      e.preventDefault();\n    }\n    // else: continue to default next page\n  } finally {\n    kit.setButtonLoading(false);\n  }\n});\n</script>"
  }
}
6. 時間指定アクション — 自動進行、カウントダウン、遅延 UI

時間に基づくロジックには、setTimeout/setInterval を CatalogKit のイベントと組み合わせて使用します。

{
  "id": "auto_advance_timer",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\n// Auto-advance after 10 seconds on the 'intro' page\nkit.on('pageenter:intro', () => {\n  const timer = setTimeout(() => {\n    kit.goNext();\n  }, 10000);\n\n  // Clean up if user leaves manually\n  kit.on('pageexit:intro', () => clearTimeout(timer));\n});\n</script>"
  }
}
{
  "id": "countdown_display",
  "type": "html",
  "props": {
    "content": "<div id=\"countdown\" style=\"text-align:center;font-size:24px;font-weight:bold;\"></div>\n<script>\nconst kit = window.CatalogKit.get();\nlet seconds = 30;\nconst el = document.getElementById('countdown');\nconst interval = setInterval(() => {\n  seconds--;\n  el.textContent = 'Offer expires in ' + seconds + 's';\n  if (seconds <= 0) {\n    clearInterval(interval);\n    kit.goNext(); // auto-advance when timer expires\n  }\n}, 1000);\nel.textContent = 'Offer expires in ' + seconds + 's';\n\nkit.on('pageexit:' + kit.getPageId(), () => clearInterval(interval));\n</script>"
  }
}
7. 条件が満たされるまで「続行」を無効にする

ユーザーが特定の操作を完了するまでボタンを無効にし、その後有効にします。

{
  "id": "gate_logic",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\n// Disable Continue until terms are accepted AND email is filled\nkit.setButtonDisabled(true);\n\nfunction checkReady() {\n  const termsAccepted = kit.getField('accept_terms') === true;\n  const hasEmail = !!kit.getField('email');\n  kit.setButtonDisabled(!(termsAccepted && hasEmail));\n}\n\nkit.on('fieldchange:accept_terms', checkReady);\nkit.on('fieldchange:email', checkReady);\ncheckReady();\n</script>"
  }
}
8. 送信のインターセプト — 完了前にバックエンドへデータを送信する

最終送信をインターセプトして、独自のバックエンドへデータを送信します。失敗した場合は送信をブロックします。

{
  "id": "submit_handler",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\nkit.on('submit', async (e) => {\n  kit.setButtonLoading(true);\n  try {\n    const res = await fetch('https://api.myapp.com/submissions', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(e.formState),\n    });\n    if (!res.ok) {\n      kit.setValidationError('email', 'Failed to save — please try again');\n      e.preventDefault();\n    }\n  } catch (err) {\n    kit.setValidationError('email', 'Network error');\n    e.preventDefault();\n  } finally {\n    kit.setButtonLoading(false);\n  }\n});\n</script>"
  }
}
9. リアクティブな DOM を使用したリアルタイム価格計算

インタラクティブなウィジェットでは、fieldchange イベントと DOM の直接操作を組み合わせます。

{
  "id": "price_calc",
  "type": "html",
  "props": {
    "content": "<div style=\"background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;padding:20px;text-align:center;\">\n  <div style=\"font-size:14px;color:#666;\">Your estimated price</div>\n  <div id=\"price\" style=\"font-size:36px;font-weight:bold;color:#16a34a;\">$0</div>\n  <div id=\"price-breakdown\" style=\"font-size:12px;color:#999;\"></div>\n</div>\n<script>\nconst kit = window.CatalogKit.get();\n\nfunction update() {\n  const qty = Number(kit.getField('quantity')) || 0;\n  const tier = kit.getField('tier') || 'basic';\n  const rates = { basic: 29, pro: 49, enterprise: 99 };\n  const rate = rates[tier] || 29;\n  const total = qty * rate;\n  document.getElementById('price').textContent = '$' + total.toLocaleString();\n  document.getElementById('price-breakdown').textContent = qty + ' x $' + rate + '/mo (' + tier + ')';\n}\n\nupdate();\nkit.on('fieldchange:quantity', update);\nkit.on('fieldchange:tier', update);\n</script>"
  }
}
10. 外部データに基づく条件付きページスキップ

ページに入るタイミングで、そのページを完全にスキップするかどうかを判断します。

{
  "id": "skip_checker",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\nkit.on('pageenter:address_page', async () => {\n  // If we already have this user's address from a previous session, skip\n  const userId = kit.getUrlParam('uid');\n  if (!userId) return;\n\n  const res = await fetch('https://api.myapp.com/users/' + userId + '/address');\n  const data = await res.json();\n  if (data.address) {\n    kit.setField('address', data.address);\n    kit.goNext(); // immediately skip past this page\n  }\n});\n</script>"
  }
}
11. サーバーデータを取得し、テンプレート補間で表示する

バックエンドからデータを取得して vars/globals に保存し、{{var:key}} または {{global:key}} テンプレートを使用して HTML コンテンツに表示します。DOM を直接操作する必要はありません。

[
  {
    "id": "pricing_fetcher",
    "type": "html",
    "props": {
      "content": "<script>\nconst kit = window.CatalogKit.get();\n\nkit.on('pageenter:pricing', async () => {\n  kit.setButtonLoading(true);\n  try {\n    const res = await fetch('https://api.myapp.com/pricing?email=' + encodeURIComponent(kit.getField('email')));\n    const data = await res.json();\n    kit.setVar('plan_name', data.plan_name);\n    kit.setVar('monthly_price', '$' + data.price);\n    kit.setVar('discount_pct', data.discount ? data.discount + '%' : '');\n    kit.setGlobal('customer_tier', data.tier);\n  } catch (err) {\n    kit.setVar('plan_name', 'Standard');\n    kit.setVar('monthly_price', 'Contact us');\n  } finally {\n    kit.setButtonLoading(false);\n  }\n});\n</script>"
    }
  },
  {
    "id": "pricing_display",
    "type": "html",
    "props": {
      "content": "<div style=\"background:#f8fafc;border-radius:12px;padding:24px;text-align:center;\">\n  <h3>Your plan: {{var:plan_name}}</h3>\n  <div style=\"font-size:32px;font-weight:bold;\">{{var:monthly_price}}/mo</div>\n  <div style=\"color:#16a34a;\">{{var:discount_pct}} discount applied</div>\n</div>"
    }
  }
]

テンプレート構文リファレンス:

構文ソースリアクティブ?例
{{field_id}}kit.getField(id) — フォームフィールドはい{{email}}、{{quantity}}
{{var:key}}kit.getVar(key) — スクリプト変数はい(再レンダリングをトリガー){{var:plan_name}}、{{var:monthly_price}}
{{global:key}}kit.getGlobal(key) — ページ間で共有されるグローバル値いいえ(レンダリング時に読み込まれます。再レンダリングを強制するには setVar と組み合わせてください){{global:customer_tier}}
12. スクリプトでネストされたチェックボックス入力を読み取る(複合 ID)

チェックボックスにネストされた入力(証明 URL、ウォレットアドレスなど)がある場合は、入力 ID のみではなく、複合 ID 形式 checkboxId.optionValue.inputId を必ず使用してください。これは、ネストされた入力に関するスクリプト作成で最も多い誤りです。

{
  "id": "thread_verifier",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\n\n// Given a checkboxes component:\n//   id: \"threads_checklist\"\n//   options: [{ value: \"thread_1\", inputs: [{ id: \"proof_url\", ... }] }, ...]\n\n// ✅ CORRECT — use compound ID: checkboxId.optionValue.inputId\nconst url1 = kit.getField('threads_checklist.thread_1.proof_url');\nconst url2 = kit.getField('threads_checklist.thread_2.proof_url');\n\n// ❌ WRONG — bare input ID returns undefined\n// kit.getField('proof_url')  → always undefined\n\n// Listen for changes on nested inputs — also needs compound ID\nkit.on('fieldchange:threads_checklist.thread_1.proof_url', (e) => {\n  console.log('Thread 1 URL changed to:', e.value);\n});\n\n// Validate all nested inputs before navigation\nkit.on('beforenext:my_page', async (e) => {\n  const urls = [];\n  for (let i = 1; i <= 3; i++) {\n    const url = kit.getField('threads_checklist.thread_' + i + '.proof_url');\n    if (url && url.trim()) urls.push(url);\n  }\n  if (urls.length === 0) {\n    kit.setValidationError('threads_checklist', 'Please paste at least one URL');\n    e.preventDefault();\n  }\n});\n</script>"
  }
}

vars、globals、fields の使い分け:

  • setField — 値をフォーム送信ペイロードに含め、バリデーションの対象にする場合(ユーザー向けデータ)
  • setVar — テンプレートに表示したい中間データや計算データを扱う場合。ただし、フォームデータとしては送信しません(API から取得した価格、ラベル、ステータスメッセージなど)。再レンダリングをトリガーします。
  • setGlobal — ページ遷移後も値を保持する必要があり、スクリプトで読み取るか、一度だけ表示すればよい場合(認証トークン、ユーザーのティアなど)。単独では再レンダリングをトリガーしません。

Iframe の src もこれらのテンプレートに対応しています: {{var:key}} と {{global:key}} は、動的な埋め込み URL 用に type: "iframe" の src プロパティ内で使用できます。

ベストプラクティス
  • 必ず最初に window.CatalogKit.get() を呼び出してインスタンスを取得します。window.CatalogKit はレジストリオブジェクトで、.get() しかありません。window.CatalogKit.on(...) や window.CatalogKit.getField(...) を直接呼び出すと、"is not a function" がスローされます。正しいパターンは const kit = window.CatalogKit.get(); kit.on(...) です。これは複数カタログの分離に必要です。
  • 必要に応じてリスナーをクリーンアップします。古いリスナーが残らないよう、kit.off() を使うか、クリーンアップのスコープを pageexit にします。
  • 非同期処理の前に setButtonLoading(true) を使い、finally ブロック内で setButtonLoading(false) を呼び出します。
  • 独自のエラー DOM より setValidationError を優先します。ネイティブの検証システムと連携し、自動的にスクロールします。
  • 動的な条件付き UI には setComponentProp(id, 'hidden', true/false) を使います。callout、banner、image などの表示コンポーネントと入力コンポーネントを含む、すべてのコンポーネントタイプで機能します。非表示のコンポーネントは検証の対象外になります。
  • コールバック内で if チェックを行うグローバルイベントではなく、スコープ付きイベント(beforenext:page_id)を使います。
  • DOM イベント(blur、focus、click、input)には DOM を直接使います。CatalogKit に追加されるのを待つ必要はありません。
  • 時間制御のロジックには pageenter/pageexit と setTimeout を使います。終了時には必ずタイマーをクリーンアップします。
  • スクリプトは try/catch 内で実行されます。エラーはコンソールに記録されますが、カタログのレンダラーがクラッシュすることはありません。
  • beforenext と submit の非同期コールバックは完全に await されるため、その中で安全に await fetch() を使えます。
  • 結果ページでは kit.getQuizScores() を使うと、完全なコンテキスト(質問ラベル、選択肢、回答、解説)付きでスコアにアクセスできます。
トラブルシューティング
症状原因対処方法
サーバーが 422 エラーを返すが、フロントエンドにはエラーが表示されないbeforenext リスナーが設定されていないsetValidationError() と preventDefault() を呼び出す beforenext スクリプトを含む html コンポーネントを追加します。サーバーサイドのフォーム検証を参照してください
エラーは表示されるが、ページが移動してしまうbeforenext コールバックで e.preventDefault() が指定されていない検証エラーを設定した後に e.preventDefault() を追加します
window.CatalogKit.on is not a functionインスタンスではなくレジストリのメソッドを呼び出しているconst kit = window.CatalogKit.get(); kit.on(...) を使用します
kit.getField() がネストされたチェックボックス入力に対して undefined を返す複合 ID ではなく、単独の入力 ID を使用しているkit.getField("checkboxId.optionValue.inputId") を使用します。例:kit.getField("tasks.step_1.proof_url") を使用し、kit.getField("proof_url") は使用しません。クックブック #12を参照してください。
ネストされた入力に対する fieldchange:inputId リスナーが一度も実行されない同じ原因です。単独の入力 ID は、フォーム状態の複合キーと一致しませんkit.on("fieldchange:checkboxId.optionValue.inputId", ...) を使用します
setValidationError を呼び出しても何も表示されない渡された componentId が間違っています。現在のページにある入力コンポーネントの id と一致する必要がありますスキーマ内のコンポーネントの id が、第 1 引数と一致していることを確認します
スクリプトが実行されないhtml コンポーネントが現在のページにないか、content プロパティに <script> タグがないhtml コンポーネントがページの components 配列に含まれていること、またコンテンツが <script>...</script> で囲まれていることを確認します
チェックアウトでクラッシュする、または本番環境と開発環境で動作が異なるpreventDefault() と setTimeout の後に、DOM 操作(document.querySelector)でカートのチェックアウトボタンをクリックしているsuccess_page_id を使ってチェックアウトを自動的に制御するか、kit.startCheckout() を呼び出します。チェックアウトの開始を参照してください
チェックアウト時に Unexpected token '<', "<!DOCTYPE"... is not valid JSON が表示されるCLI が古く、開発サーバーにチェックアウト用のフェッチインターセプターがないnpm install -g @officexapp/catalogs-cli@latest を実行して更新します。カタログディレクトリ内の .env ファイルに STRIPE_SECRET_KEY と STRIPE_PUBLISHABLE_KEY の両方が設定されていることを確認します
Stripe チェックアウトに「未設定」エラーが表示される.env に Stripe キーがないカタログプロジェクトのディレクトリに、STRIPE_SECRET_KEY=sk_test_... と STRIPE_PUBLISHABLE_KEY=pk_test_... を記載した .env ファイルを作成します。開発サーバーが確認するのはカタログディレクトリであり、ホームディレクトリではありません
チェックアウト時に「Stripe key mode mismatch」エラーが表示されるチェックアウト設定の stripe_publishable_key が pk_test_* で、サーバーのシークレットキーが sk_live_*(またはその逆)になっている本番環境の場合: stripe_publishable_key にライブ用の公開可能キー(pk_live_*)を設定します。開発環境の場合: .env でテストキー(sk_test_* + pk_test_*)を使用します。両方のキーは、同じ Stripe アカウントの同じモード(テストまたはライブ)のものである必要があります。CLI は catalog push 時にテストキーを検出すると警告します
ローカル開発中、再読み込みするとページが最初に戻るセッションの永続化により、localStorage から古いセッションが復元されている開発ツールバーの「キャッシュをクリア」をクリックするか、URL に ?dev_force_page=page_id を追加して、再開モーダルを回避し、任意のページに直接移動します
.ts カタログファイルの変更が保存後に反映されないCLI が古く、tsImport の ESM モジュールキャッシュによって再読み込みが妨げられているnpm install -g @officexapp/catalogs-cli@latest を実行して更新します。v0.7.5 以降ではキャッシュバスティングにより修正されています
インポートしたモジュール(例:./products.ts)の変更が検出されないCLI が古く、ルートのカタログファイルしか監視していないCLI を更新します。v0.7.5 以降ではカタログディレクトリ全体を再帰的に監視し、.ts/.js/.json の変更を検出します
アセットの変更(画像、CSS)がブラウザーに表示されないCLI が古く、アセットファイルの変更時にブラウザーの更新が実行されないCLI を更新します。v0.7.5 以降ではアセットの変更時に再読み込みシグナルが送信されます。また、ブラウザーキャッシュを回避するため、強制再読み込み(Cmd+Shift+R)もお試しください
デバッグモードと開発用 URL パラメーター

カタログ URL に次のクエリパラメーターを追加します。

パラメーター用途例
?debug_mode=verboseコンソールにすべてのログを出力します(formState、クイズの回答、リスナー数、fieldchange)http://localhost:3456/?debug_mode=verbose
?debug_mode=slimコンソールに最小限のログを出力します(登録、スクリプト、pageenter、クイズのスコア)http://localhost:3456/?debug_mode=slim
?dev_force_page=page_idエントリーページとセッション再開モーダルを経由せず、読み込み時に指定したページへ直接移動しますhttp://localhost:3456/?dev_force_page=next_steps

パラメーターは組み合わせて使用できます:http://localhost:3456/?dev_force_page=checkout_page&debug_mode=verbose


設定としての TypeScript(推奨)

カタログを完全な型安全性を備えた .ts ファイルとして作成し、CLI 経由でプッシュします。これは AI エージェントに推奨されるワークフローです。生の JSON よりも読み書きや差分の確認が簡単です。

セットアップ

エディターでの完全な型チェックと自動補完を利用するには、型パッケージをインストールします。

npm install --save-dev @officexapp/catalog-types
仕組み
  1. .ts ファイルを作成し、CatalogSchema オブジェクトをデフォルトエクスポートとして出力します
  2. CLI 経由でプッシュします。CLI は TS をトランスパイルし、関数(フック、スクリプト)をシリアライズして、生成された JSON スキーマを API にアップロードします
catalog.ts の例
import { CatalogSchema } from "@officexapp/catalog-types";

const catalog: CatalogSchema = {
  name: "Spring Sale Landing Page",
  slug: "spring-sale",
  seo: {
    title: "Spring Sale 2025 — 40% Off Everything",
    description: "Grab your favorite items at 40% off. Limited time only.",
    favicon: "https://cdn.example.com/favicon.png",
    og_image: "https://cdn.example.com/og-spring-sale.jpg",
  },
  settings: {
    theme: { primary_color: "#4F46E5", font: "Inter", mode: "light" },
    completion: { redirect_url: "https://example.com/thank-you" },
  },
  pages: {
    landing: {
      title: "Get Started",
      components: [
        { id: "hero", type: "heading", props: { text: "Spring Sale 2025", level: 1 } },
        { id: "email", type: "email", props: { label: "Your Email", required: true } },
        { id: "name", type: "short_text", props: { label: "Your Name", required: true } },
      ],
      submit_label: "Submit",
    },
  },
  routing: { entry: "landing", edges: [] },
};

export default catalog;
主なポイント
  • 型をインポートします。CatalogSchema、コンポーネントの型などは @officexapp/catalog-types からインポートします。
  • 関数は自動的にシリアライズされます。フック(on_enter、on_change、beforenext)を TS で実際の関数として記述できます。CLI が JSON スキーマ用の文字列にシリアライズします。
  • デフォルトエクスポートを使用します。CLI は export default catalog(または CatalogSchema オブジェクトのデフォルトエクスポート)を想定しています。
  • JSON.stringify は不要です。CLI が TS → JSON → API アップロードの一連の処理を実行します。

⚠️ AI エージェントへの警告: .ts カタログファイルを手動で JSON にコンパイルしないでください。tsc、tsx、esbuild を使ったり、TS → JSON の変換手順を手動で実行したりしないでください。CLI(catalogs catalog push catalog.ts)が、TypeScript のトランスパイル、関数のシリアライズ、API へのアップロードを単一のコマンドで処理します。.ts ファイルをそのまま CLI に渡してください。すべての処理が実行されます。

TypeScript カタログのプッシュ
# Set your token (only required env var)
export CATALOG_KIT_TOKEN="cfk_..."

# Push and publish — pass the .ts file directly, the CLI handles compilation
catalogs catalog push catalog.ts --publish

# Or via npx (no global install needed)
npx @officexapp/catalogs-cli catalog push catalog.ts --publish

CLI はパイプライン全体を内部で処理します。TS のトランスパイル → デフォルトエクスポートの抽出 → 関数のシリアライズ → JSON の API へのアップロードを行います。これらの手順を自分で実行する必要はありません。


CLI(@officexapp/catalogs-cli)

npm から CLI をインストールします。

npm install -g @officexapp/catalogs-cli

Node.js の要件: Node 18~22(LTS を推奨)。Node 24 以降はサポートされていません。実行時に TypeScript カタログファイルを読み込むために使用される tsx ESM ローダーフックが動作しなくなります。Node 20 または 22 LTS を推奨します。CLI は起動時にこの要件を確認します。Node のバージョンが 18~22 の範囲外の場合は実行を拒否し、NVM で互換性のあるバージョンをインストールする手順を表示します。

設定

CLI では明示的な認証トークンが必要です。設定ファイルや .env ファイルからトークンを暗黙的に取得することはありません。これにより、誤って別のアカウントで操作するのを防ぎます。

環境変数必須デフォルト説明
CATALOG_KIT_TOKENはい—API キー(形式:cfk_...)
CATALOG_KIT_API_URLいいえhttps://api.catalogkit.ccAPI URL を上書きします(通常は不要です)

認証情報の解決方法(次の 2 つの方法のみ。記載順に使用):

  1. --token CLI フラグ — catalogs --token cfk_... catalog push schema.json
  2. CATALOG_KIT_TOKEN 環境変数 — export CATALOG_KIT_TOKEN="cfk_..."

認証には設定ファイル(~/.catalog-kit/config.json)を読み込みません。これは意図的な仕様です。トークンの暗黙的な解決により、複数の CLI セッションが気づかないうちに異なるアカウントを使用するバグが発生したためです。ただし、catalog dev と catalog push は、認証以外の変数(例:STRIPE_SECRET_KEY、STRIPE_PUBLISHABLE_KEY)については、カタログディレクトリにある .env、.env.local、.env.development ファイルを自動的に読み込みます。すでに設定されている環境変数が上書きされることはありません。

安全機能: 変更を伴う操作(catalog push、video upload)の前に、CLI は認証済みのユーザー情報(サブドメイン+メールアドレス)を表示します。これにより、正しいアカウントで操作していることを確認できます。

よくある間違い: 環境変数は CATALOGS_TOKEN ではなく CATALOG_KIT_TOKEN です。API URL のデフォルト値は https://api.catalogkit.cc にハードコードされています。CATALOGS_API_URL(誤った名前)は設定しないでください。

コマンド
catalogs catalog dev my-catalog.ts             # Preview locally at http://localhost:3456
catalogs catalog dev my-catalog.ts --port 8080 # Custom port
catalogs catalog push schema.json --publish    # Push a JSON catalog (auto-uploads local assets)
catalogs catalog push catalog.ts --publish     # Push a TypeScript catalog (functions auto-serialized)
catalogs catalog list                           # List all your catalogs
catalogs video upload ./intro.mp4               # Upload a video
catalogs video status VIDEO_ID                  # Check transcoding progress
catalogs whoami                                 # Show your user identity, email, subdomain, credits

インストールせずに npx @officexapp/catalogs-cli <command> を実行することもできます。

クイックスタートの例
# 1. Set your token (REQUIRED — no other auth sources are used)
export CATALOG_KIT_TOKEN="cfk_..."

# 2. Test connection — verify you're on the right account
catalogs whoami

# 3. Preview locally (no token needed — purely offline)
catalogs catalog dev my-catalog.ts

# 4. Push to cloud (prints your identity before pushing)
catalogs catalog push my-catalog.ts --publish

# Or pass the token directly:
catalogs --token cfk_... catalog push my-catalog.ts --publish

ローカル開発とローカルアセット

catalogs catalog dev <file>

デプロイせずに、カタログをローカルでプレビューします。ホットリロード対応のローカルサーバーを起動します。

catalogs catalog dev my-catalog.ts
# => Local preview: http://localhost:3456
# => Assets served from: /path/to/your/catalog/dir
# => Watching for changes...

トークンは不要です。dev モードはローカルのみで動作します。カタログファイルを編集して保存すると、SSE によりブラウザーが自動的に更新されます。 開発サーバーの機能:

  • 本番品質のレンダリング — ライブサイトと同じフォント(Outfit + DM Sans)、CSS クラス(.cf-*)、コンポーネントのスタイルを使用します
  • 共通エンジン — 条件、ルーティング、バリデーションには、本番環境と完全に同じコード(ビルド時に shared/engine/ からインライン化)を使用し、開発環境と本番環境の差異をなくします
  • 自動リロード — カタログファイル、またはカタログディレクトリ内の .ts/.js/.json ファイルを保存すると、ブラウザーが自動的に更新されます(SSE ベースで、手動更新は不要です)。インポートされたモジュールも監視対象です。カタログが ./products.ts や ./pages/intro.ts をインポートしている場合、これらのファイルを変更すると、完全な再解析とリロードが実行されます。アセットファイル(画像、CSS など)の変更でもブラウザーが更新されます。TypeScript モジュールはリロードのたびにキャッシュバスターが適用され、常に最新のコンテンツが読み込まれます
  • カバーページのレイアウト — 本番環境と完全に一致する、浮遊コンテンツのアニメーション付きダークグラデーションオーバーレイ
  • インタラクティブなフォーム状態 — 入力値は保持され、フォームの値に基づく条件付きルーティングが機能します
  • フォームのバリデーション — 次のページへ進む前に validatePage() が実行されます。必須フィールド、メールアドレス/URL/数値の形式、最小値/最大値の制約を検証します。無効なフィールドには赤い枠線とエラーメッセージが表示され、最初のエラーまで自動スクロールします
  • ページアクション — page.actions は、アイコン、side_statement、reassurance に対応したスタイル付きボタン(primary、secondary、danger、ghost)として表示されます。アクションの検証を行い、formState に __action_{pageId} を設定し、オファーの受諾を確認して、次のページへ移動します
  • 事前入力とデフォルト値 — コンポーネントの default_value は、マウント時に formState を初期化します。settings.url_params.prefill_mappings を使って URL パラメーターから事前入力できます。prefill_mode: "hidden" は事前入力されたフィールドを非表示にし、prefill_mode: "readonly" は表示専用テキストとして表示します
  • ページの自動スキップ — 表示されている必須入力にすべて値が設定済みの場合、auto_skip: true のページはスキップされます(replaceState を使用)
  • ブラウザー履歴 — pushState/popstate と連携しているため、ブラウザーの戻るボタンで前のページに戻れます。自動スキップには replaceState を使用し、履歴に表示されないようにします
  • セッションの永続化 — formState、currentPageId、履歴、URL パラメーターは localStorage(カタログのスラッグをキーに使用)に保存されます。ユーザーがファネルの途中で離脱した場合、再訪時に「再開」/「最初からやり直す」の確認画面が表示されます。セッションは Stripe のチェックアウトへのリダイレクト後も維持されます。すべてのフォームデータと元の URL パラメーター(例:?ref=abc123、UTM タグ)は支払い後に自動的に復元されるため、成功ページ/チェックアウト後のページ(複数のチェックアウトを伴うアップセルフローを含む)でも kit.getField() と kit.getUrlParam() が機能します。セッションが消去されるのは、フォームの最終送信時、またはユーザーが「最初からやり直す」を選択したときのみです。リセットするには開発ツールバーの「キャッシュをクリア」ボタンを使用するか、?dev_force_page=page_id を付加して再開モーダルを完全にスキップしてください
  • カートとオファー — ページのオファーを accept_field/accept_value で収集する、完全なカート UI(フローティングボタン+スライド式ドロワー)です。カートの内容はページをまたいで保持され、アイテムを削除できます
  • カートの設定 — settings.cart:position(4 つのコーナー)、hide_button、title、checkout_button_text、destination_url(外部へのリダイレクト)、destination_page(内部ページへの移動)
  • 下部固定バー — settings.sticky_bar または page.sticky_bar:遅延、スクロール方向に応じた表示/非表示、テンプレートの補間({{fieldId}})、スタイルのバリエーション(solid、glass、glass_dark、gradient)、メインアクションの実行、セカンダリアクション、field:<id>:<value> の実行に対応します
  • __variants の解決 — コンポーネントの props とアクションにある *__variants キーを、ヒント(catalog.hints.defaults と URL の ?variant=slug から取得)と照合して解決します。スコアが最も高い条件一致が優先されます
  • CatalogKit スクリプティング API — window.CatalogKit は、getField、setField、getPageId、goNext、goBack、goToPage、on/off イベントリスナー(pageenter、pageexit、fieldchange など)、setValidationError を公開します。html コンポーネント内のインライン <script> タグは new Function() で実行されます(再レンダリング時の再実行を避けるため、フィンガープリントを使用します)
  • 動画視聴のトラッキング — ネイティブの <video> 要素は、timeupdate を通じて watch_percent を報告します。require_watch_percent を設定したページでは、しきい値に達するまで移動できません
  • 非表示コンポーネントの適切な処理 — hidden: true(コンポーネントレベルまたは props.hidden)が設定されたコンポーネントは、本番環境と同じ動作でレンダリング対象から除外されます
  • 表示条件 — visibility 条件グループを持つコンポーネントは、formState と URL パラメーターに基づいてリアルタイムに評価されます(13 種類すべての演算子に対応)
  • 開発ツールバー — 上部に固定されたバーに、カタログのスラッグ+スキーマバージョンのバッジ(v1.0)、ページボタン、検査ボタン、デバッグ切り替え、キャッシュをクリアするボタン、Stripe の状態インジケーター(緑=有効、黄=スタブ)、イベントビューアー、本番環境へのリンク(公開済みの場合、新しいタブで本番 URL を開きます)、最小化ボタンが表示されます。最小化(—)をクリックすると、右上のフローティングピルに折りたたまれます。ピルをクリックすると再展開されます。最小化状態は sessionStorage を介してリロード後も維持されます
  • ページグラフ — ツールバーの「ページ」をクリックすると、すべてのページとルーティングエッジを視覚的に表示する全画面モーダルが開きます。エントリーポイントからの幅優先レイアウトに Cytoscape.js を使用します。エントリーページには緑の枠線が付き、現在のページはテーマカラーで光ります。エッジのラベルには「条件付き」または「デフォルト」と表示されます。ページノードをクリックすると、そのページへ直接強制移動できます。これにより auto_skip が回避され、フォームの状態にかかわらず、どのページも確認できます。大規模なカタログのパンとズームに対応しています
  • 要素インスペクター — ツールバーの「検査」ボタンをクリックするか、Shift+Alt キーを長押しして有効にします。カーソルが十字に変わります。カート、下部固定バー、チェックアウトページ、ポップアップを含むコンポーネントにカーソルを合わせると、参照パス、種類、ラベルがツールチップに表示されます。クリックすると、AI エージェント向けの pageId/componentId 参照を含む構造化 JSON がコピーされます。コピー後はインスペクターが自動的に無効になります。または、「検査」ボタンをもう一度クリックして無効にできます
  • イベントビューアー — ツールバーの「イベント」をクリックすると、すべての開発イベント(ページビュー、フィールドの変更、チェックアウトイベント)を一覧表示する、検索・絞り込み可能な全画面モーダルが開きます。イベントの種類で絞り込み、イベントデータを検索して、タイムスタンプを確認できます。イベントはターミナルと SSE エンドポイントにもストリーミングされます
  • デバッグモード — ツールバーの「デバッグ」をクリックすると、モードが「なし」→「簡易」→「詳細」の順に切り替わります。モードは URL パラメーター(?debug_mode=slim または ?debug_mode=verbose)に反映され、history.replaceState() を介してページのリロード後も維持されます
  • URL によるページの強制指定 — ?dev_force_page=page_id を付加すると、読み込み時にエントリーページをスキップして、任意のページへ直接移動できます。また、セッション再開モーダルも表示されません。ファネルをたどらずに奥のページをテストする場合に便利です。例:http://localhost:3456/?dev_force_page=next_steps。ページはカタログのスキーマに存在する必要があります。無効なページ ID は無視され、エントリーページに戻ります
  • カタログのバージョン — スキーマバージョン(例:v1.0)は、schema.schema_version から取得され、ツールバーのカタログのスラッグの横にバッジとして表示されます
  • 本番環境へのリンク — ツールバーの「Prod」リンクをクリックすると、公開済みカタログの URL が新しいタブで開きます。URL は次の順に自動解決されます:(1)カタログの url フィールド、(2)サブドメインベースの URL(https://{subdomain}.catalogkit.cc/{slug})、(3)フォールバックの https://catalogkit.cc/c/{catalog_id}。カタログがまだ公開されていない場合は、灰色で無効状態として表示されます
  • バリデーションオーバーレイ — バリデーションのエラーと警告は左下に固定表示され、ホットリロード後に再表示されます。閉じるボタンで非表示にできます
  • ルーティング — 共通エンジンを使用して、ローカルで条件付きページルーティングが機能します(すべての演算子、条件グループ、エッジの優先順位、デフォルトエッジに対応)
  • ローカルの Stripe チェックアウト — .env に STRIPE_SECRET_KEY=sk_test_... と STRIPE_PUBLISHABLE_KEY=pk_test_... を追加すると、開発サーバーが Stripe REST API を介してローカルに実際の Stripe チェックアウトセッションを作成します。公開可能キーは Payment Element(Link および動的な支払い方法を含む)を有効にし、シークレットキーはホスト型/埋め込み型/カスタムチェックアウトセッションを有効にします。サブスクリプション、トライアル、プロモーションコード、顧客メールアドレスの事前入力、stripe_price_id 参照に対応します。キーが見つからない場合は、説明を含むスタブにフォールバックします
  • ローカル開発イベント — ページビュー、フィールドの変更、チェックアウトイベントは、ターミナルにストリーミングされ、/__dev_events_stream の SSE を介してブロードキャストされます。AI エージェントはこれを購読できます。最近のイベントは GET /__dev_events?limit=50 から JSON で取得できます。本番環境への影響はありません
  • SSE 再接続のバックオフ — EventSource 接続(自動リロード用の /__dev_sse、イベント用の /__dev_events_stream)は、接続失敗時に指数バックオフ(1s → 2s → 4s → ... → 最大 30s)を使用し、サーバー再起動時のブラウザーによる再接続の集中を防ぎます
開発サーバーの HTTP エンドポイント

ローカル開発サーバーは、http://localhost:3456(またはカスタムの --port)で次のエンドポイントを公開します。開発者はクライアント側で上書きする必要なく、これらを直接使用できます。

エンドポイントメソッド説明
/checkout/sessionPOSTStripe Checkout セッションを作成します(.env に STRIPE_SECRET_KEY が必要です)。本番 API と完全に同等です
/checkout/session/statusGETリダイレクト後にセッションのステータスを取得します。クエリ:?session_id=cs_...
/checkout/intentPOST[非推奨] PaymentIntent または SetupIntent を作成します(.env に STRIPE_SECRET_KEY が必要です)。代わりに ui_mode: "custom" を指定して /checkout/session を使用してください
/events または /ePOST単一のイベントを追跡します(JSON ボディ)。ターミナルに記録され、SSE 経由でブロードキャストされます
/events/batch または /e/batchPOST最大 25 件のイベントをまとめて追跡します(JSON ボディ:{ "events": [...] })。本番フロントエンドが使用するエンドポイントと同じです
/public/catalogs/:userId/:slugGET読み込み済みのカタログを JSON として取得します。本番環境と同じ形式のレスポンスを返します。:userId には dev-user を使用してください。バリアントの解決には、オプションで /:variantSlug サフィックスを指定できます
/routing/variantGET/POST決定論的なバリアントルーティングです。GET:?slug=X&hint=Z。POST:{ "slug": "X", "hint": "Z" }。AI(Gemini)の代わりにキーワードマッチングを使用し、variant_slug、target_slug、reason を返します
/__dev_sseGET自動再読み込み用の SSE ストリームです。カタログディレクトリ内のファイルが変更されると data: reload\n\n を送信します(.ts/.js/.json は完全な再解析を実行し、アセットは更新を実行します)
/__dev_events_streamGET開発イベント用の SSE ストリームです。ページビュー、フィールドの変更、チェックアウトイベントを JSON としてブロードキャストします
/__dev_eventsGET最近の開発イベントを JSON 配列として取得します。クエリパラメーター:?limit=50(デフォルトは 50)
/__dev_eventPOSTカスタムの開発イベントを送信します(JSON ボディ:{ type, data, ts })
/__dev_metaGET開発サーバーのメタデータ:slug、スキーマのバージョン、ページ数、Stripe のステータス、本番 URL
/assets/*GETカタログディレクトリ内のローカルファイル(画像、動画、スクリプト)を提供します

チェックアウトエンドポイントは、本番 API と同じリクエストボディを受け付け、すべての高度な機能に対応しています。

{
  "user_id": "dev-user",
  "catalog_slug": "my-catalog",
  "tracer_id": "tr_...",
  "line_items": [
    { "offer_id": "product-1", "title": "Product", "amount_cents": 2999, "payment_type": "one_time", "currency": "usd", "quantity": 1 }
  ],
  "form_state": { "email": "[email protected]", "name": "Jane Doe" },
  "coupon_code": "SAVE20",
  "client_reference_id": "custom-ref-123"
}

ローカル環境で完全に同等に利用できるチェックアウト機能:

  • メタデータ内の {{field_id}} テンプレート解決(例:"company": "{{company_name}}" は form_state から解決されます)
  • client_reference_id — リクエストボディ、スキーマテンプレート、または tracer_id フォールバックから取得します
  • reuse_payment_method — Stripe カスタマーを検索または作成し、リピートチェックアウト時に保存済みの支払い方法が表示されるようにします
  • クーポンコード — Stripe の割引を使って指定の coupon_code を適用するか、allow_discount_codes が設定されている場合にプロモーションコードの入力を有効にします
  • カスタマー情報の事前入力(customer_email、customer_name) — prefill_fields を介して form_state から取得します
  • すべての stripe_overrides(capture_method、setup_future_usage、consent_collection、payment_method_options、明細書表記、transfer_data)
  • trial_end_behavior を指定した無料トライアル、3D セキュア、請求先住所の収集、支払いの説明、カスタム免責事項テキスト

イベントエンドポイント — エージェントはイベントを追跡、監視できます。

# Track a single event
curl -X POST http://localhost:3456/events \
  -H "Content-Type: application/json" \
  -d '{"event_type":"page_view","page_id":"intro","catalog_slug":"my-catalog"}'

# Track a batch (same as production frontend)
curl -X POST http://localhost:3456/e/batch \
  -H "Content-Type: application/json" \
  -d '{"events":[{"event_type":"page_view","page_id":"intro"},{"event_type":"field_change","field_id":"email"}]}'

公開カタログエンドポイント — エージェントは読み込み済みのスキーマを JSON として取得できます。

# Fetch catalog (same response shape as production)
curl http://localhost:3456/public/catalogs/dev-user/my-catalog

# Fetch with variant
curl http://localhost:3456/public/catalogs/dev-user/my-catalog/enterprise-variant

ルーティングバリアントエンドポイント — AI を使わずにバリアント選択をテストできます。

# GET with query params
curl "http://localhost:3456/routing/variant?slug=my-catalog&hint=enterprise%20SaaS"

# POST with JSON body
curl -X POST http://localhost:3456/routing/variant \
  -H "Content-Type: application/json" \
  -d '{"slug":"my-catalog","hint":"enterprise SaaS company"}'

ルーティングエンドポイントは、Gemini AI の代わりにバリアントのヒントや説明に対してキーワードマッチングを行います。{ "ok": true, "data": { "variant_slug": "...", "reason": "hint_keyword_match" } } を返します。

SSE ストリーム — EventSource または curl で接続できます。

# Watch for file changes (auto-reload)
curl -N http://localhost:3456/__dev_sse

# Watch dev events (page views, field changes, checkout)
curl -N http://localhost:3456/__dev_events_stream

Dev プレビューの機能対応状況:

  • サポートされているコンポーネントの種類(入力): short_text、long_text、email、phone、url、number、password、dropdown、multiple_choice、checkboxes、picture_choice、slider、star_rating、switch/checkbox、opinion_scale、date、datetime、time、date_range、address、currency、file_upload(スタブ)、signature(スタブ)
  • サポートされているコンポーネントの種類(表示): heading、paragraph、image、video、html、banner、callout、divider、pricing_card、testimonial、faq、accordion、timeline、file_download、iframe、table、social_links、tabs、countdown、comparison_table、progress_bar、modal
  • 本番環境のみの機能(Dev プレビューでは利用不可):DynamoDB への永続化を含む完全な分析パイプライン、Gemini による AI 事前入力、カスタムドメインの解決、EVM/Solana/Bitcoin アドレス入力、window.__catalogkit_components によるカスタムコンポーネント、動画のトランスコード/画像の圧縮アップロード
ローカルファイル参照

カタログスキーマ内で相対パスを指定して、画像、動画、スクリプト、ファイルを参照できます。これらは dev モードでローカルから利用でき、push 時には自動的に CDN にアップロードされます。

// my-catalog.ts
export default {
  slug: "my-funnel",
  schema_version: "1.0",
  pages: {
    intro: {
      title: "Welcome",
      components: [
        {
          id: "hero",
          type: "image",
          props: { src: "./images/hero.png" }         // local file
        },
        {
          id: "demo",
          type: "video",
          props: { src: "./videos/demo.mp4" }         // local file
        },
        {
          id: "custom_logic",
          type: "html",
          props: {
            content: '<script src="./scripts/checkout.js"></script>'  // local script
          }
        },
        {
          id: "brochure",
          type: "file_download",
          props: {
            src: "./files/brochure.pdf",              // local file
            filename: "brochure.pdf"
          }
        }
      ]
    }
  },
  // ...
};

仕組み:

コンテキスト./images/hero.png の処理
catalogs catalog devローカルファイルシステムから http://localhost:3456/assets/images/hero.png で配信
catalogs catalog pushCDN に自動アップロードされ、https://d1abc...cloudfront.net/images/compressed/.../hero.webp に置き換え

ルール:

  • 相対パス(./、../)は、カタログファイルのディレクトリを基準に解決されます
  • 絶対 URL(https://...)はそのまま使用されます
  • 画像はアップロード時に WebP に自動圧縮されます(無料)
  • 動画はアップロード時に HLS に自動トランスコードされます(クレジットが必要です)
  • その他のファイル(PDF、ZIP、ドキュメント)はそのままアップロードされます(50MB ごとにクレジットが必要です)
  • フォルダー構成は不要です。カタログファイルからの相対位置であれば、ファイルを好きな場所に配置できます

プロジェクトのレイアウト例:

my-project/
  my-catalog.ts          # your catalog schema
  images/
    hero.png
    logo.svg
  videos/
    demo.mp4
  scripts/
    calculator.js
  files/
    brochure.pdf
Dev モードの動作
機能Dev モード本番環境
Stripe チェックアウト.env に STRIPE_SECRET_KEY がある場合、/checkout/session、/checkout/session/status、/checkout/intent(非推奨)経由でライブ動作。それ以外の場合はスタブライブ
イベントローカルのみ — ターミナル + SSE ストリーム(/__dev_events_stream)本番環境の分析
ファイル配信ローカルファイルシステムCDN(CloudFront)
ホットリロードファイル保存時(SSE)該当なし
デバッグパネルCtrl+D — formState、カート、ルーティング、イベント該当なし
検証Dev バナー内のライブオーバーレイ該当なし

AI によるバリアントルーティングと事前入力

自然言語のヒントを使って、訪問者を最適なカタログバリアントに自動でルーティングし、条件を満たすフォームフィールドに事前入力します。あらゆるオーディエンスの組み合わせに対応する何百ものバリアントを作成する代わりに、条件付きの質問がコンテキストを利用できる場合に自動回答され、スキップされる単一のカタログを使用できます。

ヒントを使って訪問者をルーティングする(GET — クエリパラメーター)
# Using user_id:
GET https://api.catalogkit.cc/public/route-variant?user_id=USER_ID&slug=my-catalog&hint="female entrepreneur interested in social media"

# Using custom domain instead:
GET https://api.catalogkit.cc/public/route-variant?domain=funnels.mycompany.com&slug=my-catalog&hint="female entrepreneur interested in social media"

注: 読みやすくするため、ヒントの値は引用符で囲んでください。ブラウザーは " を %22 に、スペースを +/%20 に自動的にエンコードします。パラメーター名には hint と hints のどちらも使用できます。user_id または domain のいずれかを指定してください。

ヒントを指定して訪問者をルーティングする(POST — JSON ボディ)

URL エンコードが気になる場合は、JSON ボディを使う POST 版を使用してください。

# Using user_id:
curl -X POST https://api.catalogkit.cc/public/route-variant \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "USER_ID",
    "slug": "my-catalog",
    "hint": "female entrepreneur interested in social media"
  }'

# Using custom domain:
curl -X POST https://api.catalogkit.cc/public/route-variant \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "funnels.mycompany.com",
    "slug": "my-catalog",
    "hint": "female entrepreneur interested in social media"
  }'

hint/hints と user_id/domain のどちらも受け付けます。

レスポンス(GET と POST で共通):

{
  "ok": true,
  "data": {
    "variant_slug": "problem-aware-female",
    "target_slug": "welcome-female-catalog",
    "reason": "ai_matched",
    "prefill": {
      "company_size": "11-50",
      "interest": "social_media"
    }
  }
}

reason の値:ai_matched(LLM が最適な候補を選択)、ai_prefill_only(バリエーションがなく、フィールドの事前入力のみ)、ai_cached(キャッシュされた結果)、weighted_random(重みに基づいてランダムに選択)、hybrid_ai(ハイブリッドモードで LLM が選択)、hybrid_random_fallback(ハイブリッドモードで LLM が失敗し、ランダムに選択)、single_variant(バリエーションが 1 つのみ)、no_variants(カタログにバリエーションがない)、fallback(LLM が判断できず、最初のバリエーションを返した)。バリエーションのルーティング先が別のカタログの場合、target_slug が含まれます。AI による事前入力が可能なフィールドがヒントから照合された場合、prefill が含まれます。

フォールバックの動作:

  • バリエーションが 0 件で、事前入力可能なフィールドもない → null を返し、ベースカタログをそのまま表示します
  • バリエーションが 0 件で、事前入力可能なフィールドがある → 事前入力のみを行うために LLM を呼び出し、reason: "ai_prefill_only" を返します
  • バリエーションが 1 つ → そのまま返します(バリエーション選択のための LLM 呼び出しはスキップ)。フィールドがある場合は事前入力も実行します
  • バリエーションが 2 つ以上 → バリエーションの選択と事前入力のために LLM を 1 回呼び出します。LLM が失敗した場合は、最初のバリエーションにフォールバックします
  • API エラー → ベースカタログを通常どおり表示します(ルーティングの失敗は通知されません)
  • enabled: false のバリエーションは、上記すべてのルーティング数から除外されます(ただし、直接 URL からは引き続きアクセスできます)
フロントエンドのヒント URL

フロントエンドは AI ルーティングを自動的に処理します。URL に hint を追加するだけで利用できます。パスベースの URL とカスタムドメインの両方に対応しています。

# Path-based URL:
https://SUBDOMAIN.catalogkit.cc/my-catalog?hint="female entrepreneur"&ref=253

# Custom domain URL (works the same way):
https://funnels.mycompany.com/my-catalog?hint="female entrepreneur"&ref=253

# Silent redirect (for affiliates — suppresses event tracking):
https://SUBDOMAIN.catalogkit.cc/my-catalog?hint="problem aware male"&silent_redirect=true&ref=253

# After AI routing resolves, browser URL updates to the target catalog slug:
# (uses target_slug when the variant routes to a different catalog, otherwise variant_slug)
https://SUBDOMAIN.catalogkit.cc/my-catalog/welcome-female-catalog?ref=253

AI ルーティングと事前入力の処理中、フロントエンドにはブランドに合わせた読み込み画面が最大 10 秒間表示されます(settings.loading_tips によるヒントのローテーションは任意です)。処理が完了すると、訪問者にはパーソナライズされたカタログが表示され、条件を満たすページは自動的にスキップされます。ルーティングに失敗した場合、またはタイムアウトした場合は、ベースカタログが通常どおり表示されます。

AI 事前入力 — 確認事項をスキップする

フォームフィールドを AI 事前入力可能としてマークすると、ヒントに基づいて自動回答できます。ページに auto_skip: true を設定すると、AI がすでに回答した質問を訪問者がスキップし、ファネルのより先の段階へすぐに進めます。

フィールドで AI 事前入力を有効にする:

{
  "id": "company_size",
  "type": "dropdown",
  "agent_hint": "Number of employees in the company",
  "ai_prefill": {
    "enabled": true,
    "confidence": 0.8,
    "instructions": "Match to the closest range"
  },
  "prefill_mode": "readonly",
  "props": {
    "label": "Company Size",
    "options": [
      { "value": "1-10", "heading": "1-10 employees" },
      { "value": "11-50", "heading": "11-50 employees" },
      { "value": "51-200", "heading": "51-200 employees" },
      { "value": "201+", "heading": "201+ employees" }
    ]
  }
}

ai_prefill の設定:

フィールド型デフォルト説明
enabledboolean—オプトイン切り替え(必須)
confidencenumber0.7最小信頼度しきい値(0-1)。AI はフィールドごとに信頼度スコアを割り当て、この値を下回るものは破棄されます
instructionsstring—この特定のフィールドを入力する際の AI 向け追加ガイダンス(例:「ヒントに特定の会社名が含まれている場合のみ入力する」)

仕組み:

  1. 訪問者が ?hint="50-person digital marketing agency interested in social media" を指定してアクセスします
  2. API がカタログスキーマから ai_prefill.enabled のフィールドをすべて抽出します
  3. 1 回の LLM 呼び出しで、バリアントの振り分けとフィールドの事前入力の両方を処理します
  4. 選択フィールド(ドロップダウン、multiple_choice など)では、AI が選択肢の値から正確に選びます。無効な値はサーバー側で拒否されます
  5. 自由記述フィールドでは、ヒントにほぼ完全一致する内容が含まれる場合にのみ AI が入力します(instructions と confidence で制御)
  6. 事前入力された値は、次の優先順位でフォームの状態にマージされます:default_value < AI prefill < URL params(明示的な指定が常に優先されます)
  7. すべての表示フィールドに値が設定されている場合、auto_skip: true のページはスキップされます

バリアントがなくても動作します。 バリアントが 0 件でも AI 事前入力可能なフィールドがあるカタログでは、ヒントを処理して該当する質問に事前入力できます。バリアントの設定は不要です。

読み込みメッセージ: カタログ設定の loading_message で、読み込み画面のメッセージをカスタマイズできます。デフォルトは "Loading Your Catalog..." です:

{
  "settings": {
    "loading_message": "Preparing your personalized plan..."
  }
}

読み込み時のヒント: loading_tips を追加すると、AI の読み込み画面で読み込みメッセージの下にヒントやメッセージを順番に表示できます:

{
  "settings": {
    "loading_message": "Finding the best options for you...",
    "loading_tips": [
      "Analyzing your preferences",
      "Matching you with the right plan",
      "Almost ready!"
    ]
  }
}
プロパティ型デフォルト説明
loading_messagestring"Loading Your Catalog..."読み込み画面のメインメッセージ
loading_tipsstring[]—読み込みメッセージの下に順番に表示するヒント

サンドボックスモード

本番環境に影響を与えずに、安全にカタログを編集できます。サンドボックスは、独自の URL とスキーマを持つカタログの完全な複製です。変更を加えてライブプレビューし、準備ができたら反映できます。

サンドボックスを作成する
POST https://api.catalogkit.cc/api/v1/catalogs/:id/sandbox
{
  "suffix": "redesign-v2"
}

レスポンス(201):

{
  "ok": true,
  "data": {
    "catalog_id": "01ABC...",
    "slug": "spring-sale--redesign-v2",
    "name": "Spring Sale Landing Page (Sandbox: redesign-v2)",
    "sandbox_of": "01HXY...",
    "parent_slug": "spring-sale",
    "url": "https://SUBDOMAIN.catalogkit.cc/spring-sale--redesign-v2"
  }
}

サンドボックスは、独自の URL を持つ通常のカタログです。PUT /api/v1/catalogs/:sandbox_id を使って自由に編集できます。本番カタログには影響しません。フロントエンドには琥珀色の「SANDBOX」バナーが表示されるため、サンドボックスモードであることを常に確認できます。

カタログのサンドボックスを一覧表示する
GET https://api.catalogkit.cc/api/v1/catalogs/:id/sandboxes
サンドボックスを本番環境に反映する

サンドボックスのスキーマを親カタログにコピーします:

POST https://api.catalogkit.cc/api/v1/catalogs/:sandbox_id/promote
{
  "delete_sandbox": true
}

デフォルトでは、反映後にサンドボックスは削除されます。保持するには "delete_sandbox": false を設定します。

サンドボックスを破棄する
DELETE https://api.catalogkit.cc/api/v1/catalogs/:sandbox_id
サンドボックスを含むカタログの一覧表示

デフォルトでは、GET /api/v1/catalogs はサンドボックスを非表示にします。?include_sandboxes=true を追加すると、サンドボックスも含まれます。各カタログのレスポンスには sandbox_of が含まれます(通常のカタログでは null、サンドボックスでは親カタログの ID)。結果はページネーションされます。大量のリストを順に取得するには、?limit=50&cursor=... を使用します。


Element Inspector(DevEx)

AI エージェントのワークフロー向けの組み込み開発者ツールです。Shift+Alt を押しながら、ライブカタログ内の任意の要素にカーソルを合わせると、詳細なコンテキストが表示されます。クリックすると、AI エージェントがユーザーの指しているものを正確に特定するために使える、構造化された JSON ブロックをコピーできます。

使用方法:

  1. ブラウザーで任意のカタログを開きます
  2. Shift+Alt を押すと、「Inspector active」インジケーターが表示されます(該当する場合はカタログのスラッグとバリアントも表示されます)
  3. 任意の要素にカーソルを合わせると、インディゴ色の枠線で強調表示され、複数行のツールチップが表示されます。ツールチップには次の情報が含まれます:
    • 参照パス(例:landing/hero-title)とコンポーネントの種類
    • コンポーネントの DOM から抽出されたラベルテキスト
    • カタログのコンテキスト — スラッグ、カタログ ID のプレフィックス、バリアントのスラッグ、サンドボックスのステータス
  4. 任意の場所をクリックすると、構造化された JSON ブロックがクリップボードにコピーされます
  5. JSON を AI エージェントとの会話に貼り付けます。要素の特定と変更に必要な情報がすべて含まれています

コピーされる JSON の形式:

{
  "ref": "landing/hero-title",
  "page_id": "landing",
  "component_id": "hero-title",
  "component_type": "heading",
  "label": "Get Started Today",
  "schema_path": "schema.pages.landing.components[id=\"hero-title\"]",
  "catalog_id": "01HXY...",
  "catalog_slug": "spring-sale",
  "variant_slug": "new-headline",
  "api_endpoint": "PUT https://api.catalogkit.cc/api/v1/catalogs/01HXY..."
}

コピーされる JSON のフィールド:

フィールド説明
ref人が読みやすい参照:pageId/componentId または pageId/componentId#subElement
page_idこのコンポーネントを含むページ
component_idページ内でのコンポーネントの一意の ID
component_typeコンポーネントの種類(例:heading、email、multiple_choice、image)
label表示されるラベル/見出しのテキスト(存在する場合)
sub_elementコンポーネント内のサブ要素(例:label、button、input:text、radio、option:b)
schema_pathカタログのスキーマ JSON 内の正確なパス
catalog_idAPI 呼び出しに使用する完全なカタログ ID
catalog_slugカタログの URL スラッグ
variant_slugアクティブなバリアントのスラッグ(バリアントを表示している場合)
variant_idアクティブなバリアント ID(バリアントを表示している場合)
sandbox_of親カタログ ID(サンドボックスの場合)
api_endpointカタログ更新にすぐ使える PUT エンドポイント

サブ要素の指定: インスペクターは、コンポーネント内の子要素まで確認します。ラベル、ボタン、入力欄、画像、見出し、またはオプションカードにカーソルを合わせると、# サフィックス付きのより詳細な参照が表示されます。例:landing/email_field#label、landing/cta#button、quiz_page/q1#option:b。

グローバル UI 要素: インスペクターは、ページのコンテンツ領域外の要素も対象にします。これらの要素では、page_id: "__global" または page_id: "__checkout" が使用されます。

要素component_idcomponent_type
フローティングカートボタン__cart_buttoncart_button
カートのサイドバードロワー__cart_drawercart_drawer
個々のカートアイテム__cart_item_{offer_id}cart_item
カートのチェックアウトボタン__cart_checkout_buttoncart_checkout_button
固定表示の下部バー__sticky_bottom_barsticky_bottom_bar
チェックアウトページ__checkout_pagecheckout_page
チェックアウトの支払いボタン__checkout_pay_buttoncheckout_pay_button
ポップアップダイアログ__popup_{popupId}popup

詳細パネル: クリックしてコピーすると、コピーされた完全な JSON を表示する、閉じることのできるパネルが右下に表示されます。このパネルは Shift+Alt を離した後も表示されたままなので、取得した内容を確認できます。

AI エージェントのワークフロー例:

  1. ユーザーが Shift+Alt を押し、見出しにカーソルを合わせてクリックし、コピーします
  2. ユーザーが Claude に貼り付けて、「この要素を変更してください:{...copied JSON...} を『Welcome Back』に」と伝えます
  3. AI エージェントは JSON から catalog_id、page_id、component_id、api_endpoint を読み取ります
  4. AI エージェントは GET /api/v1/catalogs/{catalog_id} でカタログを取得し、schema.pages.{page_id}.components 内で id == component_id のコンポーネントを見つけ、テキストを更新して PUT で書き戻します

Cart & Checkout

組み込みのチェックアウトを使いましょう — 独自に構築しないでください

Catalog Kit には、本番環境ですぐに使える完全なチェックアウトページが備わっています。独自のチェックアウト UI、決済フォーム、Stripe 連携を一から構築しないでください。組み込みのチェックアウトですべて対応できます。

  • 注文概要 — 画像、タイトル、価格、削除ボタン、クーポンコード、概要行(小計/税額/合計)を含むカート内商品
  • Stripe 決済 — インラインの Payment Element(カード、Link、動的な決済方法)、埋め込み型チェックアウト、またはホスト型リダイレクト
  • 3D Secure — 自動表示されるチャレンジ用ポップアップ、説明バナー、バッジによる銀行レベルの認証
  • 無料トライアル — トライアルバッジ、$0 の確認に使う SetupIntent、またはガード付きトライアル用の手動キャプチャによる保留
  • 保存済みの決済方法 — メールアドレスを使って Stripe Customer を検索または作成し、再訪時に保存済みのカードを表示
  • スキップボタン — カスタマイズ可能なテキストを設定した「支払いをせずに続行」、または無効にして支払いを必須化
  • お客様の声と免責事項 — 社会的証明のカード、支払いボタンの下に表示するカスタム免責事項テキスト
  • 支払い後の遷移 — 成功ページへの自動リダイレクト、URL パラメーターのクリーンアップ、コンバージョンイベントの発火
  • レスポンシブな 2 列レイアウト — デスクトップ(左/右)とモバイル(上/下)の列順を設定可能
  • 複数チェックアウトのアップセルチェーン — 複数の type: "checkout" ページを経由し、それぞれで独自の Stripe 決済を実行

クイックスタート — 3 つの手順でファネルにチェックアウトを追加できます。

  1. Stripe キーを設定する — スキーマに stripe_publishable_key を追加し、設定 API 経由で stripe_secret_key を追加します
  2. settings.checkout を設定する — payment_type、success_page_id、外観や決済に関するオプションを設定します
  3. ページオファーを追加する — カートに商品が追加されるよう、少なくとも 1 つのページで offer を定義します(下記の「ページオファー」を参照)
{
  "settings": {
    "checkout": {
      "payment_type": "one_time",
      "stripe_publishable_key": "pk_test_...",
      "success_page_id": "thank_you",
      "button_text": "Pay Now",
      "prefill_fields": { "customer_email": "comp_email" }
    }
  }
}

以上です。success_page_id が設定されていると、プラットフォームはそのページへの遷移を自動的にインターセプトし、最初にチェックアウト UI を表示します。訪問者がファネルを進み、オファーを承諾して成功ページに到達すると、注文概要とインラインのカード入力欄を備えたチェックアウトが表示されます。Stripe での支払い後は、お礼ページに進みます。カスタム HTML、Stripe SDK の読み込み、リダイレクト処理、フロントエンドでの Webhook 解析は不要です。

各チェックアウトモードの使い分け:

シナリオモード設定
ほとんどのファネル(推奨)Payment Elementstripe_publishable_key を設定し、ui_mode は未設定のままにするか "custom" に設定します
設定を最小限にして、すべて Stripe に任せる埋め込み型ui_mode: "embedded" を設定します
公開可能キーが不要/最もシンプルな連携ホスト型リダイレクトstripe_publishable_key を省略するか、ui_mode: "hosted" を設定します

要点: success_page_id を指定して settings.checkout を設定し、ページオファーを準備すれば、プラットフォームが決済フロー全体を処理します。成功ページへの遷移をインターセプトし、チェックアウトを自動的に表示します。組み込みシステムでは本当に対応できない動作が必要な場合にのみ、カスタムのチェックアウトロジックを構築してください。


Catalog Kit には組み込みのカートとチェックアウトシステムがあります。カスタムのカート HTML を作る必要はありません。フローティングカートボタン、スライド式カートドロワー、完全なチェックアウトページが標準で提供されています。

仕組み
  1. ページオファー — 各ページでは offer オブジェクトを定義できます。訪問者がフォームフィールドからオファーを承諾すると、商品が自動的にカートに追加されます。
  2. カートボタン — カート内の商品数を示すフローティングボタンが右下隅に表示されます。商品がカートに入っている場合のみ表示されます。
  3. カートドロワー — カートボタンをクリックすると、承諾済みのオファーがすべて表示される右側のスライド式パネルが開きます。画像、タイトル、価格、削除ボタンが表示されます。「チェックアウトに進む」ボタンから訪問者はチェックアウトページに進みます。
  4. チェックアウトページ — カート内の商品すべての注文概要を表示し、Stripe で決済します。
名前付きカート

すべてのカートには名前があります。デフォルトのカートは "default" です。ページではこのカートへの追加が提示され、カートドロワーにはこのカートが表示され、特に指定がない限り、このカートに対して決済が行われます。

名前付きカートを使うと、各チェックアウトステップで異なる商品セットに対して決済する複数チェックアウトのアップセルファネルを実行できます。

const kit = window.CatalogKit.get();

// Page offers auto-add to the default cart. You can also add programmatically:
kit.cart().add({
  offer_id: "plan-1", page_id: "pricing",
  title: "Pro Plan", subtitle: "Unlimited access for your team",
  price_display: "$29/mo",
  amount_cents: 2900, payment_type: "subscription", interval: "month",
});

// Create a separate cart for upsells
kit.cart("upsell").add({
  offer_id: "addon-1", page_id: "extras",
  title: "Priority Support", price_display: "$9/mo",
  amount_cents: 900, payment_type: "subscription", interval: "month",
});

// Read items
kit.cart().items();           // → default cart items
kit.cart("upsell").items();   // → upsell cart items

// Move items between carts
kit.cart().moveTo("upsell", "plan-1");   // move one item by offer_id
kit.cart("upsell").moveTo("default");    // move all items back

// Remove / clear
kit.cart().remove("plan-1");
kit.cart("upsell").clear();
kit.clearAllCarts();          // wipe everything
kit.getCartNames();           // → ["default", "upsell"] (non-empty carts)
表示商品と決済商品

各名前付きカートには、表示商品(訪問者に表示される商品)と決済商品(Stripe で請求される商品)の2つのレイヤーがあります。

デフォルトでは両者は同じです。カートで setPaymentItems() を呼び出すと、両者を分離できます。

const kit = window.CatalogKit.get();

// Show a bundle in the cart UI
kit.cart().add({
  offer_id: "bundle-99", page_id: "custom",
  title: "Premium Bundle", price_display: "$99", amount_cents: 9900,
});

// But charge itemized line items via Stripe
kit.cart().setPaymentItems([
  { offer_id: "license", page_id: "custom", title: "Team License (5 seats)", amount_cents: 7500 },
  { offer_id: "onboarding", page_id: "custom", title: "Onboarding Fee", amount_cents: 2400 },
]);

kit.startCheckout(); // Default cart: UI shows "Premium Bundle $99", Stripe charges two line items

// Clear the override — Stripe goes back to charging display items
kit.cart().setPaymentItems(null);

これはカートごとに機能します。kit.cart("upsell").setPaymentItems([...]) を呼び出すと、upsell カートのみが上書きされます。

⚠️ mode_override: "payment" の制約: チェックアウトで mode_override: "payment"(ガード付きトライアルなど)を使用する場合、setPaymentItems() 内のすべてのアイテムに amount_cents を含める必要があります。stripe_price_id のみを含むアイテムは payment モードでは除外され、請求額が $0 になります。

ページタイプとしてのチェックアウト

チェックアウトはルーティンググラフ内のページであり、type: "checkout" です。モーダルやオーバーレイではなく、第一級のページです。他のページと同様に、ルーティング、進行状況バー、戻るボタン、BFS の深度に関与します。

シンプルな単一チェックアウト:

{
  "pages": {
    "landing": { "title": "Pick your plan", "components": [/* ... */] },
    "checkout": {
      "type": "checkout",
      "title": "Complete Your Order",
      "components": []
    },
    "thank-you": { "title": "Thank you!", "components": [/* ... */] }
  },
  "routing": {
    "entry": "landing",
    "edges": {
      "landing": { "next": "checkout" },
      "checkout": { "next": "thank-you" }
    }
  }
}

名前付きカートを使った複数チェックアウト:

各チェックアウトページでは、cart_name を使って決済対象のカートを指定します。

{
  "pages": {
    "landing": { "title": "Pick your plan", "components": [/* ... */] },
    "checkout-main": {
      "type": "checkout",
      "title": "Complete Your Order",
      "cart_name": "default",
      "components": []
    },
    "upsell": { "title": "Add premium support?", "components": [/* ... */] },
    "checkout-upsell": {
      "type": "checkout",
      "title": "Add-on Payment",
      "cart_name": "upsell",
      "checkout": { "title": "Premium Support" },
      "components": []
    },
    "thank-you": { "title": "Thank you!", "components": [/* ... */] }
  },
  "routing": {
    "entry": "landing",
    "edges": {
      "landing": { "next": "checkout-main" },
      "checkout-main": { "next": "upsell" },
      "upsell": { "next": "checkout-upsell" },
      "checkout-upsell": { "next": "thank-you" }
    }
  }
}

cart_name の解決順序: page.cart_name → page.checkout.cart_name → settings.checkout.cart_name → "default"。ほとんどのカタログでは設定する必要はありません。デフォルトのカートがそのまま機能します。

主な動作:

  • type: "checkout" — コンポーネントの代わりに Stripe のチェックアウト UI をレンダリングします。
  • page.checkout — このチェックアウトページに限り、settings.checkout のフィールドを上書きする任意の Partial<CheckoutSettings> です。ステップごとに異なるタイトル、決済設定、スキップ動作を指定できます。
  • cart_name — このチェックアウトページが決済する名前付きカートです。デフォルトのカートを使う場合は省略します。
  • components: [] — 必須フィールドですが、チェックアウトページでは無視されます。常に空の配列を設定してください。
  • スキップボタン — 「支払わずに続ける」を選ぶと、ファネルを終了せずにルーティングエッジに従って次のページに進みます。
  • 戻るボタン — ブラウザーの戻る操作による通常の履歴のポップです。
  • カートのクリア — Stripe 決済が成功すると、すべてのカートが自動的にクリアされます。
  • Stripe からのリダイレクト — 決済後、Stripe は ck_page URL パラメーターを付けてリダイレクトします。このパラメーターで決済を開始したチェックアウトページを特定し、その後ルーティング上の次のページに自動的に進みます。
チェックアウトステップ間での Stripe Customer の再利用

settings.checkout で reuse_payment_method: true を設定すると、最初のチェックアウトでメールアドレスを使って Stripe Customer を作成(または検索)します。customer_id はセッションに自動的に保存され、後続のチェックアウトステップに渡されます。2回目のチェックアウトには、最初のチェックアウトで保存されたカードが表示されます。

{
  "settings": {
    "checkout": {
      "reuse_payment_method": true,
      "prefill_fields": { "customer_email": "comp_email" }
    }
  }
}

スクリプトは不要です。レンダラーがステップ間の customer_id の受け渡しを自動的に処理します。

スクリプト駆動型のフローでは、window.CatalogKit から顧客 ID を取得することもできます。

const kit = window.CatalogKit.get();
kit.getStripeCustomerId();          // → "cus_xxx" after first checkout
kit.setStripeCustomerId("cus_yyy"); // manually set (e.g. from your own API)
チェックアウト設定

settings.checkout でチェックアウトを設定します。

{
  "settings": {
    "checkout": {
      "payment_type": "one_time",             // "one_time" | "subscription" | "pay_what_you_want"
      "title": "Complete Your Order",
      "subheading": "No commitment. Cancel anytime.", // Optional plain text below the title
      "stripe_publishable_key": "pk_live_...",
      "cart_name": "default",                 // Which named cart auto-checkout charges. Omit for "default"

      // Payment options
      "allow_discount_codes": true,            // Show promo code field at Stripe checkout
      "free_trial": { "enabled": true, "days": 14 },  // Subscriptions only
      "payment_methods": ["card", "link"],
      "payment_description": "My Product",
      "client_reference_id": "{{comp_email}}", // Template strings supported. Can also be set dynamically via kit.setClientReferenceId() which takes priority

      // Custom metadata — passed to Stripe session/intent metadata. Supports {{field_id}} templates.
      // Editor keys merge with (and can override) system fields: catalog_id, catalog_slug, user_id, tracer_id.
      "metadata": {
        "ref": "spring-promo",
        "customer_email": "{{comp_email}}",
        "plan": "{{selected_plan}}"
      },

      // Prefill from form fields
      "prefill_fields": {
        "customer_email": "comp_email",        // Component ID to read email from
        "customer_name": "comp_name",
        "customer_phone": "comp_phone"
      },

      // Saved payment methods (for multi-checkout upsell chains)
      "reuse_payment_method": true,            // Find-or-create Stripe Customer by email, show saved cards on subsequent checkouts. Requires prefill_fields.customer_email
      "customer_metadata": {                   // Optional — merged onto Stripe Customer with cck_ prefix on every checkout (create + reuse). Supports {{field_id}} templates
        "ref": "{{ref_param}}",
        "source": "my-catalog"
      },

      // Skip
      "allow_skip": true,                      // Allow "Continue without paying" button (default: true, set false to require payment)
      "skip_button_text": "No thanks, continue free", // Custom text for skip button (default: "Continue without paying")

      // Layout
      "layout_desktop": "cart_left",           // "cart_left" (default) | "cart_right" — column order on desktop (lg+)
      "layout_mobile": "cart_top",             // "cart_top" (default) | "cart_bottom" — column order on mobile (<lg)

      // Appearance
      "ui_mode": "custom",                     // "custom" (Payment Element, default) | "embedded" (Stripe embedded checkout) | "hosted" (redirect)
                                               // When omitted + stripe_publishable_key is set: "custom" (Payment Element with Link support)
      "button_text": "Subscribe Now",
      "card_header_text": "Complete Checkout",  // Text at top of payment column (default: "Complete Checkout")
      "below_button": {                         // Optional content below the pay button (empty by default)
        "type": "text",                         // "text" (default) | "button" — plain text or clickable link
        "text": "90 day refunds",
        "url": "https://..."                    // Only used when type is "button"
      },
      "summary_lines": [                       // Display-only lines below cart items (no calculations — editor defines all text)
        { "label": "Subtotal", "value": "$97.00" },
        { "label": "Tax (8%)", "value": "$7.76" },
        { "label": "Total", "value": "$104.76", "bold": true }
      ],
      "testimonial": {
        "enabled": true,
        "text": "Best investment I've made...",
        "author": "Jane S.",
        "avatar": "https://..."
      },
      "show_disclaimer": true,
      "disclaimer_text": "By purchasing you agree to our Terms",
      "components": [],                        // Extra display components below order summary

      // Error modal — customizable modal shown when checkout fails (e.g. 3DS, fund holds)
      "error_modal": {
        "enabled": true,
        "headline": "Payment issue",
        "message": "Your bank requires extra verification that couldn't be completed.\n\nThis sometimes happens with certain cards. You can try a different card or use our alternative checkout.",
        "alt_action": {
          "label": "Use alternative checkout",
          "url": "https://pay.example.com/alt"
        },
        "dismiss_label": "Try a different card"
      },

      // After payment
      "send_receipt": true,
      "success_redirect": "https://...",
      "success_page_id": "thank_you"
    }
  }
}

さらに、設定 API を使って Stripe のシークレットキーを設定する必要があります(上記の「設定の更新」を参照)。

{ "stripe_secret_key": "rk_live_..." }

Stripe の分離: 各カタログには、独自の stripe_publishable_key に紐付いた Stripe インスタンスが割り当てられます。同じブラウザーセッションで異なる Stripe アカウントを使用する複数のカタログをユーザーが表示した場合でも、各カタログのチェックアウトでは正しい Stripe アカウントが使用されます。カタログ間でキーや決済状態が混在することはありません。

ページのオファー(カートアイテム)

任意のページで offer を定義できます。訪問者のフォームフィールドが accept_value と一致すると、オファーが自動的にカートに追加されます:

{
  "id": "pricing",
  "title": "Choose Your Plan",
  "components": [
    {
      "id": "offer_choice",
      "type": "multiple_choice",
      "label": "Select an option",
      "options": [
        { "value": "accept", "heading": "Yes, I want this!" },
        { "value": "decline", "heading": "No thanks" }
      ]
    }
  ],
  "offer": {
    "id": "growth-bundle",
    "title": "Growth Bundle",
    "subtitle": "Everything you need to scale",  // Subheading below item title in cart/checkout
    "price_display": "$49/mo",
    "price_subtext": "/month",          // Gray subtitle below price in cart/checkout.
    // Auto-derives from interval when omitted (e.g. interval:"month" → "/month").
    // Set to "" to suppress entirely. Use any string to override (e.g. "per year", "billed annually").
    "stripe_price_id": "price_...",   // Use a Stripe Price ID...
    // OR use inline pricing (no pre-configured Stripe price needed):
    // "amount_cents": 4900,          // $49.00
    // "currency": "usd",             // default: "usd"
    // "payment_type": "subscription", // "one_time" | "subscription" | "pay_what_you_want"
    // "interval": "month",           // for subscriptions: "day" | "week" | "month" | "year"
    "image": "https://...",
    "accept_field": "offer_choice",     // Component ID to watch
    "accept_value": "accept"            // Value that triggers add-to-cart
  }
}

重要: Stripe のチェックアウトに進むすべてのオファーには、stripe_price_id(事前設定済みの Stripe Price)または amount_cents(インライン価格)のいずれかが必要です。どちらも指定されていない場合、チェックアウトは「Missing required param」エラーで失敗します。UI に人が読める価格を表示するには price_display を使用してください。

⚠️ mode_override: "payment" の制約: チェックアウト設定で mode_override: "payment"(保護付きトライアルなど)を使用する場合、すべてのオファーに amount_cents を含める必要があります。stripe_price_id だけでは除去され、請求額が $0 になります。これは setPaymentItems() で渡すアイテムにも当てはまります。

オファーの表示フィールド — ビジュアルレイアウト

各カート/チェックアウトアイテムには、2 列に配置される4 つの表示フィールドがあります:

┌──────────────────────────────────────────────────┐
│  [image]   title                  price_display   │
│            subtitle               price_subtext   │
└──────────────────────────────────────────────────┘
フィールド位置例備考
title左側、太字"OfficeX Membership"必須
subtitle左側、タイトルの下に小さなグレー文字で表示"200+ Tools & Playbooks for Agents"アイテムの任意のサブ見出し
price_display右側、太字の緑色"7-Day Free Trial"、"$30/mo"、"Included"視覚的な価格ラベル
price_subtext右側、価格の下に小さなグレー文字で表示"then $30/month"、"/month"、"with membership"省略すると interval から自動生成されます。非表示にするには "" を設定します

例 — トライアル付きのメンバーシップ+追加アイテム込み:

// Page 1 offer
"offer": {
  "id": "membership",
  "title": "OfficeX Membership",
  "subtitle": "200+ Tools & Playbooks for Agents",
  "price_display": "7-Day Free Trial",
  "price_subtext": "then $30/month",
  "amount_cents": 3000,
  "payment_type": "subscription",
  "interval": "month"
}

// Page 2 offer
"offer": {
  "id": "tiktok-farm",
  "title": "TikTok Farm USA",
  "subtitle": "Automated US-based posting",
  "price_display": "Included",
  "price_subtext": "with membership",
  "amount_cents": 0
}

よくある間違い:

  • すべてを price_display にまとめないでください(例:"7-Day Free Trial then $30/month")。2 行レイアウトが使えなくなり、サブテキストが表示されません
  • アイテムの説明を title に入れないでください。2 行目には subtitle を使用してください
  • subtitle(アイテムタイトルの下、左側)と price_subtext(価格の下、右側)を混同しないでください

カートアイテムはページをまたいで蓄積されます。各ページで異なるオファー(例:1 ページ目にメイン商品、2 ページ目にアップセル、3 ページ目に注文時の追加商品)を提示できます。受け入れられたすべてのオファーが Stripe Checkout の明細項目になります。

1 ページでの複数オファー/注文時の追加商品

ページの offer フィールドには、単一の PageOffer を指定できます。同じページで複数の注文時の追加商品を提示するには、メインアイテムには標準の offer を使用し、追加商品には kit.cart().add() を使用します:

  1. メインオファー — 通常どおりページの offer フィールドを使用します(承諾時に自動でカートに追加されます)。
  2. 追加オファー — <script> を含む html コンポーネントを追加し、承諾をリッスンして kit.cart().add() を呼び出します。
{
  "id": "pricing_page",
  "title": "Choose Your Plan",
  "components": [
    {
      "id": "main_choice",
      "type": "multiple_choice",
      "label": "Main plan",
      "options": [
        { "value": "accept", "heading": "Yes, sign me up!" },
        { "value": "decline", "heading": "No thanks" }
      ]
    },
    {
      "id": "bump_choice",
      "type": "multiple_choice",
      "label": "Add priority support?",
      "options": [
        { "value": "accept", "heading": "Yes, add it — $9/mo" },
        { "value": "decline", "heading": "No thanks" }
      ]
    },
    {
      "id": "bump_script",
      "type": "html",
      "props": {
        "content": "<script>\nconst kit = window.CatalogKit.get();\nkit.on('fieldchange:bump_choice', () => {\n  const val = kit.getField('bump_choice');\n  if (val === 'accept') {\n    kit.cart().add({\n      offer_id: 'priority-support',\n      page_id: 'pricing_page',\n      title: 'Priority Support',\n      subtitle: 'Direct Slack channel with the team',\n      price_display: '$9/mo',\n      amount_cents: 900,\n      payment_type: 'subscription',\n      interval: 'month',\n    });\n  } else {\n    kit.cart().remove('priority-support');\n  }\n});\n</script>"
      }
    }
  ],
  "offer": {
    "id": "pro-plan",
    "title": "Pro Plan",
    "price_display": "$29/mo",
    "amount_cents": 2900,
    "payment_type": "subscription",
    "interval": "month",
    "accept_field": "main_choice",
    "accept_value": "accept"
  }
}

要点:

  • 標準の offer は、accept_field/accept_value を使った自動的なカートへの追加/カートからの削除でメインアイテムを処理します
  • 追加商品には fieldchange リスナー内で kit.cart().add() と kit.cart().remove() を使用します。同じカート、同じチェックアウトが使われます
  • 追加オファーごとに一意の offer_id が必要です(カートは offer_id で重複を排除します)
  • Stripe のチェックアウトが正しく動作するよう、すべてのカートアイテムに amount_cents と payment_type を含めてください
  • 必要に応じてスクリプトによる追加商品をいくつでも追加できます。上限はありません
カートアイテムのボタン

カートアイテムには任意の button を指定できます。これはカートドロワーとチェックアウトの概要で、価格の横にサイドリンクとして表示されます:

{
  "offer": {
    "id": "growth-bundle",
    "title": "Growth Bundle",
    "price_display": "$49/mo",
    "stripe_price_id": "price_...",
    "button": { "label": "Details", "url": "https://example.com/growth", "style": "secondary", "size": "sm" }
  }
}
チェックアウトページのレイアウト

チェックアウトページは、デスクトップでは2列レイアウト、モバイルでは1列レイアウトです。列の順序は、デスクトップとモバイルで個別に設定できます。

デフォルトのレイアウト:

  • デスクトップ:左にカート(注文概要)、右にカード(支払い)
  • モバイル:上にカート、下にカード

カート列(注文概要):

  • 注文概要(画像、価格、削除ボタン付きのカート商品)
  • 概要行 — 税金、合計など(summary_lines が設定されている場合)
  • クーポンコード入力欄(allow_discount_codes が有効な場合)
  • お客様の声(testimonial.enabled が true の場合)
  • カスタムコンポーネント(components 配列が設定されている場合)

カード列(支払い — スティッキー):

  • ヘッダーテキスト(card_header_text で設定可能、デフォルト:「Complete Checkout」)
  • メッセージバナー(card_message で設定可能。例:「7日間は請求されません」)
  • 顧客情報フィールド(メールアドレス、氏名、電話番号 — prefill_fields による)
  • カード入力フィールド(Stripe Elements — stripe_publishable_key が設定されている場合)
  • 支払いボタン
  • ボタンの下のコンテンツ(below_button で設定可能。デフォルトは空)
  • スキップボタン(allow_skip が true の場合)
  • エラーメッセージ
  • 3DS の「追加の認証が必要です」という通知(require_3ds が true の場合)
  • 「Powered by Stripe」バッジ
レイアウト設定
フィールド型デフォルト説明
layout_desktop"cart_left" | "cart_right""cart_left"デスクトップ(lg+)での列の順序です。"cart_right" にすると、注文概要が右、支払いが左になります。
layout_mobile"cart_top" | "cart_bottom""cart_top"モバイル(<lg)での列の順序です。"cart_bottom" にすると、支払いカードが上、注文概要が下になります。
card_header_textstring"Complete Checkout"支払いカード列の上部に表示されるテキストです。
card_messagestring—card_header_text の下に表示される色付きのメッセージバナーです(例:「7日間は請求されません」、「安全なチェックアウト」、「いつでもキャンセル可能」)。設定されている場合にのみ表示されます。
below_buttonobject—チェックアウトボタンの下に表示する任意のコンテンツです。デフォルトは空です。
below_button.type"text" | "button""text"プレーンテキストまたはクリック可能なリンクとして表示します。
below_button.textstring—表示するテキストの内容です。
below_button.urlstring—ボタンタイプの URL です(新しいタブで開きます)。
// Example: swap columns — payment on left, cart on right (desktop)
{ "checkout": { "layout_desktop": "cart_right" } }

// Example: show payment card first on mobile
{ "checkout": { "layout_mobile": "cart_bottom" } }

// Example: customize card header, message banner, and refund notice
{
  "checkout": {
    "card_header_text": "Secure Payment",
    "card_message": "You won't be charged for 7 days",
    "below_button": { "type": "text", "text": "90 day money-back guarantee" }
  }
}

// Example: refund policy link button below checkout
{
  "checkout": {
    "below_button": {
      "type": "button",
      "text": "View refund policy",
      "url": "https://example.com/refunds"
    }
  }
}
スキップボタン

スキップボタンを使うと、訪問者は支払いをせずに続行できます。埋め込みカードモードとインラインカードモードのどちらでも、支払いボタンの下に表示されます。

フィールド型デフォルト説明
allow_skipbooleantrueスキップボタンの表示/非表示を切り替えます。支払いを必須にするには false を設定します。
skip_button_textstring"Continue without paying"スキップボタンに表示するカスタムテキストです。
// Example: hide the skip button entirely
{ "checkout": { "allow_skip": false } }

// Example: customize the skip button text
{ "checkout": { "skip_button_text": "Maybe later — continue for free" } }
注文概要の行

注文概要カード内で、カート商品の下に表示専用の行を追加します。小計、税金、手数料、割引、合計などに便利です。計算は行われません — 各ラベルと値のテキストは、表示したい内容をそのままエディターで定義します。

フィールド型必須説明
summary_lines[].labelstringはい左揃えのラベル(例:「小計」、「税金(8%)」)
summary_lines[].valuestringはい右揃えの値(例:「$97.00」)
summary_lines[].boldbooleanいいえtrue の場合、行を太字で表示します(合計に使用します)
// Example: subtotal + tax + total
{
  "checkout": {
    "summary_lines": [
      { "label": "Subtotal", "value": "$97.00" },
      { "label": "Tax (8%)", "value": "$7.76" },
      { "label": "Total", "value": "$104.76", "bold": true }
    ]
  }
}

// Example: discount line
{
  "checkout": {
    "summary_lines": [
      { "label": "Subtotal", "value": "$150.00" },
      { "label": "Launch Discount", "value": "-$53.00" },
      { "label": "Total", "value": "$97.00", "bold": true }
    ]
  }
}
チェックアウトの UI モード

チェックアウトページでは、3 つの支払いモードが自動的に決定されます。

ConfigModeBehavior
stripe_publishable_key set, no ui_mode or "custom"Payment Element(デフォルト)Stripe Payment Element が右側の列に表示され、カード、Link、および動的な決済方法に対応します。Checkout Sessions の ui_mode: "custom" を使用します。
stripe_publishable_key set, ui_mode: "embedded"埋め込み型チェックアウトStripe の完全な埋め込み型チェックアウト UI が右側の列に表示されます。カード情報の入力と支払いはすべて Stripe が処理します。
No stripe_publishable_key, or ui_mode: "hosted"ホスト型リダイレクト支払いボタンをクリックすると、Stripe のホスト型チェックアウトページにリダイレクトされます。

Payment Element は、ローカル開発と本番環境の両方で推奨されるデフォルトです。ユーザーはページ上にとどまり、Link(ワンクリックチェックアウト)が自動的に有効になり、3DS チャレンジと動的な決済方法に対応します。また、すべての Stripe テストカード(4242 4242 4242 4242、3DS 用の 4000 0027 6000 3184 など)を利用できます。

重要: stripe_publishable_key は、サーバーの Stripe シークレットキーと同じモード(テスト/ライブ)である必要があります。本番環境では pk_live_* を使用してください。開発環境では pk_test_* を使用してください。テストキーが検出されると、CLI は catalog push の実行時に警告します。モードが一致しない場合、チェックアウト時に「Stripe key mode mismatch」エラーが発生します。

対応するチェックアウト先

Stripe ホスト型チェックアウトがデフォルトですが、Polar.sh、LemonSqueezy、Gumroad、または任意のカスタム URL にリダイレクトすることもできます。

カートのカスタマイズ(settings.cart)

フローティングカートボタン、スライドアウト式ドロワー、チェックアウトフローをカスタマイズします。すべての HTML フィールドで {{field_id}} によるテンプレート補間に対応しています。

{
  "settings": {
    "cart": {
      // Config
      "icon": "bag",                           // "cart" (default) | "bag" | "basket" | image URL
      "title": "Your Selection",               // Drawer title (default: "Your Cart")
      "checkout_button_text": "Complete Order", // Override "Proceed to Checkout" text
      "destination_url": "https://pay.example.com?email={{email}}", // External URL (skips built-in checkout)
      "destination_page": "checkout",          // Internal page ID (navigates within funnel instead of Stripe overlay)
      "checkout_url": "https://pay.example.com/checkout", // Alternative external checkout URL (supports {{field_id}} templates)
      "position": "bottom-left",               // "bottom-right" (default) | "bottom-left" | "top-right" | "top-left"
      "hide_button": false,                    // Hide floating button (use kit.openCart() to open programmatically)

      // HTML/CSS slots
      "header_html": "<div class='my-header'>Custom Header</div>",  // Replaces default header
      "footer_html": "<p>30-day money-back guarantee</p>",          // Inserted above checkout button
      "empty_html": "<p>Browse our offers to get started</p>",      // Replaces default empty state
      "css": ".ck-cart-drawer { border-radius: 16px; } .ck-cart-checkout-btn { background: linear-gradient(135deg, #667eea, #764ba2) !important; }"
    }
  }
}

外部スタイル設定用のCSS クラスフック:.ck-cart-drawer、.ck-cart-header、.ck-cart-empty、.ck-cart-item[data-offer-id="..."]、.ck-cart-footer、.ck-cart-footer-custom、.ck-cart-checkout-btn。

アイコンのプリセット:

PresetDescription
"cart"ショッピングカート(デフォルト)
"bag"ショッピングバッグ
"basket"ショッピングバスケット
"https://..."カスタム画像 URL
カートイベント(分析+JS)

以下のイベントが自動的に追跡されます:cart_add、cart_remove、checkout_start、checkout_error、payment_info_added、checkout_complete、checkout_skip。

window.CatalogKit 経由のJavaScript イベント:

EventPayloadCancelableDescription
cart_add{ item, items }Noオファーがカートに追加されました
cart_remove{ offer_id, items }Noオファーがカートから削除されました
cart_open{ items }Noカートのドロワーが開きました
cart_close{ items }Noカートのドロワーが閉じました
before_checkout{ items, cartName, preventDefault() }Yesチェックアウトの前に発火します。キャンセルして独自に処理するには preventDefault() を呼び出します。cartName はカートを識別します
const kit = window.CatalogKit.get();

// Track cart additions
kit.on('cart_add', (e) => {
  console.log('Added:', e.item.title, 'Total items:', e.items.length);
});

// Intercept checkout — redirect to external payment
kit.on('before_checkout', (e) => {
  e.preventDefault();
  window.location.href = 'https://my-custom-checkout.com?cart=' + e.cartName + '&items=' + e.items.length;
});

// ── Named carts ──
kit.cart().add({ offer_id: "plan-1", page_id: "pricing", title: "Pro Plan", amount_cents: 2900 });
kit.cart().items();                      // Frozen array of default cart items
kit.cart().remove("plan-1");             // Remove by offer_id
kit.cart("upsell").add({ ... });         // Add to a named cart
kit.cart("upsell").items();              // Read named cart
kit.cart().moveTo("upsell", "plan-1");   // Move item between carts
kit.cart("upsell").clear();              // Clear one cart
kit.clearAllCarts();                     // Clear all carts
kit.getCartNames();                      // Non-empty cart names

// ── Payment item overrides (per cart) ──
kit.cart().setPaymentItems([             // Override what Stripe charges (null = use display items)
  { offer_id: "license", page_id: "custom", title: "Team License", amount_cents: 7500 },
  { offer_id: "onboard", page_id: "custom", title: "Onboarding", amount_cents: 2400 },
]);
kit.cart().paymentItems();               // Current override or null

// ── Cart drawer + checkout ──
kit.openCart();                          // Open the cart drawer (shows default cart)
kit.closeCart();                         // Close the cart drawer
kit.startCheckout();                     // Show checkout for default cart (fires before_checkout first)
kit.startCheckout("upsell");             // Show checkout for a specific named cart

// ── Stripe Customer reuse ──
kit.getStripeCustomerId();               // Customer ID from prior checkout step, or null
kit.setStripeCustomerId("cus_xxx");      // Manually set (e.g. from your own API)

// ── Client reference ID (custom Stripe tracking) ──
kit.setClientReferenceId('my-ref-123');  // Set before checkout — overrides schema template
kit.getClientReferenceId();              // Current value or null

// Set dynamically with custom logic in before_checkout:
kit.on('before_checkout', (e) => {
  const email = kit.getField('email');
  const plan = kit.getField('plan');
  kit.setClientReferenceId(`${email}::${plan}::${Date.now()}`);
});

チェックアウトの開始

訪問者が組み込みのチェックアウトページに進む方法は 3 つあります。チェックアウトはページのトポロジーに依存しません。ページには任意の数の出辺を設定できます。これにより、訪問者がチェックアウトを複数回通過する可能性のある、アップセル、追加商品、段階的なオファーなどの複数チェックアウトフローに対応できます。

1. success_page_id によるゲート(推奨)

settings.checkout.success_page_id が設定されている場合、プラットフォームは、ナビゲーション先がそのページになるたびにチェックアウトを自動的に表示します。訪問者にはチェックアウト UI 全体が表示され、支払いを済ませるか(または allow_skip が true の場合はスキップして)、その後サクセスページに進みます。通常どおりサクセスページへ遷移するように設定するだけで、チェックアウトページが遷移をインターセプトします。

{
  "routing": {
    "entry": "landing",
    "edges": [
      { "from": "landing", "to": "offers" },
      { "from": "offers", "to": "review" },
      { "from": "review", "to": "next_steps" }   // ← checkout auto-shows before landing here
    ]
  },
  "settings": {
    "checkout": {
      "success_page_id": "next_steps",  // ← the gate
      "stripe_publishable_key": "pk_test_...",
      "payment_type": "subscription"
    }
  }
}
2. カートのチェックアウトボタン

訪問者はいつでも、フローティングカートボタンをクリックしてカートのドロワーを開き、「Proceed to Checkout」をクリックできます。これにより before_checkout イベントが発火し、チェックアウトページが表示されます。settings.checkout 以外の設定は不要です。

3. プログラムから kit.startCheckout(cartName?) を呼び出す

カスタムボタンやスクリプトで制御するフローでは、kit.startCheckout() を呼び出すと、チェックアウトページをプログラムから表示できます。特定の名前付きカートでチェックアウトするには、カート名を任意で指定します(デフォルトは "default" です)。

{
  "id": "custom_checkout_btn",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\ndocument.getElementById('my_pay_btn').addEventListener('click', () => {\n  kit.startCheckout();\n});\n</script>\n<button id=\"my_pay_btn\" style=\"padding:12px 24px;background:#16a34a;color:white;border:none;border-radius:8px;font-weight:bold;cursor:pointer;\">Pay Now</button>"
  }
}

名前付きカートでチェックアウトする場合:

const kit = window.CatalogKit.get();
kit.startCheckout("upsell"); // Checks out the "upsell" cart specifically

この処理では、before_checkout(キャンセル可能。ペイロードに cartName を含みます)が発火した後、組み込みのチェックアウトページが表示されます。カートのチェックアウトボタンをクリックする場合と同じ動作ですが、カートドロワーを開く必要はありません。

よくあるアンチパターン:DOM 操作でカートのチェックアウトボタンをクリックする

beforenext をインターセプトして遷移を防ぎ、カートを開いてから setTimeout と document.querySelector を使ってカートのチェックアウトボタンをクリックする方法は絶対に使用しないでください。React の状態更新と DOM 操作の競合状態が発生し、本番環境でクラッシュする原因になります。代わりに、上記のいずれかの方法を使用してください。

// ❌ WRONG — fragile DOM hack, causes race conditions and crashes
kit.on('beforenext:review', (e) => {
  e.preventDefault();
  kit.openCart();
  setTimeout(() => {
    document.querySelector('.ck-cart-checkout-btn').click(); // DON'T DO THIS
  }, 300);
});

// ✅ CORRECT — use success_page_id so checkout auto-shows before the success page
// Or call kit.startCheckout() programmatically
レシピ:チェックアウト前のレビューページ

支払いの前に概要や確認ページを表示するのは、よくあるパターンです。success_page_id を使って、チェックアウト後に成功ページへ進むようにします。

  1. レビュー用ページを作成する。概要コンテンツ(見出し、コールアウト、保証など)を含めます
  2. レビュー用ページから成功ページへ遷移させる。チェックアウトページが自動的に遷移をインターセプトします
  3. 送信ボタンのラベルを設定する。CTA を指定します(例:"submit_label": "Start Free Trial")
  4. settings.checkout を設定する。success_page_id に成功ページを指定します
// Review page — has an outgoing edge to end_screen
pages["review"] = {
  title: "Start Your 7-Day Trial",
  components: [
    { id: "summary_heading", type: "heading", props: { text: "Review Your Order", level: 1, align: "center" } },
    { id: "guarantee", type: "callout", props: { style: "success", title: "90-Day Guarantee", text: "If you don't see value, we'll refund you." } },
  ],
  submit_label: "Start Free Trial",
  submit_reassurance: "Cancel anytime. 90-day money-back guarantee.",
};

// Routing — review routes to end_screen; checkout intercepts via success_page_id
edges: [
  { from: "last_offer", to: "review", conditions: { match: "all", rules: [{ field: "cart_flag", operator: "equals", value: "yes" }] } },
  { from: "last_offer", to: "end_screen", is_default: true },
  { from: "review", to: "end_screen" },
]

// settings.checkout gates end_screen behind payment
settings: {
  checkout: {
    payment_type: "subscription",
    success_page_id: "end_screen",  // ← checkout auto-shows before this page
    title: "Complete Your Order",
    button_text: "Start Free Trial",
    // ... 3DS, trial, components, testimonial, disclaimer
  }
}

訪問者がレビュー用ページで「Start Free Trial」をクリックすると、プラットフォームが注文概要付きの組み込みチェックアウトを自動的に表示し、Stripe での支払い後、end_screen にリダイレクトします。

他のページでカートのチェックアウトボタンをクリックした訪問者を、まずレビュー用ページにリダイレクトするには、before_checkout を使用します。

const kit = window.CatalogKit.get();
kit.on('before_checkout', (e) => {
  if (kit.getPageId() !== 'review') {
    e.preventDefault();
    kit.closeCart();
    kit.goToPage('review');
  }
  // On review page, navigating to end_screen triggers checkout via success_page_id gate
});

チェックアウト後のリダイレクト

Stripe からカタログ URL にリダイレクトされた後、レンダラーは自動的に次の処理を行います。

  1. URL パラメーター(?checkout=success または ?redirect_status=succeeded)を使ってチェックアウトの成功を検出します
  2. localStorage からセッション全体を復元します(formState、history、元の URL パラメーター、カート内の商品)。チェックアウト前に収集されたすべてのフィールドは、成功ページ/チェックアウト後のページで kit.getField() から取得できます
  3. settings.checkout.success_page_id に直接遷移します(エントリーページはスキップされます)
  4. カートの詳細(商品、表示価格、数量)、Stripe のリダイレクトパラメーター、実際の Stripe セッション金額(/checkout/session/status から非同期で取得)を含む checkout_complete アナリティクスイベントを発火します。value.amount_cents フィールドは収益の集計に使われます
  5. URL から Stripe のクエリパラメーターを削除します(再読み込みのないクリーンな URL になります)
  6. 「続きから再開しますか?」モーダルを表示しないようにします(購入後のアップセルページで使えるようにセッションは保持されます)

セッションは、複数回の Stripe リダイレクト(複数のチェックアウトからなるアップセルチェーン)をまたいで保持されます。セッションがクリアされるのは、フォームが最終送信されたとき、または「最初からやり直す」が明示的に選択されたときだけです。

Stripe リダイレクトをまたいだ URL パラメーターの保持

Stripe はリダイレクト時に、元の URL クエリパラメーター(例:?ref=abc123&utm_source=google)をすべて削除します。レンダラーはこれらを自動的に保持します。

  • チェックアウト前に、段階的なセッション保存の一部として、すべての URL パラメーターが localStorage に保存されます。
  • Stripe のリダイレクト後に、保存された URL パラメーターが復元され、Stripe のリダイレクトパラメーターとマージされます。?ref=abc123 などの元のパラメーターは、再び kit.getUrlParam("ref") や kit.getAllUrlParams() から取得できます。
  • マージの優先順位:Stripe の実際のリダイレクトパラメーター(checkout、session_id、redirect_status)が保存された値より優先されます。それ以外の値は元のパラメーターから補われます。

これにより、アトリビューションパラメーター(ref、utm_source、utm_medium、utm_campaign など)、事前入力のマッピング、その他のカスタム URL パラメーターは、追加設定なしで Stripe のリダイレクト後も保持されます。成功ページ/チェックアウト後のページのスクリプトから、通常どおり読み取れます。

const kit = window.CatalogKit.get();
kit.on('pageenter:next_steps', () => {
  const ref = kit.getUrlParam('ref');           // "abc123" — preserved from original visit
  const allParams = kit.getAllUrlParams();       // { ref: "abc123", utm_source: "google", ... }
  console.log('Referral:', ref);
});

success_page_id が設定されていない場合、訪問者は 1 ページ目に移動します。必ず設定してください。

{
  "settings": {
    "checkout": {
      "success_page_id": "next_steps"
    }
  }
}
モード別の Stripe URL パラメーター
モードStripe がリターン URL に追加するパラメーター
Hostedsession_id
Embeddedsession_id
Custom(Payment Element)payment_intent、payment_intent_client_secret、redirect_status

すべてのモードに checkout=success も含まれます(フレームワークによって設定されます)。これらのパラメーターは検出後に自動的に URL から削除されるため、成功ページにはクリーンな URL が表示されます。

チェックアウト成功時のトラッキングピクセルとコンバージョンイベント

成功ページの読み込み時に、checkout_complete イベントが自動的に発火します。カスタムコンバージョントラッキング(例:Meta Pixel、Google Ads)を実行するには、成功ページに <script> タグを含む html コンポーネントを追加し、CatalogKit の pageenter イベントをリッスンします。

{
  "id": "conversion_pixel",
  "type": "html",
  "props": {
    "content": "<script>\nconst kit = window.CatalogKit.get();\nkit.on('pageenter:next_steps', () => {\n  // Fire conversion pixel\n  if (window.fbq) fbq('track', 'Purchase', { value: 49.00, currency: 'USD' });\n  if (window.gtag) gtag('event', 'purchase', { value: 49.00, currency: 'USD' });\n});\n</script>"
  }
}

これは、チェックアウト後にレンダラーが成功ページへ直接移動して pageenter を発火するため、ピクセルが正確に 1 回だけ発火します。


チェックアウトのイベントライフサイクル

チェックアウトフロー中に発火するイベント(内部分析向けの GA4 に準拠した命名):

イベント発火するタイミング主なペイロード
checkout_startユーザーがチェックアウトに進むとき(success_page_id ゲート、カートのチェックアウトボタン、または kit.startCheckout())item_count、cart_name
payment_info_added決済フォームが操作可能になったとき(Payment Element の「ready」または埋め込みチェックアウトのマウント時)payment_mode(「custom」/「embedded」)、item_count、value.amount_cents
checkout_complete決済成功後に Stripe からリダイレクトされたとき、または $0 のカートが Stripe を経由せずに処理されたときvalue.amount_cents(Stripe の実際の合計額または表示カート合計額)、value.item_count、value.items[](offer_id、title、price_display、amount_cents)、value.display_amount_cents、value.stripe_amount_total、value.stripe_currency、value.stripe_payment_status、value.stripe_customer_email、value.stripe_line_items[]、value.session_id、value.payment_intent。$0 のカートの場合:value.{ item_count: 0, amount_cents: 0, zero_cart: true }
checkout_error決済が失敗したとき(Stripe エラー、3DS の失敗、ネットワークの問題、資金保留の拒否)error_message、error_code(取得可能な場合は Stripe エラーコード)
checkout_skipユーザーが「支払わずに続行」をクリックしたときitem_count

注:

  • checkout_complete には、表示カート値(カタログスキーマから取得)とStripe の実際の値(リダイレクト後に Stripe セッションから取得)の両方が含まれます。value.amount_cents フィールドには、取得可能な場合は Stripe の実際の合計額が設定され、取得できない場合は表示カート合計額が使用されます。これは、ダッシュボード分析で使われる revenue_cents の集計カウンターに反映されます。
  • checkout_complete はセッションごとに重複排除されるため、決済後にページを再読み込みしても再度発火しません。
  • checkout_complete は、チェックアウト時にカートが空の $0 カートでも zero_cart: true を付けて発火するため、分析パイプラインで有料の完了と無料の完了を区別できます。
  • カートデータは Stripe へのリダイレクト前に localStorage に保存されるため、商品詳細は往復後も保持され、イベントに含まれます。
  • payment_info_added は Stripe の決済フォームが操作可能になったときに発火します。ユーザーがカード情報を入力したときではありません(Stripe はキー入力イベントを公開していません)。
  • ホスト型モードのチェックアウト(stripe.com へのリダイレクト)では、決済フォームが Stripe のドメイン上にあるため、payment_info_added は発火しません。
  • すべてのイベントは内部フレームワーク分析専用であり、GA4/GTM には自動的に送信されません。外部のピクセル/タグ連携には、kit.on('pageenter:success_page') または Webhook 転送(単一イベントとバッチ配信)を使用してください。

チェックアウト — 3D Secure とトライアル保護

Catalog Kit は、決済失敗やチャージバックから無料トライアルのファネルを保護するための高度な Stripe チェックアウト機能をサポートしています。このシステムでは、すべてのモードに共通する決済バックエンドとして Checkout Sessions(POST /checkout/session)を使用します。

Checkout セッション(/checkout/session)

主要なエンドポイントです。Stripe Checkout Session を作成します。次の 3 つの ui_mode 値をサポートしています。

  • "custom"(stripe_publishable_key が設定されている場合のデフォルト)— { session_id, client_secret } を返します。フロントエンドでクライアントシークレットを使って Payment Element をマウントし、その後 stripe.confirmPayment() を呼び出します。Link、動的な支払い方法、3DS、トライアル、サブスクリプションをサポートしており、すべて Checkout Session が処理します。
  • "embedded" — { session_id, client_secret } を返します。フロントエンドで Stripe の埋め込み型チェックアウト UI をマウントします。
  • "hosted"(公開可能キーが設定されていない場合のデフォルト)— { session_id, session_url } を返します。フロントエンドで Stripe のホスト型ページにリダイレクトします。
セッション状態の確認(GET /checkout/session/status)

Payment Element からのリダイレクトが成功した後、フロントエンドは GET /checkout/session/status?session_id=cs_...&user_id=...&catalog_slug=... を呼び出して、セッションが完了したことを確認します。{ status, payment_status, amount_total, amount_subtotal, currency, customer_email, line_items[] } を返します。amount_total と line_items は、実際の Stripe の収益を記録する checkout_complete アナリティクスイベントで使用されます。

Checkout intent の方式(/checkout/intent)— 非推奨

非推奨: 代わりに ui_mode: "custom" を指定して POST /checkout/session を使用してください。このエンドポイントは、raw PaymentIntents/SetupIntents を作成するカスタム HTML+バックエンド統合との後方互換性を保つため、引き続き動作します。

レガシーの Card Element フローを使用する場合、フロントエンドは POST /checkout/intent を呼び出して Stripe PaymentIntent または SetupIntent を作成します。方式はチェックアウト設定から決まります。

チェックアウト設定Intent の種類動作
デフォルト(上書きなし)PaymentIntent、自動キャプチャシンプルな 1 回限りの請求
setup_future_usage: "off_session"PaymentIntent +カードを保存今すぐ請求し、将来の支払いにカードを再利用
capture_method: "manual"PaymentIntent、オーソリのみカードの利用可能額を確保し、後で Webhook 経由でキャプチャ
free_trial.enabled(確保なし)SetupIntentカード+3DS を確認し、請求額 $0 でカードを保存
free_trial.enabled + capture_method: "manual"PaymentIntent、手動キャプチャ+off_sessionトライアル終了後の請求に備えて3DS+利用可能額の確保+カード保存
3D セキュア認証

すべてのカード決済で、銀行レベルの認証(OTP、生体認証、銀行アプリ)を必須にします。トライアル終了時の決済失敗を減らすうえで最も効果の大きい設定です。登録時に、カード名義人が実在し認証済みであることを確認します。

{
  "settings": {
    "checkout": {
      "payment_type": "subscription",
      "require_3ds": true,
      "free_trial": { "enabled": true, "days": 14 },
      "trial_end_behavior": "cancel"
    }
  }
}

有効にすると、Intent に payment_method_options.card.request_three_d_secure: "any" が含まれます。フロントエンドには「3D Secure Checkout」ヘッダーバッジと、銀行から認証を求められることをユーザーに伝える青い説明バナー(右カラムの支払いボタンの下)が表示されます。トライアルが有効な場合、ボタンのテキストはデフォルトで「Start Free Trial」になります。

インラインのカード入力欄では、3DS 認証画面が Stripe 管理のポップアップとしてページ上に表示されるため、ユーザーがチェックアウトページから離れることはありません。

トライアル終了時の動作

トライアル終了時に顧客の支払い方法が失敗した場合に Stripe が行う処理を設定します。

値動作
"cancel"(デフォルト)サブスクリプションをキャンセルします。再試行は行いません
"create_invoice"請求書を作成し、Stripe のスマートスケジュールで再試行します
"pause"支払いが解決するまでサブスクリプションを一時停止します

推奨の組み合わせ:require_3ds: true + trial_end_behavior: "cancel" — 事前にカードを確認し、それでも失敗した場合は確実にキャンセルします。

Stripe Overrides(高度なパススルー)

stripe_overrides を使用して Stripe の動作を制御します。これらは /checkout/session(すべてのモード)と、非推奨の /checkout/intent エンドポイントに適用されます。

{
  "settings": {
    "checkout": {
      "stripe_overrides": {
        "payment_intent_data": {
          "capture_method": "manual",
          "setup_future_usage": "off_session",
          "statement_descriptor": "ACME TRIAL"
        }
      }
    }
  }
}

利用可能なオーバーライド:

フィールド型説明
mode_override"payment" | "subscription" | "setup"Checkout Session のモードを強制します(例:保護されたトライアルでは "payment")。/checkout/session にのみ適用されます。
payment_intent_data.capture_method"automatic" | "manual""manual" = オーソリのみ(資金を確保し、後で売上を確定)
payment_intent_data.setup_future_usage"off_session" | "on_session"今後の請求に備えてカードを保存します
payment_intent_data.statement_descriptorstring銀行明細に表示するテキスト(最大22文字)
payment_intent_data.statement_descriptor_suffixstringデフォルトの明細表記に追加されます
payment_intent_data.transfer_data{ destination, amount? }Stripe Connect の振替
subscription_data.descriptionstringsubscription_data にマージされます
subscription_data.metadataRecord<string, string>サブスクリプションに追加するメタデータ
consent_collection.terms_of_service"required" | "none"チェックアウト時に利用規約への同意を必須にします
consent_collection.promotions"auto" | "none"Stripe のプロモーションに関する同意
payment_method_options.card.capture_method"automatic" | "manual"支払い方法ごとの売上確定方法(例:カードのみの与信保留)
payment_method_options.card.request_three_d_secure"any" | "automatic"支払い方法ごとに3DSを強制します("any" = 常にチャレンジを実行)

Checkout Session モードでサブスクリプション項目に mode_override: "payment" を設定すると、Catalog Kit はインライン項目から定期料金を自動的に削除します。payment モードで定期料金を指定する場合は、stripe_price_id ではなく amount_cents を使用してください。

チェックアウトのエラーモーダル

3D Secure 認証に失敗した場合、資金の確保が拒否された場合、または Stripe エラーが発生した場合、デフォルトではインラインの小さなエラーバナーが表示されます。重要度の高いファネル(特に3DSと資金保留を組み合わせたもの)では、編集者はカスタムメッセージと、代替チェックアウトへの任意のリンクを含む全画面エラーモーダルを有効にできます。

設定のみで対応 — スクリプトは不要です。

{
  "settings": {
    "checkout": {
      "require_3ds": true,
      "error_modal": {
        "enabled": true,                          // Show modal instead of just inline banner
        "headline": "Payment issue",              // Default: "Payment unsuccessful"
        "message": "Your bank requires extra verification that couldn't be completed.\n\nThis sometimes happens with certain cards. You can try a different card or use our alternative checkout.",
        "alt_action": {                           // Optional — primary button linking to alternative checkout
          "label": "Use alternative checkout",
          "url": "https://pay.example.com/alt"
        },
        "dismiss_label": "Try a different card"   // Default: "Try again"
      }
    }
  }
}
フィールド型デフォルト説明
enabledboolean—必須です。true の場合、チェックアウトエラー時にインラインバナーだけでなくモーダルオーバーレイを表示します
headlinestring"Payment unsuccessful"モーダルのタイトル
messagestringStripe エラーメッセージにフォールバック本文テキスト。改行には \n を使用できます
alt_action.labelstring—代替チェックアウトリンクのボタンテキスト
alt_action.urlstring代替ボタンのリンク先URL
dismiss_labelstring"Try again"閉じる/再試行ボタンのテキスト

alt_action を指定すると、モーダルには2つのボタンが表示されます。代替アクションはテーマ付きのプライマリボタン、閉じるボタンはセカンダリボタンとして表示されます。alt_action を指定しない場合、閉じるボタンがプライマリとして表示されます。

スクリプトで対応 — checkout_error イベントで制御します。

const kit = window.CatalogKit.get();
kit.on("checkout_error", (e) => {
  // e.error_message — human-readable error string
  // e.error_code    — Stripe error code when available (e.g. "card_declined", "authentication_required")
  console.log("Checkout failed:", e.error_message);

  // Custom handling: show your own modal, redirect, log to analytics, etc.
  if (e.error_code === "authentication_required") {
    window.location.href = "https://pay.example.com/alt";
  }
});

どちらの方法も単独でも併用でも機能します。エラーが発生すると、error_modal が有効かどうかにかかわらず、必ず checkout_error イベントが発火します。

独自の請求サーバーを用意する

Catalog Kit はチェックアウトの一連の処理(インテントの作成、3DS、オーバーライド)を担いますが、支払い後のライフサイクルは管理しません。Stripe の Webhook はサーバー間通信であり、catalogkit.cc のリダイレクト URL とは独立しています。Stripe ダッシュボードで、自分のサーバーを指すように設定してください。

設定方法:

  1. Stripe ダッシュボード → 開発者 → Webhook → エンドポイントを追加
  2. フローに応じてイベントを登録します:
    • シンプルな支払い:payment_intent.succeeded
    • オーソリとキャプチャ:payment_intent.amount_capturable_updated、payment_intent.succeeded
    • SetupIntent(トライアル):setup_intent.succeeded
    • Checkout Session:checkout.session.completed
    • 失敗:payment_intent.payment_failed、invoice.payment_failed
  3. サーバーでは、Catalog Kit に提供したものと同じ Stripe シークレットキーを使用します
  4. すべてのインテントには、照合用のメタデータ(catalog_id、catalog_slug、user_id、tracer_id、line_items_json、trial_days)に加え、チェックアウト設定で定義したカスタム metadata キー(フォームの状態から {{field_id}} テンプレートを解決したもの)が含まれます
レシピ:シンプルな一回限りの支払い

オーバーライドは不要です。settings.checkout と stripe_publishable_key を設定すれば、チェックアウトページがすべて処理します:

{
  "settings": {
    "checkout": {
      "payment_type": "one_time",
      "title": "Complete Your Purchase",
      "stripe_publishable_key": "pk_test_...",
      "prefill_fields": { "customer_email": "comp_email" },
      "button_text": "Pay Now"
    }
  }
}

訪問者がカード情報をその場で入力して「今すぐ支払う」をクリックすると、支払いが直ちに処理されます。リダイレクトも、オーソリも、将来の利用もありません。

レシピ:無料トライアル付きサブスクリプション(オーソリなし)

オーソリを行わずにカードを確認し、トライアル終了後の請求に備えて保存します:

{
  "settings": {
    "checkout": {
      "payment_type": "subscription",
      "title": "Start Your 14-Day Trial",
      "stripe_publishable_key": "pk_test_...",
      "require_3ds": true,
      "free_trial": { "enabled": true, "days": 14 },
      "trial_end_behavior": "cancel",
      "prefill_fields": { "customer_email": "comp_email" },
      "button_text": "Start Free Trial"
    }
  }
}

これにより、SetupIntent が作成されます。カードは3DSで確認されて保存されますが、請求額は0ドルです。請求サーバーは setup_intent.succeeded を受け取り、保存された支払い方法を使ってサブスクリプションを作成します。

レシピ:保護機能付き7日間トライアル(オーソリ+3DS+将来の事前認証)

コンバージョンを最も保護できる無料トライアルのパターンです。0ドルのサブスクリプション・トライアルの代わりに、サブスクリプションの全額を顧客のカードで「保留中」としてオーソリし、トライアルの結果に応じてキャプチャまたは取り消します。

手順1 — Catalog Kit の設定:

{
  "settings": {
    "checkout": {
      "payment_type": "subscription",
      "title": "Start Your 7-Day Trial",
      "subheading": "Your card will be verified but not charged during the trial.",
      "stripe_publishable_key": "pk_test_...",
      "require_3ds": true,
      "free_trial": { "enabled": true, "days": 7 },
      "stripe_overrides": {
        "payment_intent_data": {
          "capture_method": "manual",
          "setup_future_usage": "off_session"
        }
      },
      "prefill_fields": { "customer_email": "comp_email" },
      "button_text": "Start Free Trial",
      "show_disclaimer": true,
      "disclaimer_text": "A temporary hold for the full subscription amount will appear on your card. This is NOT a charge — it verifies your funds and is released if you cancel during the trial.",
      "testimonial": {
        "enabled": true,
        "text": "Pays for itself with just one customer. Most founders see results within the first week.",
        "author": "OfficeX Founders"
      }
    }
  }
}

注:オーソリには具体的な金額が必要なため、明細項目では stripe_price_id ではなく amount_cents を使用してください。

内部での処理: Payment Element(デフォルト)の場合、Checkout Session がトライアルとオーソリの設定を自動的に処理します。非推奨の /checkout/intent エンドポイントの場合は、free_trial.enabled と capture_method: "manual" を検出し、capture_method: "manual" と setup_future_usage: "off_session" を指定した PaymentIntent を作成します。訪問者はページを離れることなくカード情報を入力し、3DS認証を完了して、オーソリを行えます。

手順2 — 請求サーバーでライフサイクルを処理する:

Webhook: payment_intent.amount_capturable_updated
├── Retrieve PaymentIntent → read latest_charge.payment_method_details.card.capture_before
├── Schedule capture at: min(trial_end, capture_before - 1 hour)
│   ⚠️ Visa often expires at 4d 18h (114 hours), Mastercard/Amex ~7 days
└── Store { payment_intent_id, customer_id, payment_method_id, capture_deadline }

Cron/Scheduler: At capture deadline
├── If user cancelled → paymentIntents.cancel(id)  (releases hold)
├── If trial active → paymentIntents.capture(id)    (converts to real charge)
└── After capture → Create Subscription using saved payment_method for Month 2+

User cancels during trial:
└── paymentIntents.cancel(id)  — MUST do this to release hold
    (Uncaptured auths incur card network fees and freeze customer funds)

手順3 — サーバーでエッジケースを処理する:

シナリオ対応
カードがオーソリとキャプチャの分離に対応していない(LATAM のデビットカード、インドの銀行、一部のプリペイドカード)Stripe は確定時に 402 を返します。代替策:1ドルの確認手数料を直ちに請求するか、SetupIntent(0ドルで確認)に切り替えます
capture_before がトライアル終了より早い(Visa は約4.75日)トライアル終了日ではなく、capture_before - 1 hour に基づいて早めにキャプチャします
ユーザーの銀行に「保留中」の請求が表示され、サポートへの問い合わせにつながる「トライアル開始」メールで説明します:「[amount] のオーソリが保留中として表示される場合があります。これは請求ではなく、解約すると解除されます」
キャプチャ前にオーソリが期限切れになるStripe が自動的に取り消します。サーバーでこれを検出し、再オーソリを行うか、適切にキャンセルする必要があります

このパターンがある理由: 標準的なサブスクリプション・トライアル(0ドルでカードを保存する方式)では、カードの利用可能額を事前に確認していないため、トライアル終了時の支払い失敗率が約15~25%になります。手動キャプチャのオーソリにより、その時点で利用可能な残高があることを確認でき、3DSによりカード所有者が本人であることを確認できます。これらを組み合わせることで、失敗率は約3~5%に低下します。


イベントトラッキング(無料)

訪問者のイベントは、カタログのフロントエンドによって、ファーストパーティの同一オリジンリクエストを使用して自動的に追跡されます(広告ブロッカーの影響を受けません)。API を介してカスタムイベントを送信することもできます。

POST https://api.catalogkit.cc/events

有効なイベントタイプ: page_view、field_change、field_complete、form_submit、funnel_complete、action_click、exit_intent、session_start、session_resume、cart_add、cart_remove、checkout_start、checkout_error、checkout_skip、checkout_complete、payment_info_added、offer_declined、lead_captured、video_play、video_pause、video_progress、video_complete、video_chapter、video_seek、page_auto_skipped、popup_shown、popup_dismissed、popup_converted

funnel_complete は、送信された状態が確認されたときに form_submit とともに発生する、念を入れた完了イベントです。どちらも分析では送信数にカウントされます。

最大 25 件のイベントをまとめて送信できます:POST /events/batch に { "events": [...] } を指定します。

注: カタログのフロントエンドでは、信頼性を高めるため、CloudFront 経由でプロキシされた同一オリジンのパス(/e、/e/batch)を使用します。上記のクロスオリジン API エンドポイントは、サーバー側または外部との連携用です。


トラブルシューティング

ローカル開発サーバーに変更が反映されない

catalogs catalog dev のローカルプレビューで新機能やスキーマの変更が反映されない場合、CLI が古いレンダラーバンドルを提供している可能性があります。次の手順で解決できます。

  1. 実行中の開発サーバーを停止します(ターミナルで Ctrl+C を押します)
  2. 強制的に再起動します: catalogs catalog dev your-catalog.ts

AI エージェントを使用している場合は、**「catalog-kit CLI を強制的に再起動して」**と指示できます。エージェントは既存のプロセスを終了し、catalogs catalog dev を再起動します。

CLI は公開時にレンダラーのコピーをバンドルします。ローカルのモノレポ開発中は、隣接する renderer/dist/ ディレクトリにフォールバックします。レンダラーのソースコードを更新した場合は、まずビルドして(cd renderer && npm run build)、その後、開発サーバーを再起動してください。

コメント