All Systems Operational
Powered By
profound-logo
profound-logoProfound CMS
⌘K
Admin

模板构建器中的脚本编写

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

Continue Reading
Previous‹支持编辑模式的静态渲染NextCreate Profound Next›

混合

渲染器项目参数化路由组件类型SSE设置管理面板代理支持编辑模式的静态渲染模板构建器中的脚本编写Create Profound Next

无头

快速入门Json 与 Claude 代码组件 Zod 拉取

Mcp

Mcp

Cms 功能

功能文档模板模板构建器功能翻译功能组织功能

激励

我们的方法

术语

混合式与无头式对比

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


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"


使用 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" } })

返回:

[
  { "code": "us", "name": "美国", "flag": "US" }
]

实际案例

示例 1:来自其他文档的 Hero 区块标题

你有一个 hero-block,需要展示来自 article 文档的标题。

你的文章文档(标识符:"homepage-hero"):

{
  "headline": "更快构建,更聪明交付",
  "subheadline": "专为开发者打造的现代 CMS"
}

在 hero 区块标题字段中的 CEL 脚本:

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

结果: Hero 区块显示 "更快构建,更聪明交付"


示例 2:通过代码获取国家名称

你正在构建 /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"
  • 结果:"沙特阿拉伯"

示例 3:基于语言环境的条件内容

根据用户的语言环境展示不同标题。

meta.locale == "ar-SA" ? "مرحبا بكم" : "Welcome"

如果语言环境为 "ar-SA": 返回 "مرحبا بكم" 如果语言环境为其他值: 返回 "Welcome"


示例 4:链式文档查找

你的 article 文档有一个 countryCode 字段,你想获取完整的国家名称。

文章文档:

{ "headline": "来自美国的新闻", "countryCode": "us" }

CEL 脚本:

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

执行过程:

  1. documents.get("article", "us-news") 返回 { "headline": "来自美国的新闻", "countryCode": "us" }
  2. .countryCode 提取 "us"
  3. documents.get("country", "us") 返回 { "code": "us", "name": "美国", ... }
  4. .name 提取 "美国"

结果: "美国"


示例 5:回退值

如果某个文档可能不存在,可以提供回退值:

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
  : "未知作者"

示例 6:处理列表

你的文章包含标签,需要检查是否存在特定标签:

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

返回: 如果文章包含“公告”标签则为 true

获取第一个标签:

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

返回: "公告"(第一个标签)

统计标签数量:

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

返回: 2(标签数量)


参数化路由与 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" }

参数绑定: 每个路由参数都可以绑定到一个文档架构以便校验:

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

这项绑定告诉 CMS:

  1. 从 URL 中提取 lang 片段
  2. 根据 language 架构进行校验(查找 content.code 匹配的文档)
  3. 若校验通过,在解析参数中提供完整文档

示例:基于语言的落地页

路由配置:

  • 路径:/{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

解析方式:

URLmeta.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} 路由:

  1. 将 country 参数与 country 架构校验
  2. 将 lang 参数与 language 架构校验
  3. 可选地验证 lang 是否存在于 country.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 的示例

// 获取第一个片段(通常为语言代码)
meta.segments[0]

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

// 检查是否处于管理区域
"admin" in meta.segments ? "管理模式" : "公共模式"

// 回退:如果参数未绑定则使用片段
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(语言-地区)格式:

// 检查语言环境是否为 RTL 语言
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"

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

meta.params

路由参数始终是字符串。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)

meta.segments

原始 URL 片段数组:

// 按索引访问(从 0 开始)
meta.segments[0]           // 第一个片段
meta.segments[1]           // 第二个片段

// 检查长度
size(meta.segments)        // 片段数量

// 检查成员关系
"products" in meta.segments  // 路径中是否包含 "products"?

meta.docId

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

// 仅在编辑现有文档时可用
meta.docId != null ? "正在编辑" : "新建"

// 在条件逻辑中使用
meta.docId != null ? documents.get("article", meta.docId).status : "草稿"

meta.title

当前文档标题:

// 用于显示
"正在编辑:" + meta.title

// 根据标题进行条件判断
meta.title.contains("草稿") ? "进行中" : "已发布"

documents.ref() —— 链式查找

当架构已知而标识符是动态的时,可使用更简洁的语法:

// 传统写法
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 调用外部服务
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() —— 语法更清晰

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

以下演练将创建一个可通过 /{lang}/landingPage 访问的多语言落地页。

第 1 步:创建 Greeting 文档架构

在 CMS 管理界面中,创建名为 greeting 的自定义架构:

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

第 2 步:创建 Greeting 文档

为每种语言创建文档:

文档: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"
}

第 3 步:创建路由

创建如下配置的路由:

  • 路径:/{lang}/landingPage
  • 模式:/{lang}/landingPage
  • 状态:Live
  • 参数绑定:
  {
    "lang": "language"
  }

第 4 步:在 CEL 脚本中添加区块

在该路由中添加一个 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

第 5 步:在 Next.js 中消费

在 Next.js 应用中创建一个捕获所有路由:

// app/[...slug]/page.tsx
import { getCmsClient } from '@repo/renderer';

interface PageProps {
  params: { slug: string[] };
}

export default async function Page({ params }: PageProps) {

第 6 步:测试路由

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

URL预期标题
/ko/landingPage환영합니다
/en/landingPageWelcome
/ja/landingPageようこそ

解析流程

当用户访问 /ko/landingPage 时:

  1. 路由匹配:CMS 匹配 /{lang}/landingPage 模式
  2. 参数提取:meta.params.lang = "ko"
  3. 校验:CMS 校验 "ko" 是否存在于 language 架构中
  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

参数提取算法

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) 抓取时:

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

这允许你使用任意唯一标识符灵活引用文档。

:
"ようこそ"
,
"subheadline"
:
"私たちのプラットフォームへようこそ"
}
,
"type"
:
"string"
},
{ "name": "ctaText", "type": "string" },
{ "name": "ctaUrl", "type": "string" }
]
}
const
client
=
getCmsClient
({
cmsUrl: process.env.CMS_URL!,
apiKey: process.env.CMS_API_KEY!,
websiteId: process.env.CMS_WEBSITE_ID!,
});
const path = '/' + params.slug.join('/');
// 获取路由及解析后的参数
const { route, resolvedParams } = await client.routes.getRouteByPath.query({
websiteId: process.env.CMS_WEBSITE_ID!,
path,
});
// 获取该路由的区块
const blocks = await client.blocks.getBlocks.query({
websiteId: process.env.CMS_WEBSITE_ID!,
blockIds: route.block_ids,
// 为 CEL 上下文传入解析参数
context: {
meta: {
locale: resolvedParams?.lang?.document?.content?.code ?? 'en',
params: Object.fromEntries(
Object.entries(resolvedParams ?? {}).map(([k, v]) => [k, v.value])
),
segments: params.slug,
docId: null,
title: route.title ?? '',
},
schema: {},
},
});
// 渲染区块
return (
<main>
{blocks.map((block) => (
<BlockRenderer
key={block.id}
block={block}
routeParams={resolvedParams}
language={resolvedParams?.lang?.value}
/>
))}
</main>
);
}
:
string
|
null
;
/** 当前文档标题 */
title: string;
}
- 如果模式以 : 或 {} 开头,则提取为参数
- 否则必须完全匹配
5. 返回:{ country: "us", lang: "en" }