Una guía práctica para escribir expresiones CEL en el CMS.
Una guía práctica para escribir expresiones CEL en el CMS.
CEL (Common Expression Language) es un lenguaje de scripting ligero integrado en nuestro CMS. Te permite escribir expresiones dinámicas que pueden extraer datos de documentos, leer parámetros de URL y calcular valores al vuelo.
Esto es lo que ocurre cuando se ejecuta un script CEL:
Tu script El motor Resultado
| | |
v v v
documents.get("article", "intro") --> Obtiene de la base de datos --> { headline: "Bienvenida", body: "..." }
.headline --> Extrae el campo --> "Bienvenida"
Piensa en CEL como un lenguaje de consultas de solo lectura. No puede modificar nada en la base de datos, solo lee datos y devuelve un resultado calculado. Esto lo hace seguro para usarlo en cualquier parte del CMS.
Toda expresión CEL tiene acceso a tres cosas:
| Objeto | Qué es | Ejemplo |
|---|---|---|
documents | Obtiene cualquier documento del CMS | documents.get("country", "us") |
meta | Información sobre la solicitud actual (idioma, parámetros de URL) | meta.locale, meta.params.slug |
schema | Las definiciones de campos del documento actual | schema.fields |
La característica más potente de CEL es obtener documentos desde cualquier lugar de tu CMS.
Sintaxis: documents.get(schemaName, identifier)
Supongamos que tienes un documento article almacenado con el identificador "welcome-post":
// Almacenado en el CMS como: article / welcome-post
{
"headline": "Bienvenido a nuestra plataforma",
"author": "Sarah Chen",
"body": "Estamos emocionados de anunciar...",
"tags": ["anuncio", "noticias"]
}
Para obtener el documento completo:
documents.get("article", "welcome-post")
Devuelve:
{
"headline": "Bienvenido a nuestra plataforma",
"author": "Sarah Chen",
"body": "Estamos emocionados de anunciar...",
"tags": ["anuncio", "noticias"]
}
Para obtener solo el titular:
documents.get("article", "welcome-post").headline
Devuelve: "Bienvenido a nuestra plataforma"
Para obtener el autor:
documents.get("article", "welcome-post").author
Devuelve: "Sarah Chen"
Cuando tu página tiene rutas dinámicas (como /articles/[slug]), puedes usar meta.params para obtener el parámetro de la URL y recuperar el documento correcto.
Si alguien visita /articles/welcome-post:
documents.get("article", meta.params.slug).headline
Devuelve: "Bienvenido a nuestra plataforma"
Así es como creas páginas dinámicas: el mismo script CEL funciona para cualquier artículo, simplemente usando el slug que haya en la URL.
Sintaxis: documents.find(schemaName) o documents.find(schemaName, filter)
// Obtener todos los países
documents.find("country")
Devuelve:
[
{ "code": "us", "name": "Estados Unidos", "flag": "US" },
{ "code": "sa", "name": "Arabia Saudita", "flag": "SA" },
{ "code": "gb", "name": "Reino Unido", "flag": "GB" }
// Obtener países con un filtro
documents.find("country", { "where": { "code": "us" } })
Devuelve:
[
{ "code": "us", "name": "Estados Unidos", "flag": "US" }
]
Tienes un hero-block que debe mostrar un titular tomado de un documento article.
Tu documento de artículo (identificador: "homepage-hero"):
{
"headline": "Crea más rápido, entrega con inteligencia",
"subheadline": "El CMS moderno para desarrolladores"
}
Script CEL en el campo de título del bloque hero:
documents.get("article", "homepage-hero").headline
Resultado: El bloque hero muestra "Crea más rápido, entrega con inteligencia"
Estás creando una página en /countries/[code] y quieres mostrar el nombre completo del país.
Tus documentos de país:
// country / us
{ "code": "us", "name": "Estados Unidos", "flag": "US", "languages": ["en", "es"] }
// country / sa
{ "code": "sa", "name": "Arabia Saudita", "flag": "SA", "languages": ["ar", "en"
Script CEL:
documents.get("country", meta.params.code).name
Cuando alguien visita /countries/us:
meta.params.code = "us""Estados Unidos"Cuando alguien visita /countries/sa:
meta.params.code = "sa""Arabia Saudita"Muestra titulares diferentes dependiendo del locale del usuario.
meta.locale == "ar-SA" ? "مرحبا بكم" : "Welcome"
Si el locale es "ar-SA": Devuelve "مرحبا بكم"
Si el locale es cualquier otro: Devuelve "Welcome"
Tu article tiene un campo countryCode y quieres obtener el nombre completo del país.
Documento de artículo:
{ "headline": "Noticias desde EE. UU.", "countryCode": "us" }
Script CEL:
documents.get("country", documents.get("article", "us-news").countryCode).name
Qué ocurre:
documents.get("article", "us-news") devuelve { "headline": "Noticias desde EE. UU.", "countryCode": "us" }.countryCode extrae "us"documents.get("country", "us") devuelve { "code": "us", "name": "Estados Unidos", ... }.name extrae "Estados Unidos"Resultado: "Estados Unidos"
Si un documento podría no existir, puedes proporcionar un valor de respaldo:
documents.get("article", meta.params.slug) != null
? documents.get("article", meta.params.slug).headline
: "Artículo no encontrado"
O comprobar si existe un campo específico:
documents.get("article", "intro").author != null
? documents.get("article", "intro").author
: "Autor desconocido"
Tu artículo tiene etiquetas y quieres comprobar si existe una etiqueta específica:
"featured" in documents.get("article", "welcome-post").tags
Devuelve: true si el artículo tiene la etiqueta "featured"
Obtener la primera etiqueta:
documents.get("article", "welcome-post").tags[0]
Devuelve: "announcement" (la primera etiqueta)
Contar las etiquetas:
size(documents.get("article", "welcome-post").tags)
Devuelve: 2 (número de etiquetas)
Las rutas paramétricas son la clave para construir páginas dinámicas y localizadas. Cuando defines un patrón de ruta como /{lang}/landingPage, el CMS extrae parámetros de la URL y los pone a tu disposición mediante meta.params.
Definición del patrón de ruta:
Las rutas usan la sintaxis :paramName o {paramName} para definir segmentos dinámicos:
| Patrón | URL de ejemplo | Parámetros extraídos |
|---|---|---|
/:lang/landingPage | /ko/landingPage | { lang: "ko" } |
/{country}/{lang}/products | /us/en/products | { country: "us", lang: "en" } |
/articles/:slug | /articles/welcome-post | { slug: "welcome-post" } |
Vinculaciones de parámetros: Cada parámetro de ruta puede vincularse a un esquema de documento para su validación:
{
"pattern": "/{lang}/landingPage",
"param_bindings": {
"lang": "language"
}
}
Esta vinculación le indica al CMS:
lang de la URLlanguage (busca el documento cuyo content.code coincida)Configuración de la ruta:
/{lang}/landingPage/{lang}/landingPage{ "lang": "language" }Tus documentos de saludo:
// greeting / ko
{ "code": "ko", "headline": "환영합니다", "subheadline": "우리 플랫폼에 오신 것을 환영합니다" }
// greeting / en
{ "code": "en", "headline": "Welcome", "subheadline": "Welcome to our platform" }
// greeting / ja
{ "code": "ja", "headline"
Script CEL para obtener contenido localizado:
documents.get("greeting", meta.params.lang).headline
Cómo se resuelve:
| URL | meta.params.lang | Resultado |
|---|---|---|
/ko/landingPage | "ko" | "환영합니다" |
/en/landingPage | "en" | "Welcome" |
/ja/landingPage | "ja" | "ようこそ" |
Para rutas como /{country}/{lang}/products:
Configuración de la ruta:
{
"pattern": "/{country}/{lang}/products",
"param_bindings": {
"country": "country",
"lang": "language"
}
}
Scripts CEL:
// Obtener el nombre del país
documents.get("country", meta.params.country).name
// Obtener la lista de productos localizada en función del país
documents.find("product", { "where": { "country": meta.params.country } })
// Combinado: mostrar un saludo específico del país en el idioma del usuario
documents.get("greeting", meta.params.lang).headline + " from " + documents.get("country", meta.params.country).name
Cascada de validación:
El CMS valida los parámetros de forma jerárquica. Para rutas /{country}/{lang}:
country contra el esquema countrylang contra el esquema languagelang esté en el array country.languages[] (validación jerárquica)meta.segments proporciona la ruta URL sin procesar como un array, útil cuando necesitas acceso posicional sin parámetros con nombre.
Cómo funciona:
| Ruta URL | meta.segments |
|---|---|
/articles/tech/ai-news | ["articles", "tech", "ai-news"] |
/ko/landingPage | ["ko", "landingPage"] |
/us/en/products/featured | ["us", "en", "products", "featured"] |
/ | [] |
| Caso de uso | Mejor enfoque |
|---|---|
| Parámetros con nombre del patrón de ruta | meta.params.lang |
| Acceso basado en posición | meta.segments[0] |
| Obtener la profundidad de la ruta | size(meta.segments) |
| Comprobar si la ruta contiene un segmento | "admin" in meta.segments |
// Obtener el primer segmento (a menudo el código de idioma)
meta.segments[0]
// Comprobar la profundidad de la ruta
size(meta.segments) > 2 ? "deep" : "shallow"
// Verificar si estamos en la sección de administración
"admin" in meta.segments ? "admin mode" : "public mode"
// Respaldo: usar el segmento si el parámetro no está vinculado
has(meta.params.lang) ? meta.params.lang : meta.segments[0]
El objeto meta contiene todo el contexto sobre la solicitud actual:
| Propiedad | Tipo | Descripción | |
|---|---|---|---|
meta.locale | string | Código de locale actual (por ejemplo, "en-US", "ko-KR", "ar-SA") | |
meta.params | Record<string, string> | Parámetros de ruta extraídos del patrón de URL | |
meta.segments | string[] | Ruta URL dividida en segmentos | |
meta.docId | string \\ | null | UUID del documento actual (null para documentos nuevos) | |
meta.title | string | Título del documento actual |
El código de locale sigue el formato BCP 47 (idioma-región):
// Comprobar locales de escritura RTL
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"
// Obtener solo la parte del idioma
meta.locale.split("-")[0] // No admitido: usa meta.params.lang en su lugar
Los parámetros de ruta siempre son cadenas. El CMS los valida contra los esquemas vinculados antes de evaluar:
// Acceder a un parámetro con nombre
meta.params.lang // "ko"
meta.params.country // "us"
meta.params.slug // "welcome-post"
// Comprobar si un parámetro existe
has(meta.params.category) // true/false
// Usarlo para obtener documentos
documents.get("greeting", meta.params.lang)
documents.ref("airports").get(meta.params.code)
Segmentos de URL sin procesar como un array:
// Acceder por índice (base 0)
meta.segments[0] // Primer segmento
meta.segments[1] // Segundo segmento
// Comprobar la longitud
size(meta.segments) // Número de segmentos
// Comprobar pertenencia
"products" in meta.segments // ¿La ruta incluye "products"?
El UUID del documento actual, útil para scripts autorreferenciales:
// Solo disponible cuando se editan documentos existentes
meta.docId != null ? "editing" : "creating new"
// Usar en lógica condicional
meta.docId != null ? documents.get("article", meta.docId).status : "draft"
El título del documento actual:
// Usar para mostrar
"Editing: " + meta.title
// Condicional según el título
meta.title.contains("Draft") ? "work in progress" : "published"
Para una sintaxis más limpia cuando el esquema es conocido pero el identificador es dinámico:
// Enfoque tradicional
documents.get("airports", meta.params.code).name
// Usando ref() - el esquema separado del identificador dinámico
documents.ref("airports").get(meta.params.code).name
Ambos son equivalentes, pero ref() deja más claro qué parte es dinámica.
documents.get("schema", "identifier") // Obtener un documento
documents.get("schema", "id").fieldName // Obtener un campo específico
documents.find("schema") // Obtener todos los documentos
documents.find("schema", { "where": {...}}) // Consulta filtrada
documents.ref("schema").get(identifier) // Búsqueda encadenada
meta.locale // "en-US", "ar-SA", etc.
meta.params.xyz // Parámetro de URL llamado "xyz"
meta.segments // Ruta URL como array: ["articles", "intro"]
meta.segments[0] // Primer segmento de la ruta
meta.docId // ID del documento actual (o null)
meta.title // Título del documento actual
// Comparación
== != < <= > >=
// Lógica
&& || !
// Ternario (if-else)
condition ? valueIfTrue : valueIfFalse
// Pertenencia
"value" in listOrMap
size(list) // Contar elementos
size(string) // Longitud de la cadena
"text".startsWith("te") // true
"text".endsWith("xt") // true
"text".contains("ex") // true
has(object.property) // Comprobar si la propiedad existe
Si algo sale mal, verás uno de estos:
| Error | Qué significa |
|---|---|
SYNTAX_ERROR | Hay un error tipográfico en tu script (falta una comilla, operador incorrecto) |
TYPE_ERROR | Estás combinando tipos que no funcionan juntos |
RUNTIME_ERROR | El script se ejecutó pero encontró un problema (variable indefinida) |
FETCH_LIMIT_EXCEEDED | Estás obteniendo demasiados documentos (máximo 50) |
TIMEOUT | El script tardó demasiado (máximo 5 segundos) |
AST_DEPTH_EXCEEDED | La expresión es demasiado anidada (profundidad máxima: 50) |
SCRIPT_TOO_LONG | El script supera el límite de 5000 caracteres |
El motor CEL está diseñado para ser extensible. Las capacidades planificadas incluyen:
// Futuro: llamar a servicios externos mediante MCP
mcp.translate(meta.params.text, "en", meta.params.lang)
mcp.analyze(documents.get("article", meta.params.id).body)
// Futuro: generación de contenido impulsada por IA
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"])
Estas capacidades se agregarán mediante el sistema de funciones registradas, manteniendo la compatibilidad con los scripts existentes.
documents. o meta. y el editor mostrará las opciones disponiblesdocuments.get("schema", "id"), luego añade .fieldName!= null ? ... : ...documents.get() o documents.find() cuenta para el límite de 50 consultashas(meta.params.category) antes de accederEste recorrido crea una página de aterrizaje multilingüe accesible en /{lang}/landingPage.
En el administrador del CMS, crea un esquema personalizado llamado greeting:
{
"name": "greeting",
"fields": [
{ "name": "code", "type": "string", "required": true },
{ "name": "headline", "type": "string", "required": true },
{ "name": "subheadline"
Crea documentos para cada idioma:
Documento: greeting / ko
{
"code": "ko",
"headline": "환영합니다",
"subheadline": "우리 플랫폼에 오신 것을 환영합니다",
"ctaText": "시작하기",
"ctaUrl": "/ko/get-started"
}
Documento: greeting / en
{
"code": "en",
"headline": "Welcome",
"subheadline": "Welcome to our platform",
"ctaText": "Get Started",
"ctaUrl": "/en/get-started"
}
Documento: greeting / ja
{
"code": "ja",
"headline": "ようこそ",
"subheadline": "私たちのプラットフォームへようこそ",
"ctaText": "始める",
"ctaUrl": "/ja/get-started"
}
Crea una ruta con la siguiente configuración:
/{lang}/landingPage/{lang}/landingPage {
"lang": "language"
}
Añade un bloque hero a la ruta con estos scripts CEL para cada campo:
Campo de titular:
documents.get("greeting", meta.params.lang).headline
Campo de subtitular:
documents.get("greeting", meta.params.lang).subheadline
Campo de texto del CTA:
documents.get("greeting", meta.params.lang).ctaText
Campo de URL del CTA:
documents.get("greeting", meta.params.lang).ctaUrl
Crea una ruta catch-all en tu aplicación Next.js:
// app/[...slug]/page.tsx
import { getCmsClient } from '@repo/renderer';
interface PageProps {
params: { slug: string[] };
}
export default async function Page({ params }: PageProps) {
Visita estas URLs para ver contenido localizado:
| URL | Titular esperado |
|---|---|
/ko/landingPage | 환영합니다 |
/en/landingPage | Welcome |
/ja/landingPage | ようこそ |
Cuando un usuario visita /ko/landingPage:
/{lang}/landingPagemeta.params.lang = "ko"languagedocuments.get("greeting", meta.params.lang) se resuelven con contenido en coreanointerface CelMeta {
/** Código de locale actual (p. ej., 'en-US') */
locale: string;
/** Parámetros de ruta extraídos de la URL */
params: Record<string, string>;
/** Segmentos de la ruta URL */
segments: string[];
/** ID del documento actual (si se edita un documento existente) */
La función extractParams procesa rutas URL:
Patrón: /{country}/{lang}/products
Ruta: /us/en/products
Algoritmo:
1. Normalizar ambos (eliminar las barras finales)
2. Dividir en segmentos: ["us", "en", "products"] y ["{country}", "{lang}", "products"]
// Vinculación simple (usa el campo "code" para la búsqueda)
{ "lang": "language" }
// Vinculación detallada (campo slug personalizado)
{
"lang": {
"schemaName": "language",
"slugField": "code"
},
"slug": {
"schemaName": "article",
"slugField": "slug"
Al obtener mediante documents.get(schema, identifier):
idcontent.codecontent.slugtitleEsto permite referencias de documentos flexibles mediante cualquier identificador único.