mcpField
C
catalog-kit

catalog-kit

github.com/officexapp营销与增长080 次安装26 周前更新
代码仓库

关于 catalog-kit

Catalog Kit 供营销团队通过 AI Agent 创建和管理产品目录、落地页及转化漏斗,支持基于 Schema 编写、发布、实验分析和本地开发。

主要功能

  1. 01根据 JSON Schema 构建目录
  2. 02发布到子域名或自定义域名
  3. 03运行加权 A/B 测试
  4. 04跟踪转化与访客旅程
  5. 05通过 CLI 本地预览和校验
  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 Agent 构建和管理营销目录、落地页和多步骤转化漏斗。使用 60 多种组件类型创建目录,即时发布目录,使用加权变体运行 A/B 测试,并实时监控转化分析。

在 OfficeX 上安装: officex.app/store/en/app/catalog-kit

你可以做什么

  • 创建目录 — 根据 JSON 架构构建潜在客户信息收集表单、产品目录和多步骤漏斗
  • 即时发布 — 目录可立即在你的子域名(SUBDOMAIN.catalogkit.cc)或自定义域名上线
  • 查看分析数据 — 查看访客、转化、页面流失情况、字段填写完成情况、引荐来源和收入
  • 运行 A/B 测试 — 使用加权变体拆分流量,找出转化效果最佳的方案
  • AI 变体路由与预填 — 根据自然语言提示,自动将访客路由到最合适的目录变体,并预填符合条件的表单字段
  • 沙盒编辑 — 克隆目录以安全地进行更改,不影响线上版本;准备就绪后再发布
  • 元素检查器 — 按住 Shift+Alt,将鼠标悬停在任意元素上进行检查(页面组件、顶部导航栏、购物车按钮、购物车抽屉、购物车商品、底部固定栏、结账页面、弹出窗口),并复制其准确的 pageId/componentId 引用,供 AI Agent 使用
  • 查看访客旅程 — 逐步追踪每位访客的具体操作
  • 管理访问权限 — 为团队成员或集成创建 API 密钥
  • 托管媒体服务 — 为你存储、压缩并通过 CDN 提供图片和视频,无需自备 S3 存储桶
  • 上传图片(免费) — 自动进行 WebP 压缩、生成缩略图并通过 CDN 分发,不消耗额度
  • 上传视频 — 自动转码为 HLS,以支持自适应流式传输,并通过 CDN 提供
  • 上传和下载文件 — 将可下载文件(PDF、ZIP、文档)托管在 S3 上并通过 CDN 分发,按每 50MB 计费
  • 购物车与结账 — 内置购物车界面(悬浮按钮 + 滑出式抽屉),可收集页面优惠并将其发送到 Stripe Checkout。无需自定义购物车 HTML
  • Agent API — AI Agent 可通过有状态会话 API 无界面地填写目录表单,并使用服务端验证和渐进式披露
  • 以 TypeScript 作为配置 — 使用完整类型安全的 .ts 文件编写目录,然后通过 CLI 推送
  • 本地预览 — catalogs catalog dev my-catalog.ts 可在本地预览,支持热重载、可见性条件、调试面板和验证叠加层
  • 开发工具栏 — 顶部固定工具栏显示目录 slug、架构版本徽标(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 结账 — 将 STRIPE_SECRET_KEY=sk_test_... 和 STRIPE_PUBLISHABLE_KEY=pk_test_... 添加到 .env 后,开发服务器便会在本地创建真实的 Stripe 结账会话和内嵌卡片字段。与生产环境完全一致:元数据模板、client_reference_id、reuse_payment_method(已保存的支付方式)、优惠券代码以及所有 stripe_overrides。密钥不会离开你的计算机
  • 本地开发事件 — 页面浏览、字段更改和结账事件会流式传输到终端和 SSE 端点(/__dev_events_stream),AI Agent 可订阅该端点。支持 POST /events、POST /events/batch 和 /e/batch(与生产环境相同)。不会污染生产环境
  • 本地 API 一致性 — 开发服务器提供 GET /public/catalogs/dev-user/:slug(以 JSON 格式获取目录)和 POST /routing/variant(基于关键词匹配进行确定性变体路由),让 Agent 能够在本地测试相同的 API 接口
  • 自定义 JavaScript — 通过带有 <script> 标签的 html 组件和 window.CatalogKit API 桥接层注入自定义客户端逻辑
  • 自定义 HTML 组件 — 使用 type: "html" 组件在目录中渲染任意 HTML/CSS/JS
  • 自定义 React 组件 — 在 window.__catalogkit_components 上注册 React 组件,构建完全自定义的交互式 UI

脚本与自定义逻辑 — 概览

Catalog Kit 提供两种脚本系统来实现自定义逻辑。了解何时使用各系统至关重要:

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 组件有双重用途 — 既可渲染可见的 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。


自定义域名

通过你自己的域名(例如 shop.example.com)而非 <subdomain>.catalogkit.cc 提供目录。SSL 证书会自动配置——只需添加 DNS 记录,其余工作由系统处理。

工作原理
  1. 使用你的域名调用 POST /api/v1/domains
  2. API 配置 SSL 证书(ACM),并返回 3 条待添加的 DNS 记录
  3. 在你的 DNS 服务商处添加 DNS 记录
  4. 轮询调用 POST /api/v1/domains/refresh,直到状态变为 active
  5. 你的目录即可通过 https://your-domain.com/slug 访问

系统在 CloudFront 上使用整合式 SAN 证书——一个分发即可服务所有自定义域名。无需为每个域名单独配置基础设施。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 Agent 工作流

对于以编程方式连接自定义域名的 AI Agent:

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”**(灰色云朵图标,而非橙色)。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 传播延迟等待 5-10 分钟,让 CloudFront 完成部署
限制
  • 每个账户只能使用一个自定义域名
  • ACM 每张合并证书最多支持 100 个 SAN,足以为每个 CloudFront 分配支持约 95 个自定义域名
  • Apex 域名(例如没有子域前缀的 example.com)需要 CNAME 扁平化或 ALIAS 记录;Cloudflare、Route53、DNSimple 支持此功能,传统 DNS 不支持
  • 添加 DNS 记录后,SSL 配置通常需要 2-10 分钟
  • 自定义域名解析会为每个未缓存请求增加一次 DynamoDB 查询(内存缓存 60s)

管理目录

列出你的目录
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"
}

更改 slug 时,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 不匹配时才跳过缓存。适合高效固定到已知版本。
(无参数)使用常规缓存:30s 新鲜缓存 + 60s 过期缓存重新验证。最有利于访客体验。

示例: 更新目录后,验证更改是否已上线:

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

公开目录响应始终包含 version,以便确认当前提供的是哪个版本:

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

分析与结果

所有分析端点都需要身份验证。每次分析调用消耗 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)。

访客旅程

追踪单个访客在目录中的完整旅程:

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

按时间顺序返回所有事件,并附带摘要:事件总数、首次/最后一次出现时间、浏览过的页面,以及是否已提交。


Agent API(无头表单提交)

AI Agent 无需浏览器即可通过编程方式与目录交互。Agent 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)
便于 Agent 使用的架构字段

创建目录时,添加以下可选字段以改善 AI Agent 的交互体验:

  • 目录根节点上的 agent — { enabled: true, description: "What this catalog does" }
  • 页面上的 agent_context — 向 Agent 说明页面的用途
  • 组件上的 agent_hint — 从语义角度说明字段的含义

使用加权变体进行 A/B 测试

在架构中添加加权变体,以测试目录的不同版本。设置 variant_routing: "random" 以进行加权随机路由,设置 "hint" 以进行基于 AI 的路由,或设置 "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 的变体会在同一目录中应用个性化提示。设置 enabled: false 可在不删除变体的情况下将其从所有路由方式(hint、random、hybrid)中排除——这适用于暂停表现不佳的 A/B 测试组。直接访问 URL(/slug/variant-slug)仍可使用已禁用的变体,以免现有链接失效。


个性化架构:变体与动态优惠

问:我能否根据 ?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 调用即可完成变体路由,速度很快(约 400ms)。如果再按页面选择报价,延迟和成本都会翻倍。
测试每个变体都可以独立测试,并有已知的转化指标。动态报价会让 A/B 分析在统计上变得不可靠。
其他做法
  • 添加更多变体 — 4–8 个变体搭配 AI 提示路由即可满足大多数个性化需求,而且每个变体都是可完整测试的转化漏斗
  • 在组件属性上使用 __variants — 在单一目录结构中,根据不同的提示值更改标题、图片、文案,甚至报价标题和价格
  • 使用 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
  }
}

使用 PUT 请求将文件上传到预签名的 upload_url。必须将 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>

压缩会自动进行——请在图片组件中使用 compressed_url 作为 src。

检查压缩状态
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 显示组件中,将 cdn_url 用作 src。

获取下载 URL
GET https://api.catalogkit.cc/api/v1/files/:fileId/download

返回预签名下载 URL(有效期为 1 小时),并带有 Content-Disposition: attachment,供浏览器下载。

列出文件
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 会通过带重试机制的持久化队列,将访客事件转发到该 URL。事件采用至少投递一次的方式:

  • 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 仅用于被动转发事件——事件发生后会异步投递,不会阻止页面导航。你的端点必须按至少投递一次来处理投递,并根据 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:YOUR_PAGE_ID(而不只是 beforenext),仅在相关页面上运行

另请参阅:CatalogKit 全局 API 部分包含更多高级示例,包括根据服务响应动态路由、实时失焦验证和条件 UI。


变体分析

每个 catalog 都会自动添加其 catalog_id(例如 catalog_550e8400-e29b-41d4-a716-446655440000)作为标签。若要比较不同 catalog 变体之间的分析数据(例如用于 A/B 测试),请将基础 catalog 的 catalog_id 添加到每个变体的 schema.tags 中。之后,使用匹配的 tag_patterns 限定范围的 API 密钥即可查询所有带标签的变体的分析数据。


Catalog 架构参考

Catalog 架构以 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 用作页面 ID——这些 ID 保留给 CatalogKit 内置的结账、检查器和提交系统使用。一般也应避免使用以 __ 开头的 ID。CLI 的 catalog validate 命令会对此进行检查。

主题

在 settings.theme 下设置主题选项:

  • primary_color(必填)— 按钮、强调元素和活动状态的十六进制颜色值
  • 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 — 页面背景的十六进制颜色值
  • 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网站图标图片的 URL(ICO、PNG 或 SVG)。替换默认浏览器网站图标
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 会检测爬虫的 user-agent,并将其重定向到服务端 HTML 端点(/meta/:slug),该端点会返回包含所有正确 <meta> 标签的轻量页面——无需 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输入框内的占位文本
hiddenboolean从界面中隐藏字段(旧版方式——建议改用组件级 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 使用相同的两步流程):

{
  "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 显示组件会渲染图片,并采用合理的默认设置(全宽、填充适配);也可通过 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——使用图片上传 API 返回的 compressed_url,自动优化为 WebP
altstring""用于无障碍访问的替代文本
widthnumber/string—HTML width 属性(有助于防止布局偏移)
heightnumber/string—HTML height 属性(有助于防止布局偏移)
border_radiusnumber16边框圆角,单位为 px
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 显示组件支持三个文本层级:

{
  "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"文本对齐方式

将这三层组合起来,即可组成完整的标题块:眉题(小号、大写)、主标题(粗体)和副标题(较细)。

页面操作与 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_statementstring与 side_statement 相同,但用于默认提交按钮(页面级)
submit_reassurancestring与 reassurance 相同,但用于默认提交按钮(页面级)
button_disabled_messagestring点击禁用按钮时显示的错误消息(默认值:"请填写所有必填字段")。与 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 映射)时,会完全跳过此页面。规则:

  • 仅当页面至少有一个可见输入,且所有输入都有值时才会跳过
  • 仅用于显示的页面(没有输入)永远不会自动跳过
  • 在 on_enter 钩子运行后执行,因此钩子可以设置值以满足跳过条件
  • 被跳过的页面不会出现在浏览器历史记录中(点击“后退”按钮会跳过这些页面)
  • 每跳过一个页面,都会触发一个 page_auto_skipped 分析事件
使用自动跳过串联目录

一种常见模式是将两个目录串联起来,例如,注册表单将用户重定向到引导流程,并传递已收集的数据,以便跳过已经填写过的页面。

步骤 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 并启用 auto_skip

在接收数据的目录中,设置 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 属性(复选框/多选题): 在复选框或多选题组件上将其设为 true 时,必须选中所有选项(而非只选中一个)。所有嵌套的必填输入也会进行验证,不受选中状态影响。
  • 布尔字段(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 或大模型提供实时反馈。

组件宽度(多列布局)

任何组件都可以设置 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)

评分不区分大小写,并且能够处理类型不匹配的情况——correct_answer: "Call To Action" 与用户选择 "call to action" 相匹配;对于单选输入,correct_answer: ["c"](仅含一个元素的数组)与 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 时,流程分为两步:

  1. 访客自由选择答案(选项不会锁定)
  2. 点击继续后,答案会显示出来:
    • 正确答案显示绿色边框
    • 错误选项显示红色边框
    • 反馈横幅显示 correct_message / wrong_message(如果设置了 option_messages[value],则显示对应的选项消息)
    • 显示解释文本(如果提供)
    • 选项会锁定
    • 横幅会显示“答案已显示!请在上方查看结果,然后点击‘继续’以继续。”
    • 页面会自动滚动,确保“继续”按钮始终可见
  3. 访客再次点击继续,即可前往下一页

适用于 multiple_choice(单选)和 checkboxes(多选)组件。省略 reveal_on_select 或设为 false 即可使用默认行为(不显示行内反馈——改为在后续页面使用 reveal_answers)。

显示答案(结果页)

在页面配置中添加 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
  }
}
钱包地址输入框

三种经过验证的钱包地址输入类型,支持内联验证:

  • evm_address — Ethereum/EVM 地址(0x + 40 个十六进制字符)
  • 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" }
}

这三种类型都会呈现为等宽字体的文本输入框,支持实时格式验证和视觉反馈(绿色对勾 / 红色 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 | string400高度,单位为 px 或 CSS 值
widthstring"100%"CSS 宽度值
border_radiusnumber16圆角半径,单位为 px
sandboxstring"allow-scripts allow-same-origin allow-forms"iframe sandbox 属性
allowstring""iframe allow 属性(例如:"camera; microphone")
borderbooleanfalse是否显示 iframe 边框
titlestring"Embedded content"无障碍标题

iframe URL 会以响应式方式重新解析:当访客填写 comp_email 时,iframe 会立即使用更新后的 URL 重新加载。

模态框(信息弹窗)

一个用于打开可滚动模态对话框的按钮。非常适合放置条款与条件、隐私政策、详细产品信息,或任何会让页面显得杂乱的内容。正文支持 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)。

属性类型默认值描述
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
  }
}

传递给组件的属性:

属性描述
themeColor目录的主题色(十六进制字符串)
formState所有表单字段值的只读快照
setField(componentId, value)设置任意表单字段值
...props架构中的所有其他属性(例如上面的 quantity)

重要说明:

  • 脚本必须将组件注册到 window.__catalogkit_components — 页面加载后,渲染器最多会轮询 5 秒
  • 组件会由错误边界包装 — 如果组件抛出错误,系统会显示友好的错误消息,而不是导致目录崩溃
  • React 已全局可用(目录已加载 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 Agent 编写脚本时至关重要: 嵌套输入值只能通过其复合 ID 访问。单独使用 input.id 将始终返回 undefined。例如,给定一个 id: "tasks" 的 checkboxes 组件、一个 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 Agent 的重要提示: 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": "要发送的消息", "props": { "readonly": true } }。

选项属性:

属性类型说明
valuestring(必填) 选项的唯一标识符
headingstring(必填) 主要显示文本
subheadingstring标题下方的辅助文本
imagestring缩略图 URL(圆角,32x32)
ribbonstring显示在选项上的徽标文本(例如 "Recommended"、"Best Value")。适用于 multiple_choice 和 picture_choice
ribbon_bgstring徽标背景颜色(默认为主题颜色)
ribbon_colorstring徽标文本颜色(默认为白色)
buttonEmbeddedButton侧边链接按钮(参见嵌入式按钮)
messagestring选中选项时显示在其下方的自定义消息(参见选项消息)
inputsarray嵌套子组件 — 默认始终可见,必填输入项填写完成后会自动勾选
expand_on_selectboolean为 true 时,只有勾选复选框后才显示嵌套输入项(不会自动勾选)。默认值: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> 标签使用全局 API window.CatalogKit:

<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 全局 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)获取任意表单字段的当前值。对于嵌套的 checkbox/multiple_choice 输入,请使用复合 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。供开发工具栏的“页面”图表使用。生产环境中不可用
组件属性
kit.setComponentProp(id, prop, value)在运行时覆盖任意组件属性(例如 hidden、label、options)。适用于所有组件类型,包括展示类和输入类组件
命名购物车(kit.cart(name?) 返回一个 CartHandle)
kit.cart(name?)返回指定购物车名称对应的 CartHandle(默认为 "default")。命名购物车可用于多次结账的追加销售漏斗
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?)将一项(按 offer_id)或所有项目移至另一个命名购物车
kit.getCartNames()列出非空购物车的名称
kit.clearAllCarts()清除所有命名购物车及付款覆盖项
Stripe 客户(多次结账复用)
kit.getStripeCustomerId()获取之前结账步骤中的 Stripe 客户 ID,若无则返回 null
kit.setStripeCustomerId(id)手动设置下一次结账使用的 Stripe 客户 ID
购物车抽屉(默认购物车)
kit.openCart()以编程方式打开购物车抽屉
kit.closeCart()关闭购物车抽屉
kit.startCheckout(cartName?)以编程方式触发指定购物车的内置结账页面(默认为 "default")。触发 before_checkout,然后显示结账页面。请使用此方法,而不是通过 DOM 点击购物车的结账按钮
客户端参考 ID(作为 client_reference_id 传递给 Stripe)
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 的别名
Events — 作用域生命周期钩子

Events 遵循 event 或 event:scope_id 模式。未指定作用域的监听器会对所有页面/字段触发。指定作用域的监听器仅对指定页面或字段触发。

EventScopePayloadAsync?Description
fieldchange字段 ID{ fieldId, value, prevValue }否表单字段值已更改。对于嵌套的 checkbox/multiple_choice 输入,作用域必须使用复合 ID(例如 fieldchange:checkboxId.optionValue.inputId),而不是单独的输入 ID。
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 组件支持两项功能,让桥接实用易用:

  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

根据用户选择显示或隐藏组件,无需调用服务器。适用于所有组件类型 — 输入组件和展示组件(提示框、横幅、图片等)。

{
  "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. 失焦时实时验证电子邮件(DOM 事件 + CatalogKit)

使用原生 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,然后在 HTML 内容中使用 {{var:key}} 或 {{global:key}} 模板显示。无需直接操作 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 格式 checkboxId.optionValue.inputId,而不是裸输入 ID。这是嵌套输入相关脚本中最常见的错误。

{
  "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}} 可用于 type: "iframe" 的 src 属性,以生成动态嵌入式 URL。

最佳实践
  • 重要:始终先调用 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)。
  • 优先使用 setValidationError,而不是自定义错误 DOM——它与原生验证系统集成,并会自动滚动到相应位置。
  • 使用 setComponentProp(id, 'hidden', true/false) 实现动态条件 UI——适用于所有组件类型(如 callout、banner、image 等显示组件,以及输入组件)。隐藏的组件不会参与验证。
  • 使用作用域事件(beforenext:page_id),而不是使用全局事件并在回调中进行 if 检查。
  • 对于 DOM 事件(blur、focus、click、input),直接使用 DOM——无需等待 CatalogKit 添加这些事件。
  • 使用 setTimeout 配合 pageenter/pageexit 实现定时逻辑——退出时务必清理计时器。
  • 脚本在 try/catch 中执行——错误会记录到控制台,但不会导致目录渲染器崩溃。
  • beforenext 和 submit 上的异步回调会被完全等待——可以放心在其中使用 await fetch()。
  • 在结果页使用 kit.getQuizScores(),以获取包含完整上下文的分数(问题标签、选项、答案、解释)。
故障排除
现象原因修复方法
服务返回 422/错误,但前端没有显示错误未绑定 beforenext 监听器添加一个带有 beforenext 脚本的 html 组件,调用 setValidationError() 和 preventDefault()——参见服务端表单验证
显示了错误,但页面仍然跳转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 是否与第一个参数匹配
脚本未执行当前页面上没有 html 组件,或 content 属性缺少 <script> 标签确保 html 组件位于页面的 components 数组中,并且内容用 <script>...</script> 包裹
结账崩溃,或在生产环境和开发环境中的行为不同在 preventDefault() 和 setTimeout 之后使用 DOM 操作(document.querySelector)点击购物车结账按钮使用 success_page_id 自动控制结账,或调用 kit.startCheckout()。参见触发结账
结账时出现 Unexpected token '<', "<!DOCTYPE"... is not valid JSONCLI 过时——开发服务器的 fetch 拦截器缺少结账路由运行 npm install -g @officexapp/catalogs-cli@latest 进行更新。确保 .env 文件位于 catalog 目录中,且其中包含 STRIPE_SECRET_KEY 和 STRIPE_PUBLISHABLE_KEY
Stripe 结账显示“未配置”错误.env 中缺少 Stripe 密钥在 catalog 项目目录中创建 .env 文件,包含 STRIPE_SECRET_KEY=sk_test_... 和 STRIPE_PUBLISHABLE_KEY=pk_test_...。开发服务器会检查 catalog 目录,而不是你的主目录
结账时出现“Stripe key mode mismatch”错误结账设置中的 stripe_publishable_key 是 pk_test_*,但服务器的密钥是 sk_live_*(或反之)**用于生产环境:**将 stripe_publishable_key 设置为你的 live 可发布密钥(pk_live_*)。**用于开发环境:**在 .env 中使用测试密钥(sk_test_* + pk_test_*)。两个密钥必须来自同一个 Stripe 账户,且处于相同模式(测试或 live)。如果检测到测试密钥,CLI 会在 catalog push 时发出警告
本地开发时,重新加载页面会跳转到起始页会话持久化从 localStorage 恢复了过期会话点击开发工具栏中的“清除缓存”,或在 URL 后附加 ?dev_force_page=page_id,直接跳转到任意页面并跳过恢复对话框
保存后,.ts catalog 文件的更改未生效CLI 版本过时,tsImport ESM 模块缓存阻止了重新加载运行 npm install -g @officexapp/catalogs-cli@latest 进行更新。v0.7.5+ 通过缓存失效机制修复了此问题
未检测到导入模块(例如 ./products.ts)的更改过时的 CLI 只监视根 catalog 文件更新 CLI。v0.7.5+ 会递归监视整个 catalog 目录中的 .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 Agent 的工作流——比原始 JSON 更易于阅读、编写和比较差异。

设置

安装类型包,以获得完整的编辑器类型检查和自动补全:

npm install --save-dev @officexapp/catalog-types
工作原理
  1. 创建一个 .ts 文件,将 CatalogSchema 对象作为默认导出
  2. 通过 CLI 推送——CLI 会转译 TS、序列化函数(钩子、脚本),并将生成的 JSON schema 上传到 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;
要点
  • 导入类型:从 @officexapp/catalog-types 导入 CatalogSchema、组件类型等。
  • 函数会自动序列化——你可以在 TS 中将钩子(on_enter、on_change、beforenext)编写为实际函数。CLI 会将它们序列化为 JSON schema 所需的字符串。
  • 默认导出——CLI 需要 export default catalog(或任何默认导出的 CatalogSchema 对象)。
  • 无需使用 JSON.stringify——CLI 会处理整个 TS → JSON → API 上传流程。

**⚠️ AI Agent 警告:**不要手动将 .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 loader hooks 无法工作。我们建议使用 Node 20 或 22 LTS。CLI 启动时会强制检查此要求——如果你的 Node 版本不在 18–22 范围内,它将拒绝运行,并显示通过 NVM 安装兼容版本的说明。

配置

CLI 需要显式提供身份验证令牌,不会静默读取配置文件或 .env 文件中的令牌。这可以防止意外使用错误的账户进行操作。

环境变量必填默认值说明
CATALOG_KIT_TOKEN是—你的 API 密钥(格式:cfk_...)
CATALOG_KIT_API_URL否https://api.catalogkit.cc覆盖 API URL(很少需要)

身份验证解析(仅有两个来源,按以下顺序):

  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 确实会自动从目录中加载 .env、.env.local 和 .env.development 文件,以获取非身份验证变量(例如 STRIPE_SECRET_KEY、STRIPE_PUBLISHABLE_KEY)。已设置的环境变量绝不会被覆盖。

安全功能: 执行任何会修改数据的操作(catalog push、video upload)之前,CLI 会显示经过身份验证的身份(子域名 + 电子邮件),以便你确认操作使用的是正确账户。

常见错误: 环境变量是 CATALOG_KIT_TOKEN,不是 CATALOGS_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 会渲染为带样式的按钮(primary、secondary、danger、ghost),支持图标、side_statement 和 reassurance。操作会执行验证,在 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(以目录 slug 为键)。用户在流程中途离开后返回时,会显示“Resume” / “Start Over”提示。会话可跨 Stripe 结账重定向保留 — 付款后会自动恢复所有表单数据和原始 URL 参数(例如 ?ref=abc123、UTM 标签),因此 kit.getField() 和 kit.getUrlParam() 在成功页/结账后页面中仍可使用(包括多次结账的追加销售流程)。只有在最终提交表单或用户主动选择“Start Over”时才会清除。使用开发工具栏中的“Clear Cache”按钮重置,或添加 ?dev_force_page=page_id 以完全跳过会话恢复模态框
  • 购物车与优惠 — 完整的购物车 UI(浮动按钮 + 滑出式抽屉),通过 accept_field/accept_value 收集页面优惠。购物车会在页面间保留,且可移除商品
  • 购物车设置 — settings.cart:position(四个角)、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 解析 — 根据提示解析组件属性和操作中的 *__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 中),与生产环境行为一致
  • 可见性条件 — 根据 formState 和 URL 参数实时计算具有 visibility 条件组的组件(支持全部 13 种运算符)
  • 开发工具栏 — 顶部固定栏显示:目录 slug + 架构版本徽标(v1.0)、Pages 按钮、Inspect 按钮、Debug 开关、Clear Cache 按钮、Stripe 状态指示器(绿色 = 已启用,黄色 = 模拟)、Events 查看器、Prod 链接(已发布时在新标签页中打开生产环境 URL)和最小化按钮。点击最小化按钮(—)可折叠为右上角的浮动胶囊;点击胶囊可重新展开。最小化状态通过 sessionStorage 在重新加载后保留
  • 页面图 — 点击工具栏中的“Pages”可打开全屏模态框,查看所有页面和路由边的可视化关系图。使用 Cytoscape.js,并从入口点开始采用广度优先布局。入口页带有绿色边框,当前页面会以主题色发光。边标签显示“conditional”或“default”。点击任意页面节点可强制直接跳转至该页面 — 这会绕过 auto_skip,无论表单状态如何都能检查任意页面。支持平移和缩放,适用于大型目录
  • 元素检查器 — 点击工具栏中的“Inspect”按钮(或按住 Shift+Alt)即可启用。光标会变为十字准星。将鼠标悬停在任意组件上(包括购物车、底部固定栏、结账页、弹出窗口),会显示包含其引用路径、类型和标签的提示框。点击即可复制结构化 JSON,其中包含供 AI Agent 使用的 pageId/componentId 引用。复制后检查器会自动停用;也可再次点击 Inspect 按钮将其关闭
  • 事件查看器 — 点击工具栏中的“Events”可打开全屏模态框,其中包含可搜索、可筛选的开发事件表格(页面浏览、字段更改、结账事件)。可按事件类型筛选、搜索事件数据并查看时间戳。事件也会流式输出到终端和 SSE 端点
  • 调试模式 — 点击工具栏中的“Debug”可循环切换模式:none → slim → verbose。模式会反映在 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)显示在工具栏目录 slug 旁,从 schema.schema_version 获取
  • 生产环境链接 — 工具栏中的“Prod”链接会在新标签页中打开已发布的目录 URL。URL 会按以下顺序自动解析:(1) 目录的 url 字段;(2) 基于子域名的 URL(https://{subdomain}.catalogkit.cc/{slug});(3) 回退至 https://catalogkit.cc/c/{catalog_id}。目录尚未发布时,该链接会显示为灰色/禁用状态
  • 验证叠加层 — 验证错误和警告会显示在左下角的固定叠加层中,热重载后会再次显示,也可通过关闭按钮关闭
  • 路由 — 使用共享引擎在本地执行条件页面路由(支持所有运算符、条件组、边优先级和默认边)
  • 本地 Stripe 结账 — 将 STRIPE_SECRET_KEY=sk_test_... 和 STRIPE_PUBLISHABLE_KEY=pk_test_... 添加到 .env 后,开发服务器会通过 Stripe REST API 在本地创建真实的 Stripe 结账会话。可发布密钥会启用 Payment Element(支持 Link 和动态支付方式);密钥会启用托管式/嵌入式/自定义结账会话。支持订阅、试用、促销代码、客户电子邮件预填和 stripe_price_id 引用。未找到密钥时会回退至说明性模拟模式
  • 本地开发事件 — 页面浏览、字段更改和结账事件会流式输出到终端,并通过 /__dev_events_stream 上的 SSE 广播。AI Agent 可订阅。最近的事件可通过 GET /__dev_events?limit=50 以 JSON 格式获取。不会对生产环境造成任何影响
  • SSE 重连退避 — EventSource 连接(/__dev_sse 用于自动重新加载,/__dev_events_stream 用于事件)在连接失败时采用指数退避(1s → 2s → 4s → ... → 最多 30s),防止服务器重启期间浏览器不断发起重连风暴
开发服务器 HTTP 端点

本地开发服务器在 http://localhost:3456(或自定义 --port)上公开以下端点。开发者可直接使用这些端点,无需客户端覆盖:

端点方法说明
/checkout/sessionPOST创建 Stripe 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 时允许输入促销代码
  • 通过 prefill_fields 使用 form_state 预填客户信息(customer_email、customer_name)
  • 所有 stripe_overrides(capture_method、setup_future_usage、consent_collection、payment_method_options、账单描述符、transfer_data)
  • 免费试用(含 trial_end_behavior)、3D Secure、账单地址收集、付款说明、自定义免责声明文本

事件端点——Agent 可以跟踪和监控事件:

# 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"}]}'

公共目录端点——Agent 可以以 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

开发预览版功能对照:

  • 支持的组件类型(输入): 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
  • 仅生产环境支持的功能(开发预览版不支持):包含 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 push自动上传到 CDN,并替换为 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
开发模式行为
功能开发模式生产环境
Stripe 结账如果 .env 中设置了 STRIPE_SECRET_KEY,则通过 /checkout/session、/checkout/session/status 和 /checkout/intent(已弃用)实时运行(使用测试密钥),否则使用桩实现实时运行
事件仅本地——终端输出 + SSE 流(/__dev_events_stream)生产环境分析
文件提供本地文件系统CDN(CloudFront)
热重载保存文件时触发(SSE)不适用
调试面板Ctrl+D — formState、购物车、路由、事件不适用
验证在开发横幅中实时显示叠加层不适用

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 编码问题,请使用 POST 替代方式并传入 JSON 请求体:

# 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(只有一个变体)、no_variants(目录中没有变体)、fallback(LLM 无法决定,返回第一个变体)。当变体路由到其他目录时,会包含 target_slug。当根据提示匹配到可由 AI 预填的字段时,会包含 prefill。

回退行为:

  • 0 个变体,没有可预填字段 → 返回 null,基础目录按原样呈现
  • 0 个变体,有可预填字段 → 仅为预填调用 LLM,返回 reason: "ai_prefill_only"
  • 1 个变体 → 直接返回(跳过变体 LLM),如果存在字段,仍会运行预填
  • 2 个或更多变体 → 单次 LLM 调用处理变体和预填;如果 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. 一次 LLM 调用同时处理变体路由和字段预填
  4. 对于选择字段(下拉菜单、多选题等),AI 会从确切的选项值中选择;无效值会在服务端被拒绝
  5. 对于自由文本字段,只有当提示中包含近乎完全匹配的内容时,AI 才会填写(由 instructions 和 confidence 控制)
  6. 预填值会合并到表单状态,优先级为:default_value < AI prefill < URL params(显式指定的值始终优先)
  7. 当所有可见字段都有值时,会跳过设置了 auto_skip: true 的页面

无需变体也能使用。 即使目录没有任何变体,只要包含可由 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=... 浏览较长的列表。


元素检查器(DevEx)

用于 AI Agent 工作流的内置开发者工具。在实时目录中,将鼠标悬停在任意元素上时按住 Shift+Alt,即可查看丰富的上下文信息;然后点击即可复制结构化 JSON 块,供 AI Agent 精确定位用户所指的内容。

使用方法:

  1. 在浏览器中打开任意目录
  2. 按住 Shift+Alt —— 屏幕上会出现“检查器已启用”指示器(如适用,还会显示目录 slug 和变体)
  3. 将鼠标悬停在任意元素上——元素会以靛蓝色边框高亮显示,并出现多行提示框,其中包含:
    • 引用路径(例如 landing/hero-title)和组件类型
    • 从组件 DOM 中提取的标签文本
    • 目录上下文——slug、目录 ID 前缀、变体 slug、沙盒状态
  4. 点击任意位置,即可将结构化 JSON 块复制到剪贴板
  5. 将 JSON 粘贴到 AI Agent 对话中——其中包含定位和修改该元素所需的全部信息

复制的 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在目录 schema JSON 中的精确路径
catalog_id用于 API 调用的完整目录 ID
catalog_slug目录的 URL slug
variant_slug当前活动变体的 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 Agent 工作流示例:

  1. 用户按住 Shift+Alt,将鼠标悬停在标题上,然后点击复制
  2. 用户将内容粘贴到 Claude 中:“将此元素改为:{...复制的 JSON...},文本为‘欢迎回来’”
  3. AI Agent 从 JSON 中读取 catalog_id、page_id、component_id 和 api_endpoint
  4. AI Agent 通过 GET /api/v1/catalogs/{catalog_id} 获取目录,在 schema.pages.{page_id}.components 中找到 id == component_id 的组件,更新文本,然后通过 PUT 写回

购物车与结账

使用内置结账功能 — 不要自行构建

Catalog Kit 自带一个完整、可用于生产环境的结账页面。请勿从头构建自定义结账界面、支付表单或 Stripe 集成。内置结账功能可处理所有事务:

  • 订单摘要 — 购物车商品及其图片、标题、价格、移除按钮、优惠码和摘要行(小计/税费/总计)
  • Stripe 支付 — 内嵌 Payment Element(银行卡、Link、动态支付方式)、嵌入式结账或托管式重定向
  • 3D Secure — 银行级验证,支持自动弹出验证窗口、说明横幅和徽章
  • 免费试用 — 试用徽章、用于 $0 验证的 SetupIntent,或用于受保护试用的手动捕获预授权
  • 已保存的支付方式 — 按电子邮件查找或创建 Stripe Customer,并在用户再次访问时显示已保存的银行卡
  • 跳过按钮 — 显示可自定义文字的“无需付款,继续”,也可禁用该按钮以要求付款
  • 客户评价和免责声明 — 社会认同卡片,以及显示在付款按钮下方的自定义免责声明文字
  • 付款后跳转 — 自动重定向到成功页面、清理 URL 参数并触发转化事件
  • 双列响应式布局 — 可配置桌面端(左/右)和移动端(上/下)的列顺序
  • 多结账追加销售链 — 依次通过多个 type: "checkout" 页面,每个页面都使用独立的 Stripe 支付

快速开始 — 只需 3 步,即可将结账功能添加到任何漏斗:

  1. 设置 Stripe 密钥 — 在架构中添加 stripe_publishable_key,并通过设置 API 添加 stripe_secret_key
  2. 配置 settings.checkout — 设置 payment_type、success_page_id 以及所需的外观和支付选项
  3. 添加页面优惠 — 至少在一个页面上定义 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 后,平台会自动拦截跳转到该页面的导航,并优先显示结账界面。访客填写漏斗信息、接受优惠后,在即将进入成功页面时,结账界面就会显示完整订单摘要和内嵌银行卡输入字段。通过 Stripe 付款后,访客会继续前往感谢页面。无需自定义 HTML,无需加载 Stripe SDK,无需处理重定向,也无需在前端解析 webhook。

不同结账模式的适用场景:

场景模式配置
大多数漏斗(推荐)Payment Element设置 stripe_publishable_key,将 ui_mode 留空或设为 "custom"
最少配置,由 Stripe 处理所有事务Embedded设置 ui_mode: "embedded"
无可发布密钥/最简单的集成方式Hosted redirect省略 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 扣款的商品)。

默认情况下,两者相同。调用购物车上的 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——可选的 Partial<CheckoutSettings>,用于覆盖 settings.checkout 中针对当前结账页面的字段。可为每个步骤设置不同的标题、支付配置或跳过行为。
  • cart_name——此结账页面用于扣款的命名购物车。省略时使用默认购物车。
  • components: []——必填字段,但在结账页面中会被忽略。始终设为空数组。
  • 跳过按钮——“继续,不付款”会沿路由边前往下一页(不会结束漏斗)。
  • 返回按钮——通过浏览器返回执行常规历史记录回退。
  • 清空购物车——Stripe 支付成功后会自动清空所有购物车。
  • Stripe 返回——付款后,Stripe 会通过 ck_page URL 参数重定向回来,以标识发起付款的结账页面,随后自动前往路由中的下一页。
在结账步骤间复用 Stripe Customer

在 settings.checkout 中设置 reuse_payment_method: true 后,首次结账会通过电子邮件查找或创建 Stripe Customer。customer_id 会自动持久化到会话中,并传递给后续结账步骤——第二次结账时会显示首次结账时保存的银行卡。

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

无需编写脚本。渲染器会自动处理步骤间的 customer_id 传递。

对于由脚本驱动的流程,也可以通过 window.CatalogKit 获取 Customer 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 实例,并限定使用其 stripe_publishable_key。如果用户在同一浏览器会话中查看多个使用不同 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”错误。使用 price_display 显示用户可读的价格。

⚠️ mode_override: "payment" 限制: 如果结账设置使用 mode_override: "payment"(例如受限试用),则每个优惠都必须包含 amount_cents;单独使用 stripe_price_id 会被移除,导致收费金额为 0。这也适用于通过 setPaymentItems() 传入的商品。

优惠显示字段 — 视觉布局

每个购物车/结账商品包含四个显示字段,分为两列排列:

┌──────────────────────────────────────────────────┐
│  [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")——这会导致无法采用双行布局,并使副文本不可见
  • 不要将商品描述放在 title 中——请使用 subtitle 显示次要内容
  • 不要混淆 subtitle(位于商品标题下方、左侧)和 price_subtext(位于价格下方、右侧)

购物车商品会在不同页面之间累积——每个页面都可以展示不同的优惠(例如,第 1 页展示主商品,第 2 页展示追加销售商品,第 3 页展示订单加购商品)。所有被接受的优惠都会成为 Stripe Checkout 的订单项。

在同一页面上设置多个优惠/订单加购项

页面的 offer 字段仅接受单个 PageOffer。若要在同一页面上展示多个订单加购项,请使用原生 offer 设置主要商品,并使用 kit.cart().add() 添加其他加购项:

  1. 主要优惠 — 按常规使用页面的 offer 字段(接受后会自动添加到购物车)。
  2. 其他优惠 — 添加一个 html 组件,并在其中加入 <script>,用于监听接受操作并调用 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 去重)
  • 每个购物车商品都要包含 amount_cents 和 payment_type,确保 Stripe 结账正常运行
  • 可以按需添加任意数量的脚本加购项,不设上限
购物车商品按钮

购物车商品支持可选的 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" }
  }
}
结账页面布局

结账页面在桌面端采用双栏布局(移动端为单栏)。桌面端和移动端的栏顺序可分别配置。

默认布局:

  • 桌面端:左侧为购物车(订单摘要),右侧为卡片(付款)
  • 移动端:顶部为购物车,底部为卡片

购物车栏(订单摘要):

  • 订单摘要(包含图片、价格和移除按钮的购物车商品)
  • 摘要行 — 税费、总额等(设置 summary_lines 时)
  • 优惠码输入框(启用 allow_discount_codes 时)
  • 用户评价(testimonial.enabled 为 true 时)
  • 自定义组件(设置 components 数组时)

卡片栏(付款 — 固定显示):

  • 标题文本(通过 card_header_text 配置,默认为:"Complete Checkout")
  • 消息横幅(通过 card_message 配置,例如 "You won't be charged for 7 days")
  • 客户信息字段(电子邮件、姓名、电话 — 来自 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 下方的彩色消息横幅(例如 "You won't be charged for 7 days"、"Secure checkout"、"Cancel anytime")。仅在设置后显示。
below_buttonobject—可选内容,显示在结账按钮下方。默认为空。
below_button.type"text" | "button""text"显示为纯文本或可点击链接。
below_button.textstring—要显示的文本内容。
below_button.urlstring—button 类型的 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是左对齐标签(例如 "Subtotal"、"Tax (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 }
    ]
  }
}
结账界面模式

结账页面支持三种支付模式,会自动确定:

配置模式行为
已设置 stripe_publishable_key,未设置 ui_mode 或设为 "custom"Payment Element(默认)Stripe Payment Element 在右侧栏中渲染,支持银行卡、Link 和动态支付方式。基于 Checkout Sessions ui_mode: "custom"。
已设置 stripe_publishable_key,且 ui_mode: "embedded"嵌入式结账Stripe 完整的嵌入式结账界面在右侧栏中渲染。Stripe 负责所有银行卡信息输入和支付。
未设置 stripe_publishable_key,或 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。

图标预设:

预设描述
"cart"购物车(默认)
"bag"购物袋
"basket"购物篮
"https://..."自定义图片 URL
购物车事件(分析 + JS)

以下事件会自动跟踪:cart_add、cart_remove、checkout_start、checkout_error、payment_info_added、checkout_complete、checkout_skip。

JavaScript 事件,通过 window.CatalogKit 触发:

事件负载是否可取消描述
cart_add{ item, items }否商品已加入购物车
cart_remove{ offer_id, items }否商品已从购物车移除
cart_open{ items }否购物车抽屉已打开
cart_close{ items }否购物车抽屉已关闭
before_checkout{ items, cartName, preventDefault() }是在结账前触发——调用 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()}`);
});

触发结账

访客可以通过三种方式进入内置结账页面。结账不受页面拓扑结构限制——页面可以拥有任意数量的出边。这支持多次结账流程(例如追加销售、附加项、分层优惠),访客可能会多次进入结账页面。

1. success_page_id 门控(推荐)

设置 settings.checkout.success_page_id 后,每当导航将跳转至该页面时,平台都会自动显示结账页面。访客会看到完整的结账界面,完成支付(如果 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. 购物车结账按钮

访客随时可以点击浮动购物车按钮 → 打开购物车抽屉 → 点击“前往结账”。此操作会触发 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. 触发 checkout_complete 分析事件,包含购物车详情(商品、显示价格、数量)、Stripe 重定向参数,以及实际的 Stripe 会话金额(通过 /checkout/session/status 异步获取)。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 的参数
托管session_id
嵌入式session_id
自定义(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,因此你的像素恰好触发一次。


结账事件生命周期

结账流程中触发的事件(内部分析采用与 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 会话获取)。如果有 Stripe 实际总额,value.amount_cents 字段会设为该金额;否则回退为显示的购物车总额。该字段会计入仪表板分析所用的 revenue_cents 汇总计数器
  • checkout_complete 按会话去重;付款后刷新页面不会再次触发该事件
  • $0 购物车(结账时购物车为空)也会触发 checkout_complete,并包含 zero_cart: true,以便分析管道区分付费和免费结账
  • 重定向到 Stripe 之前,购物车数据会持久化到 localStorage,因此商品详情在往返跳转后仍会保留,并出现在事件中
  • Stripe 付款表单变为可交互时会触发 payment_info_added,而不是用户输入卡片信息时(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/session)

主要端点 — 用于创建 Stripe Checkout Session。支持三个 ui_mode 值:

  • "custom"(设置了 stripe_publishable_key 时的默认值)— 返回 { session_id, client_secret }。前端使用客户端密钥挂载 Payment Element,然后调用 stripe.confirmPayment()。支持 Link、动态支付方式、3DS、试用期和订阅 — 均由 Checkout Session 处理。
  • "embedded" — 返回 { session_id, client_secret }。前端挂载 Stripe 的嵌入式结账界面。
  • "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 会由 checkout_complete 分析事件用于记录 Stripe 的实际收入。

结账意图策略(/checkout/intent)— 已弃用

已弃用: 请改用 POST /checkout/session 并设置 ui_mode: "custom"。为兼容会创建原始 PaymentIntent/SetupIntent 的自定义 HTML+后端集成,此端点仍可使用。

使用旧版 Card Element 流程时,前端会调用 POST /checkout/intent 来创建 Stripe PaymentIntent 或 SetupIntent。策略根据结账配置确定:

结账配置意图类型处理方式
默认(无覆盖项)PaymentIntent,自动扣款简单的一次性扣款
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 Secure 验证

对每笔银行卡支付强制进行银行级身份验证(OTP、生物识别、银行应用)。这是降低试用期结束时支付失败率最有效的一项设置 — 它会在注册时验证持卡人身份真实且已通过认证。

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

启用后,意图会包含 payment_method_options.card.request_three_d_secure: "any"。前端会显示“3D Secure Checkout”标题徽章,并在右侧栏的支付按钮下方显示蓝色说明横幅,告知用户其银行将要求进行验证。试用期有效时,按钮文本默认为“开始免费试用”。

使用内嵌银行卡信息栏时,3DS 验证会以 Stripe 管理的弹出窗口形式显示在页面上方 — 用户无需离开结账页面。

试用期结束行为

控制客户的支付方式在试用期结束时支付失败后,Stripe 将如何处理:

值行为
"cancel"(默认)取消订阅 — 不重试
"create_invoice"创建发票,并按照 Stripe 的智能计划重试
"pause"暂停订阅,直至支付问题解决

推荐组合:require_3ds: true + trial_end_behavior: "cancel" — 提前验证银行卡;如果支付仍然失败,则妥善取消订阅。

Stripe 覆盖项(高级透传)

使用 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.descriptionstring合并到 subscription_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 模式下,定期价格请使用 amount_cents 而非 stripe_price_id。

结账错误弹窗

当 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"弹窗标题
messagestring回退为 Stripe 错误消息正文文本。支持使用 \n 换行
alt_action.labelstring—备用结账链接按钮的文本
alt_action.urlstring—备用按钮跳转到的 URL
dismiss_labelstring"Try again"关闭/重试按钮上的文本

提供 alt_action 时,弹窗会显示两个按钮:备用操作按钮为主题色主按钮,关闭按钮为次要按钮。未提供 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 Dashboard 中配置 Webhook,并将其指向你自己的服务器。

设置:

  1. Stripe Dashboard → Developers → Webhooks → 添加你的端点
  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"
    }
  }
}

访客在页面内输入银行卡信息,点击 “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 验证银行卡并将其保存,但不会扣款。你的计费服务器会监听 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"
      }
    }
  }
}

注意:在线项目中使用 amount_cents(而非 stripe_price_id),因为意图需要具体金额才能进行预授权。

**底层运作方式:**使用 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 — 在服务器上处理边缘情况:

场景操作
银行卡不支持单独预授权/扣款(拉美借记卡、印度银行、部分预付卡)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 Agent,可以告诉它:“强制重启 catalog-kit CLI”——Agent 应终止现有进程,然后重新启动 catalogs catalog dev。

CLI 会在发布时打包一份渲染器副本。在本地 monorepo 开发期间,它会改用同级目录下的 renderer/dist/。如果你更新了渲染器源代码,请先重新构建(cd renderer && npm run build),然后重启开发服务器。

评论

支持 Markdown