Where blocks may go
The one rule the editor and the server use to decide whether a block may sit inside another, and how a block declares what it is as HTML.
The editor and the server decide every placement with the same rule, so a block the editor lets you drop is one the server saves, and an API write or an import is held to what the editor holds you to. The rule combines three things:
- The slot is one the container declares. A container only draws the slots it declares, so a block filed anywhere else would be saved and never shown. A block that declares no slots holds no blocks at all.
- The names the blocks give. A block's
parentlists the only containers it may sit directly inside, and a slot'sallowlists the only blocks it takes. - HTML's content model. Nothing clickable inside a link or a button, no header or
footer inside another header or footer, and a
mainonly inside plain boxes. These follow a block's settings, not just its name: a Card with its link switched on is a link, so it refuses a Button anywhere inside it.
A refusal says why, in the same words in the editor and from the server.
Pages saved before the rule
A page saved before a block declared these rules may already break them. It still opens and still saves your other changes: each break it already held is kept as a warning, with its blocks where they were. A write that adds a new break is refused. Moving a block that breaks the rule somewhere else that also breaks it, or changing a setting so a block breaks it, counts as new.
Where a page's saved value cannot be read back to compare against (a single, or a field translated per language), a break is kept with a warning and written to the server log rather than refused.
The editor offers to repair such a page. A Repair this page notice lists each block that breaks the rule and where it will go: just after the nearest container that allows it, with its settings and everything inside it. Nothing changes until you choose Repair, no block is removed, and one undo takes the whole repair back. Locked areas are repaired first. A block with nowhere on the page it may go stays where it is, and the notice says so.
Declare what your block is
A block from a plugin can say what it is as HTML through an optional contentModel. It
receives the block's props and returns what the block is (is) and, for a container,
what may sit beneath it (accepts). This is experimental: the shape may change before
the page builder is stable.
import { defineBlock } from "@nextlyhq/plugin-sdk/blocks";
export const promo = defineBlock<{ href?: string }>({
name: "acme/promo",
version: 1,
description: "A promotion panel that can link somewhere as a whole.",
example: { props: {} },
props: { href: { type: "url", role: "content" } },
slots: { children: {} },
// Linked, the panel renders an <a>: it is interactive, and nothing interactive
// may sit anywhere inside it.
contentModel: props =>
props.href
? { is: ["flow", "interactive"], accepts: { without: ["interactive"] } }
: { is: ["flow"] },
render: ({ props, renderSlot, className }) =>
props.href ? (
<a className={className} href={props.href}>
{renderSlot("children")}
</a>
) : (
<div className={className}>{renderSlot("children")}</div>
),
});islists the kinds the block's own markup is or contains:flow,phrasing,interactive,header,footer,mainorform.accepts.contentis what its slots' direct children must be:flow(the default) orphrasing, for a container that only takes text-level blocks.accepts.withoutlists kinds refused anywhere beneath it, at any depth.
A block that declares nothing is flow content that accepts flow content, which is how every block was treated before, so existing plugin blocks keep working. The function must be pure and synchronous; if it throws or returns something else, the block is treated as declaring nothing.
Related
- Blocks from plugins — declaring blocks and their slots
- Locked areas — fixing what a slot holds