Практическое руководство по написанию выражений CEL в CMS.
Практическое руководство по написанию выражений CEL в CMS.
CEL (Common Expression Language) — это лёгкий язык сценариев, встроенный в нашу CMS. Он позволяет писать динамические выражения, получать данные из документов, читать параметры URL и вычислять значения на лету.
Что происходит при выполнении скрипта CEL:
Ваш скрипт Движок Результат
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": "Welcome to Our Platform",
"author": "Sarah Chen",
"body": "We're excited to announce...",
"tags": ["announcement", "news"]
}
Получить весь документ:
documents.get("article", "welcome-post")
Результат:
{
"headline": "Welcome to Our Platform",
"author": "Sarah Chen",
"body": "We're excited to announce...",
"tags": ["announcement", "news"]
}
Получить только заголовок:
documents.get("article", "welcome-post").headline
Результат: "Welcome to Our Platform"
Получить автора:
documents.get("article", "welcome-post").author
Результат: "Sarah Chen"
Если на странице используются динамические маршруты (например, /articles/[slug]), можно использовать meta.params, чтобы получить параметр URL и загрузить нужный документ.
Если пользователь открывает /articles/welcome-post:
documents.get("article", meta.params.slug).headline
Результат: "Welcome to Our Platform"
Так создаются динамические страницы: один и тот же скрипт CEL работает для любой статьи, используя значение slug из URL.
Синтаксис: documents.find(schemaName) или documents.find(schemaName, filter)
// Получить все страны
documents.find("country")
Результат:
[
{ "code": "us", "name": "United States", "flag": "US" },
{ "code": "sa", "name": "Saudi Arabia", "flag": "SA" },
{ "code": "gb", "name": "United Kingdom", "flag": "GB" }
]
// Получить страны с фильтром
documents.find("country", { "where": { "code": "us" } })
Результат:
[
{ "code": "us", "name": "United States", "flag": "US" }
]
CEL поддерживает получение переведённого содержимого документов двумя способами: автоматический перевод на основе локали и явный поиск перевода.
Когда задан meta.locale (например, из параметров маршрута или настроек пользователя), documents.get() автоматически объединяет переведённое содержимое:
// Если meta.locale равно "fr", возвращает французский перевод,
// объединённый с базовым документом
documents.get("greeting", "welcome").headline
Как это работает:
meta.locale не равно "en" или "en-US", перевод ищется в таблице translations.{ ...baseContent, ...translatedContent }.Переведённые поля имеют приоритет над базовыми, а для непереведённых полей используются значения базового документа.
Если нужно получить перевод на конкретный язык независимо от текущей локали:
Синтаксис: 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
Документы приветствия с переводами:
// Базовый документ: greeting / welcome
{ "headline": "Welcome", "subheadline": "Welcome to our platform" }
// Перевод (язык: "fr")
{ "headline": "Bienvenue", "subheadline": "Bienvenue sur notre plateforme" }
// Перевод (язык: "es")
{ "headline": "Bienvenido", "subheadline": "Bienvenido a nuestra plataforma" }
Скрипты CEL:
// При meta.locale = "fr"
documents.get("greeting", "welcome").headline
// Возвращает: "Bienvenue"
// Явный испанский перевод
documents.translated("greeting", "welcome", "es").headline
// Возвращает: "Bienvenido"
// Шаблон с резервным значением для отсутствующего перевода
documents.translated("greeting", "welcome", meta.params.lang) != null
? documents.translated("greeting", "welcome", meta.params.lang).headline
: documents.get("greeting", "welcome").headline
У вас есть hero-block, который должен показывать заголовок из документа article.
Документ статьи (идентификатор: "homepage-hero"):
{
"headline": "Build Faster, Ship Smarter",
"subheadline": "The modern CMS for developers"
}
Скрипт CEL в поле заголовка hero-блока:
documents.get("article", "homepage-hero").headline
Результат: hero-блок отображает "Build Faster, Ship Smarter".
Вы создаёте страницу /countries/[code] и хотите отображать полное название страны.
Документы стран:
// country / us
{ "code": "us", "name": "United States", "flag": "US", "languages": ["en", "es"] }
// country / sa
{ "code": "sa", "name": "Saudi Arabia", "flag": "SA", "languages": ["ar", "en"] }
Скрипт CEL:
documents.get("country", meta.params.code).name
При открытии /countries/us:
meta.params.code = "us";"United States".При открытии /countries/sa:
meta.params.code = "sa";"Saudi Arabia".Показывайте разные заголовки в зависимости от локали пользователя.
meta.locale == "ar-SA" ? "Welcome, everyone" : "Welcome"
Если локаль "ar-SA": возвращается "Welcome, everyone".
При любой другой локали: возвращается "Welcome".
У документа article есть поле countryCode, и вы хотите получить полное название страны.
{ "headline": "News from the US", "countryCode": "us" }
documents.get("country", documents.get("article", "us-news").countryCode).name
Что происходит:
documents.get("article", "us-news") возвращает статью..countryCode извлекает "us".documents.get("country", "us") возвращает страну..name извлекает "United States".Результат: "United States".
Если документ может отсутствовать, укажите резервное значение:
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
Результат: 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 из URL;language;Конфигурация маршрута:
/{lang}/landingPage;/{lang}/landingPage;{ "lang": "language" }.Скрипт CEL:
documents.get("greeting", meta.params.lang).headline
Он загружает содержимое для языка из URL.
Для маршрута /{country}/{lang}/products:
{
"pattern": "/{country}/{lang}/products",
"param_bindings": {
"country": "country",
"lang": "language"
}
}
// Получить название страны
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
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 в виде массива:
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) // Цепочка поиска
documents.translated("schema", "id", "fr") // Получить перевод для указанной локали
meta.locale // "en-US", "ar-SA" и т. д.
meta.params.xyz // Параметр URL с именем "xyz"
meta.segments // Путь URL как массив
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.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) перед обращением к параметру.Этот раздел посвящён продвинутым шаблонам связывания документов и построения реляционных структур содержимого.
Самый простой вариант: один документ ссылается на другой по идентификатору.
// Статья хранит ID автора; получить имя автора
documents.get("author", documents.get("article", "intro").authorId).name
// Традиционный вариант
documents.get("country", documents.get("airport", meta.params.code).countryCode).name
// С использованием ref()
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
Каждый вызов documents.get(), documents.find() и documents.ref().get() отслеживается для инвалидации кэша. Когда связанный документ изменяется, CMS знает, какие выражения CEL нужно вычислить заново.
Отслеживаемые зависимости:
schema:identifier — зависимость от конкретного документа;schema:identifier — то же, но через цепочный синтаксис;schema:* — зависимость на уровне схемы.В этом руководстве создаётся многоязычная целевая страница, доступная по адресу /{lang}/landingPage.
В административной панели CMS создайте пользовательскую схему greeting:
{
"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 с соответствующими полями headline, subheadline, ctaText и ctaUrl.
Создайте страницу со следующей конфигурацией:
/{lang}/landingPage;lang → компонент language.{
"lang": "language"
}
Добавьте hero-блок и используйте следующие выражения:
documents.get("greeting", meta.params.lang).headline
documents.get("greeting", meta.params.lang).subheadline
documents.get("greeting", meta.params.lang).ctaText
documents.get("greeting", meta.params.lang).ctaUrl
Добавьте catch-all-маршрут. ParametricRoutePage разрешает страницу, извлекает meta.params из URL, вычисляет привязки 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.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. Разделить на сегменты
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.Это позволяет гибко ссылаться на документы по любому уникальному идентификатору.