ChaiBuilder Logo

Rendering Model

ChaiBuilder is built for content-heavy, public-facing sites, and the rendering model follows from that: pages are static once built, and an edit goes live by invalidating exactly the page that changed. No rebuild, no redeploy.

This page is the conceptual view. For cache layers, hosting differences, and the manual revalidation route, see Caching & Revalidation.

Two render paths

The same block tree is rendered by two different engines, and most confusing rendering questions come from mixing them up.

Builder canvas Published page
Where it runs The client, inside the editor Your server
Data Live, fetched per interaction Resolved once per render, then cached
Purpose Show the editor what they are doing Serve visitors
Reads The draft row The published row

The canvas is a faithful preview, not the production renderer. When behaviour differs between the two, the published output is the one that counts - so confirm real changes with a preview or on the live page, not only in the canvas.

Draft and published are separate rows

Every page exists as up to two rows: a draft and a published version.

  • Editing in the builder writes to the draft. Nothing a visitor can see has changed yet.
  • Publishing copies the draft to the published row and invalidates that page's cache.
  • The public route only ever reads the published row - unless draft mode is on.

Site-level settings (theme and design tokens) follow the same split and publish together as one site settings row, which is why a theme change can appear to publish more than the page you were looking at. See Preview & Publish.

The lifecycle of a public request

  1. A visitor requests /pricing. The catch-all route is marked static, so Next.js checks the cache first.
  2. Cache miss (first visit after a deploy or an invalidation): the server resolves the slug to a published page, runs the data providers, renders the block tree to HTML, and stores the result.
  3. The render declares its dependencies. As it goes, each step attaches a cache tag to the entry being stored: the page itself, every global block merged into it, the data sources it read from. The cached page ends up labelled with everything that could make it wrong.
  4. Cache hit (every later visit): the cached HTML is served. No database query, no provider execution.
  5. An editor publishes a change, or an external system fires a tag. Every cache entry carrying that tag is invalidated - which may be one page or a thousand, depending on the tag.
  6. The next visitor to an invalidated page is back at step 2, and everyone after them at step 4.

Step 3 is what makes step 5 precise. Nothing maintains a list of which pages depend on which records; each render records its own dependencies, and invalidation just fires a tag and lets the labels decide. See Caching & Revalidation for the tags a render collects and how to add your own.

Two consequences worth internalising: deploys are fast because no pages are generated at build time, and the first visit after any invalidation is the slow one. Nothing here requires a rebuild to publish content.

What happens during a render

A single page render does more than concatenate HTML:

  • Blocks to markup - the block tree is walked and each registered block renders its component.
  • Bindings resolved - {{ }} paths and pipes are resolved on the server against page-type data, global data, and the current repeater item. A missing value becomes an empty string.
  • Conditions resolved - a block hidden by a condition is omitted from the HTML entirely, not hidden with CSS. It is a render-time decision, so it cannot vary per visitor on a cached page. See Conditional Visibility.
  • Design tokens expanded - tokens are bundles of Tailwind classes, so they resolve to plain classes in the output. There is no runtime lookup.
  • Theme emitted - the site's theme values are written into the page as CSS variables.
  • Metadata and JSON-LD - title, description, canonical, social tags, and structured data are produced from the page's SEO fields.
  • Images - image blocks render through Next.js image optimisation, which is why remote image hosts must be allow-listed in next.config.ts.

Data providers run on the server during step 2, and they are not all cached the same way: global data persists across renders, while page-type providers, collection fetches, and block providers run on every render. A slow one of those is a slow first render for every invalidation. Running them every time is also what lets them return per-record cache tags, so a page can be invalidated by the record it rendered rather than by page type.

Preview uses draft mode, and is never cached

Previewing from the builder enables Next.js draft mode for your session and reopens the page on the public route. In that mode the page is rendered per request against draft content, with no persistent caching, and a banner marks the session. Exiting preview turns it back off.

This is why preview always looks current while the live page can lag by one invalidation: they are deliberately different paths.

What static rendering rules out

Cached HTML is shared by every visitor. That is the whole performance story, and it sets a boundary:

  • No per-visitor content in a cached page. Geolocation, logged-in state, A/B assignment, and personalised prices cannot come from the static render.
  • No per-request logic in a block's server data. It runs once per render, not once per visitor.

When you need per-visitor behaviour, the escape hatches are ordinary Next.js: a client component that fetches after hydration, a dynamic route you own outside the builder's catch-all, or edge logic in front of the page. The builder does not stand in your way; it just will not do it for you inside a static page.

© ChaiBuilder. All rights reserved.