在 CMS 中编写 CEL 表达式的实用指南。
在 CMS 中编写 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"
当页面包含动态路由(例如 /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
工作流程:
meta.locale 不是 "en" 或 "en-US",则在 translations 表中查找翻译{ ...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
假设有一个 hero-block,需要显示从 article 文档中获取的标题。
documents.get("article", "homepage-hero").headline
结果: Hero 区块显示 "Build Faster, Ship Smarter"。
在 /countries/[code] 页面中显示完整的国家名称:
documents.get("country", meta.params.code).name
访问 /countries/us 时,meta.params.code 为 "us",结果为 "United States"。
meta.locale == "ar-SA" ? "Welcome, everyone" : "Welcome"
如果区域设置为 "ar-SA",返回 "Welcome, everyone",否则返回 "Welcome"。
documents.get("country", documents.get("article", "us-news").countryCode).name
该表达式先获取文章的 countryCode,再获取对应国家,最后提取国家名称。
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"
"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.locale | string | 当前区域设置代码,例如 "en-US"、"ko-KR"、"ar-SA" |
meta.params | Record<string, string> | 从 URL 路由模式中提取的参数 |
meta.segments | string[] | 按片段拆分的 URL 路径 |
meta.docId | string | null | 当前文档 UUID,新建文档时为 null |
meta.title | string | 当前文档标题 |
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.translate(meta.params.text, "en", meta.params.lang)
mcp.analyze(documents.get("article", meta.params.id).body)
// 未来:由 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"])
这些能力将通过已注册的函数系统添加,同时保持与现有脚本的向后兼容性。
documents. 或 meta.,编辑器会显示可用选项documents.get("schema", "id") 测试,再添加 .fieldName!= null ? ... : ... 添加回退值documents.get() 或 documents.find() 都会计入 50 次获取限制has(meta.params.category)本节介绍如何链接文档并构建关系型内容结构的高级模式。
最简单的形式是:一个文档通过标识符引用另一个文档。
// 文章存储作者 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
文档存储引用另一个文档的 ID:
documents.get("author", documents.get("article", meta.params.slug).authorId).name
文档通过语义代码而不是 UUID 相互引用:
// 机场 → 国家 → 货币
documents.get("currency",
documents.get("country",
documents.get("airport", meta.params.code).countryCode
).currencyCode
).symbol
// 在产品文档中获取相关分类详情
documents.get("category", doc.categoryId).description
// 根据产品原产国计算运费
documents.get("shipping-rates", doc.originCountry).baseRate * doc.weight
文档相互引用时要注意获取次数:
documents.find("article", { "where": { "authorId": documents.get("article", meta.params.slug).authorId } })
当字段可能引用不同 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 表达式需要重新计算。
跟踪的依赖包括:
schema:identifier——特定文档依赖schema:identifier——通过链式语法实现的同类依赖schema:*——schema 级依赖(该 schema 中的任意文档)本示例创建一个可通过 /{lang}/landingPage 访问的多语言落地页。
在 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" }
]
}
为每种语言创建文档,例如 greeting/ko、greeting/en 和 greeting/ja,并分别填写对应的标题、副标题、CTA 文案和 CTA URL。
使用以下配置创建页面:
/{lang}/landingPagelang 映射到 language 组件{
"lang": "language"
}
为 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
添加一个捕获所有路径的路由。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 })}
/>
);
}
访问以下 URL 查看本地化内容:
| URL | 预期标题 |
|---|---|
/ko/landingPage | 欢迎 |
/en/landingPage | Welcome |
/ja/landingPage | 欢迎光临 |
用户访问 /ko/landingPage 时:
/{lang}/landingPage 模式meta.params.lang = "ko""ko" 存在于 language schema 中documents.get("greeting", meta.params.lang) 解析为韩语内容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) 获取文档时:
id 获取content.code 字段content.slug 字段title 字段因此,可以使用任意唯一标识符灵活地引用文档。