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

Scripting In Template Builder

A practical guide to writing CEL expressions in the CMS.

Continue Reading
Previous‹Static Rendering With Edit Mode SupportNextCreate Profound Next›

Hybrid

Setup Hybrid CMS ProjectParametric Pages with ComponentsTypes of ComponentsSetup server sent events (SSE) content refetchSetup admin panel proxyStatic Rendering with Edit Mode SupportScripting in Template BuilderGetting Started Hybrid

Headless

Quick startSplit Screen JSON Component Builder with LLMComponent Zod Pull

MCP

MCP with Claude Code / Codex

Feature

Documentation Site TemplateFeature Template BuilderTranslation ServiceOrganizations & Website Heirarchy

Motivation

Our Approch

Terminology

'Hybrid' vs 'Headless'

A practical guide to writing CEL expressions in the CMS.


How CEL Works

CEL (Common Expression Language) is a lightweight scripting language built into our CMS. It lets you write dynamic expressions that can pull data from documents, read URL parameters, and compute values on the fly.

Here's what happens when a CEL script runs:

Your Script                    The Engine                     Result
    |                              |                              |
    v                              v                              v
documents.get("article", "intro") --> Fetches from database --> { headline: "Welcome", body: "..." }
         .headline                --> Extracts the field    --> "Welcome"

Think of CEL as a read-only query language. It can't modify anything in the database - it just reads data and returns a computed result. This makes it safe to use anywhere in the CMS.


The Building Blocks

Every CEL expression has access to three things:

ObjectWhat It IsExample
documentsFetch any document from the CMSdocuments.get("country", "us")
metaInfo about the current request (locale, URL params)meta.locale, meta.params.slug
schemaThe current document's field definitionsschema.fields

Self-Referencing with doc

When writing CEL expressions within a document editor, you can access the current document's field values using the doc object. This enables computed fields and cross-field references.

// Access current document's price field
doc.price

// Calculate total from current document fields
doc.price * doc.quantity

// Conditional based on current document status
doc.status == "published" ? doc.title : "Draft: " + doc.title

The doc object contains all field values from the document being edited. This is useful for:

  • Computed fields (e.g., doc.price * doc.quantity)
  • Conditional display logic based on document state
  • Validation-style expressions

Fetching Documents

The most powerful feature of CEL is fetching documents from anywhere in your CMS.

Getting a Single Document

Syntax: documents.get(schemaName, identifier)

Let's say you have an article document stored with the identifier "welcome-post":

// Stored in the CMS as: article / welcome-post
{
  "headline": "Welcome to Our Platform",
  "author": "Sarah Chen",
  "body": "We're excited to announce...",
  "tags": ["announcement", "news"]
}

To fetch the whole document:

documents.get("article", "welcome-post")

Returns:

{
  "headline": "Welcome to Our Platform",
  "author": "Sarah Chen",
  "body": "We're excited to announce...",
  "tags": ["announcement", "news"]
}

To fetch just the headline:

documents.get("article", "welcome-post").headline

Returns: "Welcome to Our Platform"

To fetch the author:

documents.get("article", "welcome-post").author

Returns: "Sarah Chen"


Using URL Parameters

When your page has dynamic routes (like /articles/[slug]), you can use meta.params to get the URL parameter and fetch the right document.

If someone visits /articles/welcome-post:

documents.get("article", meta.params.slug).headline

Returns: "Welcome to Our Platform"

This is how you build dynamic pages - the same CEL script works for any article, just using whatever slug is in the URL.


Fetching Multiple Documents

Syntax: documents.find(schemaName) or documents.find(schemaName, filter)

// Get all countries
documents.find("country")

Returns:

[
  { "code": "us", "name": "United States", "flag": "US" },
  { "code": "sa", "name": "Saudi Arabia", "flag": "SA" },
  { "code": "gb", "name": "United Kingdom", "flag": "GB" }

// Get countries with a filter
documents.find("country", { "where": { "code": "us" } })

Returns:

[
  { "code": "us", "name": "United States", "flag": "US" }
]

Translations

CEL supports fetching translated document content in two ways: automatic locale-based translation and explicit translation lookup.

Automatic Translation via meta.locale

When meta.locale is set (e.g., from route parameters or user preferences), documents.get() automatically merges translated content:

// If meta.locale is "fr", returns French translation merged with base document
documents.get("greeting", "welcome").headline

How it works:

  1. Fetches the base document content
  2. If meta.locale is not "en" or "en-US", looks up translation in the translations table
  3. Merges translated fields over base content: { ...baseContent, ...translatedContent }

This means translated fields override base fields, while untranslated fields fall back to the base document.

Explicit Translation with documents.translated()

For cases where you need to fetch a specific translation regardless of the current locale:

Syntax: documents.translated(schemaName, identifier, locale)

// Always fetch Spanish translation
documents.translated("greeting", "welcome", "es").headline

// Fetch translation based on URL parameter
documents.translated("product", meta.params.id, meta.params.lang).description

// Compare translations
documents.translated("article", "intro", "en").title + " / " + documents.translated("article", "intro", "fr").title

Translation Example

Your greeting documents with translations:

// Base document: greeting / welcome
{ "headline": "Welcome", "subheadline": "Welcome to our platform" }

// Translation (language: "fr")
{ "headline": "Welcome", "subheadline": "Welcome to our platform" }

// Translation (language: "es")
{ "headline": "Welcome", "subheadline": "Welcome to our platform" }

CEL scripts:

// With meta.locale = "fr"
documents.get("greeting", "welcome").headline
// Returns: "Welcome"

// Explicit Spanish translation
documents.translated("greeting", "welcome", "es").headline
// Returns: "Welcome"

// Fallback pattern for missing translations
documents.translated("greeting", "welcome", meta.params.lang) != null
  ? documents.translated("greeting", "welcome", meta.params.lang).headline
  : documents.get("greeting", "welcome").headline

Real-World Examples

Example 1: Hero Block Title from Another Document

You have a hero-block that should display a headline pulled from an article document.

Your article document (identifier: "homepage-hero"):

{
  "headline": "Build Faster, Ship Smarter",
  "subheadline": "The modern CMS for developers"
}

CEL script in the hero block's title field:

documents.get("article", "homepage-hero").headline

Result: The hero displays "Build Faster, Ship Smarter"


Example 2: Country Name from Code

You're building a page at /countries/[code] and want to display the full country name.

Your country documents:

// 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 script:

documents.get("country", meta.params.code).name

When someone visits /countries/us:

  • meta.params.code = "us"
  • Result: "United States"

When someone visits /countries/sa:

  • meta.params.code = "sa"
  • Result: "Saudi Arabia"

Example 3: Conditional Content Based on Locale

Show different headlines based on the user's locale.

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

If locale is "ar-SA": Returns "Welcome, everyone" If locale is anything else: Returns "Welcome"


Example 4: Chained Document Lookups

Your article has a countryCode field, and you want to get the full country name.

Article document:

{ "headline": "News from the US", "countryCode": "us" }

CEL script:

documents.get("country", documents.get("article", "us-news").countryCode).name

What happens:

  1. documents.get("article", "us-news") returns { "headline": "News from the US", "countryCode": "us" }
  2. .countryCode extracts "us"
  3. documents.get("country", "us") returns { "code": "us", "name": "United States", ... }
  4. .name extracts "United States"

Result: "United States"


Example 5: Fallback Values

If a document might not exist, you can provide a fallback:

documents.get("article", meta.params.slug) != null
  ? documents.get("article", meta.params.slug).headline
  : "Article Not Found"

Or check if a specific field exists:

documents.get("article", "intro").author != null
  ? documents.get("article", "intro").author
  : "Unknown Author"

Example 6: Working with Lists

Your article has tags, and you want to check if a specific tag exists:

"featured" in documents.get("article", "welcome-post").tags

Returns: true if the article has the "featured" tag

Get the first tag:

documents.get("article", "welcome-post").tags[0]

Returns: "announcement" (the first tag)

Count the tags:

size(documents.get("article", "welcome-post").tags)

Returns: 2 (number of tags)


Parametric Routes and meta.params

Parametric routes are the key to building dynamic, localized pages. When you define a route pattern like /{lang}/landingPage, the CMS extracts parameters from the URL and makes them available via meta.params.

How Route Parameters Work

Route Pattern Definition: Routes use :paramName or {paramName} syntax to define dynamic segments:

PatternExample URLExtracted Params
/:lang/landingPage/ko/landingPage{ lang: "ko" }
/{country}/{lang}/products/us/en/products{ country: "us", lang: "en" }
/articles/:slug/articles/welcome-post{ slug: "welcome-post" }

Parameter Bindings: Each route parameter can be bound to a document schema for validation:

{
  "pattern": "/{lang}/landingPage",
  "param_bindings": {
    "lang": "language"
  }
}

This binding tells the CMS:

  1. Extract the lang segment from the URL
  2. Validate it against the language schema (looks for document where content.code matches)
  3. If valid, make the full document available in resolved params

Example: Language-Based Landing Page

Route configuration:

  • Path: /{lang}/landingPage
  • Pattern: /{lang}/landingPage
  • Param bindings: { "lang": "language" }

Your greeting documents:

// greeting / ko
{ "code": "ko", "headline": "Welcome", "subheadline": "Welcome to our platform", "ctaText": "Get Started", "ctaUrl": "/ko/get-started" }

// greeting / en
{ "code": "en", "headline": "Welcome", "subheadline": 

CEL script to fetch localized content:

documents.get("greeting", meta.params.lang).headline

How it resolves:

URLmeta.params.langResult
/ko/landingPage"ko""Welcome"
/en/landingPage"en""Welcome"
/ja/landingPage"ja""Welcome"

Advanced Pattern: Country + Language Routes

For routes like /{country}/{lang}/products:

Route configuration:

{
  "pattern": "/{country}/{lang}/products",
  "param_bindings": {
    "country": "country",
    "lang": "language"
  }
}

CEL scripts:

// Get country name
documents.get("country", meta.params.country).name

// Get localized product list based on country
documents.find("product", { "where": { "country": meta.params.country } })

// Combined: Show country-specific greeting in user's language
documents.get("greeting", meta.params.lang).headline + " from " + documents.get("country", meta.params.country).name

Validation cascade: The CMS validates parameters hierarchically. For /{country}/{lang} routes:

  1. Validates country param against country schema
  2. Validates lang param against language schema
  3. Optionally validates that lang is in country.languages[] array (hierarchical validation)

meta.segments - Raw URL Path Access

meta.segments provides the raw URL path as an array, useful when you need positional access without named parameters.

How it works:

URL Pathmeta.segments
/articles/tech/ai-news["articles", "tech", "ai-news"]
/ko/landingPage["ko", "landingPage"]
/us/en/products/featured["us", "en", "products", "featured"]
/[]

When to Use meta.segments vs meta.params

Use CaseBest Approach
Named parameters from route patternmeta.params.lang
Position-based accessmeta.segments[0]
Getting path depthsize(meta.segments)
Checking if path contains segment"admin" in meta.segments

Examples with meta.segments

// Get first segment (often language code)
meta.segments[0]

// Check path depth
size(meta.segments) > 2 ? "deep" : "shallow"

// Check if we're in admin section
"admin" in meta.segments ? "admin mode" : "public mode"

// Fallback: Use segment if param not bound
has(meta.params.lang) ? meta.params.lang : meta.segments[0]

Complete meta Object Reference

The meta object contains all context about the current request:

PropertyTypeDescription
meta.localestringCurrent locale code (e.g., "en-US", "ko-KR", "ar-SA")
meta.paramsRecord<string, string>Route parameters extracted from URL pattern
meta.segmentsstring[]URL path split into segments
meta.docId`string \null`Current document UUID (null for new documents)
meta.titlestringCurrent document title

meta.locale

The locale code follows BCP 47 format (language-region):

// Check locale for RTL languages
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"

// Get language part only
meta.locale.split("-")[0]  // Not supported - use meta.params.lang instead

meta.params

Route parameters are always strings. The CMS validates them against bound schemas before evaluation:

// Access named parameter
meta.params.lang           // "ko"
meta.params.country        // "us"
meta.params.slug           // "welcome-post"

// Check if parameter exists
has(meta.params.category)  // true/false

// Use in document fetch
documents.get("greeting", meta.params.lang)
documents.ref("airports").get(meta.params.code)

meta.segments

Raw URL segments as an array:

// Access by index (0-based)
meta.segments[0]           // First segment
meta.segments[1]           // Second segment

// Check length
size(meta.segments)        // Number of segments

// Check membership
"products" in meta.segments  // Does path include "products"?

meta.docId

The current document's UUID, useful for self-referential scripts:

// Only available when editing existing documents
meta.docId != null ? "editing" : "creating new"

// Use in conditional logic
meta.docId != null ? documents.get("article", meta.docId).status : "draft"

meta.title

The current document's title:

// Use for display
"Editing: " + meta.title

// Conditional based on title
meta.title.contains("Draft") ? "work in progress" : "published"

documents.ref() - Chained Lookups

For cleaner syntax when the schema is known but identifier is dynamic:

// Traditional approach
documents.get("airports", meta.params.code).name

// Using ref() - schema separate from dynamic identifier
documents.ref("airports").get(meta.params.code).name

Both are equivalent, but ref() makes the dynamic part clearer.


Quick Reference

Document Fetching

documents.get("schema", "identifier")       // Get one document
documents.get("schema", "id").fieldName     // Get a specific field
documents.find("schema")                    // Get all documents
documents.find("schema", { "where": {...}}) // Filtered query
documents.ref("schema").get(identifier)     // Chained lookup
documents.translated("schema", "id", "fr")  // Get with explicit locale

Context Variables

meta.locale          // "en-US", "ar-SA", etc.
meta.params.xyz      // URL parameter named "xyz"
meta.segments        // URL path as array: ["articles", "intro"]
meta.segments[0]     // First path segment
meta.docId           // Current document ID (or null)
meta.title           // Current document title
doc.fieldName        // Current document's field value (in editor context)

Operators

// Comparison
==  !=  <  <=  >  >=

// Logic
&&  ||  !

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

// Membership
"value" in listOrMap

Common Functions

size(list)                    // Count items
size(string)                  // String length
"text".startsWith("te")       // true
"text".endsWith("xt")         // true
"text".contains("ex")         // true
has(object.property)          // Check if property exists
hasProperty(obj, "key")       // Check if object has key (alternative syntax)

Error Messages

If something goes wrong, you'll see one of these:

ErrorWhat It Means
SYNTAX_ERRORTypo in your script (missing quote, bad operator)
TYPE_ERRORYou're mixing types that don't work together
RUNTIME_ERRORThe script ran but hit a problem (undefined variable)
FETCH_LIMIT_EXCEEDEDYou're fetching too many documents (max 50)
TIMEOUTScript took too long (max 5 seconds)
AST_DEPTH_EXCEEDEDExpression too deeply nested (max depth: 50)
SCRIPT_TOO_LONGScript exceeds 5000 character limit

Extensibility & Future Capabilities

The CEL engine is designed for extensibility. Future planned capabilities include:

Planned: MCP Server Integration

// Future: Call external services via MCP
mcp.translate(meta.params.text, "en", meta.params.lang)
mcp.analyze(documents.get("article", meta.params.id).body)

Planned: AI Capabilities

// Future: AI-powered content generation
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"])

These capabilities will be added through the registered function system, maintaining backward compatibility with existing scripts.


Tips

  1. Use autocomplete - Type documents. or meta. and the editor will show available options
  2. Start simple - Test with documents.get("schema", "id") first, then add .fieldName
  3. Check for null - If a document might not exist, add a fallback with != null ? ... : ...
  4. Don't over-fetch - Each documents.get() or documents.find() counts against the 50-fetch limit
  5. Prefer meta.params over meta.segments - Named parameters are validated and more reliable
  6. Use has() for optional params - Check has(meta.params.category) before accessing
  7. Use documents.ref() for dynamic identifiers - Clearer syntax when schema is static but identifier is dynamic
  8. Use doc.fieldName for self-references - Access current document fields within computed expressions

Document-to-Document References

This section covers advanced patterns for linking documents together and building relational content structures.

Basic Reference Pattern

The simplest form: one document references another by identifier.

// Article stores author ID, fetch author's name
documents.get("author", documents.get("article", "intro").authorId).name

Chained Lookups with documents.ref()

For cleaner syntax when the identifier is dynamic:

// Traditional approach
documents.get("country", documents.get("airport", meta.params.code).countryCode).name

// Using ref() - clearer when schema is known but identifier is dynamic
documents.ref("country").get(documents.get("airport", meta.params.code).countryCode).name

Multi-Level Reference Chains

Build deep relationships by chaining multiple lookups:

// Airport → Country → Region → Continent
documents.get("continent",
  documents.get("region",
    documents.get("country",
      documents.get("airport", meta.params.code).countryCode
    ).regionCode
  ).continentCode
).name

Reference with Translation

Combine document references with translations:

// Get localized country name for an airport
documents.translated("country",
  documents.get("airport", meta.params.code).countryCode,
  meta.params.lang
).name

Reference Patterns by Use Case

Pattern 1: Foreign Key Lookup

Document stores an ID referencing another document.

// article / tech-news
{ "title": "Tech Update", "authorId": "author-123", "categoryId": "cat-tech" }
// Resolve author name
documents.get("author", documents.get("article", meta.params.slug).authorId).name

// Resolve category with fallback
documents.get("article", meta.params.slug).categoryId != null
  ? documents.get("category", documents.get("article", meta.params.slug).categoryId).name
  : "Uncategorized"

Pattern 2: Code-Based References

Documents reference each other by semantic codes rather than UUIDs.

// airport / JFK
{ "code": "JFK", "name": "John F. Kennedy International", "countryCode": "us" }

// country / us
{ "code": "us", "name": "United States", "currencyCode": "usd" }

// currency / usd
{ "code": "usd", "symbol": 
// Airport → Country → Currency chain
documents.get("currency",
  documents.get("country",
    documents.get("airport", meta.params.code).countryCode
  ).currencyCode
).symbol
// For JFK: Returns "$"

Pattern 3: Self-Referential with doc Context

Use doc for computed fields that reference other documents based on the current document's values.

// In a product document, fetch related category details
documents.get("category", doc.categoryId).description

// Computed shipping cost based on product's origin country
documents.get("shipping-rates", doc.originCountry).baseRate * doc.weight

Pattern 4: Bidirectional References

When documents reference each other, be careful of fetch limits.

// Get article's author, then get author's other articles (watch fetch count!)
documents.find("article", { "where": { "authorId": documents.get("article", meta.params.slug).authorId } })

Pattern 5: Polymorphic References

When a field can reference different schemas:

// content-block / hero-1
{ "type": "hero", "sourceType": "article", "sourceId": "welcome-post" }

// content-block / hero-2
{ "type": "hero", "sourceType": "product", "sourceId": "featured-item" }
// Dynamic schema lookup based on sourceType
documents.get("content-block", "hero-1").sourceType == "article"
  ? documents.get("article", documents.get("content-block", "hero-1").sourceId).headline
  : documents.get("product", documents.get("content-block", "hero-1").sourceId).name

Dependency Tracking

Every documents.get(), documents.find(), and documents.ref().get() call is tracked for cache invalidation. When a referenced document changes, the CMS knows which CEL expressions need re-evaluation.

Tracked dependencies include:

  • get: schema:identifier - Specific document dependency
  • ref: schema:identifier - Same as get, via chained syntax
  • query: schema:* - Schema-level dependency (any document in schema)

Best Practices for References

  1. Minimize chain depth - Each level adds latency and fetch count
  2. Cache intermediate results - If you need the same nested value twice, fetch the parent once
  3. Use null checks - References can break if documents are deleted
  4. Prefer codes over UUIDs - Codes are readable in expressions and stable across environments
  5. Watch fetch limits - Complex reference chains can hit the 50-fetch limit quickly
// Bad: Fetches same document twice
documents.get("author", documents.get("article", "intro").authorId).name + " - " +
documents.get("author", documents.get("article", "intro").authorId).bio

// Better: Use conditional to check once
documents.get("article", "intro").authorId != null
  ? documents.get("author", documents.get("article", "intro").authorId).name
  : "Unknown Author"

Appendix A: Complete Parametric Route Example

This walkthrough creates a multi-language landing page accessible at /{lang}/landingPage.

Step 1: Create the Greeting Document Schema

In the CMS admin, create a custom schema called greeting:

{
  "name": "greeting",
  "fields": [
    { "name": "code", "type": "string", "required": true },
    { "name": "headline", "type": "string", "required": true },
    { "name": "subheadline"

Step 2: Create Greeting Documents

Create documents for each language:

Document: greeting / ko

{
  "code": "ko",
  "headline": "Welcome",
  "subheadline": "Welcome to our platform",
  "ctaText": "Get Started",
  "ctaUrl": "/ko/get-started"
}

Document: greeting / en

{
  "code": "en",
  "headline": "Welcome",
  "subheadline": "Welcome to our platform",
  "ctaText": "Get Started",
  "ctaUrl": "/en/get-started"
}

Document: greeting / ja

{
  "code": "ja",
  "headline": "Welcome",
  "subheadline": "Welcome to our platform",
  "ctaText": "Start",
  "ctaUrl": "/ja/get-started"
}

Step 3: Create the Route

Create a route with the following configuration:

  • Path: /{lang}/landingPage
  • Pattern: /{lang}/landingPage
  • State: Live
  • Parameter Bindings:
  {
    "lang": "language"
  }

Step 4: Add Blocks with CEL Scripts

Add a hero block to the route with these CEL scripts for each field:

Headline field:

documents.get("greeting", meta.params.lang).headline

Subheadline field:

documents.get("greeting", meta.params.lang).subheadline

CTA Text field:

documents.get("greeting", meta.params.lang).ctaText

CTA URL field:

documents.get("greeting", meta.params.lang).ctaUrl

Step 5: Consume in Next.js

Create a catch-all route in your Next.js app:

// app/[...slug]/page.tsx
import { getCmsClient } from '@repo/renderer';

interface PageProps {
  params: { slug: string[] };
}

export default async function Page({ params }: PageProps) {

Step 6: Test the Routes

Visit these URLs to see localized content:

URLExpected Headline
/ko/landingPageWelcome
/en/landingPageWelcome
/ja/landingPageWelcome

How the Resolution Works

When a user visits /ko/landingPage:

  1. Route Matching: CMS matches /{lang}/landingPage pattern
  2. Parameter Extraction: meta.params.lang = "ko"
  3. Validation: CMS validates "ko" exists in language schema
  4. CEL Evaluation: Scripts like documents.get("greeting", meta.params.lang) resolve to Korean content
  5. Response: Localized blocks returned to the client

Appendix B: Technical Reference

CelMeta Interface (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) */

Parameter Extraction Algorithm

The extractParams function processes URL paths:

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

Supported Parameter Binding Formats

// Simple binding (uses "code" field for lookup)
{ "lang": "language" }

// Detailed binding (custom slug field)
{
  "lang": {
    "schemaName": "language",
    "slugField": "code"
  },
  "slug": {
    "schemaName": "article",
    "slugField": "slug"
  }

Document Lookup Priority

When fetching via documents.get(schema, identifier):

  1. UUID match: If identifier is a valid UUID, fetch by id
  2. Code field: Check content.code field
  3. Slug field: Check content.slug field
  4. Title match: Check title field

This allows flexible document references by any unique identifier.

]
] }
"Welcome to our platform"
,
"ctaText"
:
"Get Started"
,
"ctaUrl"
:
"/en/get-started"
}
// greeting / ja
{ "code": "ja", "headline": "Welcome", "subheadline": "Welcome to our platform", "ctaText": "Start", "ctaUrl": "/ja/get-started" }
"$"
,
"name"
:
"US Dollar"
}
,
"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('/');
// Get route and resolved params
const { route, resolvedParams } = await client.routes.getRouteByPath.query({
websiteId: process.env.CMS_WEBSITE_ID!,
path,
});
// Get blocks for the route
const blocks = await client.blocks.getBlocks.query({
websiteId: process.env.CMS_WEBSITE_ID!,
blockIds: route.block_ids,
// Pass resolved params for CEL context
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: {},
},
});
// Render blocks
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" }
}