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

Programación en el generador de plantillas

Una guía práctica para escribir expresiones CEL en el CMS.

Continue Reading
Previous‹Renderizado Estatico Con Soporte De Modo De EdicionNextCreate Profound Next›

Híbrido

Proyecto RenderizadorEnrutamiento ParamétricoTipos De ComponentesSseConfigurar Proxy Del Panel De AdministraciónRenderizado Estatico Con Soporte De Modo De EdicionProgramación en el generador de plantillasCreate Profound Next

Sin cabeza

Inicio rápidoJson Y Claude CodeComponente Zod Pull

Mcp

Mcp

Características Cms

Feat Docs TemplateCaracterística Constructor De PlantillasFuncionalidad TraductorFeat Organización

Motivación

Nuestro enfoque

Terminología

Hibrido Vs Headless

Una guía práctica para escribir expresiones CEL en el CMS.


Cómo funciona CEL

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.


Los componentes básicos

Toda expresión CEL tiene acceso a tres cosas:

ObjetoQué esEjemplo
documentsObtiene cualquier documento del CMSdocuments.get("country", "us")
metaInformación sobre la solicitud actual (idioma, parámetros de URL)meta.locale, meta.params.slug
schemaLas definiciones de campos del documento actualschema.fields

Obtención de documentos

La característica más potente de CEL es obtener documentos desde cualquier lugar de tu CMS.

Obtener un único documento

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"


Uso de parámetros de URL

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.


Obtener varios documentos

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" }
]

Ejemplos del mundo real

Ejemplo 1: Título de bloque hero desde otro documento

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"


Ejemplo 2: Nombre del país a partir del código

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"
  • Resultado: "Estados Unidos"

Cuando alguien visita /countries/sa:

  • meta.params.code = "sa"
  • Resultado: "Arabia Saudita"

Ejemplo 3: Contenido condicional según el locale

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"


Ejemplo 4: Búsquedas encadenadas de documentos

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:

  1. documents.get("article", "us-news") devuelve { "headline": "Noticias desde EE. UU.", "countryCode": "us" }
  2. .countryCode extrae "us"
  3. documents.get("country", "us") devuelve { "code": "us", "name": "Estados Unidos", ... }
  4. .name extrae "Estados Unidos"

Resultado: "Estados Unidos"


Ejemplo 5: Valores de respaldo

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"

Ejemplo 6: Trabajar con listas

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)


Rutas paramétricas y meta.params

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.

Cómo funcionan los parámetros de ruta

Definición del patrón de ruta: Las rutas usan la sintaxis :paramName o {paramName} para definir segmentos dinámicos:

PatrónURL de ejemploPará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:

  1. Extrae el segmento lang de la URL
  2. Valídalo contra el esquema language (busca el documento cuyo content.code coincida)
  3. Si es válido, deja el documento completo disponible en los parámetros resueltos

Ejemplo: página de aterrizaje basada en idioma

Configuración de la ruta:

  • Ruta: /{lang}/landingPage
  • Patrón: /{lang}/landingPage
  • Vinculaciones de parámetros: { "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:

URLmeta.params.langResultado
/ko/landingPage"ko""환영합니다"
/en/landingPage"en""Welcome"
/ja/landingPage"ja""ようこそ"

Patrón avanzado: rutas país + idioma

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}:

  1. Valida el parámetro country contra el esquema country
  2. Valida el parámetro lang contra el esquema language
  3. Opcionalmente valida que lang esté en el array country.languages[] (validación jerárquica)

meta.segments: acceso sin procesar a la ruta URL

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 URLmeta.segments
/articles/tech/ai-news["articles", "tech", "ai-news"]
/ko/landingPage["ko", "landingPage"]
/us/en/products/featured["us", "en", "products", "featured"]
/[]

Cuándo usar meta.segments vs meta.params

Caso de usoMejor enfoque
Parámetros con nombre del patrón de rutameta.params.lang
Acceso basado en posiciónmeta.segments[0]
Obtener la profundidad de la rutasize(meta.segments)
Comprobar si la ruta contiene un segmento"admin" in meta.segments

Ejemplos con 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]

Referencia completa del objeto meta

El objeto meta contiene todo el contexto sobre la solicitud actual:

PropiedadTipoDescripción
meta.localestringCódigo de locale actual (por ejemplo, "en-US", "ko-KR", "ar-SA")
meta.paramsRecord<string, string>Parámetros de ruta extraídos del patrón de URL
meta.segmentsstring[]Ruta URL dividida en segmentos
meta.docIdstring \\ | nullUUID del documento actual (null para documentos nuevos)
meta.titlestringTítulo del documento actual

meta.locale

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

meta.params

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)

meta.segments

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"?

meta.docId

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"

meta.title

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"

documents.ref() - búsquedas encadenadas

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.


Guía rápida

Obtención de documentos

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

Variables de contexto

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

Operadores

// Comparación
==  !=  <  <=  >  >=

// Lógica
&&  ||  !

// Ternario (if-else)
condition ? valueIfTrue : valueIfFalse

// Pertenencia
"value" in listOrMap

Funciones comunes

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

Mensajes de error

Si algo sale mal, verás uno de estos:

ErrorQué significa
SYNTAX_ERRORHay un error tipográfico en tu script (falta una comilla, operador incorrecto)
TYPE_ERROREstás combinando tipos que no funcionan juntos
RUNTIME_ERROREl script se ejecutó pero encontró un problema (variable indefinida)
FETCH_LIMIT_EXCEEDEDEstás obteniendo demasiados documentos (máximo 50)
TIMEOUTEl script tardó demasiado (máximo 5 segundos)
AST_DEPTH_EXCEEDEDLa expresión es demasiado anidada (profundidad máxima: 50)
SCRIPT_TOO_LONGEl script supera el límite de 5000 caracteres

Extensibilidad y capacidades futuras

El motor CEL está diseñado para ser extensible. Las capacidades planificadas incluyen:

Planificado: integración con servidores MCP

// 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)

Planificado: capacidades de IA

// 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.


Consejos

  1. Usa el autocompletado: escribe documents. o meta. y el editor mostrará las opciones disponibles
  2. Empieza por algo simple: prueba primero con documents.get("schema", "id"), luego añade .fieldName
  3. Comprueba si es null: si un documento podría no existir, añade un respaldo con != null ? ... : ...
  4. No obtengas más de la cuenta: cada documents.get() o documents.find() cuenta para el límite de 50 consultas
  5. Prefiere meta.params sobre meta.segments: los parámetros con nombre están validados y son más fiables
  6. Usa has() para parámetros opcionales: comprueba has(meta.params.category) antes de acceder
  7. Usa documents.ref() para identificadores dinámicos: sintaxis más clara cuando el esquema es estático pero el identificador es dinámico

Apéndice A: ejemplo completo de ruta paramétrica

Este recorrido crea una página de aterrizaje multilingüe accesible en /{lang}/landingPage.

Paso 1: crear el esquema de documentos Greeting

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"

Paso 2: crear documentos Greeting

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"
}

Paso 3: crear la ruta

Crea una ruta con la siguiente configuración:

  • Ruta: /{lang}/landingPage
  • Patrón: /{lang}/landingPage
  • Estado: Live
  • Vinculaciones de parámetros:
  {
    "lang": "language"
  }

Paso 4: añadir bloques con scripts CEL

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

Paso 5: consumir en Next.js

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) {

Paso 6: probar las rutas

Visita estas URLs para ver contenido localizado:

URLTitular esperado
/ko/landingPage환영합니다
/en/landingPageWelcome
/ja/landingPageようこそ

Cómo funciona la resolución

Cuando un usuario visita /ko/landingPage:

  1. Coincidencia de ruta: el CMS coincide con el patrón /{lang}/landingPage
  2. Extracción de parámetros: meta.params.lang = "ko"
  3. Validación: el CMS valida que "ko" exista en el esquema language
  4. Evaluación CEL: scripts como documents.get("greeting", meta.params.lang) se resuelven con contenido en coreano
  5. Respuesta: bloques localizados devueltos al cliente

Apéndice B: referencia técnica

Interfaz CelMeta (TypeScript)

interface 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) */

Algoritmo de extracción de parámetros

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"]

Formatos admitidos para la vinculación de parámetros

// 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"

Prioridad de búsqueda de documentos

Al obtener mediante documents.get(schema, identifier):

  1. Coincidencia de UUID: si el identificador es un UUID válido, se obtiene por id
  2. Campo code: comprueba el campo content.code
  3. Campo slug: comprueba el campo content.slug
  4. Coincidencia de título: comprueba el campo title

Esto permite referencias de documentos flexibles mediante cualquier identificador único.

]
] }
:
"ようこそ"
,
"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('/');
// Obtener la ruta y los parámetros resueltos
const { route, resolvedParams } = await client.routes.getRouteByPath.query({
websiteId: process.env.CMS_WEBSITE_ID!,
path,
});
// Obtener los bloques de la ruta
const blocks = await client.blocks.getBlocks.query({
websiteId: process.env.CMS_WEBSITE_ID!,
blockIds: route.block_ids,
// Pasar parámetros resueltos para el contexto 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: {},
},
});
// Renderizar bloques
return (
<main>
{blocks.map((block) => (
<BlockRenderer
key={block.id}
block={block}
routeParams={resolvedParams}
language={resolvedParams?.lang?.value}
/>
))}
</main>
);
}
docId: string | null;
/** Título del documento actual */
title: string;
}
3. Comparar el número de segmentos (deben ser iguales)
4. Para cada par de segmentos:
- Si el patrón empieza con : o {}, extraer como parámetro
- De lo contrario, debe coincidir exactamente
5. Devolver: { country: "us", lang: "en" }
}
}