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.

Everything an editor puts on a page is a block. A page is not a document with regions; it is a tree of blocks, and that tree is the page's content.
A block has two halves, and holding them apart explains most of the product:
Write a component and describe its props, and the block becomes something a non-technical editor can drop on a page, configure, and style.
Every block in the tree carries an id and a type string, plus its own props and styles. Blocks nest: a section holds a container, which holds a heading and a button. Some blocks exist purely to hold others.
Page
└── Section
└── Container
├── Heading "Pricing"
├── Text "Simple, flat pricing"
└── Repeater over the plans collection
└── Card
├── Text {{ plan.name }}
└── Button {{ plan.ctaLabel }}
The tree is stored as JSON on the page row. It is data, not code, which is why a page can be diffed, versioned, restored, and copied between environments.
A block definition is mostly metadata. Grouped by what it is for:
| Group | Declares | Why it matters |
|---|---|---|
| Identity | A type string, a label, a group, an icon, an optional description | The type string is the permanent identity; the rest is how the block appears in the library |
| Children | Whether it wraps other blocks, what children it starts with, what it will accept, whether it can be nested | Turns loose blocks into a structure editors cannot easily break |
| Props | A schema plus UI hints | Becomes the settings panel |
| Special props | Which props are translatable, AI-editable, or editable inline on the canvas | The same prop can be plain text to one system and a translation target to another |
| Data | An optional data provider, and what it depends on | Lets a block fetch its own server data - see Data Providers |
| Scope | Which page types the block is allowed on | Keeps a blog-only block out of the landing-page library |
| Guardrails | Whether it can be deleted, moved, or duplicated | Editor ergonomics, not security. Anything expressed here is advisory |
The last row is worth reading twice. Block guardrails shape what the builder UI offers; they are not an authorization boundary. Access control lives in roles and permissions.
Beyond its own configured props, every block is handed the same render context:
| It receives | Use it for |
|---|---|
lang |
The current language |
draft |
Whether this is a draft render |
inBuilder |
Whether it is rendering in the canvas rather than for a visitor |
pageProps |
Facts about the page: slug, page type, page id, language |
pageData |
The resolved data for this render, which is what bindings read from |
inBuilder is the escape hatch that keeps the canvas usable. A carousel that autoplays for
visitors can hold still while it is being edited; a block with no content yet can render a
placeholder instead of collapsing to nothing.
{{ }} expression, resolved on the server at render
time. See Data Binding.A prop holding {{ blog.title }} is still just a prop. Nothing about the block changes; the value
arrives resolved.
One consequence catches people out: blocks store values, not references. An image block holds a URL, not a pointer to an asset record, so replacing the asset in the media library does not update pages that already use it.
| Family | Where it comes from | Notes |
|---|---|---|
| Shipped blocks | The builder's own library | Structural blocks, text, media, forms, repeaters |
| Your blocks | Registered by your project | Ordinary React components; this is the main extension point |
| Global blocks | Built by editors in the builder | A reusable subtree referenced from many pages: header, footer, CTA band |
A global block is not really a block type. It is a page whose block tree is referenced from other pages, so editing it once updates every page that references it. What sits in the consuming page is a lightweight reference that the server replaces with the real tree before rendering. See Global Blocks.
The stored page JSON refers to block types by string. When a page renders, each block is looked up in the registry by that string, and a block whose type is not registered renders nothing - no error, no placeholder, no visible gap in the page.
That single behaviour drives a few rules:
The upside of the same design: a block that throws while rendering can be contained. When the app provides an error boundary to the renderer, a failing custom block degrades to nothing instead of taking the whole page down with it.
| Symptom | Usual cause |
|---|---|
| A block vanished from the published page | Its type is not registered in the render path, or a condition on it resolved to false |
| A block renders in the canvas but not live | Registration happens in the editor entry but not the public route |
| Editor's text is ignored | A data provider on that block returns the same prop name; provider output wins |
| Empty text where a binding was | The bound value was missing, and a missing binding resolves to an empty string |
| Styling disappeared after a token change | Deleting a design token strips the classes it carried |
{{ }} layer over props.