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.
Before you write anything: the survey
Every item below was missed by an integration that otherwise followed this page, and found only when a person opened the Studio. Look for each one first and write down what you found. The toolchain. Record the Node version, the package manager and whether corepack pins one (apackageManager field here or in a parent directory)
before the first install. Check free disk against what the install adds —
library mode’s @avocadostudio-ai/orchestrator-core is about 66 MB of
dependencies, and split mode does not need it. Next.js older than 15 or Astro
older than 5 is an upgrade to do first, not during.
Sections, not entries. List the components each route renders and map one
block per rendered section — even when several sections read the same CMS
entry. A blog post that is one entry rendered as a hero, a body and a related-posts
grid is three blocks. Mapped as one, a click anywhere selects the whole post and
the panel shows every field.
Known blockers.
Keep the site’s own data layer. Fetch pages the way the site already does
and overlay only the fields Avocado edits, rather than rewriting templates to
read Avocado’s projected props — otherwise every production read depends on the
lens, and the site’s original data module is left behind as dead code. A Next
site with a separate preview route re-implements the page composition there;
keep it in step with the public route, because nothing else will.
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, or an object { sections, elements, hidden, groups } that also shapes the add-block picker), getManifest (full override), getSiteConfig, editorOrigins (editor origins allowed by CORS, added to EDITOR_CORS_ORIGINS), onPublish, publishSecret, maxPagesRemoved.
Declare the site’s languages in getSiteConfig, even when there is one: { locales: ["en-US"], defaultLocale: "en-US" }. With them, a request to write in a language the site does not have produces a question instead of a translation.
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.
Production shape. The default mode: "auto" decides published versus editor per request, so every page renders dynamically. A production site uses mode: "static" on app/[[...slug]]/page.tsx, a mode: "preview" route at app/preview-draft/[[...slug]]/page.tsx, and a middleware or proxy that rewrites editor requests to the second.
Middleware. Next 15 uses middleware.ts with createEditorMiddleware from @avocadostudio-ai/site-sdk/middleware. Next 16 renamed the convention and reads config by static analysis, so it uses 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 rejected on 16. Either file goes at the project root, or in src/ if the app uses one.
A preview route you write yourself is gated first. Its first line is const editor = await requireEditorContext(await searchParams) from @avocadostudio-ai/site-sdk/draft, before it reads any CMS draft or orchestrator draft. It answers 404 unless the request is an authorized editor render: draft mode or a valid secret in production. A preview route that read drafts first and asked second has shown unreleased pages to anyone who typed its URL.
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.
Three declarations narrow it further, and both the panel and the ops engine honour them:
fixed: trueon a block type hides move, delete and add, and the ops engine refuses them. Use it for a section the template draws in a fixed place.readOnly: true, with areadOnlyReason, on a field the site shows but cannot store — shared asset alt text, a slug, a date. The panel shows it disabled with the reason, and writes are refused.shared: trueon a block type makes every instance with the same block id one piece of content — a header or footer injected into every page. An edit on one page reaches every page’s draft.
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; the converters for Storyblok, Contentful, Sanity Portable Text and Strapi are re-exported from@avocadostudio-ai/site-sdk/lens, so you need no direct dependency on@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, Sanity and Contentful (
@avocadostudio-ai/site-sdk/lens/storyblok,/lens/sanity,/lens/contentful).
applyDraft and applyDraftBlocks from @avocadostudio-ai/site-sdk/lens write only the mapped block props into the object your route already fetched, and return it unchanged when there is no draft.
Two rules govern every write, and both are load-bearing:
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 detects the framework, POSTs the site config and the draft secret to POST /sites/register, and — unless the orchestrator reports that the secret does not match its own — writes the env file that framework reads: .env.local with DRAFT_MODE_SECRET, ORCHESTRATOR_URL and the NEXT_PUBLIC_* names on Next, .env with DRAFT_MODE_SECRET, ORCHESTRATOR_URL and AVOCADO_SITE_ID on Astro. The secret comes from --secret, the env file, or is generated.
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 still written the env file with a secret it could not check; 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, --token (access token for an orchestrator with a password; defaults to ORCHESTRATOR_ACCESS_TOKEN), --cwd. 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. Against an orchestrator that has one, the script checks for it and stops before writing anything, saying where the right value is: for a standalone orchestrator run from an Avocado checkout, DRAFT_MODE_SECRET in that checkout’s .env. Re-run with --secret <value>.
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
Survey the site
The survey — toolchain, the section map, and the known blockers. Report it before changing anything.
3
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.4
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.
5
Declare your blocks
Seam 3 — Custom blocks. Register your own components and the props you are prepared to let marketing change.
6
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.
7
Adapt the content source
Seam 5 — a field table if there is a CMS behind this, CMS adapters otherwise.
8
Register the site
Seam 6 —
npx avocado-register --name "...". The site appears in the editor’s dashboard on the next open or refresh.9
Prove it with a number
Run
editableCoverage and panelCoverage from @avocadostudio-ai/site-sdk/coverage and put them in CI. See below.10
Pass the QA gate
npx avocado qa, then its manual pass. Not finished until both are.11
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
The last gate: avocado qa
Coverage proves the markers are there. It cannot see what went wrong on the
integrations this gate comes from, all of which passed type-check, build and a
curl of the editor API: a live-preview SDK that threw inside the frame,
bullets that rendered as paragraphs, a preview route that had drifted from the
public one, pages that 404’d after a block schema changed. Those live in a
browser, inside the editor frame, with edited content, or across a schema
change — so that is where the last check goes.
- Clicking each visible section selects a block with a sensible name and only its fields
- Fields that cannot be written (asset alt text, slugs, dates) are not offered as editable
- A chat edit and a panel edit both update the preview within a few seconds
- Rich text in the panel looks like rich text on the page (lists, headings, links)
- Asking for a language the site does not have produces a question, not an overwrite
- Publishing one page changes only that page in the CMS, and the public site shows it after reload
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 skills on that page, which encode 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.