Journey Docs
CMS

Components and Modules

The block-type registry, field kinds, owner scoping, the module compiler, saved sections, theme kits, and media assets

A module is one placed instance of a block type. Block types live in a registry (core.cms_block_type_schemas); instances live in core.cms_page_blocks and are positioned by the layout tree.

block_type is an open string backed by the registry rather than a closed union in code, so new modules are a data operation, not a deploy — with the important exception of field kinds, which are code-curated.

The registry

Each block type declares an ordered set of fields. A field name must be a stable, identifier-safe JSON content key — lowercase letters and underscores only, matching /^[a-z][a-z_]*$/. Display names go in label instead, so renaming a label never rewrites stored content.

Platform block types use the same semantic-slug pattern (hero, rich_text, pricing_table), capped at 50 characters. ULIDs and legacy org_13__… keys contain digits, so they cannot collide with a new platform type.

Writes are gated by admin:block-schemas:manage. There is no partner self-serve surface.

Owner scoping

Block types are optionally owned by an organization, brand, or property via owner_entity_type / owner_entity_id. A NULL owner means platform-wide.

For an owned module the client supplies a plain slug plus an owner, and the server stores a derived, globally unique block_type, returning the plain slug and owner alongside it. Two organizations can each hold a pricing_table without colliding.

A page's available palette is the union of platform modules plus every module owned by the page's own entity and each of its ancestors (property → brand → organization).

This deliberately differs from theme-kit resolution, which short-circuits on the first match. A theme is a single value to override; a module palette is a set to accumulate.

Placing a block whose type is not available to its page's owner is rejected at both resolution points — the flat block endpoint and inline layout modules — so an owned module cannot reach another partner's page by either route.

The two readers of "which block types exist" are split:

EndpointQuestionPermission
GET /admin/pages/:pageGroupId/block-schemaspage-scoped palette, owner read off cms_page_ownershipadmin:pages:read
GET /admin/cms/block-schemasregistry management, with owner filtersadmin:block-schemas:read

Requesting another owner's palette is unexpressible on the page-scoped route rather than merely refused.

Field kinds

Field kinds are a fixed, code-curated set mirrored by a database CHECK constraint. Growing the set is an engineering change — a migration and a deploy — not an admin API operation.

textFieldrichTextFieldimageField
linkFieldurlFieldiconField
booleanFieldnumberFieldchoiceField
colorFielddateFielddateTimeField
groupFieldobjectFieldpropertyField
programField

groupField and objectField are group-like and may nest further field definitions. propertyField and programField resolve against Journey property and loyalty-program records rather than storing free values.

Conditional visibility

A field may be shown or hidden based on another field's value. Operators are deliberately narrow:

OperatorOperand
EQUAL, NOT_EQUALa literal value
EMPTY, NOT_EMPTYnone

Only field kinds with a closed, schema-known value domain may control visibility, so a criterion's operand can be validated at schema-write time. Regex and numeric comparators are excluded. Hidden fields are skipped during content validation, so a hidden required field does not block a save.

The module compiler

Registering fields alone gives a type structure but no presentation. The compiler adds type-level html and css on cms_block_type_schemas, with a small Liquid-shaped dialect that injects that type's own fields:

  • {{ module.field_name }}, {% if %}, and {% for %} over a groupField.
  • No {% include %}, no macros, and no module JavaScript.
  • Unknown field references fail at schema-write time.
  • Field values auto-escape; richTextField compiles Lexical to HTML.

Interactivity is HTML-native only: <details>/<summary> for disclosure, and <dialog popover> with the Popover API for modals. The sanitizer strips scripts, on* handlers, and javascript: URLs. Accordion and modal behavior therefore need neither author JS nor a website helper.

New owner-scoped creates require html (css optional). Platform-wide creates stay fields-only, and platform modules such as hero, signup, and perks remain website components. Existing owner-scoped rows without a template keep the generic CmsCustomBlock rendering until someone PATCHes html onto them.

Compilation happens in the API — live for drafts and assistant or iframe preview, then snapshotted as compiledHtml / compiledCss onto cms_page_blocks on publish. A public read of a published page serves the snapshot and does not recompile.

Editing a type's template does not rewrite live pages. Pages already published keep their snapshot until they are republished.

custom_html remains instance-level chrome, not a reusable type.

Saved sections

A saved section is an owner-scoped, named library item holding a full root section tree plus its module content.

  • Inserting one creates a live-linked instance. Editing and publishing the library item updates every page still referencing it. Unlink clones the current tree onto the page and drops the link.
  • Saved sections have their own draft and publish. A library edit does not reach live partner sites until the saved section is published, and publishing it invalidates cached public reads for every referencing page without republishing those pages.
  • Owner isolation matches the module palette: the picker lists the page owner's items plus ancestors. Sibling brands and unrelated organizations are unexpressible from the picker and rejected on insert.
  • Saving converts the source section on that draft into a linked instance, so the original page stays on the live link rather than drifting as a disconnected copy.

No new permission was added — list, save, insert, and unlink use admin:pages:read / admin:pages:manage, like the rest of the page palette.

Supporting modules

ModulePathRole
Theme kitssrc/core/cms/theme-kits/owner-scoped brand values with property → brand → organization inheritance; resolution short-circuits on first match
Media assetssrc/core/cms/media-assets/CMS-hosted images served from the media CDN; includes import-from-URL and research capture
Assistantsrc/core/cms/assistant/the CMS Assistant
Pagessrc/core/cms/pages/pages, blocks, layout, compiler, lexical, theme, promotion

Source: api/src/core/cms/, api/src/core/cms/pages/blocks/.

On this page