This is the canonical onboarding path for any Next.js 15 (App Router) site. For background, see Core Concepts and the Integration Overview. Goal: keep your existing routes serving published content, enable AI editing through Next.js Draft Mode cookies, and register the site so it shows up in the editor’s dashboard. NoDocumentation Index
Fetch the complete documentation index at: https://docs.avocadostudio.dev/llms.txt
Use this file to discover all available pages before exploring further.
/preview route required.
Related:
- Editor Quickstart — env vars, iframe URL pattern, smoke checks
- Custom Blocks — register your own component types alongside (or instead of) the built-in blocks
- Architecture — how the three services communicate
How it works in two helpers
The SDK collapses the entire integration into two factory functions. Most adopters need exactly two new files; nothing else changes in your project.| Helper | What it gives you | Mount at |
|---|---|---|
createEditorApiHandler | One catch-all route that serves blocks, pages, draft, draft/disable, and publish | app/api/editor/[...path]/route.ts |
createSitePage | A Page component + generateStaticParams with draft mode, navigation, footer, editor overlay, and 404 fallbacks already wired in | app/[[...slug]]/page.tsx |
getPage, getSlugs, getSiteConfig), then register the site with npx avocado-register. That’s the whole integration.
If you need fine-grained control instead, the low-level primitives section shows the underlying handlers (createBlocksHandler, createDraftEnableHandler, createDraftDisableHandler, fetchEditorPage, fetchEditorSlugs).
Walkthrough
Install the SDK
From your Next.js project root:Peer dependencies (
next ≥ 15, react ≥ 19) should already be in your project. The SDK has no other runtime dependencies.Mount the catch-all editor API route
Create This single file exposes:
app/api/editor/[...path]/route.ts:GET /api/editor/blocks— block manifest (auto-built from the SDK’s built-in registry, override viagetManifestfor custom blocks)GET /api/editor/pages—{ pages: PageDoc[] }for editor session bootstrapGET /api/editor/draft?secret=...&redirect=...— Draft Mode entry, validatessecretagainstDRAFT_MODE_SECRET, only allows internal redirectsGET /api/editor/draft/disable?redirect=...— Draft Mode exitPOST /api/editor/publish— receives published pages back from the editor
Replace your page route with createSitePage
Create (or replace)
app/[[...slug]]/page.tsx:createSitePage handles, in order:- Detecting Draft Mode via
next/headersand switching reads tofetchEditorPage/fetchEditorSlugs(which call the orchestrator) - Building site nav/header chrome from
getSiteConfig - Rendering blocks via the SDK’s
renderBlocksand the shared block library - Mounting the live
EditorOverlaywhen in editor mode - Falling back to your CMS data if the orchestrator is unreachable
- Returning a 404 (or “Draft unavailable”) fallback when no page exists for the slug
lib/my-cms.ts does not change — createSitePage calls into it.Register the site with the orchestrator
Make sure the orchestrator is running (The CLI (shipped inside
pnpm dev:orchestrator from the Avocado repo, or your hosted instance), then from your Next.js project directory:@ai-site-editor/site-sdk) will:- Generate a
DRAFT_MODE_SECRETif.env.localdoesn’t already have one (32 random bytes, hex-encoded). - Write
ORCHESTRATOR_URL,DRAFT_MODE_SECRET,NEXT_PUBLIC_DEFAULT_SITE_ID,NEXT_PUBLIC_SITE_NAME,NEXT_PUBLIC_EDITOR_ORIGINto.env.localif missing (existing values are never overwritten). - POST your site config to
${ORCHESTRATOR_URL}/sites/register.
npx avocado-register --help. Common ones: --id, --port, --orchestrator, --secret, --session, --purpose.After it succeeds, the site appears in the editor’s dashboard the next time you open or refresh http://localhost:4100.Verify the contract
Start your dev server, then run these from a second terminal. All four should pass:And one negative check that’s worth running by hand because it’s the security-critical one:If all five behave as shown, the integration is complete.
Open the editor and confirm round-trip
Open
http://localhost:4100. Your site should be in the dashboard. Click its tile, then send a simple edit from the chat panel like “change the hero headline to Hello world”. You should see:- The AI generate an operation
- The preview update inside the iframe
- An undo entry appear in the history
TypeScript types
The SDK re-exports the core types from@ai-site-editor/shared. Import what your fetchers need:
PageDoc has shape { id: string; slug: string; meta?: PageMeta; blocks: BlockInstance[] }. BlockInstance is { id: string; type: string; props: Record<string, unknown> }. See packages/shared/src/schemas.ts in the repo for the Zod schemas that back these types.
Block manifest
The manifest is what tells the editor which block types exist and what props each one accepts.createEditorApiHandler builds it automatically from the SDK’s built-in block registry — you only need to think about it if you have custom React components.
Example response shape from GET /api/editor/blocks:
getManifest function to createEditorApiHandler and the SDK uses yours instead of the built-in one.
Component matching
The editor never infers components from DOM class names. It matches by stabletype strings that must agree across three places:
Environment variables
The values written bynpx avocado-register into your project’s .env.local:
| Variable | Where it lives | Purpose |
|---|---|---|
DRAFT_MODE_SECRET | site .env.local | Validates ?secret= on Draft Mode entry. Generated by avocado-register if missing. |
ORCHESTRATOR_URL | site .env.local | Where the SDK fetches draft pages from. Defaults to http://127.0.0.1:4200 in dev. |
NEXT_PUBLIC_DEFAULT_SITE_ID | site .env.local | The site ID this project corresponds to in the orchestrator. |
NEXT_PUBLIC_SITE_NAME | site .env.local | Display name shown in the editor’s site picker. |
NEXT_PUBLIC_EDITOR_ORIGIN | site .env.local | Editor origin for postMessage trust checks. Defaults to http://localhost:4100. |
apps/editor/.env, set by you), single-tenant build-time:
| Variable | Purpose |
|---|---|
VITE_SITE_ORIGIN | Where the editor’s iframe loads from |
VITE_SITE_DRAFT_SECRET | Must equal the site’s DRAFT_MODE_SECRET — the editor uses this to construct the bootstrap URL the iframe loads |
VITE_SITE_DRAFT_SECRET (built into the editor) and DRAFT_MODE_SECRET (read by the site at runtime) is the single most common failure — avocado-register surfaces it as a warning, and the orchestrator’s /sites/register response includes a warnings array for the same reason.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Editor iframe loads but no edits work, header says Degraded | Manifest route not mounted or returning empty | curl http://localhost:3000/api/editor/blocks — should return version + non-empty blocks array. Check that app/api/editor/[...path]/route.ts exists and exports GET. |
curl /api/editor/draft?secret=… returns 401 even with the right secret | The site’s DRAFT_MODE_SECRET doesn’t match what you’re passing | cat .env.local | grep DRAFT_MODE_SECRET — make sure you copied the full value, not just a prefix. Restart the Next.js dev server after editing .env.local. |
| Editor opens the iframe but the page renders published content, not the draft | Page loader isn’t branching on Draft Mode | If you wrote the loader by hand, check it calls (await draftMode()).isEnabled and switches to fetchEditorPage / fetchEditorSlugs. Or switch to createSitePage, which does this for you. |
Iframe loads but EditorOverlay doesn’t appear | Draft cookie not being sent because of sameSite | If your Next.js site is on a different origin from the editor, the draft cookie needs SameSite=None; Secure. The SDK helper sets sameSite=lax by default, which works for localhost:3000 ↔ localhost:4100 but not cross-origin HTTPS. For production, mount the editor on the same domain or override the cookie via your own draft route. |
npx avocado-register says “could not reach the orchestrator” | Orchestrator isn’t running | Start pnpm dev:orchestrator, or pass --orchestrator http://your-host:4200. |
avocado-register warns “DRAFT_MODE_SECRET mismatch” | The secret in your project’s .env.local doesn’t match the one the editor was built with (VITE_SITE_DRAFT_SECRET) | Either rebuild the editor against your project’s secret, or pass --secret <editor-value> to avocado-register to overwrite your .env.local. |
| Site registered but doesn’t appear in the editor | The editor caches the site list in localStorage and only fetches GET /sites on mount | Hard-refresh the editor (Cmd+Shift+R). |
| Custom React components render fine on the live site but the editor refuses to edit them | Component type isn’t in the manifest | Register the component via getManifest — see Custom Blocks. |
import.meta.env.VITE_SITE_DRAFT_SECRET change isn’t picked up | Vite snapshots import.meta.env.* at startup | Restart pnpm dev:editor. HMR doesn’t re-evaluate .env. |
Low-level primitives
IfcreateEditorApiHandler and createSitePage are too opinionated for your project — for example you have a custom routing layer, you mount the editor API at a non-standard path, or you need to compose draft mode with your own middleware — the same building blocks are exported individually:
{ GET, POST, OPTIONS } object you mount at any route you like. fetchEditorPage(slug, session, siteId) and fetchEditorSlugs(session, siteId) are the primitives createSitePage calls internally — use them directly inside your own page component if you need to compose them with other data sources.
The contract these primitives implement is the same one createEditorApiHandler mounts:
- Block manifest:
GET /api/editor/blocks(or wherever you mount it) - Pages snapshot:
GET /api/editor/pages - Draft enter:
GET /api/editor/draft?secret=...&redirect=/...— must validate the secret and reject non-internal redirects - Draft exit:
GET /api/editor/draft/disable?redirect=/... - Publish:
POST /api/editor/publish
VITE_SITE_ORIGIN and the bootstrap URL builder accordingly — the editor expects the standard paths by default.
Optional: dedicated /preview/* route group
If you want stronger isolation between published and draft content (e.g. a separate route group with its own middleware, layout, or feature flags), you can add a/preview/* route group that calls into fetchEditorPage directly. This is opt-in and not part of the standard onboarding path — most adopters don’t need it because Draft Mode cookies already give you per-request isolation.