Skip to main content
This step is optional, and deliberately separate. Everything in the Next.js integration works without it: the editor frames your site, clicking a block selects it, the property panel opens and edits every declared field, chat edits apply, and publish writes them back. What this adds is the part that makes the preview feel like the page rather than a form beside it — clicking a headline and typing into it.

What you get

The property panel is built from the block manifest and never looks at the rendered page. The overlay does the opposite: it finds every job it has by walking [data-editable-target] in the DOM. So these four are the ones that turn on here, and only here: Everything else works either way. That is why this is a second step rather than a requirement — and why nothing warns you about skipping it.

Mark the element that draws each prop

The path is the one the block manifest names the field at — the same grammar an operation uses, because it is the same path. The list-item form (features[0].title) is described under How a block is rendered. Every attribute is inert when the editor is absent. editableProps takes no editorMode argument for that reason: most sites render blocks through components shared with their public pages, and threading a flag down to each field is the step that does not get done. Pass kind for any image field. Without it the overlay infers an image from the prop’s name, against the convention Avocado’s own blocks use (imageUrl, *.src) — so a field named photoUrl gets an image picker in the property panel and no button in the preview, and nothing reports the disagreement. kind takes the same word your block manifest uses. For an image the attribute goes on the wrapper around the <img>, never on the image. The overlay appends its Change button into the marked element, and nothing can be appended into a void element. Marking the image itself gives you a field that is listed in the property panel, highlights on hover, and has no button — with no error on either side.
Import from @avocadostudio-ai/site-sdk/markers, not from .../editor. Both entries export these helpers and both work, but .../editor also exports EditorOverlay and the live-preview provider — and the components you are marking up are, by design, the ones your public pages render too. A 'use client' component reaching for the two-line attribute helper from there pulls the whole editor into the public bundle: measured on a real integration at +66 kB First Load JS on every page, for byte-identical markup. /markers has no React in it at all.

When a list row is its own component

The path is scoped from the block down — items[3].question — which a component can only write if it knows where it sits. That holds while one component draws the whole block, and stops holding the moment a list row is drawn by a component of its own: it knows it has a question and cannot know it is items[3]. Mark the wrapper with the scope, and the children stay ignorant:
Scopes nest and compose outermost first, so a left[1] scope inside a sections[0] scope makes a child’s text into sections[0].left[1].text. A block boundary ends the composition — a scope outside a block never reaches into it — and an element’s own editableProps is not inside the scope it sets for its children.
Without a scope, the alternative is threading a prefix prop from every parent into every child that can appear in a list. Forgetting one is silent and wrong, not silent and absent: the child marks a bare question, the overlay resolves it against the enclosing block, and an edit to a headline inside a column patches a prop the section does not have. Nothing errors, in the preview or in the publish.

When the row has no wrapper to mark

A scope is an attribute, so it needs an element — and a list whose rows map straight into a flex or grid container has none to give it. The wrapper you add to hold the scope becomes the flex item, and the layout the rows had is now the layout of a column of wrappers: gaps land in the wrong places, align-items applies to the wrong box, and a grid’s rows stop being the grid’s children. display: contents is the whole answer — the element stays in the tree the overlay walks and lays out as if it were not there, so the rows go on being their parent’s children. Pass { display: "contents" } and the helper writes it:
Leave it off when the wrapper is one you were rendering anyway. A row that already has an <li> or a card <div> around it should carry the scope on that element rather than gain a second one to hold it.

Do it in one pass, and keep it

This is per-component work spread over as many files as you have renderers, and it is the step that gets done on a branch, not merged, and re-lost on the next one. The symptom of a half-done pass is always the same: one missing button, reported as a bug in the button. site-sdk/coverage answers exactly which manifest fields your rendered page offers, so a branch that drops the markers goes red in your own test suite instead of going quiet. Run it once when you finish this pass, and assert on it after that.