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

テンプレートビルダーでのスクリプティング

CMS で CEL 式を書くための実践的なガイド。

Continue Reading
Previous‹編集モード対応の静的レンダリングNextCreate Profound Next›

ハイブリッド

レンダラー・プロジェクトパラメトリックルーティングコンポーネントの種類Sse管理パネルプロキシの設定編集モード対応の静的レンダリングテンプレートビルダーでのスクリプティングCreate Profound Next

ヘッドレス

クイックスタートJsonとクロードコードComponent Zod Pull

Mcp

Mcp

CMS機能

Feat Docs Templateテンプレートビルダー機能Feat トランスレーターFeat 組織

やる気

私たちのアプローチ

専門用語

ハイブリッド対ヘッドレス

CMS で CEL 式を書くための実践的なガイド。


CEL の仕組み

CEL(Common Expression Language)は、当社の CMS に組み込まれている軽量スクリプト言語です。ドキュメントからデータを取得したり、URL パラメーターを読み取ったり、その場で値を計算したりできる動的な式を記述できます。

CEL スクリプトが実行されるときの流れ:

あなたのスクリプト             エンジン                        結果
    |                              |                              |
    v                              v                              v
documents.get("article", "intro") --> データベースから取得 --> { headline: "ようこそ", body: "..." }
         .headline                --> フィールドを抽出      --> "ようこそ"

CEL は読み取り専用のクエリ言語だと考えてください。データベースの内容を変更することはできず、データを読み取り計算結果を返すだけです。このため CMS のどこでも安全に利用できます。


基本構成要素

CEL 式は次の 3 つにアクセスできます:

オブジェクト内容例
documentsCMS から任意のドキュメントを取得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"


URL パラメーターを使う

ページに動的なルート(/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" }
]

実践例

例 1: 別のドキュメントからヒーローブロックのタイトルを取得

ヒーローブロックに、article ドキュメントから取得した見出しを表示したいとします。

あなたの article ドキュメント(識別子: "homepage-hero"):

{
  "headline": "より速く構築し、よりスマートに出荷",
  "subheadline": "開発者のためのモダンな CMS"
}

ヒーローブロックのタイトルフィールドに記述する CEL スクリプト:

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

結果: ヒーローには "より速く構築し、よりスマートに出荷" が表示されます


例 2: コードから国名を取得

現在 /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"
  • 結果: "サウジアラビア"

例 3: ロケールに基づく条件付きコンテンツ

ユーザーのロケールに応じて異なる見出しを表示します。

meta.locale == "ar-SA" ? "مرحبا بكم" : "Welcome"

ロケールが "ar-SA" の場合: 戻り値は "مرحبا بكم" それ以外のロケールの場合: 戻り値は "Welcome"


例 4: ドキュメントの連鎖的な参照

あなたの article には countryCode フィールドがあり、完全な国名を取得したいとします。

article ドキュメント:

{ "headline": "米国からのニュース", "countryCode": "us" }

CEL スクリプト:

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

処理の流れ:

  1. documents.get("article", "us-news") は { "headline": "米国からのニュース", "countryCode": "us" } を返します
  2. .countryCode で "us" を取り出します
  3. documents.get("country", "us") は { "code": "us", "name": "アメリカ合衆国", ... } を返します
  4. .name で "アメリカ合衆国" を取得します

結果: "アメリカ合衆国"


例 5: フォールバック値

ドキュメントが存在しない可能性がある場合は、次のようにフォールバックを用意できます:

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"

例 6: リストの操作

記事にタグがあり、特定のタグが存在するか確認したい場合:

"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(タグの数)


パラメトリックルートと meta.params

パラメトリックルートは、動的でローカライズされたページを構築するための鍵です。/{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 は次のように動作します:

  1. URL から lang セグメントを抽出
  2. language スキーマで検証(content.code に一致するドキュメントを検索)
  3. 有効であれば、解決済みパラメーターとして完全なドキュメントを利用可能にする

例: 言語別ランディングページ

ルート設定:

  • パス: /{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

解決の様子:

URLmeta.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} のようなルートでは:

  1. country パラメーターを country スキーマで検証
  2. lang パラメーターを language スキーマで検証
  3. 任意で、lang が country.languages[] 配列に含まれていることを検証(階層的検証)

meta.segments - 生の URL パスアクセス

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.segments と meta.params を使い分けるタイミング

ユースケース最適なアプローチ
ルートパターンによる名前付きパラメーターmeta.params.lang
位置に基づくアクセスmeta.segments[0]
パスの深さを取得size(meta.segments)
パスが特定のセグメントを含むか確認"admin" in meta.segments

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 オブジェクトには現在のリクエストに関するすべてのコンテキストが含まれています:

プロパティ型説明
meta.localestring現在のロケールコード(例: "en-US", "ko-KR", "ar-SA")
meta.paramsRecord<string, string>URL パターンから抽出されたルートパラメーター
meta.segmentsstring[]URL パスを分割したセグメント
meta.docIdstring \\ | null現在のドキュメント UUID(新規ドキュメントの場合は null)
meta.titlestring現在のドキュメントタイトル

meta.locale

ロケールコードは BCP 47 形式(言語-地域)に従います:

// RTL 言語かどうかを判定
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"

// 言語部分のみを取得
meta.locale.split("-")[0]  // 非対応 - 代わりに meta.params.lang を使用

meta.params

ルートパラメーターは常に文字列です。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)

meta.segments

生の URL セグメントを配列で取得します:

// インデックス(0 始まり)でアクセス
meta.segments[0]           // 最初のセグメント
meta.segments[1]           // 2 番目のセグメント

// 長さを確認
size(meta.segments)        // セグメント数

// 含まれているか確認
"products" in meta.segments  // パスに "products" が含まれるか?

meta.docId

現在のドキュメントの UUID。自己参照スクリプトに便利です:

// 既存ドキュメントを編集するときのみ利用可能
meta.docId != null ? "editing" : "creating new"

// 条件ロジックで利用
meta.docId != null ? documents.get("article", meta.docId).status : "draft"

meta.title

現在のドキュメントタイトル:

// 表示に利用
"Editing: " + meta.title

// タイトルに基づく条件分岐
meta.title.contains("Draft") ? "work in progress" : "published"

documents.ref() - 連鎖的ルックアップ

スキーマが確定していて識別子だけが動的な場合、より読みやすい構文を提供します:

// 従来の書き方
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 経由で外部サービスを呼び出す
mcp.translate(meta.params.text, "en", meta.params.lang)
mcp.analyze(documents.get("article", meta.params.id).body)

計画中: AI 機能

// 将来: 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"])

これらの機能は登録済み関数システムを通じて追加され、既存のスクリプトとの後方互換性が維持されます。


ヒント

  1. オートコンプリートを活用 - documents. や meta. と入力すると、エディターが利用可能なオプションを表示します
  2. シンプルに始める - まずは documents.get("schema", "id") で試し、その後 .fieldName を追加しましょう
  3. null を確認 - ドキュメントが存在しない可能性がある場合は、!= null ? ... : ... でフォールバックを追加します
  4. 取得しすぎない - documents.get() や documents.find() を呼び出すたびに 50 件の取得制限にカウントされます
  5. meta.segments より meta.params を優先 - 名前付きパラメーターは検証されており、より信頼できます
  6. 任意パラメーターには has() を使用 - has(meta.params.category) でアクセス前に確認します
  7. 識別子が動的な場合は documents.ref() を使用 - スキーマが固定で識別子だけが変わるときに読みやすくなります

付録 A: パラメトリックルート完全例

この手順では、/{lang}/landingPage でアクセスできる多言語ランディングページを作成します。

ステップ 1: greeting ドキュメントスキーマを作成

CMS 管理画面で greeting というカスタムスキーマを作成します:

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

ステップ 2: 挨拶ドキュメントを作成

各言語用のドキュメントを作成します:

ドキュメント: 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"
}

ステップ 3: ルートを作成

次の設定でルートを作成します:

  • パス: /{lang}/landingPage
  • パターン: /{lang}/landingPage
  • ステータス: ライブ
  • パラメーターバインディング:
  {
    "lang": "language"
  }

ステップ 4: CEL スクリプト付きのブロックを追加

ルートにヒーローブロックを追加し、各フィールドに次の 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

ステップ 5: Next.js で利用する

Next.js アプリにキャッチオールルートを作成します:

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

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

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

ステップ 6: ルートをテスト

次の URL にアクセスしてローカライズされたコンテンツを確認します:

URL期待される見出し
/ko/landingPage환영합니다
/en/landingPageWelcome
/ja/landingPageようこそ

解決が行われる仕組み

ユーザーが /ko/landingPage にアクセスした場合:

  1. ルートマッチング: CMS が /{lang}/landingPage パターンに一致させます
  2. パラメーター抽出: meta.params.lang = "ko"
  3. 検証: CMS が language スキーマに "ko" が存在することを検証
  4. CEL 評価: documents.get("greeting", meta.params.lang) などのスクリプトが韓国語のコンテンツに解決
  5. レスポンス: ローカライズされたブロックがクライアントに返されます

付録 B: 技術リファレンス

CelMeta インターフェース(TypeScript)

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) で検索するときの優先順位:

  1. UUID の一致: 識別子が有効な UUID なら id で取得
  2. code フィールド: content.code フィールドをチェック
  3. slug フィールド: content.slug フィールドをチェック
  4. タイトルの一致: title フィールドをチェック

これにより、一意の識別子であれば柔軟にドキュメントを参照できます。

:
"ようこそ"
,
"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('/');
// ルートと解決済みパラメーターを取得
const { route, resolvedParams } = await client.routes.getRouteByPath.query({
websiteId: process.env.CMS_WEBSITE_ID!,
path,
});
// ルート用のブロックを取得
const blocks = await client.blocks.getBlocks.query({
websiteId: process.env.CMS_WEBSITE_ID!,
blockIds: route.block_ids,
// 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: {},
},
});
// ブロックをレンダー
return (
<main>
{blocks.map((block) => (
<BlockRenderer
key={block.id}
block={block}
routeParams={resolvedParams}
language={resolvedParams?.lang?.value}
/>
))}
</main>
);
}
docId
:
string
|
null
;
/** 現在のドキュメントタイトル */
title: string;
}
4. 各セグメントのペアについて:
- パターンが : または {} で始まる場合はパラメーターとして抽出
- それ以外は完全一致である必要がある
5. 戻り値: { country: "us", lang: "en" }