CMS で CEL 式を書くための実践的なガイド。
CMS で CEL 式を書くための実践的なガイド。
CEL(Common Expression Language)は、当社の CMS に組み込まれている軽量スクリプト言語です。ドキュメントからデータを取得したり、URL パラメーターを読み取ったり、その場で値を計算したりできる動的な式を記述できます。
CEL スクリプトが実行されるときの流れ:
あなたのスクリプト エンジン 結果
| | |
v v v
documents.get("article", "intro") --> データベースから取得 --> { headline: "ようこそ", body: "..." }
.headline --> フィールドを抽出 --> "ようこそ"
CEL は読み取り専用のクエリ言語だと考えてください。データベースの内容を変更することはできず、データを読み取り計算結果を返すだけです。このため CMS のどこでも安全に利用できます。
CEL 式は次の 3 つにアクセスできます:
| オブジェクト | 内容 | 例 |
|---|---|---|
documents | CMS から任意のドキュメントを取得 | documents.get("country", "us") |
meta | 現在のリクエスト情報(ロケール、URL パラメーターなど) | meta.locale, meta.params.slug |
schema | 現在のドキュメントのフィールド定義 | schema.fields |
CEL の最も強力な機能は、CMS 内のどこからでもドキュメントを取得できることです。
構文: documents.get(schemaName, identifier)
"welcome-post" という識別子で保存された article ドキュメントがあるとします:
// CMS には article / welcome-post として保存
{
"headline": "私たちのプラットフォームへようこそ",
"author": "Sarah Chen",
"body": "このたびのお知らせを共有できてとてもうれしく思います...",
"tags": ["お知らせ", "ニュース"]
}
ドキュメント全体を取得する:
documents.get("article", "welcome-post")
戻り値:
{
"headline": "私たちのプラットフォームへようこそ",
"author": "Sarah Chen",
"body": "このたびのお知らせを共有できてとてもうれしく思います...",
"tags": ["お知らせ", "ニュース"]
}
見出しだけを取得する:
documents.get("article", "welcome-post").headline
戻り値: "私たちのプラットフォームへようこそ"
著者を取得する:
documents.get("article", "welcome-post").author
戻り値: "Sarah Chen"
ページに動的なルート(/articles/[slug] のような)がある場合は、meta.params を使って URL パラメーターを取得し、正しいドキュメントを取得できます。
誰かが /articles/welcome-post にアクセスした場合:
documents.get("article", meta.params.slug).headline
戻り値: "私たちのプラットフォームへようこそ"
このようにして動的ページを構築します。URL に含まれるスラッグを使用するので、同じ CEL スクリプトがどの記事にも対応します。
構文: documents.find(schemaName) または documents.find(schemaName, filter)
// すべての国を取得
documents.find("country")
戻り値:
[
{ "code": "us", "name": "アメリカ合衆国", "flag": "US" },
{ "code": "sa", "name": "サウジアラビア", "flag": "SA" },
{ "code": "gb", "name": "イギリス", "flag": "GB" }
]
// フィルター付きで国を取得
documents.find("country", { "where": { "code": "us" } })
戻り値:
[
{ "code": "us", "name": "アメリカ合衆国", "flag": "US" }
]
ヒーローブロックに、article ドキュメントから取得した見出しを表示したいとします。
あなたの article ドキュメント(識別子: "homepage-hero"):
{
"headline": "より速く構築し、よりスマートに出荷",
"subheadline": "開発者のためのモダンな CMS"
}
ヒーローブロックのタイトルフィールドに記述する CEL スクリプト:
documents.get("article", "homepage-hero").headline
結果: ヒーローには "より速く構築し、よりスマートに出荷" が表示されます
現在 /countries/[code] のページを作成しており、完全な国名を表示したいとします。
国ドキュメント:
// country / us
{ "code": "us", "name": "アメリカ合衆国", "flag": "US", "languages": ["en", "es"] }
// country / sa
{ "code": "sa", "name": "サウジアラビア", "flag": "SA", "languages": ["ar", "en"] }
CEL スクリプト:
documents.get("country", meta.params.code).name
誰かが /countries/us にアクセスしたとき:
meta.params.code = "us""アメリカ合衆国"誰かが /countries/sa にアクセスしたとき:
meta.params.code = "sa""サウジアラビア"ユーザーのロケールに応じて異なる見出しを表示します。
meta.locale == "ar-SA" ? "مرحبا بكم" : "Welcome"
ロケールが "ar-SA" の場合: 戻り値は "مرحبا بكم"
それ以外のロケールの場合: 戻り値は "Welcome"
あなたの article には countryCode フィールドがあり、完全な国名を取得したいとします。
article ドキュメント:
{ "headline": "米国からのニュース", "countryCode": "us" }
CEL スクリプト:
documents.get("country", documents.get("article", "us-news").countryCode).name
処理の流れ:
documents.get("article", "us-news") は { "headline": "米国からのニュース", "countryCode": "us" } を返します.countryCode で "us" を取り出しますdocuments.get("country", "us") は { "code": "us", "name": "アメリカ合衆国", ... } を返します.name で "アメリカ合衆国" を取得します結果: "アメリカ合衆国"
ドキュメントが存在しない可能性がある場合は、次のようにフォールバックを用意できます:
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"
記事にタグがあり、特定のタグが存在するか確認したい場合:
"featured" in documents.get("article", "welcome-post").tags
戻り値: 記事に "featured" タグがあれば true
最初のタグを取得:
documents.get("article", "welcome-post").tags[0]
戻り値: "お知らせ"(最初のタグ)
タグ数を数える:
size(documents.get("article", "welcome-post").tags)
戻り値: 2(タグの数)
パラメトリックルートは、動的でローカライズされたページを構築するための鍵です。/{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 は次のように動作します:
lang セグメントを抽出language スキーマで検証(content.code に一致するドキュメントを検索)ルート設定:
/{lang}/landingPage/{lang}/landingPage{ "lang": "language" }挨拶ドキュメント:
// greeting / ko
{ "code": "ko", "headline": "환영합니다", "subheadline": "우리 플랫폼에 오신 것을 환영합니다" }
// greeting / en
{ "code": "en", "headline": "Welcome", "subheadline": "Welcome to our platform" }
// greeting / ja
{ "code": "ja", "headline"
ローカライズされたコンテンツを取得する CEL スクリプト:
documents.get("greeting", meta.params.lang).headline
解決の様子:
| URL | meta.params.lang | 結果 |
|---|---|---|
/ko/landingPage | "ko" | "환영합니다" |
/en/landingPage | "en" | "Welcome" |
/ja/landingPage | "ja" | "ようこそ" |
/{country}/{lang}/products のようなルートの場合:
ルート設定:
{
"pattern": "/{country}/{lang}/products",
"param_bindings": {
"country": "country",
"lang": "language"
}
}
CEL スクリプト:
// 国名を取得
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
バリデーションの流れ:
CMS はパラメーターを階層的に検証します。/{country}/{lang} のようなルートでは:
country パラメーターを country スキーマで検証lang パラメーターを language スキーマで検証lang が country.languages[] 配列に含まれていることを検証(階層的検証)meta.segments は URL パスを配列として提供し、名前付きパラメーターなしで位置に基づくアクセスが必要なときに便利です。
仕組み:
| URL パス | meta.segments |
|---|---|
/articles/tech/ai-news | ["articles", "tech", "ai-news"] |
/ko/landingPage | ["ko", "landingPage"] |
/us/en/products/featured | ["us", "en", "products", "featured"] |
/ | [] |
| ユースケース | 最適なアプローチ |
|---|---|
| ルートパターンによる名前付きパラメーター | 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.locale | string | 現在のロケールコード(例: "en-US", "ko-KR", "ar-SA") | |
meta.params | Record<string, string> | URL パターンから抽出されたルートパラメーター | |
meta.segments | string[] | URL パスを分割したセグメント | |
meta.docId | string \\ | null | 現在のドキュメント UUID(新規ドキュメントの場合は null) | |
meta.title | string | 現在のドキュメントタイトル |
ロケールコードは BCP 47 形式(言語-地域)に従います:
// RTL 言語かどうかを判定
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"
// 言語部分のみを取得
meta.locale.split("-")[0] // 非対応 - 代わりに meta.params.lang を使用
ルートパラメーターは常に文字列です。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)
生の URL セグメントを配列で取得します:
// インデックス(0 始まり)でアクセス
meta.segments[0] // 最初のセグメント
meta.segments[1] // 2 番目のセグメント
// 長さを確認
size(meta.segments) // セグメント数
// 含まれているか確認
"products" in meta.segments // パスに "products" が含まれるか?
現在のドキュメントの UUID。自己参照スクリプトに便利です:
// 既存ドキュメントを編集するときのみ利用可能
meta.docId != null ? "editing" : "creating new"
// 条件ロジックで利用
meta.docId != null ? documents.get("article", meta.docId).status : "draft"
現在のドキュメントタイトル:
// 表示に利用
"Editing: " + meta.title
// タイトルに基づく条件分岐
meta.title.contains("Draft") ? "work in progress" : "published"
スキーマが確定していて識別子だけが動的な場合、より読みやすい構文を提供します:
// 従来の書き方
documents.get("airports", meta.params.code).name
// ref() を使用 - スキーマと動的識別子を分離
documents.ref("airports").get(meta.params.code).name
どちらも同じ結果ですが、ref() を使うと動的な部分がより明確になります。
documents.get("schema", "identifier") // 1 件のドキュメントを取得
documents.get("schema", "id").fieldName // 特定フィールドを取得
documents.find("schema") // すべてのドキュメントを取得
documents.find("schema", { "where": {...}}) // フィルター付きクエリ
documents.ref("schema").get(identifier) // 連鎖的ルックアップ
meta.locale // "en-US", "ar-SA" など
meta.params.xyz // "xyz" という名前の URL パラメーター
meta.segments // URL パスの配列: ["articles", "intro"]
meta.segments[0] // 最初のパスセグメント
meta.docId // 現在のドキュメント ID(または null)
meta.title // 現在のドキュメントタイトル
// 比較
== != < <= > >=
// 論理
&& || !
// 三項演算子(if-else)
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) // プロパティが存在するか確認
問題が発生した場合、次のいずれかのエラーが表示されます:
| エラー | 意味 |
|---|---|
SYNTAX_ERROR | スクリプトにタイポがある(引用符の欠落、不正な演算子など) |
TYPE_ERROR | 互換性のない型を混在させている |
RUNTIME_ERROR | スクリプトは実行されたが問題が発生した(未定義の変数など) |
FETCH_LIMIT_EXCEEDED | 取得件数が多すぎる(最大 50 件) |
TIMEOUT | スクリプトの実行に時間がかかりすぎた(最大 5 秒) |
AST_DEPTH_EXCEEDED | 式のネストが深すぎる(最大深さ 50) |
SCRIPT_TOO_LONG | スクリプトが 5000 文字の上限を超えている |
CEL エンジンは拡張を前提に設計されています。今後予定されている機能には次のものがあります。
// 将来: MCP 経由で外部サービスを呼び出す
mcp.translate(meta.params.text, "en", meta.params.lang)
mcp.analyze(documents.get("article", meta.params.id).body)
// 将来: AI によるコンテンツ生成
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"])
これらの機能は登録済み関数システムを通じて追加され、既存のスクリプトとの後方互換性が維持されます。
documents. や meta. と入力すると、エディターが利用可能なオプションを表示しますdocuments.get("schema", "id") で試し、その後 .fieldName を追加しましょう!= null ? ... : ... でフォールバックを追加しますdocuments.get() や documents.find() を呼び出すたびに 50 件の取得制限にカウントされますhas(meta.params.category) でアクセス前に確認しますこの手順では、/{lang}/landingPage でアクセスできる多言語ランディングページを作成します。
CMS 管理画面で greeting というカスタムスキーマを作成します:
{
"name": "greeting",
"fields": [
{ "name": "code", "type": "string", "required": true },
{ "name": "headline", "type": "string", "required": true },
{ "name": "subheadline"
各言語用のドキュメントを作成します:
ドキュメント: greeting / ko
{
"code": "ko",
"headline": "환영합니다",
"subheadline": "우리 플랫폼에 오신 것을 환영합니다",
"ctaText": "시작하기",
"ctaUrl": "/ko/get-started"
}
ドキュメント: greeting / en
{
"code": "en",
"headline": "Welcome",
"subheadline": "Welcome to our platform",
"ctaText": "Get Started",
"ctaUrl": "/en/get-started"
}
ドキュメント: greeting / ja
{
"code": "ja",
"headline": "ようこそ",
"subheadline": "私たちのプラットフォームへようこそ",
"ctaText": "始める",
"ctaUrl": "/ja/get-started"
}
次の設定でルートを作成します:
/{lang}/landingPage/{lang}/landingPage {
"lang": "language"
}
ルートにヒーローブロックを追加し、各フィールドに次の CEL スクリプトを設定します:
見出しフィールド:
documents.get("greeting", meta.params.lang).headline
サブ見出しフィールド:
documents.get("greeting", meta.params.lang).subheadline
CTA テキストフィールド:
documents.get("greeting", meta.params.lang).ctaText
CTA URL フィールド:
documents.get("greeting", meta.params.lang).ctaUrl
Next.js アプリにキャッチオールルートを作成します:
// app/[...slug]/page.tsx
import { getCmsClient } from '@repo/renderer';
interface PageProps {
params: { slug: string[] };
}
export default async function Page({ params }: PageProps) {
次の URL にアクセスしてローカライズされたコンテンツを確認します:
| URL | 期待される見出し |
|---|---|
/ko/landingPage | 환영합니다 |
/en/landingPage | Welcome |
/ja/landingPage | ようこそ |
ユーザーが /ko/landingPage にアクセスした場合:
/{lang}/landingPage パターンに一致させますmeta.params.lang = "ko"language スキーマに "ko" が存在することを検証documents.get("greeting", meta.params.lang) などのスクリプトが韓国語のコンテンツに解決interface CelMeta {
/** 現在のロケールコード(例: 'en-US') */
locale: string;
/** URL から抽出されたルートパラメーター */
params: Record<string, string>;
/** URL パスのセグメント */
segments: string[];
/** 現在のドキュメント ID(既存ドキュメントを編集中の場合) */
extractParams 関数は URL パスを次のように処理します:
Pattern: /{country}/{lang}/products
Path: /us/en/products
Algorithm:
1. どちらも正規化(末尾のスラッシュを削除)
2. セグメントに分割: ["us", "en", "products"] と ["{country}", "{lang}", "products"]
3. セグメント数が一致することを確認
// シンプルなバインディング("code" フィールドで検索)
{ "lang": "language" }
// 詳細なバインディング(カスタム slug フィールド)
{
"lang": {
"schemaName": "language",
"slugField": "code"
},
"slug": {
"schemaName": "article",
"slugField": "slug"
}
}
documents.get(schema, identifier) で検索するときの優先順位:
id で取得content.code フィールドをチェックcontent.slug フィールドをチェックtitle フィールドをチェックこれにより、一意の識別子であれば柔軟にドキュメントを参照できます。