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

# CMS Adapters

> Plug any content source — JSON file, Contentful, Sanity, Strapi, custom REST — into Avocado Studio via the CmsAdapter interface.

Avocado Studio doesn't ship its own content database. Pages live in your CMS (or a JSON file in git, or MDX, or an internal API); the orchestrator holds **draft / undo / chat state** in SQLite and reads **published content** from your store via a small adapter.

That means you keep your existing CMS, your existing content model, your existing publishing workflow. Avocado adds an AI-native editing surface on top. Switching content stores is a swap of one adapter, not a migration.

## The contract

Every adapter implements the same two-method interface from `@ai-site-editor/orchestrator-core`:

```ts theme={null}
export interface CmsAdapter {
  readonly id: string                                  // "json-file", "sanity", …
  getPages(): Promise<PageDoc[]>                       // seed on cold session
  onPublish?(pages: PageDoc[]): Promise<void>          // optional writeback
}
```

That's the whole contract.

* `getPages()` is called **lazily on the first chat for a fresh session**. Its result seeds SQLite so chat turns like *"edit the homepage hero"* resolve against your real content instead of a 404.
* `onPublish()` is called when a client POSTs to `/publish` on the library-mode handler with `{ session, siteId }`. The orchestrator reads the current draft from SQLite and hands the resulting `PageDoc[]` to the adapter. If you omit `onPublish`, `/publish` becomes a `200` no-op (with `written: false` in the response) and SQLite still holds the draft.

```mermaid theme={null}
flowchart LR
  studio[Content Studio] -->|chat| orch[Orchestrator]
  orch -->|adapter.getPages on cold session| store[(Your content store)]
  orch -->|adapter.onPublish on publish| store
  orch -->|drafts + undo + chat| sqlite[(SQLite working copy)]
```

The adapter is the **seed and the sink**, never the live working copy. All in-flight edits live in SQLite; that's what makes the same chat UX work against a static JSON file, a Sanity space, or anything in between.

## Bundled adapters

Two adapter implementations ship in `@ai-site-editor/orchestrator-core/cms`:

### `jsonFileAdapter`

Reads `PageDoc[]` from a JSON file on disk. Smallest possible adapter — useful when your content is already checked into git, or for prototypes before you pick a real CMS.

```ts theme={null}
import { createOrchestrator } from "@ai-site-editor/site-sdk/server"
import { jsonFileAdapter } from "@ai-site-editor/orchestrator-core/cms"
import path from "node:path"

const handler = createOrchestrator({
  adapter: jsonFileAdapter({
    path: path.join(process.cwd(), "lib", "published-content.json"),
    writeOnPublish: false  // default; flip to true to overwrite the file on publish
  })
})
```

The file may be either a bare `PageDoc[]` or an object with a `pages` key. Each entry is parsed through the lenient `PageDoc` schema, so partial or extra fields are tolerated.

### `editorApiAdapter`

Fetches `PageDoc[]` from a site's `/api/editor/pages` endpoint. Use this when your site already exposes a page-listing API, or when the orchestrator runs out-of-process from the site.

```ts theme={null}
import { editorApiAdapter } from "@ai-site-editor/orchestrator-core/cms"

const handler = createOrchestrator({
  adapter: editorApiAdapter({
    origin: "https://your-site.com",   // or set AUTO_BOOTSTRAP_SITE_ORIGIN
    path: "/api/editor/pages",         // default
    siteId: "main",                    // optional ?siteId= query
    timeoutMs: 8000,                   // default
    headers: {                         // optional — for protected endpoints
      authorization: `Bearer ${process.env.EDITOR_API_TOKEN}`
    }
  })
})
```

Pages that fail the lenient `PageDoc` schema are dropped from the seed and a warning is logged with the candidate index, slug, and first Zod issue path — pass `logger: yourLogger` if you want those routed to your own log sink.

## Wiring it up

Library-mode integration (orchestrator mounted as a Next.js catch-all route inside your site) is the recommended pattern. One file, fully drop-in:

```ts theme={null}
// app/api/avocado/[[...path]]/route.ts
import { createOrchestrator } from "@ai-site-editor/site-sdk/server"
import { jsonFileAdapter } from "@ai-site-editor/orchestrator-core/cms"
import path from "node:path"

export const runtime = "nodejs"
export const dynamic = "force-dynamic"

const handler = createOrchestrator({
  adapter: jsonFileAdapter({
    path: path.join(process.cwd(), "lib", "published-content.json")
  }),
  siteId: "my-site"  // optional; defaults to "library" when adapter is set
})

export const POST = handler
export const GET = handler
export const OPTIONS = handler
```

That's it — the first chat turn for a new session calls `getPages()`, seeds SQLite, and the planner can immediately reason about your pages.

### Publishing back

Once `adapter.onPublish` is defined, your editor (or any client) can publish a session's draft to the upstream store with one POST:

```bash theme={null}
curl -X POST https://your-site.com/api/avocado/publish \
  -H 'content-type: application/json' \
  -d '{"session":"sess_abc","siteId":"my-site"}'
# → { "ok": true, "written": true, "count": 3 }
```

The orchestrator reads the current draft for the scoped session out of SQLite and hands the resulting `PageDoc[]` to `adapter.onPublish(pages)`. If the adapter has no `onPublish`, the route still returns `200` but with `written: false` — useful for sites that publish via CI / git commit rather than a runtime writeback.

<Note>
  **Session scoping.** When `adapter` is set, sessions auto-scope to `siteId` (default `"library"`) so they bypass the legacy demo-content seed path. If you see a chat returning demo blocks instead of your content, pass an explicit `siteId` to force the scope.
</Note>

## Writing a custom adapter

If your content lives somewhere `jsonFileAdapter` and `editorApiAdapter` don't reach, write your own. The contract is small enough to inline:

```ts theme={null}
// lib/my-cms-adapter.ts
import type { CmsAdapter } from "@ai-site-editor/orchestrator-core/cms"
import type { PageDoc } from "@avocadostudio-ai/shared"
import { myCmsClient } from "./my-cms-client"

export function myCmsAdapter(opts: { spaceId: string }): CmsAdapter {
  return {
    id: "my-cms",
    async getPages(): Promise<PageDoc[]> {
      const raw = await myCmsClient.listPages(opts.spaceId)
      return raw.map(mapMyCmsToPageDoc)
    },
    async onPublish(pages: PageDoc[]): Promise<void> {
      await Promise.all(pages.map((p) => myCmsClient.upsert(mapPageDocToMyCms(p))))
    }
  }
}
```

Then pass the instance into `createOrchestrator({ adapter: myCmsAdapter({ spaceId: "..." }) })`. Failures inside `getPages()` are logged but non-fatal — the session simply starts empty.

If you build a non-trivial adapter (Storyblok, Hygraph, Payload, Directus, etc.) we'd love to merge it into `examples/`.

## Custom block schemas

If your site renders **custom block shapes** (e.g. a `Hero` with `carouselImages` instead of canonical `imageUrl`), register your schemas with the global block registry alongside the adapter:

```ts theme={null}
// app/api/avocado/[[...path]]/route.ts
import { createOrchestrator } from "@ai-site-editor/site-sdk/server"
import { jsonFileAdapter } from "@ai-site-editor/orchestrator-core/cms"

// Side-effect import: registers site-specific block schemas with the global
// block registry. MUST be the last import — ESM side effects fire in source
// order, so this runs AFTER orchestrator-core's transitive imports of
// @avocadostudio-ai/shared have registered the canonical schemas. Otherwise
// the canonical registrations will overwrite yours.
import "@/lib/my-blocks"

const handler = createOrchestrator({ adapter: jsonFileAdapter({ path: "..." }) })
```

Inside `lib/my-blocks.ts`, call `registerBlock("Hero", { schema, meta })` for each type you want to override. See [Custom blocks](/integration/custom-blocks) for the full schema shape.

<Warning>
  The schema-override import must be the **last** import in the file. ESM resolves imports up front but fires side effects in source order — if your override runs before the orchestrator's transitive deps, the canonical schemas will re-register on top of yours.
</Warning>

## Example apps

Five working examples live under [`examples/`](https://github.com/avocadostudio-ai/avocado/tree/main/examples) in the repo. Each one boots in under two minutes:

| Example                                                                                                                 | Content store   | Pattern                                      | Port |
| ----------------------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------- | ---- |
| [`sample-site`](https://github.com/avocadostudio-ai/avocado/tree/main/examples/sample-site)                             | Local JSON file | Zero-config starter — uses `jsonFileAdapter` | 3002 |
| [`contentful-site`](https://github.com/avocadostudio-ai/avocado/tree/main/examples/contentful-site)                     | Contentful      | Headless SaaS CMS                            | 3003 |
| [`contentful-marketing-site`](https://github.com/avocadostudio-ai/avocado/tree/main/examples/contentful-marketing-site) | Contentful      | Marketing-site flavor                        | 3006 |
| [`sanity-site`](https://github.com/avocadostudio-ai/avocado/tree/main/examples/sanity-site)                             | Sanity          | Headless + embedded Studio                   | 3004 |
| [`strapi-site`](https://github.com/avocadostudio-ai/avocado/tree/main/examples/strapi-site)                             | Strapi v5       | Self-hosted open-source CMS                  | 3005 |

Every CMS example (Contentful, Sanity, Strapi) ships with both wirings side-by-side: a library-mode `/api/avocado/[[...path]]/route.ts` that mounts `createOrchestrator({ adapter: ... })` (recommended), plus the legacy split-mode `/api/editor/*` route for sites that want the orchestrator deployed separately.

### Contentful

Free Community plan is enough. The setup script creates the full content model (20 block types + `page` + `siteConfig`).

```bash theme={null}
CONTENTFUL_SPACE_ID=<from app.contentful.com → Settings → General>
CONTENTFUL_DELIVERY_TOKEN=<from API keys → Content delivery tokens>
CONTENTFUL_MANAGEMENT_TOKEN=<from API keys → Content management tokens>
CONTENTFUL_ENVIRONMENT=master

pnpm --filter contentful-site contentful:setup     # idempotent
pnpm --filter contentful-site dev                  # http://localhost:3003
```

Full walkthrough: [`examples/contentful-site/README.md`](https://github.com/avocadostudio-ai/avocado/blob/main/examples/contentful-site/README.md).

### Sanity

The example ships with an **embedded Sanity Studio at `/studio`** alongside the Avocado editor.

```bash theme={null}
NEXT_PUBLIC_SANITY_PROJECT_ID=<from sanity.io/manage>
NEXT_PUBLIC_SANITY_DATASET=production
SANITY_API_TOKEN=<from API → Tokens (Editor permissions)>

pnpm --filter sanity-site sanity:schema-gen        # only after upgrading block fields
pnpm --filter sanity-site dev                      # http://localhost:3004
```

Add `http://localhost:3004` as a CORS origin in Sanity project settings (with credentials allowed).

Full walkthrough: [`examples/sanity-site/README.md`](https://github.com/avocadostudio-ai/avocado/blob/main/examples/sanity-site/README.md).

### Strapi

Self-hosted, open-source. The setup script generates Strapi v5 schema files from the Avocado block registry.

```bash theme={null}
npx create-strapi@latest strapi-backend --quickstart --skip-cloud --no-run
STRAPI_PROJECT=/absolute/path/to/strapi-backend pnpm --filter strapi-site strapi:setup
cd /absolute/path/to/strapi-backend && npm run develop    # http://localhost:1337/admin

# In another terminal:
pnpm --filter strapi-site dev                              # http://localhost:3005
```

Generate an API token from the Strapi admin (**Settings → API Tokens**) with read+write for `page` and `site-config`.

Full walkthrough: [`examples/strapi-site/README.md`](https://github.com/avocadostudio-ai/avocado/blob/main/examples/strapi-site/README.md).

## See also

* [Next.js integration](/integration/nextjs-integration) — full route-handler reference
* [Block system](/integration/block-system) — how `PageDoc` and `BlockInstance` are shaped
* [Custom blocks](/integration/custom-blocks) — registering your own React components
* [Publishing](/integration/publishing) — how `onPublish` ties into the publish flow
