Um guia prático para escrever expressões CEL no CMS.
Um guia prático para escrever expressões CEL no CMS.
CEL (Common Expression Language) é uma linguagem de script leve integrada ao nosso CMS. Ela permite escrever expressões dinâmicas que extraem dados de documentos, leem parâmetros de URL e calculam valores em tempo real.
Veja o que acontece quando um script CEL é executado:
Seu script O mecanismo Resultado
| | |
v v v
documents.get("article", "intro") --> Busca no banco de dados --> { headline: "Bem-vindo", body: "..." }
.headline --> Extrai o campo --> "Bem-vindo"
Pense no CEL como uma linguagem de consulta somente leitura. Ele não pode modificar nada no banco de dados — apenas lê dados e retorna um resultado calculado. Isso faz com que seja seguro usá-lo em qualquer lugar do CMS.
Toda expressão CEL tem acesso a três elementos:
| Objeto | O que é | Exemplo |
|---|---|---|
documents | Busca qualquer documento no CMS | documents.get("country", "us") |
meta | Informações sobre a solicitação atual (localidade, parâmetros da URL) | meta.locale, meta.params.slug |
schema | Definições dos campos do documento atual | schema.fields |
docAo escrever expressões CEL dentro de um editor de documentos, você pode acessar os valores dos campos do documento atual usando o objeto doc. Isso permite criar campos calculados e referências entre campos.
// Acessar o campo de preço do documento atual
doc.price
// Calcular o total a partir dos campos do documento atual
doc.price * doc.quantity
// Condição baseada no status do documento atual
doc.status == "published" ? doc.title : "Rascunho: " + doc.title
O objeto doc contém todos os valores dos campos do documento editado. Ele é útil para:
doc.price * doc.quantity)O recurso mais poderoso do CEL é buscar documentos de qualquer lugar do seu CMS.
Sintaxe: documents.get(schemaName, identifier)
Suponha que você tenha um documento article armazenado com o identificador "welcome-post":
// Armazenado no CMS como: article / welcome-post
{
"headline": "Bem-vindo à nossa plataforma",
"author": "Sarah Chen",
"body": "Estamos felizes em anunciar...",
"tags": ["announcement", "news"]
}
Para buscar o documento inteiro:
documents.get("article", "welcome-post")
Retorna:
{
"headline": "Bem-vindo à nossa plataforma",
"author": "Sarah Chen",
"body": "Estamos felizes em anunciar...",
"tags": ["announcement", "news"]
}
Para buscar apenas o título principal:
documents.get("article", "welcome-post").headline
Retorna: "Bem-vindo à nossa plataforma"
Para buscar o autor:
documents.get("article", "welcome-post").author
Retorna: "Sarah Chen"
Quando sua página tem rotas dinâmicas, como /articles/[slug], você pode usar meta.params para obter o parâmetro da URL e buscar o documento correto.
Se alguém visitar /articles/welcome-post:
documents.get("article", meta.params.slug).headline
Retorna: "Bem-vindo à nossa plataforma"
É assim que você cria páginas dinâmicas: o mesmo script CEL funciona para qualquer artigo, usando o slug presente na URL.
Sintaxe: documents.find(schemaName) ou documents.find(schemaName, filter)
// Obter todos os países
documents.find("country")
Retorna:
[
{ "code": "us", "name": "Estados Unidos", "flag": "US" },
{ "code": "sa", "name": "Arábia Saudita", "flag": "SA" },
{ "code": "gb", "name": "Reino Unido", "flag": "GB" }
]
// Obter países com um filtro
documents.find("country", { "where": { "code": "us" } })
Retorna:
[
{ "code": "us", "name": "Estados Unidos", "flag": "US" }
]
O CEL permite buscar conteúdo traduzido de documentos de duas maneiras: tradução automática baseada na localidade e busca explícita de tradução.
Quando meta.locale está definido, por exemplo, a partir de parâmetros de rota ou preferências do usuário, documents.get() mescla automaticamente o conteúdo traduzido:
// Se meta.locale for "fr", retorna a tradução francesa mesclada ao documento-base
documents.get("greeting", "welcome").headline
Como funciona:
meta.locale não for "en" ou "en-US", procura a tradução na tabela translations{ ...baseContent, ...translatedContent }Isso significa que os campos traduzidos substituem os campos-base, enquanto os campos sem tradução usam o documento-base.
Para buscar uma tradução específica independentemente da localidade atual:
Sintaxe: documents.translated(schemaName, identifier, locale)
// Sempre buscar a tradução em espanhol
documents.translated("greeting", "welcome", "es").headline
// Buscar a tradução com base em um parâmetro da URL
documents.translated("product", meta.params.id, meta.params.lang).description
// Comparar traduções
documents.translated("article", "intro", "en").title + " / " + documents.translated("article", "intro", "fr").title
Documentos de saudação com traduções:
// Documento-base: greeting / welcome
{ "headline": "Bem-vindo", "subheadline": "Bem-vindo à nossa plataforma" }
// Tradução (idioma: "fr")
{ "headline": "Bienvenue", "subheadline": "Bienvenue sur notre plateforme" }
// Tradução (idioma: "es")
{ "headline": "Bienvenido", "subheadline": "Bienvenido a nuestra plataforma" }
Scripts CEL:
// Com meta.locale = "fr"
documents.get("greeting", "welcome").headline
// Retorna: "Bienvenue"
// Tradução explícita para espanhol
documents.translated("greeting", "welcome", "es").headline
// Retorna: "Bienvenido"
// Padrão alternativo para traduções ausentes
documents.translated("greeting", "welcome", meta.params.lang) != null
? documents.translated("greeting", "welcome", meta.params.lang).headline
: documents.get("greeting", "welcome").headline
Você tem um hero-block que deve exibir um título principal obtido de um documento article.
Seu documento de artigo (identificador: "homepage-hero"):
{
"headline": "Crie mais rápido, publique com inteligência",
"subheadline": "O CMS moderno para desenvolvedores"
}
Script CEL no campo de título do bloco hero:
documents.get("article", "homepage-hero").headline
Resultado: o hero exibe "Crie mais rápido, publique com inteligência"
Você está criando uma página em /countries/[code] e quer exibir o nome completo do país.
Seus documentos de países:
// country / us
{ "code": "us", "name": "Estados Unidos", "flag": "US", "languages": ["en", "es"] }
// country / sa
{ "code": "sa", "name": "Arábia Saudita", "flag": "SA", "languages": ["ar", "en"] }
Script CEL:
documents.get("country", meta.params.code).name
Quando alguém visita /countries/us:
meta.params.code = "us""Estados Unidos"Quando alguém visita /countries/sa:
meta.params.code = "sa""Arábia Saudita"Mostre títulos diferentes com base na localidade do usuário.
meta.locale == "ar-SA" ? "Boas-vindas a todos" : "Boas-vindas"
Se a localidade for "ar-SA": retorna "Boas-vindas a todos"
Se a localidade for qualquer outra: retorna "Boas-vindas"
Seu article tem um campo countryCode, e você quer obter o nome completo do país.
Documento do artigo:
{ "headline": "Notícias dos EUA", "countryCode": "us" }
Script CEL:
documents.get("country", documents.get("article", "us-news").countryCode).name
O que acontece:
documents.get("article", "us-news") retorna { "headline": "Notícias dos EUA", "countryCode": "us" }.countryCode extrai "us"documents.get("country", "us") retorna { "code": "us", "name": "Estados Unidos", ... }.name extrai "Estados Unidos"Resultado: "Estados Unidos"
Se um documento talvez não exista, você pode fornecer um valor alternativo:
documents.get("article", meta.params.slug) != null
? documents.get("article", meta.params.slug).headline
: "Artigo não encontrado"
Ou verificar se um campo específico existe:
documents.get("article", "intro").author != null
? documents.get("article", "intro").author
: "Autor desconhecido"
Rotas paramétricas são essenciais para criar páginas dinâmicas e localizadas. Ao definir um padrão como /{lang}/landingPage, o CMS extrai os parâmetros da URL e os disponibiliza por meio de meta.params.
As rotas usam a sintaxe :paramName ou {paramName} para definir segmentos dinâmicos:
| Padrão | URL de exemplo | 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" } |
Cada parâmetro de rota pode ser associado a um schema de documento para validação:
{
"pattern": "/{lang}/landingPage",
"param_bindings": {
"lang": "language"
}
}
Essa associação informa ao CMS:
lang da URLlanguageConfiguração da rota:
/{lang}/landingPage/{lang}/landingPage{ "lang": "language" }Script CEL para buscar conteúdo localizado:
documents.get("greeting", meta.params.lang).headline
Resolução:
| URL | meta.params.lang | Resultado |
|---|---|---|
/ko/landingPage | "ko" | "Bem-vindo" |
/en/landingPage | "en" | "Bem-vindo" |
/ja/landingPage | "ja" | "Bem-vindo" |
Para rotas como /{country}/{lang}/products:
{
"pattern": "/{country}/{lang}/products",
"param_bindings": {
"country": "country",
"lang": "language"
}
}
Scripts CEL:
// Obter o nome do país
documents.get("country", meta.params.country).name
// Obter a lista localizada de produtos com base no país
documents.find("product", { "where": { "country": meta.params.country } })
// Combinado: mostrar uma saudação específica do país no idioma do usuário
documents.get("greeting", meta.params.lang).headline + " de " + documents.get("country", meta.params.country).name
meta.segments fornece o caminho bruto da URL como um array, sendo útil quando você precisa acessar segmentos por posição, sem parâmetros nomeados.
| Caminho da 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 | Melhor abordagem |
|---|---|
| Parâmetros nomeados do padrão da rota | meta.params.lang |
| Acesso por posição | meta.segments[0] |
| Obter a profundidade do caminho | size(meta.segments) |
| Verificar se o caminho contém um segmento | "admin" in meta.segments |
// Obter o primeiro segmento (frequentemente o código do idioma)
meta.segments[0]
// Verificar a profundidade do caminho
size(meta.segments) > 2 ? "profundo" : "superficial"
// Verificar se estamos na seção administrativa
"admin" in meta.segments ? "modo administrativo" : "modo público"
// Alternativa: usar o segmento se o parâmetro não estiver associado
has(meta.params.lang) ? meta.params.lang : meta.segments[0]
O objeto meta contém todo o contexto da solicitação atual:
| Propriedade | Tipo | Descrição |
|---|---|---|
meta.locale | string | Código da localidade atual, como "en-US", "ko-KR" ou "ar-SA" |
meta.params | Record<string, string> | Parâmetros de rota extraídos do padrão da URL |
meta.segments | string[] | Caminho da URL dividido em segmentos |
meta.docId | string | null | UUID do documento atual (null para documentos novos) |
meta.title | string | Título do documento atual |
O código de localidade segue o formato BCP 47 (idioma-região):
// Verificar a localidade para idiomas RTL
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"
// Obter somente a parte do idioma
meta.locale.split("-")[0] // Não suportado — use meta.params.lang
Os parâmetros de rota são sempre strings. O CMS valida-os usando os schemas associados antes da avaliação:
// Acessar um parâmetro nomeado
meta.params.lang // "ko"
meta.params.country // "us"
meta.params.slug // "welcome-post"
// Verificar se um parâmetro existe
has(meta.params.category) // true/false
// Usar em uma busca de documento
documents.get("greeting", meta.params.lang)
documents.ref("airports").get(meta.params.code)
O UUID do documento atual, útil para scripts autorreferenciais:
// Disponível somente ao editar documentos existentes
meta.docId != null ? "editando" : "criando novo"
// Usar em lógica condicional
meta.docId != null ? documents.get("article", meta.docId).status : "rascunho"
O título do documento atual:
// Usar para exibição
"Editando: " + meta.title
// Condicional baseada no título
meta.title.contains("Rascunho") ? "em andamento" : "publicado"
Para uma sintaxe mais limpa quando o schema é conhecido, mas o identificador é dinâmico:
// Abordagem tradicional
documents.get("airports", meta.params.code).name
// Usando ref() — schema separado do identificador dinâmico
documents.ref("airports").get(meta.params.code).name
Ambas são equivalentes, mas ref() torna a parte dinâmica mais clara.
documents.get("schema", "identifier") // Obter um documento
documents.get("schema", "id").fieldName // Obter um campo específico
documents.find("schema") // Obter todos os documentos
documents.find("schema", { "where": {...}}) // Consulta filtrada
documents.ref("schema").get(identifier) // Busca encadeada
documents.translated("schema", "id", "fr") // Obter com localidade explícita
meta.locale // "en-US", "ar-SA" etc.
meta.params.xyz // Parâmetro da URL chamado "xyz"
meta.segments // Caminho da URL como array
meta.segments[0] // Primeiro segmento do caminho
meta.docId // ID do documento atual (ou null)
meta.title // Título do documento atual
doc.fieldName // Valor do campo do documento atual (no contexto do editor)
// Comparação
== != < <= > >=
// Lógica
&& || !
// Ternário (if-else)
condition ? valueIfTrue : valueIfFalse
// Pertencimento
"value" in listOrMap
size(list) // Contar itens
size(string) // Comprimento da string
"text".startsWith("te") // true
"text".endsWith("xt") // true
"text".contains("ex") // true
has(object.property) // Verificar se a propriedade existe
hasProperty(obj, "key") // Verificar se o objeto possui a chave (sintaxe alternativa)
Se algo der errado, você verá uma destas mensagens:
| Erro | O que significa |
|---|---|
SYNTAX_ERROR | Erro de digitação no script (aspas ausentes ou operador inválido) |
TYPE_ERROR | Tipos incompatíveis estão sendo combinados |
RUNTIME_ERROR | O script foi executado, mas encontrou um problema (variável indefinida) |
FETCH_LIMIT_EXCEEDED | Muitos documentos estão sendo buscados (máximo de 50) |
TIMEOUT | O script demorou demais (máximo de 5 segundos) |
AST_DEPTH_EXCEEDED | A expressão está aninhada profundamente demais (profundidade máxima: 50) |
SCRIPT_TOO_LONG | O script excede o limite de 5.000 caracteres |
O mecanismo CEL foi projetado para ser extensível. Entre os recursos planejados estão:
// Futuro: chamar serviços externos via MCP
mcp.translate(meta.params.text, "en", meta.params.lang)
mcp.analyze(documents.get("article", meta.params.id).body)
// Futuro: geração de conteúdo com 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"])
Esses recursos serão adicionados por meio do sistema de funções registradas, mantendo a compatibilidade com os scripts existentes.
documents. ou meta. e o editor mostrará as opções disponíveisdocuments.get("schema", "id") e depois adicione .fieldName!= null ? ... : ...documents.get() ou documents.find() conta para o limite de 50 buscashas(meta.params.category) antes de acessarEsta seção aborda padrões avançados para vincular documentos e criar estruturas de conteúdo relacionais.
A forma mais simples: um documento referencia outro por identificador.
// O artigo armazena o ID do autor; buscar o nome do autor
documents.get("author", documents.get("article", "intro").authorId).name
Para uma sintaxe mais limpa quando o identificador é dinâmico:
// Abordagem tradicional
documents.get("country", documents.get("airport", meta.params.code).countryCode).name
// Usando ref() — mais claro quando o schema é conhecido e o identificador é dinâmico
documents.ref("country").get(documents.get("airport", meta.params.code).countryCode).name
Crie relacionamentos profundos encadeando várias buscas:
// Aeroporto → País → Região → Continente
documents.get("continent",
documents.get("region",
documents.get("country",
documents.get("airport", meta.params.code).countryCode
).regionCode
).continentCode
).name
Combine referências de documentos com traduções:
// Obter o nome localizado do país de um aeroporto
documents.translated("country",
documents.get("airport", meta.params.code).countryCode,
meta.params.lang
).name
Este passo a passo cria uma página de destino multilíngue acessível em /{lang}/landingPage.
No administrador do CMS, crie um schema personalizado chamado 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" }
]
}
Crie documentos para cada idioma. Adicione os scripts CEL aos campos correspondentes:
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
Crie uma página com a seguinte configuração:
/{lang}/landingPagelang → o componente language {
"lang": "language"
}
Adicione um bloco hero à rota e use os scripts CEL para cada campo.
Adicione uma rota abrangente. ParametricRoutePage resolve a página, extrai meta.params da URL, avalia seus vínculos CEL no servidor e renderiza cada bloco por meio do registro — você não precisa criar o contexto meta nem chamar o cliente de baixo nível.
// 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 })}
/>
);
}
Visite estas URLs para ver o conteúdo localizado:
| URL | Título esperado |
|---|---|
/ko/landingPage | 환영 |
/en/landingPage | Bem-vindo |
/ja/landingPage | いらっしゃいませ |
Quando um usuário visita /ko/landingPage:
/{lang}/landingPagemeta.params.lang = "ko""ko" existe no schema languagedocuments.get("greeting", meta.params.lang) resolvem o conteúdo coreanointerface CelMeta {
/** Código da localidade atual (por exemplo, 'en-US') */
locale: string;
/** Parâmetros de rota extraídos da URL */
params: Record<string, string>;
/** Segmentos do caminho da URL */
segments: string[];
/** ID do documento atual (se estiver editando um documento existente) */
docId: string | null;
/** Título do documento atual */
title: string;
}
A função extractParams processa caminhos de URL:
Padrão: /{country}/{lang}/products
Caminho: /us/en/products
Algoritmo:
1. Normalizar ambos (remover barras finais)
2. Dividir em segmentos: ["us", "en", "products"] e ["{country}", "{lang}", "products"]
3. Comparar a quantidade de segmentos (deve ser igual)
4. Para cada par de segmentos:
- Se o padrão começar com : ou {}, extrair como parâmetro
- Caso contrário, deve corresponder exatamente
5. Retornar: { country: "us", lang: "en" }
// Associação simples (usa o campo "code" para a busca)
{ "lang": "language" }
// Associação detalhada (campo slug personalizado)
{
"lang": {
"schemaName": "language",
"slugField": "code"
},
"slug": {
"schemaName": "article",
"slugField": "slug"
}
}
Ao buscar via documents.get(schema, identifier):
idcontent.codecontent.slugtitleIsso permite referências flexíveis a documentos usando qualquer identificador exclusivo.