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 page type is the answer to "I have five hundred blog posts and I do not want to build five hundred pages." You design the layout once in the builder, and every record renders through it.
Concretely, a page type is a named kind of page that says:
Every page has a page type, including ordinary static pages. The interesting ones are dynamic: their URL contains a segment that is not fixed, and their content comes from somewhere else.
One layout in the builder Many records
┌──────────────────────────┐ /blog/launching-v3
│ {{ blog.title }} │ ──► /blog/hiring-a-designer
│ {{ blog.publishedAt }} │ /blog/how-we-cache
│ {{ blog.content }} │ ...
└──────────────────────────┘
The layout is edited by editors in the builder, not by developers in a template file. That is the point of the feature: changing how every blog post looks is a visual edit, not a deploy.
| Declares | Purpose |
|---|---|
| Key and name | Its stable identity, and how it reads in the builder UI |
| Dynamic segment | The shape of the variable part of the URL |
| Data provider | Turns the resolved page into the data the layout binds to |
| Item list | Lists and searches the records this page type can render, so the builder can offer a picker |
| Link resolution | Turns a record reference into a URL, so links to it stay correct |
| Edit and create surfaces | Optional URLs the builder opens in a panel, so an editor can edit the underlying record without leaving the builder |
| Defaults | Fallback SEO, JSON-LD, meta tags, and tracking for records that do not set their own |
| Lifecycle hooks | React to a record being created, updated, or deleted |
Only the first three are essential. The rest is what turns a working dynamic page into a pleasant one to edit.
/blog/how-we-cache.Step 3 is the whole contract. The builder does not know or care whether the provider read a CMS collection, called an API, or parsed markdown in your repository.
A page type lists its records for two different reasons, and they need two different keys:
They are the same string often enough that the distinction is easy to miss, and then it bites: records keyed by id, by full path, or by an external system's reference need the identifier to be that key, not the slug. Keep them separate from the start.
Once a page type exists, the builder can do things it otherwise could not:
A reusable header, footer, or CTA band is a global block, and a global block is a page of a page type that has no slug. That is not a quirk to work around - it is why global blocks get the same machinery as pages: their own block tree, their own draft and published versions, their own revisions.
It also explains a rule that otherwise looks arbitrary: a global block is edited like a page because it is one, just one that is referenced rather than routed to.
.tsx
template for a blog post to open and edit.| Symptom | Usual cause |
|---|---|
| Every record shows the same content | The data provider ignores the incoming identifier and returns a fixed record |
| The record exists but the URL 404s | The dynamic segment does not match the slug's shape |
| Content is blank but the layout renders | The provider returned nothing under the key the bindings use; missing bindings resolve to empty strings |
| Preview works, live is stale | The record changed but nothing revalidated the pages that render that collection |
| The item picker is empty | No item list is provided, so the builder has nothing to offer |
