> ## Documentation Index
> Fetch the complete documentation index at: https://docs.avocadostudio.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Make the Page Directly Editable

> Optional second step. Mark the element that draws each field and the preview gains inline text editing, hover pills and image buttons — on top of an integration that already works without it.

This step is **optional, and deliberately separate**. Everything in
[the Next.js integration](/integration/nextjs-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:

|                          |                                                                             |
| ------------------------ | --------------------------------------------------------------------------- |
| **Inline text editing**  | click the text in the preview, type into it                                 |
| **Field pills**          | the hover label naming which field is under the cursor                      |
| **Image buttons**        | **Change** and **Remove** on an image, in the preview                       |
| **Live draft streaming** | a field streamed by chat lands in the node that draws it, before any reload |

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

```tsx theme={null}
import { editableProps } from "@avocadostudio-ai/site-sdk/markers"

function MyBlock(props) {
  return (
    <section>
      <h2 {...editableProps("heading")}>{props.heading}</h2>
      <div className="media" {...editableProps("imageUrl", { kind: "image" })}>
        <Image src={props.imageUrl} alt={props.imageAlt} fill />
      </div>
    </section>
  )
}
```

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](/integration/block-system#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.

<Warning>
  **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.
</Warning>

## 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:

```tsx theme={null}
import { editableProps, editableScopeProps } from "@avocadostudio-ai/site-sdk/markers"

{props.items.map((item, i) => (
  <div key={item.id} {...editableScopeProps(`items[${i}]`)}>
    <FaqRow item={item} />
  </div>
))}

// …and inside FaqRow, which needs no prefix prop:
<h3 {...editableProps("question")}>{item.question}</h3>
```

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.

<Warning>
  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.
</Warning>

### 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:

```tsx theme={null}
<div className="flex flex-col gap-6">
  {props.items.map((item, i) => (
    <div key={item.id} {...editableScopeProps(`items[${i}]`, { display: "contents" })}>
      <FaqRow item={item} />
    </div>
  ))}
</div>
```

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`](/integration/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.
