Skip to main content
This page is the reference, not a tutorial. It states what the integration must satisfy, so you can read it before delegating the work, or review what an agent produced against it. End state. A site registered with the orchestrator, your existing pages serving from your existing routes, your own components exposing exactly the props you declared as content, and a coverage number that proves it.
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/astro satisfies 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 /core primitives — 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_KEY or GOOGLE_GENAI_API_KEY on the orchestrator. Avocado runs on your keys and never resells tokens.

The six seams

1. The editor API — one catch-all route

Mount createEditorApiHandler 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:
Do not hand-write these. Secret validation and the internal-redirect check are security-critical and easy to get subtly wrong — an open redirect on /api/editor/draft is a real vulnerability. The helper does both, plus the draft cookie and CORS preflight.
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. publishSecret is usually process.env.PUBLISH_TOKEN, and the orchestrator sends the same value as x-publish-token. With no secret configured, the route answers 401 under NODE_ENV=production and 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": true in the body and is otherwise a 409 that says how many pages it protected. Removing one page of three is still an ordinary edit; set maxPagesRemoved if 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.
The rule is exported as checkDestructivePublish from @avocadostudio-ai/site-sdk/routes if you want to apply it somewhere else. Next.js integration

2. The page factory

Replace app/[[...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.
  • generateMetadatanot 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.
Pass 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:
This registration is the safety boundary. 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.
Import only from the SDK. registerBlock, z and the block-meta types come from @avocadostudio-ai/site-sdk/blocks; the marker helpers from @avocadostudio-ai/site-sdk/markers; the coverage gate from @avocadostudio-ai/site-sdk/coverage. Never import @avocadostudio-ai/shared, @avocadostudio-ai/blocks, @avocadostudio-ai/preview-adapter or a bare zod — a dependency of a dependency is not a specifier your source may use. Under pnpm it will not resolve; under npm’s flat hoisting it resolves today and breaks the first time something re-hoists, and two copies of zod fail in ways that look like schema bugs.
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:
Paths are scoped from the block down, so a list row drawn by its own component needs a scope on a wrapper:
Two things about this seam are worth stating plainly, because they are what integrations get wrong:
  • 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. With display: contents the rows go on being their parent’s children. Leave it off when the wrapper is one you were rendering anyway.
This is the longest part of a real integration: it is a pass over every renderer.

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-file ships a working handler. See examples/sample-site/.
  • A mainstream CMS — working examples ship for Contentful, Sanity and Strapi under examples/contentful-site/, examples/sanity-site/ and examples/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.
Two rules govern every write, and both are load-bearing:
  1. merge takes the live CMS document as its source, never a snapshot Avocado holds. Every field the table never declared survives by construction.
  2. Publishing is a field-level diff, not a snapshot overwrite, so a page nobody touched writes nothing.
CMS adapters · Multilingual · Publishing

6. Registration

From the project directory:
The bin script ships with @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 no searchParams, so the check is header-based:
It answers a rendering question, not an authorization one — what may see unpublished content is decided by 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.
  • editableCoverage compares the fields the manifest declares against the data-editable-target markers a rendered page actually carries, and reports marked/expected. A field that lost its marker in a refactor fails this instead of silently losing inline editing.
  • panelCoverage asks whether the property panel is intelligible: rowsLabelled/rowsExamined plus 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.
Both are pure functions from @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.