在 CMS 中编写 CEL 表达式的实用指南。
在 CMS 中编写 CEL 表达式的实用指南。
CEL(Common Expression Language,通用表达式语言)是内置于我们 CMS 的轻量级脚本语言。它允许你编写动态表达式,从文档中提取数据、读取 URL 参数,并即时计算数值。
当一个 CEL 脚本运行时,会发生以下事情:
您的脚本 引擎 结果
| | |
v v v
documents.get("article", "intro") --> 从数据库中获取 --> { headline: "欢迎", body: "..." }
.headline --> 提取该字段 --> "欢迎"
将 CEL 视为只读查询语言。它无法修改数据库中的任何内容——它只读取数据并返回计算结果。这使得它在 CMS 的任何位置使用都很安全。
每个 CEL 表达式都可以访问以下三类对象:
| 对象 | 功能说明 | 示例 |
|---|---|---|
documents | 从 CMS 中获取任意文档 | documents.get("country", "us") |
meta | 关于当前请求的信息(本地化、URL 参数) | meta.locale, meta.params.slug |
schema | 当前文档的字段定义 | schema.fields |
CEL 最强大的功能就是可以从 CMS 中的任何位置抓取文档。
语法: documents.get(schemaName, identifier)
假设你有一个存储为标识符 "welcome-post" 的 article 文档:
// 存储在 CMS 中:article / welcome-post
{
"headline": "欢迎来到我们的平台",
"author": "Sarah Chen",
"body": "我们很高兴地宣布...",
"tags": ["公告", "新闻"]
}
获取整个文档:
documents.get("article", "welcome-post")
返回:
{
"headline": "欢迎来到我们的平台",
"author": "Sarah Chen",
"body": "我们很高兴地宣布...",
"tags": ["公告", "新闻"]
}
只获取标题:
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" } })
返回:
[
{ "code": "us", "name": "美国", "flag": "US" }
]
你有一个 hero-block,需要展示来自 article 文档的标题。
你的文章文档(标识符:"homepage-hero"):
{
"headline": "更快构建,更聪明交付",
"subheadline": "专为开发者打造的现代 CMS"
}
在 hero 区块标题字段中的 CEL 脚本:
documents.get("article", "homepage-hero").headline
结果: Hero 区块显示 "更快构建,更聪明交付"
你正在构建 /countries/[code] 页面,需要展示完整的国家名称。
你的国家文档:
// country / us
{ "code": "us", "name": "美国", "flag": "US", "languages": ["en", "es"] }
// country / sa
{ "code": "sa", "name": "沙特阿拉伯", "flag": "SA", "languages": ["ar", "en"] }
CEL 脚本:
documents.get("country", meta.params.code).name
当有人访问 /countries/us:
meta.params.code = "us""美国"当有人访问 /countries/sa:
meta.params.code = "sa""沙特阿拉伯"根据用户的语言环境展示不同标题。
meta.locale == "ar-SA" ? "مرحبا بكم" : "Welcome"
如果语言环境为 "ar-SA": 返回 "مرحبا بكم"
如果语言环境为其他值: 返回 "Welcome"
你的 article 文档有一个 countryCode 字段,你想获取完整的国家名称。
文章文档:
{ "headline": "来自美国的新闻", "countryCode": "us" }
CEL 脚本:
documents.get("country", documents.get("article", "us-news").countryCode).name
执行过程:
documents.get("article", "us-news") 返回 { "headline": "来自美国的新闻", "countryCode": "us" }.countryCode 提取 "us"documents.get("country", "us") 返回 { "code": "us", "name": "美国", ... }.name 提取 "美国"结果: "美国"
如果某个文档可能不存在,可以提供回退值:
documents.get("article", meta.params.slug) != null
? documents.get("article", meta.params.slug).headline
: "未找到文章"
或者检查特定字段是否存在:
documents.get("article", "intro").author != null
? documents.get("article", "intro").author
: "未知作者"
你的文章包含标签,需要检查是否存在特定标签:
"公告" in documents.get("article", "welcome-post").tags
返回: 如果文章包含“公告”标签则为 true
获取第一个标签:
documents.get("article", "welcome-post").tags[0]
返回: "公告"(第一个标签)
统计标签数量:
size(documents.get("article", "welcome-post").tags)
返回: 2(标签数量)
参数化路由是构建动态本地化页面的关键。当你定义类似 /{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" } |
参数绑定: 每个路由参数都可以绑定到一个文档架构以便校验:
{
"pattern": "/{lang}/landingPage",
"param_bindings": {
"lang": "language"
}
}
这项绑定告诉 CMS:
lang 片段language 架构进行校验(查找 content.code 匹配的文档)路由配置:
/{lang}/landingPage/{lang}/landingPage{ "lang": "language" }你的问候文档:
// greeting / ko
{ "code": "ko", "headline": "환영합니다", "subheadline": "우리 플랫폼에 오신 것을 환영합니다" }
// greeting / en
{ "code": "en", "headline": "Welcome", "subheadline": "Welcome to our platform" }
// greeting / ja
{ "code": "ja", "headline"
用于提取本地化内容的 CEL 脚本:
documents.get("greeting", meta.params.lang).headline
解析方式:
| URL | meta.params.lang | 结果 |
|---|---|---|
/ko/landingPage | "ko" | "환영합니다" |
/en/landingPage | "en" | "Welcome" |
/ja/landingPage | "ja" | "ようこそ" |
对于 /{country}/{lang}/products 这样的路由:
路由配置:
{
"pattern": "/{country}/{lang}/products",
"param_bindings": {
"country": "country",
"lang": "language"
}
}
CEL 脚本:
// 获取国家名称
documents.get("country", meta.params.country).name
// 根据国家获取本地化产品列表
documents.find("product", { "where": { "country": meta.params.country } })
// 组合:以用户语言展示国家特定的问候语
documents.get("greeting", meta.params.lang).headline + " 来自 " + documents.get("country", meta.params.country).name
校验级联:
CMS 会按层级验证参数。对于 /{country}/{lang} 路由:
country 参数与 country 架构校验lang 参数与 language 架构校验lang 是否存在于 country.languages[] 数组中(层级校验)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.params.lang |
| 基于位置的访问 | meta.segments[0] |
| 获取路径深度 | size(meta.segments) |
| 检查路径是否包含某段 | "admin" in meta.segments |
// 获取第一个片段(通常为语言代码)
meta.segments[0]
// 检查路径深度
size(meta.segments) > 2 ? "深" : "浅"
// 检查是否处于管理区域
"admin" in meta.segments ? "管理模式" : "公共模式"
// 回退:如果参数未绑定则使用片段
has(meta.params.lang) ? meta.params.lang : meta.segments[0]
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 | 当前文档标题 |
语言环境代码遵循 BCP 47(语言-地区)格式:
// 检查语言环境是否为 RTL 语言
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"
// 仅获取语言部分
meta.locale.split("-")[0] // 不支持 - 请改用 meta.params.lang
路由参数始终是字符串。CMS 会在求值前用绑定的架构进行校验:
// 访问命名参数
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)
原始 URL 片段数组:
// 按索引访问(从 0 开始)
meta.segments[0] // 第一个片段
meta.segments[1] // 第二个片段
// 检查长度
size(meta.segments) // 片段数量
// 检查成员关系
"products" in meta.segments // 路径中是否包含 "products"?
当前文档的 UUID,用于自引用脚本:
// 仅在编辑现有文档时可用
meta.docId != null ? "正在编辑" : "新建"
// 在条件逻辑中使用
meta.docId != null ? documents.get("article", meta.docId).status : "草稿"
当前文档标题:
// 用于显示
"正在编辑:" + meta.title
// 根据标题进行条件判断
meta.title.contains("草稿") ? "进行中" : "已发布"
当架构已知而标识符是动态的时,可使用更简洁的语法:
// 传统写法
documents.get("airports", meta.params.code).name
// 使用 ref() —— 将架构与动态标识符分离
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) // 链式查找
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 // 当前文档标题
// 比较
== != < <= > >=
// 逻辑
&& || !
// 三元(if-else)
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) // 检查属性是否存在
如果出现问题,你会看到下列错误之一:
| 错误 | 含义 |
|---|---|
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)以下演练将创建一个可通过 /{lang}/landingPage 访问的多语言落地页。
在 CMS 管理界面中,创建名为 greeting 的自定义架构:
{
"name": "greeting",
"fields": [
{ "name": "code", "type": "string", "required": true },
{ "name": "headline", "type": "string", "required": true },
{ "name": "subheadline"
为每种语言创建文档:
文档:greeting / ko
{
"code": "ko",
"headline": "환영합니다",
"subheadline": "우리 플랫폼에 오신 것을 환영합니다",
"ctaText": "시작하기",
"ctaUrl": "/ko/get-started"
}
文档:greeting / en
{
"code": "en",
"headline": "Welcome",
"subheadline": "Welcome to our platform",
"ctaText": "Get Started",
"ctaUrl": "/en/get-started"
}
文档:greeting / ja
{
"code": "ja",
"headline": "ようこそ",
"subheadline": "私たちのプラットフォームへようこそ",
"ctaText": "始める",
"ctaUrl": "/ja/get-started"
}
创建如下配置的路由:
/{lang}/landingPage/{lang}/landingPage {
"lang": "language"
}
在该路由中添加一个 hero 区块,并为各字段编写如下 CEL 脚本:
标题字段:
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
在 Next.js 应用中创建一个捕获所有路由:
// app/[...slug]/page.tsx
import { getCmsClient } from '@repo/renderer';
interface PageProps {
params: { slug: string[] };
}
export default async function Page({ params }: PageProps) {
访问以下 URL 查看本地化内容:
| URL | 预期标题 |
|---|---|
/ko/landingPage | 환영합니다 |
/en/landingPage | Welcome |
/ja/landingPage | ようこそ |
当用户访问 /ko/landingPage 时:
/{lang}/landingPage 模式meta.params.lang = "ko"language 架构中documents.get("greeting", meta.params.lang) 之类的脚本解析为韩语内容interface CelMeta {
/** 当前语言环境代码(例如 'en-US') */
locale: string;
/** 从 URL 中提取的路由参数 */
params: Record<string, string>;
/** URL 路径片段 */
segments: string[];
/** 当前文档 ID(如果正在编辑现有文档) */
docId
extractParams 函数处理 URL 路径:
Pattern: /{country}/{lang}/products
Path: /us/en/products
Algorithm:
1. 规范化二者(移除末尾斜杠)
2. 分割为片段:["us", "en", "products"] 与 ["{country}", "{lang}", "products"]
3. 匹配片段数量(必须相等)
4. 对每一对片段:
// 简单绑定(使用 "code" 字段进行查找)
{ "lang": "language" }
// 详细绑定(自定义 slug 字段)
{
"lang": {
"schemaName": "language",
"slugField": "code"
},
"slug": {
"schemaName": "article",
"slugField": "slug"
}
}
通过 documents.get(schema, identifier) 抓取时:
id 抓取content.code 字段content.slug 字段title 字段这允许你使用任意唯一标识符灵活引用文档。