ChaiBuilder Logo

Pages vs CMS

A ChaiBuilder site holds content in two different shapes, and picking the right one for each piece of content is the single highest-leverage modelling decision you will make.

  • Visual pages live in the builder. The layout is the content: a tree of blocks, styled in place, unique per page.
  • Structured content lives in the CMS as collections. The fields are the content: many records of the same shape, with no layout of their own.

The rule of thumb: if you would design it once and fill it in many times, it is structured content. If each one is its own composition, it is a visual page.

Side by side

Visual pages Structured content
Edited in The visual builder The CMS admin panel
Shape A tree of blocks Typed fields on a collection
Layout Unique per page, editor-controlled None; supplied by whatever renders it
Good for Home, landing, pricing, campaign pages Blog posts, docs, legal, jobs, releases, team members
Stored in ChaiBuilder's own page tables Payload collections
Draft/publish Draft row and published row, published on demand Payload's own draft and publish
Scales by Adding pages by hand Adding records

Both live in the same database, and neither one owns the other. There is no hard relation between a page row and a collection record - the connection is made at render time by the bridges below.

The four bridges

Structured content is useless if it cannot reach a page, and a page is limited if it can only show hand-typed text. Four mechanisms connect them.

1. Page types - one layout, many records

A page type is a visually designed page that acts as the layout for every record in a collection. You build /blog/[slug] once in the builder; a data provider resolves the incoming slug to a record and hands its fields to the render. See Page Types for the full model.

Two things follow from this. First, the layout is edited by editors in the builder, not by developers in code. Second, a page type does not have to be backed by a CMS collection at all - it can read from a git repository, an external API, or anything else you can query on the server. The contract is the data provider, not the CMS.

2. Data binding - fields into blocks

Inside a page, {{ }} bindings pull values out of the current record, the site-wide global data, or the surrounding loop:

{{ blog.title }}
{{ global.companyName }}

Bindings resolve on the server at render time. A missing value resolves to an empty string rather than an error, which is forgiving in production and easy to miss while authoring. See Data Binding.

3. Repeaters - many records inside one page

A Repeater block iterates a collection and renders its children once per record. This is how a visual page shows a list of posts, a card grid of team members, or a table of releases. Bindings inside a repeater resolve against the current record.

4. Global data - the site-wide singleton

A global data provider supplies one object to every page render, available as global.*. Site name, contact details, social links, analytics snippets, and header or footer HTML belong here rather than being retyped on each page.

Choosing where content goes

Content Put it in Why
Home, pricing, campaign, product pages A visual page Every one is a different composition
Blog posts, docs, legal pages, job posts A collection plus a page type Same shape, many records, one layout to maintain
A repeated header, footer, or CTA band A global block, referenced from pages Edited once, updates everywhere
Navigation menus The menus collection Structured links, editable without touching layout
Site name, socials, tracking scripts The global data provider One value used by every page
Form submissions, leads A collection Data in, never authored by hand

If you find yourself duplicating a visual page and changing only the words, you wanted a page type. If you find yourself adding fields to a collection so it can control its own layout, you wanted a visual page.

Publishing is separate on each side

The two models publish independently, which is usually what you want and occasionally surprising:

  • A page has a draft row and a published row. Visitors only ever see the published row. Publishing copies one to the other and invalidates that page's cache.
  • A CMS record follows Payload's own draft and publish. Saving a record does not touch page rows at all; it invalidates the pages that render that collection, which is configured by listing the collection in the ChaiBuilder Payload plugin.

So a blog post can go live without anyone publishing a page, and redesigning the blog layout can go live without touching a single post. See Caching & Revalidation.

The CMS is swappable

Payload is the default because the starter ships it wired in, but the builder does not require it. What the builder actually depends on are two config contracts: page types and collections, each backed by a provider function you write. Point those at Supabase, a headless API, flat files in the repository, or anything else queryable from the server, and the builder does not know the difference.

What you would give up is the wiring the starter provides for free: the CMS admin UI, collection scoping per site, and automatic revalidation on record changes. See Swapping the CMS.

© ChaiBuilder. All rights reserved.