Who writes the code. Almost nobody wires this by hand end to end, and we do not recommend it. Hand it to the coding agent that already knows your codebase — see your own coding agent — and use this page to review what comes back. The contract is the same either way.
Prerequisites
- Node 22+ and your usual package manager.
- A Next.js 15 or 16 project on the App Router. On Astro 5+,
@avocadostudio-ai/astrosatisfies this contract for you — the seams below still describe what it is doing, but you do not implement them. Any other framework can satisfy the same contract through the SDK’s framework-agnostic/coreprimitives — see Non-Next.js integration — but you would be a first mover. - An Avocado orchestrator running, locally or hosted. It defaults to
http://localhost:4200. - An
ANTHROPIC_API_KEY,OPENAI_API_KEYorGOOGLE_GENAI_API_KEYon the orchestrator. Avocado runs on your keys and never resells tokens.
The six seams
1. The editor API — one catch-all route
MountcreateEditorApiHandler from @avocadostudio-ai/site-sdk/routes at app/api/editor/[...path]/route.ts, exporting GET, POST and OPTIONS.
That single handler must serve all five endpoints, and the editor calls them at exactly these paths:
Options worth knowing:
registerBlocks (runs your registrations at request time, so bundler import order cannot clobber them), blockTypes (narrows the manifest to the types this site actually renders), getManifest (full override), getSiteConfig, onPublish, publishSecret, maxPagesRemoved.
Publishing is guarded twice, and both guards fail closed.
- The secret is not optional in production.
publishSecretis usuallyprocess.env.PUBLISH_TOKEN, and the orchestrator sends the same value asx-publish-token. With no secret configured, the route answers 401 underNODE_ENV=productionand names the variable in the response; on your own machine it stays open, because publishing to it is the point, and warns once. An optional guard on an endpoint that overwrites a site’s content is not a guard. - A publish may not remove every page. The only validation this route used to do was
Array.isArray(body.pages), and[]is an array — so a client that failed to load its own state could replace the whole site with nothing and get{"ok":true}back. Emptying the site now needs"allowDelete": truein the body and is otherwise a 409 that says how many pages it protected. Removing one page of three is still an ordinary edit; setmaxPagesRemovedif you want a tighter bound than “not all of them”. A site that is already empty may still publish empty, so a new integration’s first publish is not refused.
checkDestructivePublish from @avocadostudio-ai/site-sdk/routes if you want to apply it somewhere else.
→ Next.js integration
2. The page factory
Replaceapp/[[...slug]]/page.tsx with createSitePage from @avocadostudio-ai/site-sdk/page, wired to your own getPage, getSlugs and getSiteConfig.
It must export three things:
default Page— the route component.generateStaticParams— your slugs.generateMetadata— not optional in practice. Without it every page inherits the root layout’s<title>, with no description and no social card. The SDK derives all three from the page, but Next only reads them if the route file exports the function.
siteUrl — the site’s public origin, normally process.env.NEXT_PUBLIC_SITE_URL — if you want the three tags a page cannot derive from its own content: <link rel="canonical">, og:url, and an og:image resolved to an absolute URL. A relative image path is correct in an <img src> and ignored by every social crawler, so a site that stores its images that way has blank social cards until the origin is known. Unset, the SDK emits none of the three rather than guessing: a wrong canonical is worse than an absent one.
Add app/not-found.tsx. The factory calls notFound() for an unknown slug, so a missing page has to answer a real HTTP 404 rather than a 200 with “404” in the body.
The factory also handles draft-mode detection, the editor overlay, navigation chrome, and switching between published and draft reads. Do not reimplement that branching.
Middleware. Next 15 uses src/middleware.ts with createEditorMiddleware from @avocadostudio-ai/site-sdk/middleware. Next 16 renamed the convention and reads config by static analysis, so it uses src/proxy.ts with createEditorProxy from @avocadostudio-ai/site-sdk/proxy and a config that is a static object literal — a config destructured from a factory result works on 15 and is silently ignored on 16.
3. Block registration — the boundary itself
On an existing site, your own components are the blocks. Register each one with a schema that names its content props and the kind of each field:name, price and blurb are editable; a badgeVariant you did not declare is not reachable by any operation, any prompt, or any model. Declaring less is the conservative choice, and adding a field later is one line.
The twenty built-in block types — Hero, FeatureGrid, Testimonials, FAQAccordion, CTA, Card, CardGrid, RichText, Stats, TwoColumn, Footer, SiteHeader, Embed, Banner, Carousel, Gallery, Tabs, Table, Quote, Video — are a starting catalogue for sites built from scratch. On an existing site they are optional.
→ Custom blocks
4. Markers — which element is which field
Declaring a field makes it editable in the property panel. Making it editable on the page — inline text editing, the hover pill, the image Change button — needs the renderer to say which DOM element carries which path, using@avocadostudio-ai/site-sdk/markers:
- A missing scope is wrong, not 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. display: "contents"is the answer to the layout problem. A wrapper added only to carry a scope otherwise becomes the flex or grid item, and the layout the rows had becomes the layout of a column of wrappers. Withdisplay: contentsthe rows go on being their parent’s children. Leave it off when the wrapper is one you were rendering anyway.
5. The content adapter
Two functions.getPages() returns your content as pages of blocks; onPublish(pages, config) writes edits back. How much sits behind them is a property of your CMS, not of Avocado.
- Files or JSON — nearly nothing.
@avocadostudio-ai/site-sdk/publish-handlers/json-fileships a working handler. Seeexamples/sample-site/. - A mainstream CMS — working examples ship for Contentful, Sanity and Strapi under
examples/contentful-site/,examples/sanity-site/andexamples/strapi-site/. Rich text converts through a shared pivot with converters for Storyblok, Contentful, Sanity Portable Text and Strapi in@avocadostudio-ai/richtext. - Field-level localisation, or list rows stored as their own documents — this is where the real work is. Use a field table: one declaration derives the Zod schema, the panel metadata, the projection out of the CMS and the merge back into it, instead of four hand-written things that disagree within a week. Lens packs ship for Storyblok and Sanity.
mergetakes the live CMS document as its source, never a snapshot Avocado holds. Every field the table never declared survives by construction.- Publishing is a field-level diff, not a snapshot overwrite, so a page nobody touched writes nothing.
6. Registration
From the project directory:@avocadostudio-ai/site-sdk. It generates a DRAFT_MODE_SECRET into .env.local if there is not one, fills in NEXT_PUBLIC_DEFAULT_SITE_ID, NEXT_PUBLIC_SITE_NAME and NEXT_PUBLIC_EDITOR_ORIGIN when absent, POSTs the site config to POST /sites/register, and writes ORCHESTRATOR_URL once that POST has been answered.
Pass --orchestrator: the default is http://localhost:4200, the standalone server’s address, and a library-mode orchestrator lives inside your own app instead. Nothing has to be started for it — it is up when your dev script is. If the POST cannot connect the command says so and exits 0, having already done the .env.local half; the site still loads in the editor, because a library-mode mount reports the one site it is mounted in whether or not anyone registered it.
Flags: --id, --port, --orchestrator, --secret, --session, --purpose, --preview-url. Run npx avocado-register --help for the full list.
The most common failure is a mismatch between the site’s DRAFT_MODE_SECRET and the editor’s build-time VITE_SITE_DRAFT_SECRET. The script surfaces it as a warning; do not ignore it.
One more seam, if your layout mounts third-party scripts
The editor renders your real pages in an iframe. A consent banner, analytics, a tag manager or another visual editor’s bridge mounted in the root layout will cover the page being edited and write a pageview for every block someone clicks through. A Next layout receives nosearchParams, so the check is header-based:
resolveEditorContext, which requires draft mode or a valid secret. Never gate content or credentials on isEditorRender.
Doing it in order
1
Read the concepts
Core concepts — pages, blocks, operations, draft mode. The rest of the docs assume this vocabulary.
2
Get the editor talking to an orchestrator
the quickstart. Confirm you can open the editor at
http://localhost:4100 and see a session before you change anything in your own repo.3
Mount the two helpers
Seams 1 and 2 — the editor API route and the page factory, from Next.js integration. At the end of this step the site should load inside the editor’s iframe and draft mode should toggle.
4
Declare your blocks
Seam 3 — Custom blocks. Register your own components and the props you are prepared to let marketing change.
5
Mark up the renderers
Seam 4. Work block by block and check coverage after each one rather than at the end; a bad scope is much easier to find in a diff of one component.
6
Adapt the content source
Seam 5 — a field table if there is a CMS behind this, CMS adapters otherwise.
7
Register the site
Seam 6 —
npx avocado-register --name "...". The site appears in the editor’s dashboard on the next open or refresh.8
Prove it with a number
Run
editableCoverage and panelCoverage from @avocadostudio-ai/site-sdk/coverage and put them in CI. See below.9
Optional — enable the visual editor
Visual editor, opt-in per site. Do this after the contract above is satisfied, not alongside it.
How you know it is finished
Not “it builds”, and not “the dev server started”. A site can typecheck, build, serve a valid manifest and still be unusable to edit.editableCoveragecompares the fields the manifest declares against thedata-editable-targetmarkers a rendered page actually carries, and reportsmarked/expected. A field that lost its marker in a refactor fails this instead of silently losing inline editing.panelCoverageasks whether the property panel is intelligible:rowsLabelled/rowsExaminedplus findings for list rows nobody can tell apart, polymorphic branches that never narrow, props in your content described by nothing, and block type names colliding with the built-ins.
@avocadostudio-ai/site-sdk/coverage — no browser, no screenshot, no model call — so they belong in CI. The same panel check is available to agents as the avocado-check-editing-surface MCP tool.
Target 100% editable coverage and zero panel findings. Where you cannot reach it, write down which field and why.
→ Coverage checks
Then send one edit
From the editor’s chat panel, ask for something small — “change the hero headline to ‘Hello world’”. You should see the plan stream into the preview, the page update, and an undo entry appear in the history. If it does not, start at chat troubleshooting.If you get stuck
There is no penalty for switching paths halfway — the orchestrator only ever sees the result.- Hand the remainder to your own coding agent with the prompt on that page, which encodes everything above.
- Use the onboarding agent if what you actually need is content bootstrapped from a live URL rather than a codebase wired up.
- If the contract itself is the problem — a seam that does not fit your site’s shape — get in touch. Several of the helpers on this page exist because an integration hit exactly that and told us.