Blocks
Nested Blocks & blockRef
Page-builder patterns often require blocks that contain other blocks -- a Container holding Rows, each Row holding Cards. The blockRef() helper breaks circular dependency chains so these recursive structures can be defined.
The problem
When two block types reference each other, a circular import occurs. Container needs Row in its children field, and Row needs Container in its children field. You cannot pass full definitions because each requires the other to exist first.
Using blockRef()
blockRef() creates a lightweight forward reference that carries only the properties f.blocks() needs. Full definitions are used later when generating schemas.
import { f, defineVextroBlock, blockRef, createVextroBlocksSchema } from "vextro";
const card = defineVextroBlock({
slug: "card",
label: "Card",
tableName: "cardBlocks",
group: "Layout",
fields: {
heading: f.text({ required: true }),
body: f.richText(),
image: f.image({ relationTo: "media" }),
},
});
const row = defineVextroBlock({
slug: "row",
label: "Row",
tableName: "rowBlocks",
group: "Layout",
fields: {
columns: f.select({ options: ["2", "3", "4"], defaultValue: "2" }),
children: f.blocks([
blockRef({ slug: "card", label: "Card", tableName: "cardBlocks" }),
blockRef({ slug: "container", label: "Container", tableName: "containerBlocks" }),
]),
},
});
const container = defineVextroBlock({
slug: "container",
label: "Container",
tableName: "containerBlocks",
group: "Layout",
fields: {
maxWidth: f.select({ options: ["sm", "md", "lg", "full"], defaultValue: "lg" }),
children: f.blocks([
blockRef({ slug: "row", label: "Row", tableName: "rowBlocks" }),
blockRef({ slug: "card", label: "Card", tableName: "cardBlocks" }),
]),
},
});
const { tables } = createVextroBlocksSchema({
definitions: [card, row, container],
}); This enables deeply nested page structures:
Page
Container (full width)
Row (3 columns)
Card ("Feature A")
Card ("Feature B")
Container (nested)
Row (2 columns)
Card ("Detail 1")
Card ("Detail 2") Rules
- Use
blockRef()insidef.blocks()for forward and circular references between blocks. - Use full
VextroBlockDefinitionobjects (fromdefineVextroBlock()) forcreateVextroBlocksSchema()andbuildAdminDefinitions(). - A
blockRef()requiresslug,label, andtableName. Optionally includepickerfor picker dialog configuration. - You can mix full definitions and
blockRef()refs in the samef.blocks()call -- use full definitions for non-circular references and refs for circular ones. - Use
isBlockRef(spec)to distinguish ablockRef()from a fullVextroBlockDefinitionat runtime. It returnstruefor refs andfalsefor full definitions.
Editor workflow
Each block opens its own edit page in the admin UI. When a blocks field contains nested block references, each selected block item displays an "Edit" link. Clicking the link opens the nested block's edit page in a new tab, preserving unsaved work on the parent document.
Blocks in rich text
Blocks can also be embedded inline within the TipTap rich text editor. Pass block definitions or refs to f.richText():
const section = defineVextroBlock({
slug: "section",
label: "Section",
tableName: "sectionBlocks",
fields: {
layout: f.select({ options: ["default", "wide", "narrow"] }),
content: f.richText({
blocks: [card, cta, blockRef({ slug: "testimonial", label: "Testimonial", tableName: "testimonialBlocks" })],
}),
},
}); Ref resolution
A blockRef() is resolved at schema generation time. If the referenced slug does not match any definition passed to createVextroBlocksSchema(), schema generation will throw an error listing the unresolved refs.