Blocks
Content blocks for page builders — define server-side, render client-side.
Blocks are reusable content components for page building. Define them server-side with fields and admin metadata, then render them client-side with React components. Block content is stored as JSONB in the database.
Architecture
How Blocks Work
- Define blocks in
blocks/using the file convention — each block has fields, admin config, and optional data prefetching - Use
f.blocks()on a collection field to enable block content - Register renderers on the client — React components that render each block type
- Admin shows a drag-and-drop block editor; frontend renders the block tree with prefetched data
Block Content Structure
Block content is stored as JSONB with this structure:
type BlockContent = {
_tree: BlockNode[]; // Hierarchical tree of blocks
_values: Record<string, Record<string, unknown>>; // Field values per block instance
_data?: Record<string, Record<string, unknown>>; // Prefetched relation data
};
type BlockNode = {
id: string; // Unique instance ID
type: string; // Block type name (e.g., "hero", "columns")
children: BlockNode[]; // Nested blocks
};_tree stores structure. _values stores editable field values by block instance. _data stores prefetched relation or upload data. Live Preview inline editing targets _values; add, remove, reorder, and nesting operations stay in the existing block editor.
Block Categories
Blocks can be organized into categories in the block editor:
| Category | Purpose | Examples |
|---|---|---|
layout | Structural blocks | Columns, Grid, Container, Section |
content | Text content | Heading, Paragraph, List, Quote |
media | Visual content | Image, Video, Gallery |
sections | Pre-built sections | Hero, Features, Testimonials, CTA |
interactive | User interaction | Form, Accordion, Tabs |
Custom categories are supported — use any string as the category name.
Quick Example
Server — define a hero block:
import { block } from "#questpie/factories";
export default block("hero")
.fields(({ f }) => ({
title: f.text(255).required(),
subtitle: f.textarea(),
backgroundImage: f.upload(),
ctaLabel: f.text(100),
ctaLink: f.url(),
}))
.admin({
label: "Hero Section",
icon: "ph:star",
category: "sections",
});Collection — use blocks field:
export default collection("pages").fields(({ f }) => ({
title: f.text(255).required(),
content: f.blocks(),
}));Client — render the block:
import { PreviewField } from "@questpie/admin/client";
export default function HeroRenderer({ values }) {
return (
<section style={{ backgroundImage: `url(${values.backgroundImage?.url})` }}>
<PreviewField field="title" editable="text" as="h1">
{values.title}
</PreviewField>
<PreviewField field="subtitle" editable="textarea" as="p">
{values.subtitle}
</PreviewField>
{values.ctaLabel && <a href={values.ctaLink}>{values.ctaLabel}</a>}
</section>
);
}When this renderer is used by BlockRenderer inside Live Preview, PreviewField field="title" resolves to a block value path such as content._values.<blockId>.title.
Sections
- Defining Blocks — Server-side block definitions with fields and admin config
- Renderers — Client-side React components for rendering blocks
- Prefetch — Prefetching relation data for blocks
- Live Preview — Preview annotations and iframe editing flow