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

Скриптинг в конструкторе шаблонов

Практическое руководство по написанию выражений CEL в CMS.

Continue Reading
Previous‹Статическая отрисовка с поддержкой режима редактированияNextCreate Profound Next›

Гибрид

Проект РендерераПараметрическая МаршрутизацияТипы КомпонентовSseНастройка Прокси Админ ПанелиСтатическая отрисовка с поддержкой режима редактированияСкриптинг в конструкторе шаблоновCreate Profound Next

Безголовый

Быстрый стартJson И Claude КодКомпонент Zod Pull

Mcp

Mcp

Возможности CMS

Feat Docs TemplateFeat Конструктор ШаблоновFeat ПереводчикFeat Организация

Мотивация

Наш подход

Терминология

Гибрид Против Headless

Практическое руководство по написанию выражений CEL в CMS.


Как работает CEL

CEL (Common Expression Language) — это лёгкий скриптовый язык, встроенный в нашу CMS. Он позволяет писать динамические выражения, которые могут извлекать данные из документов, считывать параметры URL и вычислять значения на лету.

Вот что происходит при выполнении скрипта CEL:

Ваш скрипт                      Движок                        Результат
    |                              |                              |
    v                              v                              v
documents.get("article", "intro") --> Извлекает из базы данных --> { headline: "Добро пожаловать", body: "..." }
         .headline                --> Извлекает поле           --> "Добро пожаловать"

Рассматривайте CEL как язык запросов только для чтения. Он не может ничего изменять в базе данных — он просто читает данные и возвращает вычисленный результат. Это делает его безопасным для использования в любом месте CMS.


Основные строительные блоки

Каждое выражение CEL имеет доступ к трём сущностям:

ОбъектЧто этоПример
documentsПолучение любого документа из CMSdocuments.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

Возвращает: "Сара Чен"


Использование параметров URL

Когда у вашей страницы есть динамические маршруты (например, /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" }
]

Примеры из реальной практики

Пример 1: заголовок блока hero из другого документа

У вас есть hero-block, который должен отображать заголовок, взятый из документа article.

Ваш документ статьи (идентификатор: "homepage-hero"):

{
  "headline": "Создавайте быстрее, публикуйте умнее",
  "subheadline": "Современная CMS для разработчиков"
}

Скрипт CEL в поле заголовка hero-блока:

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: работа со списками

У вашей статьи есть теги, и вы хотите проверить наличие конкретного тега:

"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 (количество тегов)


Параметрические маршруты и 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. Извлечь сегмент lang из URL
  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 + " from " + 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 в виде массива, что полезно, когда нужен позиционный доступ без именованных параметров.

Как это работает:

Путь URLmeta.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 ? "deep" : "shallow"

// Проверить, находимся ли мы в админ-разделе
"admin" in meta.segments ? "admin mode" : "public mode"

// Запасной вариант: использовать сегмент, если параметр не привязан
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 \| nullUUID текущего документа (null для новых документов)
meta.titlestringЗаголовок текущего документа

meta.locale

Код локали следует формату BCP 47 (язык-регион):

// Проверить локаль для языков с письмом справа налево
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 ? "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() — последовательные выборки

Для более чистого синтаксиса, когда схема известна, а идентификатор динамический:

// Традиционный подход
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
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"])

Эти возможности будут добавлены через систему зарегистрированных функций с сохранением обратной совместимости с существующими скриптами.


Советы

  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

Поле URL CTA:

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 {
  /** 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):

  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>
);
}
docId
:
string
|
null
;
/** Current document title */
title: string;
}
3. Match segment counts (must be equal)
4. For each segment pair:
- If pattern starts with : or {}, extract as param
- Otherwise, must match exactly
5. Return: { country: "us", lang: "en" }
}