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

# Non-Next.js Integration

> How to implement the editor API contract on Remix, SvelteKit, Hono, or any framework that exposes web-standard Request/Response handlers — using the SDK's framework-agnostic /core primitives. Astro has its own integration.

The default integration path uses [`createEditorApiHandler`](/integration/nextjs-integration) and [`createSitePage`](/integration/nextjs-integration#walkthrough) — both of which import from `next/headers` and `next/server`. If your site is on **Remix, SvelteKit, Hono, Cloudflare Workers, or any other framework that gives you web-standard `Request` and `Response` objects** — Astro excepted, which has [its own integration](/integration/astro-integration) — you can implement the same contract by hand using the SDK's framework-agnostic `/core` primitives instead. Same `/api/editor/*` URL paths, same wire format, same end state — your site appears in the editor's dashboard, edits round-trip through Draft Mode (or the framework's equivalent), and the rest of the docs apply.

<Note>
  **On Astro, stop here — there is a package.** [`@avocadostudio-ai/astro`](/integration/astro-integration) is an Astro integration that mounts all five routes, injects the preview bridge and handles the prerendering problem for you. Nothing on this page applies; do not implement the contract by hand.

  **Next.js and Astro are the tested frameworks today.** The `/core` primitives below are framework-agnostic by design, but the adapters shipped in the box are the Next.js ones in `@avocadostudio-ai/site-sdk/draft` and `@avocadostudio-ai/site-sdk/routes`, plus the Astro integration above. On Remix, SvelteKit, Hono or Workers you'll be a first mover. The patterns on this page are real and the primitives work, but expect to find gaps — tell us about them.
</Note>

## What you have to implement

The contract the editor speaks is **five HTTP routes** mounted under `/api/editor/*`. On Next.js the SDK's catch-all handler implements all five for you. Off Next.js, you implement them yourself, but the SDK gives you most of the logic via the `/core` exports — you only have to provide a small **adapter** that translates between your framework's request/cookie/redirect primitives and the SDK's web-standard ones.

| Route | Purpose | SDK helper to call |
| - | - | - |
| `GET /api/editor/blocks` | Block manifest (auto-built from the SDK's built-in registry, override with your own) | [`createBlocksHandler`](#1-blocks-and-pages-no-adapter-needed) |
| `GET /api/editor/pages` | `{ pages: PageDoc[] }` of published content for editor session bootstrap | [`createPagesHandler`](#1-blocks-and-pages-no-adapter-needed) |
| `GET /api/editor/draft?secret=...&redirect=...` | Draft Mode entry — validates secret, sets cookies, redirects | [`createDraftEnableHandlerCore`](#2-draft-mode-routes-needs-an-adapter) |
| `GET /api/editor/draft/disable?redirect=...` | Draft Mode exit — clears cookies, redirects | [`createDraftDisableHandlerCore`](#2-draft-mode-routes-needs-an-adapter) |
| `POST /api/editor/publish` | Receives published pages back from the editor (only if you support publish) | [`createPublishHandler`](#1-blocks-and-pages-no-adapter-needed) — read the guards below before you mount it |

### The short way: one catch-all handler

`createEditorApiHandlerCore` from `@avocadostudio-ai/site-sdk/routes/core` is
the same catch-all `createEditorApiHandler` is on Next.js, with nothing
framework-shaped left in it. It serves all five routes, takes the same options
(`getPages`, `onPublish`, `publishSecret`, `registerBlocks`, `blockTypes`,
`getSiteConfig`, `editorOrigins`, `maxPagesRemoved`, `getManifest`), and adds
two: the `draftAdapter` described in [section 2](#2-draft-mode-routes-needs-an-adapter),
and `basePath` (default `/api/editor`), used to find the route from the URL when
your framework does not hand over path segments.

```ts theme={null}
import { createEditorApiHandlerCore } from "@avocadostudio-ai/site-sdk/routes/core"

const editorApi = createEditorApiHandlerCore({
  getPages: () => getMyPages(),
  onPublish: async (pages, config) => { await publishMyPages(pages, config); return { ok: true } },
  publishSecret: process.env.PUBLISH_TOKEN,
  draftAdapter: myDraftAdapter(), // section 2
})

// Each handler takes the Request, and optionally the path segments below the mount.
app.all("/api/editor/*", (c) =>
  c.req.method === "POST" ? editorApi.POST(c.req.raw)
  : c.req.method === "OPTIONS" ? editorApi.OPTIONS(c.req.raw)
  : editorApi.GET(c.req.raw))
```

The rest of this section and the next take the routes one at a time, which is
what you want if you mount them separately.

The SDK exports come from three subpaths:

```ts theme={null}
// Web-standard handlers (no framework dependency)
import {
  createBlocksHandler,
  createPagesHandler,
  createPublishHandler,
} from "@avocadostudio-ai/site-sdk/routes"

// Draft Mode handlers (need an adapter — see below)
import {
  createDraftEnableHandlerCore,
  createDraftDisableHandlerCore,
  type DraftRouteAdapter,
} from "@avocadostudio-ai/site-sdk/routes/core"

// Draft context resolver (for your page render path — also needs an adapter)
import {
  resolveDraftContextCore,
  type DraftModeAdapter,
} from "@avocadostudio-ai/site-sdk/draft/core"

// Server-side draft fetching (no framework dependency — just fetch())
import { fetchEditorPage, fetchEditorSlugs } from "@avocadostudio-ai/site-sdk/draft"
```

## 1. Blocks and pages (no adapter needed)

`createBlocksHandler`, `createPagesHandler`, and `createPublishHandler` already accept and return web-standard `Request` and `Response` objects. They have **no Next.js dependency** — you can mount them on any framework that lets you wire a `(request: Request) => Response` handler to a route.

### Hono / Cloudflare Workers example

```ts theme={null}
import { Hono } from "hono"
import {
  createBlocksHandler,
  createPagesHandler,
  createPublishHandler,
} from "@avocadostudio-ai/site-sdk/routes"
import { getMyPages, publishMyPages } from "./lib/my-cms"

const blocks = createBlocksHandler()
const pages = createPagesHandler(() => getMyPages())
const publish = createPublishHandler(
  async (pages, config) => { await publishMyPages(pages, config); return { ok: true } },
  {
    publishSecret: process.env.PUBLISH_TOKEN,
    // The destructive-publish baseline. Pass the same getter you gave
    // createPagesHandler, or the guard runs without knowing what is live.
    getPages: () => getMyPages(),
  }
)

const app = new Hono()
app.get("/api/editor/blocks", (c) => blocks.GET(c.req.raw))
app.get("/api/editor/pages", (c) => pages.GET(c.req.raw))
app.post("/api/editor/publish", (c) => publish.POST(c.req.raw))
app.options("/api/editor/blocks", (c) => blocks.OPTIONS(c.req.raw))
```

These three handlers are the easy half — they just work as-is on any web-standard server.

### Two things `createPublishHandler` refuses

This route overwrites the site's content, so it fails closed in two states, and a hand-wired mount meets both.

* **No `publishSecret` under `NODE_ENV=production` is a 401**, whose `reason` names `PUBLISH_TOKEN`. The option used to be optional, and every integration that read it from an environment variable nobody set was serving an endpoint that replaced a site's pages for any caller. In development the handler stays open — publishing to your own machine is the point — and warns once on the first request. When a secret is set, the caller sends it as `x-publish-token`.
* **A publish that would remove every page is a 409** unless the body carries `allowDelete: true`. An empty `pages` array is almost never somebody deleting their site; it is a client publishing what it thinks it has after its own state failed to load. Pass `getPages` so the refusal can say what it protected and so `maxPagesRemoved` — a tighter bound than "not all of them" — can be enforced at all. Without a baseline the empty publish is still refused: not knowing what is there is not a reason to overwrite it with nothing. A site that is already empty may publish empty, so a first publish never trips this.

The rule itself is exported as `checkDestructivePublish` from `@avocadostudio-ai/site-sdk/routes`, so a route you write entirely by hand can apply the same one.

## 2. Draft Mode routes (needs an adapter)

The Draft Mode entry / exit routes need to do three framework-specific things:

1. **Toggle draft mode** — on Next.js this is `(await draftMode()).enable()`. On other frameworks, draft mode is usually a cookie *you* set; there's no global "enable" function. The SDK calls `enableDraftMode()` on your adapter; you decide what that means.
2. **Set cookies on the response** — every framework handles this differently. The SDK passes a list of cookies to your adapter; you attach them to whatever response object you return.
3. **Build the redirect response** — same idea. The SDK gives you the destination `URL` and the cookies; you return a framework-appropriate `Response`.

The interface you implement is small:

```ts theme={null}
import type { DraftRouteAdapter } from "@avocadostudio-ai/site-sdk/routes/core"

const adapter: DraftRouteAdapter = {
  // Called when the user is entering Draft Mode after a valid secret check.
  // On Next.js this is `(await draftMode()).enable()`. On other frameworks, this
  // is usually a no-op — the cookie set in `createRedirect` is what enables draft
  // mode in your page handlers (see section 3 below).
  enableDraftMode: async () => { /* usually nothing here */ },

  // Same shape, called by the /draft/disable route.
  disableDraftMode: async () => { /* usually nothing here */ },

  // Build a redirect response with the given cookies attached.
  // The SDK already validated the secret and resolved a safe internal redirect URL.
  createRedirect: (url: URL, cookies) => {
    // ... your framework's redirect builder, with the cookies attached
  },
}
```

### SvelteKit example

```ts theme={null}
// src/routes/api/editor/draft/+server.ts
import {
  createDraftEnableHandlerCore,
  type DraftRouteAdapter,
} from "@avocadostudio-ai/site-sdk/routes/core"

export function svelteKitAdapter(): DraftRouteAdapter {
  return {
    // SvelteKit has no built-in "draft mode" toggle. The cookies set by createRedirect
    // are what tell our render path to fetch draft content (see section 3).
    enableDraftMode: async () => {},
    disableDraftMode: async () => {},

    createRedirect: (url, cookies) => {
      const headers = new Headers({ Location: url.toString() })
      // We also add a "draft mode" cookie ourselves so our +page.server.ts files
      // can branch on it. The cookies the SDK passes are the editor session/siteId
      // cookies — we add our own draft-enabled flag alongside.
      // One Set-Cookie header per cookie: joining them with ", " is not a valid
      // way to send several, and browsers keep only part of it.
      headers.append("Set-Cookie", "__draft_enabled=1; Path=/; SameSite=Lax")
      for (const c of cookies ?? []) {
        if (c.delete) {
          headers.append("Set-Cookie", `${c.name}=; Path=/; Max-Age=0`)
        } else {
          headers.append("Set-Cookie", `${c.name}=${c.value}; Path=/; SameSite=Lax`)
        }
      }
      return new Response(null, { status: 307, headers })
    },
  }
}

export const GET = async ({ request }) => {
  return createDraftEnableHandlerCore(svelteKitAdapter())(request)
}
```

```ts theme={null}
// src/routes/api/editor/draft/disable/+server.ts
import { createDraftDisableHandlerCore } from "@avocadostudio-ai/site-sdk/routes/core"
import { svelteKitAdapter } from "../+server"

export const GET = async ({ request }) => {
  // The exit handler will pass `delete: true` cookies — make sure your
  // adapter's createRedirect handles those (the example above does).
  return createDraftDisableHandlerCore(svelteKitAdapter())(request)
}
```

### What `enableDraftMode` actually means off Next.js

Next.js has a global `draftMode()` API that flips a server-side flag, and `next/headers` reads it back from inside your page render. **No other framework has this.** On every other framework, the closest equivalent is "set a cookie that your page-render code checks."

The `__draft_enabled=1` cookie in the SvelteKit example above is illustrative — pick whatever name and shape works for your stack. The SDK doesn't care what your draft flag looks like; it only cares that:

* The `/api/editor/draft` route validates the secret and sets it
* The `/api/editor/draft/disable` route clears it
* Your page render code reads it and switches between published and draft data sources

The `enableDraftMode` / `disableDraftMode` adapter callbacks are usually no-ops on non-Next.js frameworks — the `createRedirect` callback does the real work by attaching the cookie.

<Warning>
  **Open-redirect protection is enforced inside the SDK, not in your adapter.** `createDraftEnableHandlerCore` validates the `?secret=` against `process.env.DRAFT_MODE_SECRET` — or against the adapter's own `secret`, when you set one — answering 401 for a wrong secret and 503 when none is configured, and resolves the `?redirect=` against an internal-paths-only allowlist *before* it calls `adapter.createRedirect`. Don't second-guess the URL it gives you — just turn it into a framework-appropriate response. If you bypass the core handler and roll your own validation, you must reject anything that isn't an internal path starting with `/`. See the [Next.js Integration warning](/integration/nextjs-integration#walkthrough) for the full reasoning.
</Warning>

## 3. Page render: switching between published and draft

This is the part that lives outside the editor API routes. When a user visits `/pricing` on your site:

* **Published mode** (no draft cookie): your page handler reads from your CMS / file system / database and renders normally.
* **Draft mode** (your draft cookie is set, OR `?session=…&siteId=…` query params from the editor iframe): your page handler reads from the orchestrator's `/draft/pages` endpoint instead, and shows the editor overlay.

The SDK gives you two helpers for this.

### Helper A: `fetchEditorPage` and `fetchEditorSlugs` (no adapter needed)

These are plain `fetch()` wrappers around the orchestrator's draft endpoints. Use them anywhere you can call `await fetch()`:

```ts theme={null}
import { fetchEditorPage, fetchEditorSlugs } from "@avocadostudio-ai/site-sdk/draft"

// In your page handler, after detecting draft mode:
const page = await fetchEditorPage(slug, session, siteId)
if (page) {
  // Render `page.blocks` using your block renderer
}
```

That's the whole API — no framework coupling. Both functions accept an optional `{ orchestratorUrl }` override if you don't want to set the env var.

### Helper B: `resolveDraftContextCore` (needs an adapter)

This is the helper that figures out *whether* you're in draft mode by looking at cookies, query params, and env defaults — the same logic `createSitePage` uses internally on Next.js. It needs a tiny adapter so it can read your framework's cookies:

```ts theme={null}
import {
  resolveDraftContextCore,
  type DraftModeAdapter,
} from "@avocadostudio-ai/site-sdk/draft/core"

// SvelteKit example:
async function getDraftContext(event) {
  const adapter: DraftModeAdapter = {
    isDraftMode: event.cookies.get("__draft_enabled") === "1",
    getCookie: (name) => event.cookies.get(name) ?? undefined,
  }
  const searchParams = Object.fromEntries(event.url.searchParams)
  return resolveDraftContextCore(searchParams, adapter, {
    defaultSession: "dev",
    defaultSiteId: "my-site",
  })
}
```

The function returns either `null` (you're in published mode) or `{ session, siteId, editorOrigin }` (you're in draft mode and should call `fetchEditorPage` with these values).

**The request has to say it came from the editor.** A configured `defaultSiteId` is the site's identity, not evidence about the caller, so it no longer resolves a context on its own: `resolveDraftContextCore` returns `null` unless the request carries `siteId`, `session`, `editorOrigin` or `__editor` in the query, the draft cookies your adapter reads, your framework's draft-mode flag, or a valid `secret`. Every real editor entry point sends one of those. What this changes is the anonymous request — a plain `curl` of your dev server is a visitor now, and renders published content. Before, it took the draft path, where an unknown slug is "draft unavailable" at HTTP 200 rather than a 404 — and on the Next.js factory it also cost every page its title, description and social card, because an editor render emits `noindex` and nothing else.

### A split preview route

If you serve editor renders from a separate route, as the Next.js production
shape does, `decideEditorRewrite` from `@avocadostudio-ai/site-sdk/proxy/core`
is the whole rewrite decision as a pure function. Give it the request's
`pathname`, `searchParams` and a `hasCookie(name)` check; it answers
`{ kind: "pass" }` or `{ kind: "preview", pathname, headers }`, and your
framework's middleware does the rewrite. It takes the same `previewRoute`,
`editorParam` and `draftCookie` options as the Next.js proxy. A host with no
draft-mode cookie of its own must name the cookie its draft route sets in
`draftCookie`.

`resolvePageRender` from `@avocadostudio-ai/site-sdk/page/core` is the
decision `createSitePage` makes — published, draft, draft unavailable, stale
draft, not found — without the React. It is what the Astro integration builds on.

### Putting it together (SvelteKit pseudo-code)

```ts theme={null}
// src/routes/[...slug]/+page.server.ts
import { fetchEditorPage } from "@avocadostudio-ai/site-sdk/draft"
import { getMyCmsPage } from "$lib/my-cms"
import { getDraftContext } from "$lib/draft"

export async function load(event) {
  const slug = event.params.slug || "/"
  const draft = await getDraftContext(event)

  const page = draft
    ? (await fetchEditorPage(slug, draft.session, draft.siteId)) ?? (await getMyCmsPage(slug))
    : await getMyCmsPage(slug)

  return { page, isDraft: !!draft }
}
```

The Remix / Nuxt / Hono versions look almost identical — same three calls, same fallback chain. (On Astro the integration does this for you, through `Astro.locals.avocado.getDraftPage()`.)

## 4. Register the site

This step is framework-independent. From your project directory, run the same registration CLI that the Next.js path uses:

```bash theme={null}
npx avocado-register --name "My Site" --port <your-dev-server-port>
```

The CLI detects the framework. On anything that is not Next.js it writes `DRAFT_MODE_SECRET`, `ORCHESTRATOR_URL` and `AVOCADO_SITE_ID` to `.env` — the file every dotenv loader reads — rather than Next's `NEXT_PUBLIC_*` names in `.env.local`. Only `DRAFT_MODE_SECRET` is non-negotiable, and the CLI checks it against the orchestrator's before writing it: on a mismatch it stops and says where the right value lives.

After it succeeds, the site appears in the editor's dashboard the next time you open or refresh `http://localhost:4100`.

## 5. Verify the contract

Same `curl` checks as the Next.js walkthrough — the contract is identical regardless of which framework implements it. See the [Verify the contract step](/integration/nextjs-integration#walkthrough) for the five `curl` commands that should all pass against your routes.

## What you don't get on the non-Next.js path

The Next.js `createSitePage` helper does several things automatically that you'll have to do by hand:

| What `createSitePage` does | What you'll do instead |
| - | - |
| Branches between draft and published reads via `next/headers` | Branch on your own `getDraftContext` (section 3 above) |
| Renders the SDK's built-in block library out of the box | On a React renderer, import `renderBlocks` from `@avocadostudio-ai/site-sdk` and call it yourself. Otherwise use your own block renderer that maps `block.type` strings to your components, and mark it up with `editorMarkers(enabled)` from `@avocadostudio-ai/site-sdk/markers`, which has no React in it |
| Mounts the live `EditorOverlay` for in-iframe editing | `EditorOverlay` from `@avocadostudio-ai/site-sdk/editor` is a React component. Off React there is no documented way to mount the preview bridge yet; the Astro integration's bridge is the only non-React one that ships |
| Builds the site header / nav / footer chrome from `getSiteConfig` | Build your own — or just import `buildNavItems` and `buildSiteHeaderBlock` from `@avocadostudio-ai/site-sdk/navigation` |
| Generates `generateStaticParams` from `getSlugs` | Implement your framework's equivalent (SvelteKit `entries`, Remix loaders + dynamic rendering, etc.) |
| Derives each page's `<title>`, description, Open Graph and canonical tags | Call `buildPageMetadata(page, { siteName, canonical, baseUrl })` from `@avocadostudio-ai/site-sdk/seo` and emit the tags yourself — `renderPageMetadata(metadata)` returns them as an escaped HTML string for `set:html` / `{@html}` / `v-html`. `baseUrl` is what resolves a relative `ogImage` into the absolute URL a crawler will actually fetch |

None of these are blockers — they're "you have to write the integration glue, but the building blocks exist." The SDK source under `packages/site-sdk/src/create-site-page.tsx` is the reference implementation; on a non-Next.js framework, you're translating it into your framework's idioms.

## Compared to the alternatives

If this looks like a lot of work, here are your other options:

1. **Wrap your app in a thin Next.js shell** that proxies to your existing backend. Use the standard [Next.js Integration](/integration/nextjs-integration). Most people who try the non-Next.js path end up here anyway, and it is the only fully supported shape.
2. **Hand it to your own coding agent.** The [coding agent workflow](/sites/coding-agent) puts Claude Code, Codex or Cursor to work inside your repository, where it already knows your render path and your data layer. The `avocado-integrate` skill has Next.js and Astro branches only, but an agent that can read this page can usually adapt it; results vary.
3. **Ask for an official adapter** for your framework. Astro got one that way. If we hear the same request for Remix, SvelteKit or Nuxt often enough, those become candidates for first-class support and stop being "first mover" territory.

## When something doesn't fit

The `/core` exports are real and tested (the Next.js shims in the SDK use them as their own implementation), but the *patterns* on this page are illustrative — they haven't been verified against every framework's quirks. If you hit something:

* Get in touch with the framework, the version, and the exact symptom. A concrete report on a framework nobody has wired yet is the fastest way to get it onto the tested list.
* The Next.js adapter at `packages/site-sdk/src/draft-routes.ts` is 30 lines and is the reference for what a framework adapter looks like. If you write one for your stack, we would like to see it — a working adapter is what moves a framework out of "first mover" territory.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.