profound-logoProfound CMS
⌘K
Admin
Theme
DocsTutorialBlogPhilosophy
DocsTutorialBlogPhilosophy

Hybrid

Collection of Pages with ComponentsTypes of ComponentsSetup server sent events (SSE) content refetchInstall Profound CMS as a proxyCEL Scripting in Template BuilderProject ScaffoldingMedia Library

Headless

Quick startJSON 与 Claude 代码组件 Zod 拉取

REST API

REST API 概览get连接网站 APIgetGET /routesgetGET /routegetGET /blocksget获取带有 CEL 缓存的区块getGET /blocks/generatedgetGET /componentsgetGET /components/{name}getGET /dataset/{schema_name}get获取内容变更 SSEpatchPATCH /dataset/{schema_name}postPOST /translationpatchPATCH /translationsgetGET /usagepostPOST /csvpatch修补 CSV
All Systems Operational
Powered Byprofound-logo
Theme

CEL Scripting in Template Builder

在 CMS 中编写 CEL 表达式的实用指南。

在 CMS 中编写 CEL 表达式的实用指南。


CEL 的工作原理

CEL(通用表达式语言)是内置于我们 CMS 中的轻量级脚本语言。它允许你编写动态表达式,从文档中提取数据、读取 URL 参数,并即时计算值。

CEL 脚本运行时的流程:

你的脚本                       引擎                          结果
    |                            |                            |
    v                            v                            v
documents.get("article", "intro") --> 从数据库获取 --> { headline: "Welcome", body: "..." }
         .headline                --> 提取字段       --> "Welcome"

可以把 CEL 看作一种只读查询语言。它不能修改数据库中的任何内容,只会读取数据并返回计算结果,因此可以安全地在 CMS 的任何位置使用。


基本组成

每个 CEL 表达式都可以访问三类对象:

对象说明示例
documents从 CMS 获取任意文档documents.get("country", "us")
meta当前请求的信息(区域设置、URL 参数)meta.locale、meta.params.slug
schema当前文档的字段定义schema.fields

使用 doc 引用当前文档

在文档编辑器中编写 CEL 表达式时,可以通过 doc 对象访问当前文档的字段值,从而实现计算字段和跨字段引用。

// 访问当前文档的价格字段
doc.price

// 根据当前文档的字段计算总价
doc.price * doc.quantity

// 根据当前文档状态进行条件判断
doc.status == "published" ? doc.title : "Draft: " + doc.title

doc 对象包含正在编辑的文档中的所有字段值,适用于:

  • 计算字段(例如 doc.price * doc.quantity)
  • 根据文档状态控制显示逻辑
  • 类似验证的表达式

获取文档

CEL 最强大的功能是可以从 CMS 的任意位置获取文档。

获取单个文档

语法: documents.get(schemaName, identifier)

假设有一个 article 文档,其标识符为 "welcome-post":

// 在 CMS 中存储为:article / welcome-post
{
  "headline": "欢迎来到我们的平台",
  "author": "Sarah Chen",
  "body": "我们很高兴地宣布……",
  "tags": ["announcement", "news"]
}

获取整个文档:

documents.get("article", "welcome-post")

获取标题:

documents.get("article", "welcome-post").headline

返回:"欢迎来到我们的平台"

获取作者:

documents.get("article", "welcome-post").author

返回:"Sarah Chen"


使用 URL 参数

当页面包含动态路由(例如 /articles/[slug])时,可以使用 meta.params 获取 URL 参数,并获取对应的文档。

如果用户访问 /articles/welcome-post:

documents.get("article", meta.params.slug).headline

返回:"欢迎来到我们的平台"

这就是构建动态页面的方法:同一个 CEL 脚本可以用于任何文章,只需使用 URL 中的 slug 即可。


获取多个文档

语法: documents.find(schemaName) 或 documents.find(schemaName, filter)

// 获取所有国家
documents.find("country")

返回一个文档数组:

[
  { "code": "us", "name": "美国", "flag": "US" },
  { "code": "sa", "name": "沙特阿拉伯", "flag": "SA" },
  { "code": "gb", "name": "英国", "flag": "GB" }
]
// 使用过滤条件获取国家
documents.find("country", { "where": { "code": "us" } })

翻译

CEL 支持通过两种方式获取翻译后的文档内容:基于区域设置的自动翻译,以及显式翻译查询。

通过 meta.locale 自动翻译

当设置了 meta.locale(例如来自路由参数或用户偏好)时,documents.get() 会自动合并翻译内容:

// 如果 meta.locale 为 "fr",返回与基础文档合并后的法语翻译
documents.get("greeting", "welcome").headline

工作流程:

  1. 获取基础文档内容
  2. 如果 meta.locale 不是 "en" 或 "en-US",则在 translations 表中查找翻译
  3. 将翻译字段合并到基础内容上:{ ...baseContent, ...translatedContent }

这意味着已翻译的字段会覆盖基础字段,而未翻译的字段会回退到基础文档中的值。

使用 documents.translated() 显式翻译

如果需要忽略当前区域设置并获取特定翻译,可以使用:

语法: documents.translated(schemaName, identifier, locale)

// 始终获取西班牙语翻译
documents.translated("greeting", "welcome", "es").headline

// 根据 URL 参数获取翻译
documents.translated("product", meta.params.id, meta.params.lang).description

// 比较不同翻译
documents.translated("article", "intro", "en").title + " / " + documents.translated("article", "intro", "fr").title

实际示例

示例 1:从其他文档获取 Hero 区块标题

假设有一个 hero-block,需要显示从 article 文档中获取的标题。

documents.get("article", "homepage-hero").headline

结果: Hero 区块显示 "Build Faster, Ship Smarter"。

示例 2:根据代码获取国家名称

在 /countries/[code] 页面中显示完整的国家名称:

documents.get("country", meta.params.code).name

访问 /countries/us 时,meta.params.code 为 "us",结果为 "United States"。

示例 3:根据区域设置显示不同内容

meta.locale == "ar-SA" ? "Welcome, everyone" : "Welcome"

如果区域设置为 "ar-SA",返回 "Welcome, everyone",否则返回 "Welcome"。

示例 4:链式获取文档

documents.get("country", documents.get("article", "us-news").countryCode).name

该表达式先获取文章的 countryCode,再获取对应国家,最后提取国家名称。

示例 5:回退值

documents.get("article", meta.params.slug) != null
  ? documents.get("article", meta.params.slug).headline
  : "Article Not Found"

也可以检查特定字段是否存在:

documents.get("article", "intro").author != null
  ? documents.get("article", "intro").author
  : "Unknown Author"

示例 6:处理列表

"featured" in documents.get("article", "welcome-post").tags

如果文章包含 featured 标签,则返回 true。

documents.get("article", "welcome-post").tags[0]

获取第一个标签。

size(documents.get("article", "welcome-post").tags)

获取标签数量。


参数化路由与 meta.params

参数化路由是构建动态本地化页面的关键。当定义类似 /{lang}/landingPage 的路由模式时,CMS 会从 URL 中提取参数,并通过 meta.params 提供这些参数。

路由参数的工作方式

路由可以使用 :paramName 或 {paramName} 定义动态片段:

模式示例 URL提取的参数
/:lang/landingPage/ko/landingPage{ lang: "ko" }
/{country}/{lang}/products/us/en/products{ country: "us", lang: "en" }
/articles/:slug/articles/welcome-post{ slug: "welcome-post" }

每个路由参数都可以绑定到文档 schema 以进行验证:

{
  "pattern": "/{lang}/landingPage",
  "param_bindings": {
    "lang": "language"
  }
}

CMS 会提取 URL 中的 lang 片段,并根据 language schema 验证它。如果验证成功,完整文档将可在已解析的参数中使用。

语言相关的落地页示例

documents.get("greeting", meta.params.lang).headline

对于 /ko/landingPage、/en/landingPage 和 /ja/landingPage,该表达式会分别使用 ko、en 和 ja 获取对应内容。

国家与语言组合路由

对于 /{country}/{lang}/products:

// 获取国家名称
documents.get("country", meta.params.country).name

// 根据国家获取本地化产品列表
documents.find("product", { "where": { "country": meta.params.country } })

// 组合显示特定国家、特定语言的问候语
documents.get("greeting", meta.params.lang).headline + " from " + documents.get("country", meta.params.country).name

CMS 会按层级验证参数:先验证 country,再验证 lang,还可以进一步验证 lang 是否存在于该国家的 languages[] 数组中。


meta.segments:访问原始 URL 路径

meta.segments 以数组形式提供原始 URL 路径,适合在没有命名参数时按位置访问。

URL 路径meta.segments
/articles/tech/ai-news["articles", "tech", "ai-news"]
/ko/landingPage["ko", "landingPage"]
/us/en/products/featured["us", "en", "products", "featured"]
/[]

何时使用 meta.segments 或 meta.params

使用场景推荐方式
访问路由模式中的命名参数meta.params.lang
按位置访问meta.segments[0]
获取路径深度size(meta.segments)
检查路径是否包含某个片段"admin" in meta.segments
// 获取第一个片段(通常是语言代码)
meta.segments[0]

// 检查路径深度
size(meta.segments) > 2 ? "deep" : "shallow"

// 检查是否位于管理区域
"admin" in meta.segments ? "admin mode" : "public mode"

// 如果参数未绑定,则使用路径片段
has(meta.params.lang) ? meta.params.lang : meta.segments[0]

完整的 meta 对象参考

meta 对象包含当前请求的全部上下文:

属性类型说明
meta.localestring当前区域设置代码,例如 "en-US"、"ko-KR"、"ar-SA"
meta.paramsRecord<string, string>从 URL 路由模式中提取的参数
meta.segmentsstring[]按片段拆分的 URL 路径
meta.docIdstring | null当前文档 UUID,新建文档时为 null
meta.titlestring当前文档标题

meta.locale

区域设置代码遵循 BCP 47 格式:

// 检查是否为从右到左书写的语言
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"

// 仅获取语言部分
meta.locale.split("-")[0]  // 不支持,请改用 meta.params.lang

meta.params

路由参数始终为字符串。CMS 会在执行表达式前根据绑定的 schema 验证这些参数:

meta.params.lang           // "ko"
meta.params.country        // "us"
meta.params.slug           // "welcome-post"
has(meta.params.category)  // true/false

documents.get("greeting", meta.params.lang)
documents.ref("airports").get(meta.params.code)

meta.segments

原始 URL 片段数组:

meta.segments[0]           // 第一个片段
meta.segments[1]           // 第二个片段
size(meta.segments)        // 片段数量
"products" in meta.segments  // 路径是否包含 "products"

meta.docId

当前文档的 UUID,可用于自引用脚本:

meta.docId != null ? "editing" : "creating new"
meta.docId != null ? documents.get("article", meta.docId).status : "draft"

meta.title

当前文档的标题:

"Editing: " + meta.title
meta.title.contains("Draft") ? "work in progress" : "published"

documents.ref():链式查询

当 schema 已知但标识符是动态值时,可以使用更清晰的语法:

// 传统写法
documents.get("airports", meta.params.code).name

// 使用 ref(),将 schema 与动态标识符分开
documents.ref("airports").get(meta.params.code).name

两种写法等价,但 ref() 可以更明确地突出动态部分。


快速参考

文档获取

documents.get("schema", "identifier")       // 获取一个文档
documents.get("schema", "id").fieldName     // 获取特定字段
documents.find("schema")                    // 获取所有文档
documents.find("schema", { "where": {...}}) // 过滤查询
documents.ref("schema").get(identifier)     // 链式查询
documents.translated("schema", "id", "fr")  // 使用指定区域设置获取翻译

上下文变量

meta.locale          // "en-US"、"ar-SA" 等
meta.params.xyz      // 名为 "xyz" 的 URL 参数
meta.segments        // URL 路径数组,例如 ["articles", "intro"]
meta.segments[0]     // 第一个路径片段
meta.docId           // 当前文档 ID(或 null)
meta.title           // 当前文档标题
doc.fieldName        // 当前文档的字段值(编辑器上下文中)

运算符

// 比较
==  !=  <  <=  >  >=

// 逻辑
&&  ||  !

// 三元运算符(条件判断)
condition ? valueIfTrue : valueIfFalse

// 成员判断
"value" in listOrMap

常用函数

size(list)                    // 统计项目数量
size(string)                  // 字符串长度
"text".startsWith("te")       // true
"text".endsWith("xt")         // true
"text".contains("ex")         // true
has(object.property)          // 检查属性是否存在
hasProperty(obj, "key")       // 检查对象是否包含键

错误消息

如果出现问题,你可能会看到以下错误之一:

错误含义
SYNTAX_ERROR脚本存在拼写错误,例如缺少引号或运算符错误
TYPE_ERROR混用了不兼容的类型
RUNTIME_ERROR脚本已运行,但遇到问题,例如变量未定义
FETCH_LIMIT_EXCEEDED获取的文档过多(最多 50 个)
TIMEOUT脚本运行时间过长(最多 5 秒)
AST_DEPTH_EXCEEDED表达式嵌套过深(最大深度:50)
SCRIPT_TOO_LONG脚本超过 5000 个字符的限制

可扩展性与未来能力

CEL 引擎的设计支持扩展。计划中的未来能力包括:

计划中:MCP 服务器集成

// 未来:通过 MCP 调用外部服务
mcp.translate(meta.params.text, "en", meta.params.lang)
mcp.analyze(documents.get("article", meta.params.id).body)

计划中:AI 能力

// 未来:由 AI 驱动的内容生成
ai.summarize(documents.get("article", meta.params.id).body, 100)
ai.translate(meta.params.text, meta.params.targetLang)
ai.classify(meta.params.input, ["positive", "negative", "neutral"])

这些能力将通过已注册的函数系统添加,同时保持与现有脚本的向后兼容性。


使用技巧

  1. 使用自动补全——输入 documents. 或 meta.,编辑器会显示可用选项
  2. 从简单开始——先用 documents.get("schema", "id") 测试,再添加 .fieldName
  3. 检查 null——文档可能不存在时,使用 != null ? ... : ... 添加回退值
  4. 避免获取过多数据——每次 documents.get() 或 documents.find() 都会计入 50 次获取限制
  5. 优先使用 meta.params 而不是 meta.segments——命名参数经过验证,更可靠
  6. 使用 has() 检查可选参数——访问前先检查 has(meta.params.category)
  7. 使用 documents.ref() 处理动态标识符——schema 固定而标识符动态时,语法更清晰
  8. 使用 doc.fieldName 进行自引用——在计算表达式中访问当前文档字段

文档之间的引用

本节介绍如何链接文档并构建关系型内容结构的高级模式。

基本引用模式

最简单的形式是:一个文档通过标识符引用另一个文档。

// 文章存储作者 ID,获取作者姓名
documents.get("author", documents.get("article", "intro").authorId).name

使用 documents.ref() 的链式查询

// 传统写法
documents.get("country", documents.get("airport", meta.params.code).countryCode).name

// 使用 ref(),当 schema 已知而标识符动态时更清晰
documents.ref("country").get(documents.get("airport", meta.params.code).countryCode).name

多级引用链

可以通过多次链式查询构建深层关系:

// 机场 → 国家 → 地区 → 大洲
documents.get("continent",
  documents.get("region",
    documents.get("country",
      documents.get("airport", meta.params.code).countryCode
    ).regionCode
  ).continentCode
).name

带翻译的引用

// 获取机场所在国家的本地化名称
documents.translated("country",
  documents.get("airport", meta.params.code).countryCode,
  meta.params.lang
).name

按使用场景划分的引用模式

模式 1:外键查询

文档存储引用另一个文档的 ID:

documents.get("author", documents.get("article", meta.params.slug).authorId).name

模式 2:基于代码的引用

文档通过语义代码而不是 UUID 相互引用:

// 机场 → 国家 → 货币
documents.get("currency",
  documents.get("country",
    documents.get("airport", meta.params.code).countryCode
  ).currencyCode
).symbol

模式 3:使用 doc 上下文自引用

// 在产品文档中获取相关分类详情
documents.get("category", doc.categoryId).description

// 根据产品原产国计算运费
documents.get("shipping-rates", doc.originCountry).baseRate * doc.weight

模式 4:双向引用

文档相互引用时要注意获取次数:

documents.find("article", { "where": { "authorId": documents.get("article", meta.params.slug).authorId } })

模式 5:多态引用

当字段可能引用不同 schema 时,可以根据 sourceType 选择目标 schema:

documents.get("content-block", "hero-1").sourceType == "article"
  ? documents.get("article", documents.get("content-block", "hero-1").sourceId).headline
  : documents.get("product", documents.get("content-block", "hero-1").sourceId).name

依赖跟踪

每次 documents.get()、documents.find() 和 documents.ref().get() 调用都会被跟踪,用于缓存失效。当被引用的文档发生变化时,CMS 可以知道哪些 CEL 表达式需要重新计算。

跟踪的依赖包括:

  • get:schema:identifier——特定文档依赖
  • ref:schema:identifier——通过链式语法实现的同类依赖
  • query:schema:*——schema 级依赖(该 schema 中的任意文档)

引用的最佳实践

  1. 减少链式深度——每一级都会增加延迟和获取次数
  2. 缓存中间结果——同一个嵌套值需要使用两次时,应只获取一次父文档
  3. 使用 null 检查——文档被删除时,引用可能失效
  4. 优先使用代码而不是 UUID——代码在表达式中更易读,并且跨环境更稳定
  5. 注意获取限制——复杂引用链可能很快达到 50 次获取限制

附录 A:完整的参数化路由示例

本示例创建一个可通过 /{lang}/landingPage 访问的多语言落地页。

第 1 步:创建问候语文档 schema

在 CMS 管理后台创建名为 greeting 的自定义 schema:

{
  "name": "greeting",
  "fields": [
    { "name": "code", "type": "string", "required": true },
    { "name": "headline", "type": "string", "required": true },
    { "name": "subheadline", "type": "string" },
    { "name": "ctaText", "type": "string" },
    { "name": "ctaUrl", "type": "string" }
  ]
}

第 2 步:创建问候语文档

为每种语言创建文档,例如 greeting/ko、greeting/en 和 greeting/ja,并分别填写对应的标题、副标题、CTA 文案和 CTA URL。

第 3 步:创建页面

使用以下配置创建页面:

  • 路径/模式:/{lang}/landingPage
  • 状态:已发布
  • 动态片段映射:将 lang 映射到 language 组件
{
  "lang": "language"
}

第 4 步:添加包含 CEL 脚本的区块

为 Hero 区块的各个字段添加以下脚本:

// 标题
documents.get("greeting", meta.params.lang).headline

// 副标题
documents.get("greeting", meta.params.lang).subheadline

// CTA 文案
documents.get("greeting", meta.params.lang).ctaText

// CTA URL
documents.get("greeting", meta.params.lang).ctaUrl

第 5 步:在 Next.js 中使用

添加一个捕获所有路径的路由。ParametricRoutePage 会解析页面、从 URL 中提取 meta.params、在服务器端计算 CEL 绑定,并通过注册表渲染每个区块。你不需要自行构建 meta 上下文,也不需要直接调用底层客户端。

// app/[...slug]/page.tsx
import ParametricRoutePage from 'cms-renderer/lib/renderer';
import { registry } from '@/lib/registry';
import { cmsConfig } from '@/lib/cms-config';

export const dynamic = 'force-static';

interface PageProps {
  params: Promise<{ slug: string[] }>;
}

export default async function Page({ params }: PageProps) {
  const { slug } = await params;

  return (
    <ParametricRoutePage
      registry={registry}
      apiKey={cmsConfig.apiKey}
      websiteId={cmsConfig.websiteId}
      cmsUrl={cmsConfig.cmsUrl}
      params={Promise.resolve({ slug })}
    />
  );
}

第 6 步:测试路由

访问以下 URL 查看本地化内容:

URL预期标题
/ko/landingPage欢迎
/en/landingPageWelcome
/ja/landingPage欢迎光临

解析流程

用户访问 /ko/landingPage 时:

  1. 路由匹配:CMS 匹配 /{lang}/landingPage 模式
  2. 参数提取:meta.params.lang = "ko"
  3. 验证:CMS 验证 "ko" 存在于 language schema 中
  4. CEL 计算:documents.get("greeting", meta.params.lang) 解析为韩语内容
  5. 响应:将本地化区块返回给客户端

附录 B:技术参考

CelMeta 接口(TypeScript)

interface CelMeta {
  /** 当前区域设置代码,例如 'en-US' */
  locale: string;
  /** 从 URL 中提取的路由参数 */
  params: Record<string, string>;
  /** URL 路径片段 */
  segments: string[];
  /** 当前文档 ID(编辑现有文档时存在) */
  docId: string | null;
  /** 当前文档标题 */
  title: string;
}

参数提取算法

extractParams 函数会处理 URL 路径:

模式: /{country}/{lang}/products
路径: /us/en/products

算法:
1. 将两者标准化(删除结尾斜杠)
2. 拆分为片段:["us", "en", "products"] 和 ["{country}", "{lang}", "products"]
3. 匹配片段数量(必须相等)
4. 对每一对片段:
   - 如果模式以 : 或 {} 开头,则提取为参数
   - 否则必须完全匹配
5. 返回:{ country: "us", lang: "en" }

支持的参数绑定格式

// 简单绑定(使用 "code" 字段查询)
{ "lang": "language" }

// 详细绑定(自定义 slug 字段)
{
  "lang": {
    "schemaName": "language",
    "slugField": "code"
  },
  "slug": {
    "schemaName": "article",
    "slugField": "slug"
  }
}

文档查询优先级

通过 documents.get(schema, identifier) 获取文档时:

  1. UUID 匹配:如果标识符是有效 UUID,则按 id 获取
  2. Code 字段:检查 content.code 字段
  3. Slug 字段:检查 content.slug 字段
  4. 标题匹配:检查 title 字段

因此,可以使用任意唯一标识符灵活地引用文档。

Continue Reading
Previous‹Install Profound CMS as a proxyNextProject Scaffolding›