The Visual Website Builder
You Actually Own. React + Next.js, low-code, self-hosted. Your data, your infrastructure, every feature included.
© ChaiBuilder. All rights reserved.

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.
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.
| 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.
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.
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.
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.
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.
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.
| 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.
The two models publish independently, which is usually what you want and occasionally surprising:
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.
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.
{{ }} syntax, scope, and repeaters.