Page Types
Partner and loyalty program join pages, the draft/published/archived lifecycle, clone-on-edit, member gating, routing, and environment promotion
A CMS page carries exactly one page_type. The type is fixed in code, not
registry data, because it changes which modules are available and how the page
is reached.
page_type | Purpose |
|---|---|
partner | General partner and campaign pages — the default |
loyalty_program_join | The join page for a specific loyalty program |
A loyalty_program_join page is bound to its program through
core.cms_loyalty_program_join_pages, keyed by program_id (the partner's custom loyalty program external_id) with a unique
page_group_id. One cusotm loyalty program has at most one join page, and deleting the program
cascades.
Module availability by type
The page type gates part of the palette:
| Block type | partner | loyalty_program_join |
|---|---|---|
program_join_form | unavailable | available |
signup | available | unavailable |
member_auth_action | available | unavailable |
| everything else | available | available |
A join page owns its own join form, so the generic signup and member-auth modules would be redundant and ambiguous there.
Lifecycle
state is one of draft, published, or archived.
Pages are grouped by a ULID page_group_id rather than a single row. A page
group holds at most one row per state, enforced by
idx_cms_pages_group_state_unique.
Clone-on-edit
Editing a published page does not mutate it. A draft row is cloned from the
published row and edited in isolation; publishing promotes the draft
transactionally. This is why slug uniqueness is (slug, state) rather than a
flat unique on slug — a draft intentionally shares its slug with the
still-live published sibling it will replace.
Cloning preserves layout nodeKey values and remaps internal IDs, so layout and
block identity survive the round trip and in-flight layout changes cannot leak
into the live page.
Member gating
Two columns on core.cms_pages control access:
| Column | Meaning |
|---|---|
member_auth_required | the page is member-gated |
member_gate_program_id | the program a turned-away visitor is sent to join |
member_gate_program_id overrides owner attribution; NULL falls back to it.
It is an internal id — resolution emits the program's external_id.
The gate program is resolved from the route's owner rather than from the
page's ownership record, so it does not depend on the caller having requested
the entityOwner hydrate. A client that omitted that hydrate would otherwise
silently receive null and bounce its visitors to a bare sign-in screen.
Member-gated pages are always noindex and nofollow — see
SEO details.
Routing
core.cms_page_routes maps a page group to a path, an owner, and optionally a
custom domain.
| Constraint | Effect |
|---|---|
unique page_group_id | one route per page group |
unique (owner_entity_type, owner_entity_id, path) | no duplicate path within an owner |
unique (custom_domain_id, path) where domain is not null | no duplicate path on a custom domain |
Every owner also gets one fallback host in core.cms_fallback_hosts,
created automatically the first time a route is made for that owner and reused
thereafter. Its label is derived from the owner's external id, so the hostname
is always {owner external id}.sites.journey.com — a working URL before any
custom domain exists. The suffix is CMS_SITE_HOST_SUFFIX; the label is
normalized to a valid 63-character DNS label rather than chosen by an author.
Resolution produces both hostnames and picks a canonical one:
canonicalHostname = customHostname ?? fallbackHostnameA custom domain only contributes once its status is active; a page group whose
custom domain is pending or removed continues to serve on the fallback host.
Deleting a custom domain sets the route's custom_domain_id to NULL rather
than removing the route.
Environment promotion
Pages and block schemas are promoted between environments rather than authored
separately in each. The promotion logic lives in
api/src/core/cms/pages/promotion/, driven by two scripts:
| Script | Promotes |
|---|---|
api/scripts/promote-cms-pages.ts | page groups with their layout, blocks, theme, and SEO |
api/scripts/promote-cms-block-schemas.ts | block-type schemas |
Block schemas promote first — a page cannot land in an environment whose registry lacks its module types.
Storage
| Table | Holds |
|---|---|
core.cms_pages | slug, internal name, type, state, member gate |
core.cms_page_ownership | organization, brand, or property owner |
core.cms_page_routes | path, owner, optional custom domain |
core.cms_fallback_hosts | one host label per owner |
core.cms_loyalty_program_join_pages | program to page-group binding |
core.cms_page_blocks | module instances and published compile snapshots |
Source: api/src/core/cms/pages/schema/, api/src/core/cms/pages/promotion/.