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

# File-backed sites (no CMS)

> Make a template with its copy written into the markup editable: move the copy into one JSON file per page, keep the markup and the styles, and publish as a clean git diff.

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

```mermaid theme={null}
flowchart LR
    Files["<b>Files in the repo</b><br/>src/data/pages/*.json · global.json<br/>client.json · navData.json"]
    Read["<b>getPages()</b><br/>files → PageDoc[]"]
    Orch["<b>Orchestrator</b><br/>drafts"]
    Write["<b>onPublish(pages)</b><br/>PageDoc[] → files<br/>only what changed"]
    Tpl["<b>Your templates</b><br/>block(id).props.*"]

    Files --> Read --> Orch --> Write --> Files
    Files --> Tpl
    Orch -- "getDraftPage()" --> Tpl

    style Files fill:#7ED957,stroke:#14532D,color:#0a0a0a
    style Read fill:#f1f5f9,stroke:#64748b,color:#0a0a0a
    style Orch fill:#f1f5f9,stroke:#64748b,color:#0a0a0a
    style Write fill:#f1f5f9,stroke:#64748b,color:#0a0a0a
    style Tpl fill:#f1f5f9,stroke:#64748b,color:#0a0a0a
```

The files are the published content. `getPages` and `onPublish` in your
[content module](/integration/astro-integration#the-site-renders-itself)
translate between them and `PageDoc`s; 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:

```ts src/avocado/content.ts theme={null}
registerBlocks: () =>
  registerFieldTable(FIELD_TABLE, { primitives: { imageNaming: suffixNaming('', '_alt') } }),
```

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](/integration/field-table#sections-of-one-entry-and-values-the-site-will-not-store).

## 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](/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:

```astro src/pages/index.astro theme={null}
---
import { editorMarkers } from '@avocadostudio-ai/astro/markers'
import { getPublishedPage } from '../avocado/store'

const page = (await Astro.locals.avocado.getDraftPage()) ?? (await getPublishedPage('/'))
const hero = page.blocks.find((b) => b.id === 'home-hero')
const { block, field } = editorMarkers(Astro)
---
<section id="hero" {...block(hero.id, hero.type)}>
  <h1 {...field('title')}>{hero.props.title}</h1>
  <p {...field('text')}>{hero.props.text}</p>
</section>
```

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](/integration/astro-integration#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](/integration/coverage) does not expect a marker for it:

```ts theme={null}
cs_business: {
  displayName: 'Business info',
  fields: {
    name: { kind: 'text', label: 'Business name (page titles)', panelOnly: true },
    domain: { kind: 'text', label: 'Canonical domain', panelOnly: true },
    phoneFormatted: { kind: 'text', label: 'Phone' },
    phoneForTel: { kind: 'text', label: 'Phone (tel: link)', panelOnly: true },
    email: { kind: 'text', label: 'Email (mailto: link)', panelOnly: true },
    address_city: { kind: 'text', label: 'City' },
  },
},
```

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>`:

```astro src/avocado/Img.astro theme={null}
---
import type { ImageMetadata } from 'astro'
import { Picture } from 'astro:assets'
import { editorMarkers } from '@avocadostudio-ai/astro/markers'

const assets = import.meta.glob<{ default: ImageMetadata }>(
  '/src/assets/**/*.{jpg,jpeg,png,webp,avif,gif,svg}',
  { eager: true },
)
const { src, alt, path, ...rest } = Astro.props
const asset = assets[src]?.default
const marker = editorMarkers(Astro).field(path, { kind: 'image' })
---
{asset
  ? <Picture src={asset} alt={alt} pictureAttributes={marker} {...rest} />
  : <picture {...marker}><img src={src} alt={alt} {...rest} /></picture>}
```

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`](/integration/field-table#imagelist-rows).
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:

```ts src/avocado/store.ts theme={null}
import { readFile, writeFile } from 'node:fs/promises'
import { isDeepStrictEqual } from 'node:util'

async function writeJsonIfChanged(file: string, next: unknown): Promise<boolean> {
  const text = await readFile(file, 'utf8')
  if (isDeepStrictEqual(JSON.parse(text), next)) return false
  const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? '  '
  await writeFile(file, JSON.stringify(next, null, indent) + (text.endsWith('\n') ? '\n' : ''))
  return true
}
```

**`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`](/integration/coverage) at 100%
   on every page, measured against `astro dev` with each block's `props` passed
   in, and then [`npx avocado qa`](/integration/qa).

## Related

<CardGroup cols={2}>
  <Card title="Astro integration" icon="rocket" href="/integration/astro-integration">
    The content module, `getDraftPage()`, the markers, and publishing on a static template.
  </Card>

  <Card title="The field table" icon="table" href="/integration/field-table">
    Field kinds, `fixed`, `readOnly` and `panelOnly`.
  </Card>

  <Card title="Coverage checks" icon="clipboard-check" href="/integration/coverage">
    Whether every field that renders carries a marker, and whether the panel is usable.
  </Card>

  <Card title="QA gate" icon="list-check" href="/integration/qa">
    The last step: the integration checked in a browser, inside the editor frame.
  </Card>
</CardGroup>
