Skip to main content
Avocado edits props on blocks, not markup. On a site whose copy is written straight into .astro or JSX templates — most starter kits — there is nothing for it to edit until that copy lives somewhere a publish can write back to. This page is the recipe for moving it there without touching the markup or the styles. It was worked out on the CodeStitch Beginner Astro Starter Kit: five pages of inline copy, LESS, and two data files the template already reads. Afterwards every <style> block was byte-identical, the visible text of every built page matched the original build, and publishing two edits changed exactly two lines. The examples are Astro; on Next the steps are the same, with JSX, @avocadostudio-ai/site-sdk/markers and next/image. The files are the published content. getPages and onPublish in your content module translate between them and PageDocs; the templates read the same files, or the draft on an editor render.

1. One block per rendered section, prefixed

Walk each page top to bottom and make every section it renders one block type. npx -p @avocadostudio-ai/migration-sdk avocado-scope <url> --allow-localhost against the dev server gives a second opinion on where the sections are. Prefix every type with the site’s own short name — cs_hero, cs_cta, cs_footer. Avocado ships Hero, CTA, Banner, Footer, Gallery and fifteen more, and a site type with one of those names replaces the built-in’s definition rather than erroring. Declare the table with registerFieldTable, passing the image naming — there is no CMS pack here to supply it:
src/avocado/content.ts
A template that draws its sections in its own order ignores a move or a duplicate. Mark those blocks fixed: true so the editor does not offer one — see the field table.

2. Move the copy into one JSON file per page

  • src/data/pages/<page>.json — one PageDoc per page: id, slug, title and the page’s own blocks, in order, each with its literals as props.
  • src/data/global.json — the blocks every page renders: header, footer, a shared call to action. Give them fixed ids (global-footer) and have getPages add them to every PageDoc, so the editor can select them on any page. On publish, write them back to global.json once; if two pages changed the same one differently, return { ok: false, error } and write nothing. If the version you installed has its own model for site-wide blocks, the changelog says so — prefer it to this.
Keep the data files the template already has as the source of truth. If it reads client.json and navData.json, map them to blocks in getPages and back in onPublish rather than copying them into global.json:
  • The field table has no object kind, so flatten nested objects — address.city becomes the prop address_city — and nest them again on write.
  • Rename to the field’s meaning where the file’s key is opaque, both ways: a nav row’s key is its label.
  • Write back into the original object, { ...client, phoneFormatted: props.phoneFormatted }, so the file keeps its key order and the diff stays one line.

3. Keep the markup; read props instead of literals

Each template keeps its elements, classes and <style> block. Only the literals change, to reads from the page’s blocks, with the markers from editorMarkers(Astro) beside them:
src/pages/index.astro
Repeated cards become a .map() over a list, marked with full paths (items[2].title) or with a scope on the row element the template already has — no extra wrappers. Mark each block on the element that encloses its section, and put data-avocado-root on the element that encloses every block — see preview refresh. Some things have no string form and stay out of the table: an SVG imported as a component (import Logo from '../assets/logo.svg', rendered <Logo />) and decorative icons. Making one editable means changing it to an <img>, which is a markup and CSS change — ask first.

4. Declare what renders nowhere as panelOnly

A value used only in an attribute has no element to mark. Declare it panelOnly: true so it is editable in the panel and coverage does not expect a marker for it:
Link hrefs are attributes too: declare the href kind: 'link' — coverage never expects a marker for one — and give the anchor’s label its own text field, which carries the marker.

5. Images: project paths through astro:assets

An image field is a string. Store a file under src/assets/ as its project path — /src/assets/images/landing.jpg — and resolve it at render time with import.meta.glob, whose keys are exactly those paths. <Picture> and <Image> keep their optimisation, srcset and AVIF/WebP output; any other URL falls back to a plain <img>:
src/avocado/Img.astro
The marker goes on the <picture> element — through pictureAttributes on <Picture> — because the overlay appends its Change button into the marked element, and nothing can be appended into an <img>. Localise uploaded images on publish. An image chosen in the Studio arrives as an http(s) URL, which a static host cannot rely on. In onPublish, download any such value into src/assets/images/avocado/<sha1>.<ext> and store that project path instead, so it goes through astro:assets and lands in the diff. A gallery whose rows are only an image and its alt text can be a list of { image, image_alt } rows or an imageList. Use the list when the rows need a caption or a link, or should share the table’s image naming.

6. Write each file only if it changed

onPublish receives every page, edited or not. Write a file only when its parsed content differs, and in the indentation it already has — client.json in the starter kit is indented with tabs:
src/avocado/store.ts
updatedAt is not content. The orchestrator needs one on every PageDoc, and a file-backed store has no use for it: derive it from the file’s mtime in getPages ((await stat(file)).mtime.toISOString()) and strip it before comparing and writing. A stored timestamp is a line that changes on every publish.

7. Verify: the diff is the test

Report each of these as a number or a diff:
  1. Round trip. Call getPages() and pass the result straight to onPublish. It must write nothing: git status --porcelain is empty.
  2. One synthetic edit per field kind — a text field, an html field, an image, a list row removed, a string-list item added, a nav label, a field in each data file. Each lands in exactly one file and one field, and git diff shows only those lines, indentation intact.
  3. Through the orchestrator. Make two edits in the editor, one on a page block and one on a site-wide block, publish, and read git diff: two changed lines. Revert.
  4. The public site did not move. The visible text of every built page matches the pre-integration build, and git diff on the templates shows no change inside a <style> block.
  5. The editing surface. editableCoverage at 100% on every page, measured against astro dev with each block’s props passed in, and then npx avocado qa.

Astro integration

The content module, getDraftPage(), the markers, and publishing on a static template.

The field table

Field kinds, fixed, readOnly and panelOnly.

Coverage checks

Whether every field that renders carries a marker, and whether the panel is usable.

QA gate

The last step: the integration checked in a browser, inside the editor frame.