profound-logoProfound CMS
⌘K
Admin
Theme
DocsTutorialБлогPhilosophy
DocsTutorialБлогPhilosophy

Hybrid

Collection of Pages with ComponentsTypes of ComponentsSetup server sent events (SSE) content refetchInstall Profound CMS as a proxyНаписание скриптов в конструкторе шаблоновProject ScaffoldingМедиатека

Без интерфейса

Быстрый стартSplit Screen JSON Component Builder with LLMComponent Zod Pull

REST API

Обзор REST APIgetConnect your websitegetПолучить маршрутыgetGET /routegetПолучение блоковgetGET /blocks/with-cel-cachegetGET /blocks/generatedgetПолучить компонентыgetGET /components/{name}getGET /dataset/{schema_name}getGET /content-changes (SSE)patchPATCH /dataset/{schema_name}postPOST /translationpatchPATCH /translationsgetGET /usagepostPOST /csvpatchPATCH /csv
All Systems Operational
Powered Byprofound-logo
Theme

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

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

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


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

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

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

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

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


Основные элементы

Каждому выражению CEL доступны три объекта:

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


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

Если на странице используются динамические маршруты (например, /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

Когда задан meta.locale (например, из параметров маршрута или настроек пользователя), documents.get() автоматически объединяет переведённое содержимое:

// Если meta.locale равно "fr", возвращает французский перевод,
// объединённый с базовым документом
documents.get("greeting", "welcome").headline

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

  1. Загружается содержимое базового документа.
  2. Если meta.locale не равно "en" или "en-US", перевод ищется в таблице translations.
  3. Переведённые поля объединяются с базовым содержимым: { ...baseContent, ...translatedContent }.

Переведённые поля имеют приоритет над базовыми, а для непереведённых полей используются значения базового документа.

Явный перевод через documents.translated()

Если нужно получить перевод на конкретный язык независимо от текущей локали:

Синтаксис: 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

Практические примеры

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

У вас есть 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".


Пример 2: Название страны по коду

Вы создаёте страницу /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".

Пример 3: Условное содержимое на основе локали

Показывайте разные заголовки в зависимости от локали пользователя.

meta.locale == "ar-SA" ? "Welcome, everyone" : "Welcome"

Если локаль "ar-SA": возвращается "Welcome, everyone". При любой другой локали: возвращается "Welcome".


Пример 4: Цепочка поиска документов

У документа article есть поле countryCode, и вы хотите получить полное название страны.

{ "headline": "News from the US", "countryCode": "us" }
documents.get("country", documents.get("article", "us-news").countryCode).name

Что происходит:

  1. documents.get("article", "us-news") возвращает статью.
  2. .countryCode извлекает "us".
  3. documents.get("country", "us") возвращает страну.
  4. .name извлекает "United States".

Результат: "United States".


Пример 5: Резервные значения

Если документ может отсутствовать, укажите резервное значение:

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"

Пример 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;
  3. при успешной проверке делает полный документ доступным в разрешённых параметрах.

Пример: целевая страница на основе языка

Конфигурация маршрута:

  • путь: /{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

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[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 в виде массива:

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)     // Цепочка поиска
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 Server

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() для динамических идентификаторов — это более понятный синтаксис при статической схеме и динамическом идентификаторе.
  8. Используйте doc.fieldName для самоссылок — обращайтесь к полям текущего документа в вычисляемых выражениях.

Ссылки между документами

Этот раздел посвящён продвинутым шаблонам связывания документов и построения реляционных структур содержимого.

Базовый шаблон ссылки

Самый простой вариант: один документ ссылается на другой по идентификатору.

// Статья хранит ID автора; получить имя автора
documents.get("author", documents.get("article", "intro").authorId).name

Цепочки поиска с documents.ref()

// Традиционный вариант
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 нужно вычислить заново.

Отслеживаемые зависимости:

  • get: schema:identifier — зависимость от конкретного документа;
  • ref: schema:identifier — то же, но через цепочный синтаксис;
  • query: schema:* — зависимость на уровне схемы.

Рекомендации по ссылкам

  1. Минимизируйте глубину цепочек — каждый уровень увеличивает задержку и количество запросов.
  2. Кэшируйте промежуточные результаты — если вложенное значение нужно дважды, получите родительский документ один раз.
  3. Используйте проверки на null — ссылки могут перестать работать после удаления документов.
  4. Предпочитайте коды UUID — коды легче читать в выражениях и они стабильнее между окружениями.
  5. Следите за лимитами запросов — сложные цепочки быстро достигают ограничения в 50 запросов.

Приложение A: полный пример параметрического маршрута

В этом руководстве создаётся многоязычная целевая страница, доступная по адресу /{lang}/landingPage.

Шаг 1: создание схемы документа приветствия

В административной панели 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" }
  ]
}

Шаг 2: создание документов приветствия

Создайте документы для каждого языка. Например, greeting/ko, greeting/en и greeting/ja с соответствующими полями headline, subheadline, ctaText и ctaUrl.

Шаг 3: создание страницы

Создайте страницу со следующей конфигурацией:

  • Путь/шаблон: /{lang}/landingPage;
  • Состояние: опубликовано;
  • Сопоставление динамических сегментов: lang → компонент language.
{
  "lang": "language"
}

Шаг 4: добавление блоков со скриптами CEL

Добавьте 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

Шаг 5: использование в Next.js

Добавьте 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 })}
    />
  );
}

Шаг 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: 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) используются следующие варианты:

  1. Совпадение UUID: если идентификатор является корректным UUID, поиск выполняется по id.
  2. Поле code: проверяется поле content.code.
  3. Поле slug: проверяется поле content.slug.
  4. Совпадение заголовка: проверяется поле title.

Это позволяет гибко ссылаться на документы по любому уникальному идентификатору.

Continue Reading
Previous‹Install Profound CMS as a proxyNextProject Scaffolding›