QUESTPIE

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

Mermaid

How Blocks Work

  1. Define blocks in blocks/ using the file convention — each block has fields, admin config, and optional data prefetching
  2. Use f.blocks() on a collection field to enable block content
  3. Register renderers on the client — React components that render each block type
  4. 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:

CategoryPurposeExamples
layoutStructural blocksColumns, Grid, Container, Section
contentText contentHeading, Paragraph, List, Quote
mediaVisual contentImage, Video, Gallery
sectionsPre-built sectionsHero, Features, Testimonials, CTA
interactiveUser interactionForm, Accordion, Tabs

Custom categories are supported — use any string as the category name.

Quick Example

Server — define a hero block:

src/questpie/server/blocks/hero.ts
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:

src/questpie/admin/blocks/hero.tsx
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

On this page