QUESTPIE
SchemaBlocks

The block picker

`.admin()` is the only thing standing between an editor and thirty unlabelled block types. It sets the name, the icon and the group each block appears under.

View markdown

A block with no .admin() still works. It shows up in the sidebar under "Other", wearing its type name and a default cube, which is fine for two blocks and unusable at twenty.

The options

.admin() takes a config object, or a function given { c }, the component proxy that c.icon() comes from. Icons are Iconify names, resolved by the admin at render time.

OptionTypeEffect
labelI18nTextThe name in the picker.
descriptionI18nTextHelp text beside it.
iconComponentReferencec.icon("ph:image").
categoryBlockCategoryConfigThe group: { label, icon?, order? }.
ordernumberPosition inside the group, lowest first.
hiddenbooleanKeeps the block out of the picker.

An I18nText can be a plain string or a map of locale to string, so label: { en: "Hero", sk: "Hero sekcia" } is as valid as label: "Hero".

.admin(({ c }) => ({
	label: { en: "Hero Section", sk: "Hero sekcia" },
	description: { en: "Full-width image with a headline" },
	icon: c.icon("ph:image"),
	category: { label: "Sections", icon: c.icon("ph:layout"), order: 1 },
	order: 1,
}))

How the groups come out

Blocks are grouped by their category label, lowercased. Two blocks naming the category "Sections" land together whatever else they pass, and the first one seen supplies the icon and order for the whole group.

Groups sort by the category's order, blocks sort by their own inside it, and anything without an order falls back to 999. A block with no category at all joins "Other", which carries 999 as well and lands at the bottom.

Because the label is the key, spell it once and import it.

src/questpie/server/blocks/_categories.ts
import type {
	AdminConfigContext,
	BlockCategoryConfig,
} from "@questpie/admin/factories";

export const sections = (c: AdminConfigContext["c"]): BlockCategoryConfig => ({
	label: "Sections",
	icon: c.icon("ph:layout"),
	order: 1,
});

Then every block in the folder says category: sections(c) and the group holds together on its own. A file starting with _ is not a block, so the helper can live beside them.

Retiring a block

hidden: true takes a block out of the picker while leaving it registered. Pages that already use it keep rendering, and nobody can add another one. Deleting the file instead leaves those pages holding a type nothing answers to: the editor shows "Unknown block type" and your frontend renders nothing.

Laying out the fields

Where .admin() handles the picker, .form() handles the panel an editor sees after choosing the block. It takes the same layout shapes a collection form takes, referencing fields by name through the f proxy.

.form(({ f }) => ({
	fields: [
		{ type: "section", label: "Content", fields: [f.title, f.subtitle] },
		{
			type: "section",
			label: "Layout",
			layout: "grid",
			columns: 2,
			fields: [f.alignment, f.height],
		},
	],
}))

Without .form() the fields render in declaration order, which is often enough.

  • Blocks for the builder these two methods sit on.
  • Admin collections for the same layout shapes on a collection form.

On this page