# Blocks (/docs/schema/blocks)

---
title: Blocks
description: A block is a content type an editor stacks into a page. Declare its fields once and the admin gets a picker, the row gets one jsonb column, and your app gets a tree to render.
kind: guide
package: "@questpie/admin"
---

Someone who is not you has to reorder the homepage on a Tuesday. This page
starts from one block file and ends with that block on the screen, rendered by
your own component.

## Two files per block

A block is a pair. The fields live on the server, the markup lives beside your
admin, and the generator pairs the two by filename.

```bash
questpie add block hero
# src/questpie/server/blocks/hero.ts    the fields
# src/questpie/admin/blocks/hero.tsx    the markup
```

Keep both basenames the same, and name the server export after the file. The
renderer gets its types by importing `heroBlock` from `hero.ts` under that
exact name.

## Declaring the fields

```ts title="src/questpie/server/blocks/hero.ts"
import { block } from "#questpie/factories";

export const heroBlock = block("hero")
	.admin(({ c }) => ({
		label: "Hero",
		icon: c.icon("ph:image"),
		category: { label: "Sections", order: 1 },
		order: 1,
	}))
	.fields(({ f }) => ({
		title: f.text(255).required(),
		subtitle: f.textarea(),
		backgroundImage: f.upload(),
	}))
	.prefetch({ with: { backgroundImage: true } });
```

`.fields()` is the callback a collection takes, with the same `f` proxy and the
same chain methods, over the built-in field types plus the two `@questpie/admin`
adds. The argument to `block()` is the type name stored in the tree and the key
your renderer is found by, so let it match the filename.

## Giving a collection a page builder

`f.blocks()` takes no arguments and adds one `jsonb` column named after the
field.

```ts title="src/questpie/server/collections/pages.ts"
import { collection } from "#questpie/factories";

export const pages = collection("pages")
	.fields(({ f }) => ({
		title: f.text(255).required(),
		content: f.blocks(),
	}))
	.title(({ f }) => f.title);
```

```bash
questpie generate
questpie push
```

The form for `pages` now shows a canvas where an editor picks blocks out of
your categories, drags them into order, and nests them.

<Callout type="warn" title="Blocks come from `@questpie/admin`">
	`block()` and `f.blocks()` exist only when `adminModule` is registered in
	`src/questpie/server/modules.ts`. The module contributes both, and the
	generator puts them on `#questpie/factories`. Import `block` from there, never
	from the package.
</Callout>

## What the column holds

One document, normalized. Structure and content are separate, keyed by the same
block id.

```ts
// pages.content, after an editor drops in one hero
{
	_tree: [{ id: "blk_a1", type: "hero", children: [] }],
	_values: {
		blk_a1: { title: "Sharp Cuts", backgroundImage: "asset_77" },
	},
	_data: {
		blk_a1: { backgroundImage: { id: "asset_77", url: "https://…/hero.jpg" } },
	},
}
```

`_tree` and `_values` are what the column stores and what the write schema
checks. `_data` is not stored. It is attached on every read from what each
block prefetched. Moving a block rewrites `_tree` and leaves its values
untouched. Nesting lives in `_tree` alone: a child sits in its parent's
`children`, and `_values` stays flat, one entry per block at any depth.

## Rendering it

The markup is yours. QUESTPIE hands each renderer the block's values and its
prefetched data, both typed from the block file.

```tsx title="src/questpie/admin/blocks/hero.tsx"
import type { BlockProps } from "../.generated/client";

export function HeroRenderer({ values, data, children }: BlockProps<"hero">) {
	const url = data?.backgroundImage?.url as string | undefined;
	return (
		<section style={{ backgroundImage: url ? `url(${url})` : undefined }}>
			<h1>{values.title}</h1>
			{values.subtitle && <p>{values.subtitle}</p>}
			{children}
		</section>
	);
}
```

The generator collects that folder into `admin.blocks`. Hand the map and the
saved document to `BlockRenderer` and the page is on screen.

```tsx
import admin from "@/questpie/admin/.generated/client";
import { BlockRenderer } from "@questpie/admin/client";

<BlockRenderer
	content={page.content}
	renderers={admin.blocks}
	data={page.content._data}
/>;
```

**[Rendering a page](/docs/schema/blocks/rendering)** covers the lookup rule,
every prop a renderer receives, and the live-preview wiring.

## The rest of the builder

| Method                              | What it does                                                       |
| ----------------------------------- | ------------------------------------------------------------------ |
| `.admin(config \| (ctx) => config)` | Label, description, icon, category, order and `hidden`.            |
| `.fields(({ f }) => ({ … }))`       | The block's own fields.                                            |
| `.form(({ f }) => ({ fields }))`    | Groups those fields into sections, tabs or a grid in the editor.   |
| `.allowChildren()`                  | Lets this block hold others, handed to the renderer as `children`. |
| `.prefetch(…)`                      | Loads data for the block on read.                                  |

### Blocks that hold other blocks

`.allowChildren()` makes a block a container. The editor lets one block be
dropped inside another, the nesting is kept in `BlockNode.children`, and the
renderer receives those children already rendered.

### Naming it and laying it out

**[The block picker](/docs/schema/blocks/picker)** covers every `.admin()`
option, how categories are grouped and sorted, how to share one category across
a folder of blocks, and what `.form()` does to the editing panel.

## Filtering by block content

A blocks column carries its own operators, so a `where` clause can ask what is
on the page.

| Operator                 | Value                                           | Matches                                    |
| ------------------------ | ----------------------------------------------- | ------------------------------------------ |
| `hasBlockType`           | `string`                                        | Rows with a root-level block of that type. |
| `blockCount`             | `{ op: "gte" \| "lte" \| "eq"; count: number }` | Rows by root-level block count.            |
| `isEmpty` / `isNotEmpty` | none                                            | Rows with no, or at least one, root block. |
| `isNull` / `isNotNull`   | none                                            | Rows where the column is or is not null.   |

```ts
const { docs } = await app.collections.pages.find({
	where: { content: { hasBlockType: "cta" } },
});
```

`hasBlockType` and `blockCount` read the root of `_tree` only. A `cta` nested
inside a `columns` block matches neither.

## Related

- **[Loading data for a block](/docs/schema/blocks/prefetch)** for the three
  shapes of `.prefetch()` and what each one costs a read.
- **[Fields](/docs/schema/fields)** for the types and modifiers you use inside
  `.fields()`.
- **[Collections](/docs/schema/collections)** for the table the blocks column
  lives on.
- **[Access control](/docs/schema/access-control)** for who may read the rows a
  prefetch pulls in.
