# Visual Editing (Astro) Workflow for adding CloudCannon Visual Editor support to an Astro site using `@cloudcannon/editable-regions`. The generic patterns behind these checks live in [../visual-editing-reference.md](../visual-editing-reference.md), and Astro's deltas from them in [visual-editing-reference.md](visual-editing-reference.md) — read sections on demand as checklist items link to them. For the region types and attribute reference, see [../editable-regions.md](../editable-regions.md). ## Setup steps Run the setup script to handle steps 1-3 automatically: ```bash bash skills/cloudcannon-visual-editing/scripts/setup-editable-regions.sh . ``` This installs the package (falling back to `--legacy-peer-deps` if needed), adds the Astro integration to `astro.config.mjs`, and creates `src/cloudcannon/registerComponents.ts`. Verify the results — especially that `editableRegions()` was placed inside the integrations array, not after it. Then add a conditional import in the base layout so `registerComponents` only loads inside CloudCannon's Visual Editor: ```astro ``` `window.inEditorMode` is set to `true` by CloudCannon inside the Visual Editor iframe. The dynamic `import()` keeps the registration code out of the production bundle entirely — it only loads when the page is being edited. Use a relative path for the `import()` — `@cloudcannon/...` looks like an npm scope and will resolve to the package, not your local file. **Astro 4 compatibility:** The integration requires Astro 5+. For Astro 4, skip the integration — `data-editable` HTML attributes still work but component re-rendering is not available. See [visual-editing-reference.md § How the Astro integration works](visual-editing-reference.md#how-the-astro-integration-works). When the site uses a page builder with a `BlockRenderer`, create a shared `src/cloudcannon/componentMap.ts` — see [visual-editing-reference.md § Component re-rendering](visual-editing-reference.md#component-re-rendering). ### Package exports reference | Import path | Purpose | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `@cloudcannon/editable-regions/astro-integration` | Astro integration for `astro.config.mjs` (build-time) | | `@cloudcannon/editable-regions/astro` | `registerAstroComponent()` for client-side component re-rendering | | `@cloudcannon/editable-regions/astro-react-renderer` | Side-effect import: registers a catch-all React renderer (needed when React components are used inside registered Astro components) | | `@cloudcannon/editable-regions/react` | `registerReactComponent()` for standalone React component re-rendering | ## Section census > **Hard gate.** Do not write a single `data-editable` attribute until a section census exists at `.cloudcannon/migration/visual-editing.md` and covers every page listed below. An empty or TODO'd census fails this gate — produce the table first, then implement. Before writing any editable attributes, produce a census of every visible section on every key page. Document the census in `.cloudcannon/migration/visual-editing.md`. The census prevents sections from being accidentally skipped — every section must have an explicit treatment decision. **Key pages to census:** Homepage, blog listing, blog detail, portfolio/project listing, project detail, contact, about, and any other unique page templates. Include shared partials that appear on multiple pages (header, footer, CTA banner, navigation). **For each section, document:** | Column | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Page** | Which page the section appears on | | **Section** | Descriptive name (e.g. "Hero", "Features grid", "FAQ accordion", "Footer links") | | **Treatment** | One of: `text`, `image`, `array`, `component`, `source`, `data-file`, `combined` (multiple types), `sidebar-only` | | **Binding plan** | The actual `data-prop` paths and any registered-component name. Required for `data-file`, `array`, `component`, or `select`-into-another-data-file treatments. Hyphen `—` is fine for `text`/`image`/`source` rows where the binding is obvious. See [§ Binding plan by treatment](#binding-plan-by-treatment) for the pattern per treatment type. | | **Data completeness** | Are ALL visible/configurable values in the data source? List any hardcoded values in the template that should also be in the data (icons, colors, link targets, label text) | | **Justification** | Required when treatment is `sidebar-only`. Must cite a specific technical reason, not just "complex" or "not worth it" | #### Binding plan by treatment | Treatment | Binding plan pattern | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `text` / `image` / `source` | Hyphen `—` — binding is obvious from the field name (or from `data-path` / `data-key` on source editables). | | `array` (frontmatter) | `data-prop=""` on the parent; relative `data-prop` on inner editables inside each item. | | `data-file` | `@data[]` on the parent wrapper. Descendants use relative paths — never repeat `@data[...]` inside items. | | `component` | ``. Component registered in `registerComponents.ts`. | | `data-file + component` | ``. E.g. `@data[cta]` on ``. | | `select → component` | `data-prop=""` on ``. The registered component does the slug lookup internally. | | `combined` (e.g. `data-file + array`) | `@data[].` on the parent; relative paths inside items. Static siblings (logo column, "see all" link) live outside the array wrapper. | #### Rules for `sidebar-only` justification "Uses third-party npm components" is NOT sufficient on its own — most third-party components can still be wrapped in `` for sidebar-triggered re-rendering. See [visual-editing-reference.md § Third-party component fields](../visual-editing-reference.md#third-party-component-fields). **Valid reasons:** The component genuinely can't be wrapped (shadow DOM, framework incompatibility after attempting conversion) AND the section is still wrapped in `` for re-rendering. Every `sidebar-only` section that renders a list still needs array editables for CRUD. **Example census:** | Page | Section | Treatment | Binding plan | Data completeness | Justification | | --------------------- | ------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------- | | Homepage | Hero | component + text + image + array | ``; nested `data-prop="title"`, `data-prop-src="image"`, `data-prop="actions"` array | All values in content collection | — | | Homepage | Features grid | component + array (nested text per item) | `data-prop="features"` parent; relative `data-prop="title"`, `data-prop="description"`, `data-prop-src="icon"` inside items | Icons, titles, descriptions all in frontmatter | — | | Homepage | Featured Projects | component + source (title, button) | ``; source editables on hardcoded heading/button | Heading and button text hardcoded in component — extract to data or use source editables | — | | Homepage | FAQ | component + array | `data-prop="faqs"` parent; relative `data-prop="question"`, `data-prop="answer"` inside items | All values in frontmatter; heading/description need text editables | — | | All pages | Header / Navigation | data-file | ``; nested array on `items` with relative paths | Nav items in data file? Icons? Mobile menu? | — | | All pages | Footer link columns | data-file + array | `@data[footer].columns` on parent **only**; relative `heading`, `links` (inner array), `label` (inside links). Static logo column lives outside the array wrapper | All link text, URLs, column headings in data file? | — | | All pages | Footer CTA banner | data-file + component | `` | Title, link text, URL in data file? | — | | Post / Project detail | Share block | source OR data-file | source: `data-editable="source" data-path="..." data-key="share_heading"` etc. | Heading, description above social buttons. Hardcoded page-template text is a common miss. | — | | Post / Project detail | Author card | data-file + component | Slug stored as `author: ` in frontmatter; rendered via registered `AuthorCard` (lookup inside) wrapped in ``. **Anti-pattern:** lookup in page template, object passed to static component | Name, bio, avatar pulled from `src/data/authors.json` via a `select` input. Inline author objects in page templates are a miss. | — | After completing the census, implement the editable regions section by section. Update the census with any changes made during implementation. ## Infrastructure checklist Run through these after setup, before starting on editable regions: - [ ] `@cloudcannon/editable-regions` is in `package.json` dependencies - [ ] The `editableRegions()` integration is in the `integrations` array in `astro.config.mjs` (inside the array, not after it) - [ ] `src/cloudcannon/registerComponents.ts` exists with commented-out examples - [ ] Base layout conditionally imports `registerComponents` inside `if (window.inEditorMode)` - [ ] `src/icons/` directory exists (required by `astro-icon` even if empty) - [ ] Every registered component, and everything it renders, has been grepped for `Astro.` reads other than `props`, `slots` and `request` — nothing else exists in a re-render, and the failure is invisible at build time → [Runtime shims](visual-editing-reference.md#how-the-astro-integration-works) - [ ] Every registered component, and everything it renders, has been grepped for `.svg` imports — SVG component imports throw `NoMatchingRenderer` in a re-render; use `?raw` + `set:html` → [SVG component imports](visual-editing-reference.md#module-compatibility-in-the-editable-regions-client-bundle) - [ ] `astro-icon`, if installed, is ≤ 1.1.5 — or ≥ 1.2.0 with the `Astro.locals` crash addressed → [astro-icon](visual-editing-reference.md#astro-icon) - [ ] `astro build` passes cleanly after setup ## Completeness checklist > **Rule:** if an editor can see it on the page, an editor must be able to edit it. Every item below enforces this rule. Hardcoded headings, labels, or paragraphs are not "developer-only" — they are an unfinished migration. A section is either editable or has a written exception in `.cloudcannon/migration/visual-editing.md`. Work through every item after implementing editable regions. Each item links to the relevant pattern documentation. ### Universal (every migration) - [ ] **Editor enablement**: Every collection with editable attributes on its rendered pages has `visual` in `_enabled_editors` → [configuration.md](../../cloudcannon-configuration/astro/configuration.md) - [ ] **Census coverage**: Every section in the census has editable regions OR a documented justification that meets the `sidebar-only` rules above - [ ] **Array containers**: Every array rendered from frontmatter/data has `data-editable="array"` + `data-prop` on the container AND `data-editable="array-item"` on each item → [Array editing](../visual-editing-reference.md#array-editing) - [ ] **Per-`.map()` census** — for each `.map()` in a registered component: - [ ] iterating element has `data-editable="array" data-prop=""` - [ ] each iterated row has `data-editable="array-item"` - [ ] each text field inside the row has `data-editable="text" data-prop=""` - [ ] each image inside the row has `data-editable="image" data-prop=""` - [ ] **Verify**: in visual mode, clicking a row outlines the row; clicking a field inside outlines the field. If nothing highlights, markers are missing — sidebar-editable ≠ visual-editable. → [Array editing](../visual-editing-reference.md#array-editing) - [ ] **Nested editables in array items**: Every array item has nested `data-editable="text"` / `data-editable="image"` (or `` / ``) on its visible fields. → [Array editing](../visual-editing-reference.md#array-editing) - [ ] **Array path scope**: Inside `data-editable="array-item"`, every nested `data-prop` is **relative** to the item (`data-prop="heading"`, `data-prop="links"`). → [Arrays inside data files](../visual-editing-reference.md#arrays-inside-data-files) - [ ] **Array container purity**: Every `data-editable="array"` wrapper contains **only** elements produced by the array (`data-editable="array-item"` rows plus optional `