.astro components doing the
rendering, no islands added and nothing ported to React.
astro.config.ts
/api/editor/*
routes, injects the preview bridge, and makes the pages you named renderable on
demand in astro dev.
The site renders itself
This is the mode Avocado calls site-renders-itself, and it is what makes Astro workable. Avocado supplies the schema, the draft props, the editable markers and publishing. Your components render. Nothing about your template changes shape. What you provide is a content module — one default export saying where your content lives:src/avocado/content.ts
onPublish writing the page back to a file under src/
is not a limitation to work around — it is what “published” means. The file is
committed, reviewed as a diff, and the site rebuilt from it.
The content module is the editor API’s whole configuration, so publishSecret
and maxPagesRemoved go here too. The publish route refuses a payload that
would remove every page with a 409 unless the body carries allowDelete: true —
an empty pages array is far more often a client that failed to load its own
state than somebody deleting their site — and it refuses an unconfigured publish
with a 401 under NODE_ENV=production. The injected route only exists during
astro dev or in a build with an adapter, so the production refusal matters only
if you serve it.
The editor API route cannot live in your
src/pages. It has to render on
demand, and astro build fails on an on-demand route with no adapter whether or
not anyone intends to serve it. The integration injects the route instead, which
is why it needs content as a path rather than as a value.editablePages — why it is a list
The pages you name render on demand during astro dev and are prerendered in a
build. Both halves are needed.
A prerendered route has no request: Astro.request.headers is empty, so the
middleware sees no __editor parameter and no draft cookie, and resolves every
request as a visitor’s. The preview then renders the published page —
correctly, and with no editable markers — which looks exactly like an
integration nobody wired up.
Your site cannot fix that itself. export const prerender = false on the page
makes astro build fail with NoAdapterInstalled, and !import.meta.env.DEV
is not a literal by the time Astro’s route analysis reads it, so the route stays
prerendered regardless.
It is a list rather than “every page” because a route built from
getStaticPaths cannot render on demand at all: its Astro.props come from the
path it was generated for, so on demand they are undefined and the route
throws on the first property it reads. Naming the pages leaves paginated and
collection routes exactly as they were. * matches within a path segment, **
across segments.
In a build this does nothing at all. Every route is prerendered and the
output is as static as it was before Avocado was installed.
Rendering the draft
getPages is your published source. An editor render needs the draft, and
that is Astro.locals.avocado.getDraftPage():
getDraftPage() returns null when the
orchestrator has no draft for this slug or could not be reached, and in both
cases the right answer is your published content. It defaults to the request’s
own path; pass a slug when the route renders a page it does not share a URL with.
Nothing is fetched unless a render calls it, and the result is memoised per
request per slug.
The subtree a refresh swaps is [data-avocado-root] if your template has one,
then <main>, then the whole <body>. Put data-avocado-root on the element
that encloses every block when your blocks live outside <main> — a header or
footer block does — or when you want the swap narrower than the body.
When you need to tell an editor why a draft is missing — “the orchestrator is
unreachable” rather than a silently stale page — resolvePageRender from
@avocadostudio-ai/site-sdk/page/core returns a draft-unavailable outcome that
getDraftPage collapses into null. It resolves navigation and site chrome as
well, which a template rendering its own header will want to ignore.
Production
Everything above works inastro dev with nothing configured. A deployment that
serves on-demand routes has three things to set, and all three are inert in
development — so none of them can be checked by the loop you develop in.
__editor=1 authorizes nothing. It is a routing hint the editor puts on the
iframe URL — no secret in it — so under NODE_ENV=production a request carrying
only the parameter is rendered as an ordinary visitor’s. Two things authorize: the
signed cookie /api/editor/draft?secret=… mints, and a valid secret on the
request itself, which is what covers the case the cookie cannot — the editor
renders your site in a cross-origin iframe, where third-party cookies are
frequently blocked outright. Development is unchanged; refusing there would mean
configuring a secret before a local preview could render anything.
editorOrigins is enforced in three places. The one list in
astro.config.ts decides the postMessage target, which frame may drive inline
edits, and which origin /api/editor/* answers cross-origin. You do not need
EDITOR_CORS_ORIGINS as well; it still works and is additive.
Both halves check it. The server resolves the origin on the URL against the list
and degrades an unlisted one to your first entry, so the page still renders; the
preview bridge refuses to attach at all and says so in the console, because a
frame naming an origin you never listed has nobody listening on the other side.
In development your local editor is trusted whatever its port, listed or
not — the editor’s port moves, and requiring it in the list would mean editing
committed config to run the thing locally. So an origin mistake first shows up
in production, which is the argument for naming the real one before you deploy
rather than after.
Astro’s dev toolbar renders inside the editor’s iframe — a floating pill
over the bottom of the page, covering whatever block is down there. Set
devToolbar: { enabled: false } in astro.config.ts while you are editing, or
expect to lose the footer behind it.Describing your components
Your.astro widgets are described to Avocado with a
field table — one table giving the schema the
operations engine validates against and the metadata the property panel draws.
One kind matters more on Astro than anywhere else. A widget that takes a prop
either as a value or as a slot renders it with set:html, so the stored value
is a string of markup. That is kind: 'html':
richtext instead is the mistake worth naming, because it compiles
and looks fine: richtext means a document, so the panel renders the markup
literally and a person editing it writes broken markup back into your source
file. See when the stored value is HTML.
A prop that is a decorative slot — a background <div class="absolute inset-0 …">
— is not a field at all. Leave it out of the table; an undeclared prop rides
through the ops engine untouched, which is how the page keeps it.
Options
The editor API is always mounted at
/api/editor. There was an option for that
and it has been removed: the other end of the contract is not configurable — the
editor fetches /api/editor/blocks and /pages from the browser and the
orchestrator POSTs /api/editor/publish, all spelled out — so moving only this
half mounted the API where nothing would call it, and the manifest and publish
answered 404 against a config that read correctly. A site that needs another
path mounts the route itself with createAvocadoEditorApi({ basePath }).
A worked example
examples/astro-site in the repository is the smallest site that exercises the
whole contract: two pages plus one with no <main>, a block rendered outside
<main>, a list whose rows are drawn by their own component, a kind: 'html'
headline, and an onPublish that writes JSON back under src/.
Two gates drive it, and they divide along the only line that matters here —
whether a browser is running the page.
What is not here yet
Astro support shipped from one pilot integration, and the fixture above is not a second one — it proves the contract, not that the integration survives contact with a real template. Expect to find gaps around anything neither exercises, and say so when you do — that is worth more to us than a clean report. Re-attachment after a<ClientRouter /> swap is still verified by construction:
the fixture does not use the client router, so astro:page-load and
astro:before-swap are exercised only by the initial-load path. Everything else
in the bridge is now driven in a browser on every push.