Практическое руководство по написанию выражений CEL в CMS.
Практическое руководство по написанию выражений CEL в CMS.
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)
Предположим, у вас есть документ article, сохранённый с идентификатором "welcome-post":
// Сохранено в CMS как: article / welcome-post
{
"headline": "Добро пожаловать на нашу платформу",
"author": "Сара Чен",
"body": "Мы рады объявить...",
"tags": ["анонс", "новости"]
}
Чтобы получить весь документ:
documents.get("article", "welcome-post")
Возвращает:
{
"headline": "Добро пожаловать на нашу платформу",
"author": "Сара Чен",
"body": "Мы рады объявить...",
"tags": ["анонс", "новости"]
}
Чтобы получить только заголовок:
documents.get("article", "welcome-post").headline
Возвращает: "Добро пожаловать на нашу платформу"
Чтобы получить автора:
documents.get("article", "welcome-post").author
Возвращает: "Сара Чен"
Когда у вашей страницы есть динамические маршруты (например, /articles/[slug]), можно использовать meta.params, чтобы получить параметр URL и выбрать правильный документ.
Если кто-то посещает /articles/welcome-post:
documents.get("article", meta.params.slug).headline
Возвращает: "Добро пожаловать на нашу платформу"
Так вы создаёте динамические страницы: один и тот же скрипт CEL работает для любой статьи, используя любой slug из URL.
Синтаксис: 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 для разработчиков"
}
Скрипт CEL в поле заголовка hero-блока:
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
: "Неизвестный автор"
У вашей статьи есть теги, и вы хотите проверить наличие конкретного тега:
"featured" in documents.get("article", "welcome-post").tags
Возвращает: true, если у статьи есть тег "featured"
Получить первый тег:
documents.get("article", "welcome-post").tags[0]
Возвращает: "announcement" (первый тег)
Посчитать количество тегов:
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 из URLlanguage (искать документ, где 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 + " from " + documents.get("country", meta.params.country).name
Каскад валидации:
CMS проверяет параметры иерархически. Для маршрутов /{country}/{lang}:
country по схеме countrylang по схеме languagelang содержится в массиве 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 ? "deep" : "shallow"
// Проверить, находимся ли мы в админ-разделе
"admin" in meta.segments ? "admin mode" : "public mode"
// Запасной вариант: использовать сегмент, если параметр не привязан
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 (язык-регион):
// Проверить локаль для языков с письмом справа налево
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 ? "editing" : "creating new"
// Использовать в условной логике
meta.docId != null ? documents.get("article", meta.docId).status : "draft"
Заголовок текущего документа:
// Использовать для отображения
"Editing: " + meta.title
// Условие на основе заголовка
meta.title.contains("Draft") ? "work in progress" : "published"
Для более чистого синтаксиса, когда схема известна, а идентификатор динамический:
// Традиционный подход
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 // Параметр URL с именем "xyz"
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.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
Поле URL CTA:
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}/landingPagemeta.params.lang = "ko"languagedocuments.get("greeting", meta.params.lang) возвращают корейский контентinterface CelMeta {
/** Current locale code (e.g., 'en-US') */
locale: string;
/** Route parameters extracted from URL */
params: Record<string, string>;
/** URL path segments */
segments: string[];
/** Current document ID (if editing existing document) */
Функция extractParams обрабатывает пути URL:
Pattern: /{country}/{lang}/products
Path: /us/en/products
Algorithm:
1. Normalize both (remove trailing slashes)
2. Split into segments: ["us", "en", "products"] and ["{country}", "{lang}", "products"]
// Простая привязка (использует поле "code" для поиска)
{ "lang": "language" }
// Подробная привязка (пользовательское поле slug)
{
"lang": {
"schemaName": "language",
"slugField": "code"
},
"slug": {
"schemaName": "article",
"slugField": "slug"
}
При получении через documents.get(schema, identifier):
idcontent.codecontent.slugtitleЭто позволяет гибко ссылаться на документы по любому уникальному идентификатору.