# CMS

> A content sidebar built from your own page: rules, field controls, views and the JavaScript API.

Source: https://clayjs.com/docs/plugins/cms/

A content sidebar that builds itself from your page. You name the parts of the file that count as content, and the CMS turns them into friendly fields: text, rich text, images, lists. Whoever you share edit access with sees the panel, not the markup.

Let's start with a personal page: your name, a photo, and a link. All you have to do to enable the CMS is a short description of which parts are content. Then Clay.js CMS gives you controls for editing them. You can type directly on the page or use a sidebar. Both change the same HTML.

This guide uses [HTML Clay](https://htmlclay.com) to save a file on your computer. You need the HTML Clay app, a text editor, an internet connection to load ClayJS (or download the standalone version), and a photo to upload to the page. The CMS library is free to use; you do not need a Hyperclay hosting account.

(For rule syntax, JavaScript methods, and custom controls, see the [CMS reference](https://clayjs.com/docs/plugins/cms/).)

## Create the page

Install [HTML Clay](https://htmlclay.com) and launch the app.

Create an example folder inside your home folder, such as `Documents/personal-cms`. Put a JPEG photo in the example folder and name it `portrait.jpg`. In your text editor, create `profile.htmlclay` beside it with this HTML:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My profile</title>
  <style>
    body { max-width: 32rem; margin: 3rem auto; padding: 0 1.5rem;
      font: 1.2rem/1.5 system-ui; color: #20352b; background: #f5f4ed; }
    img { width: 8rem; height: 8rem; object-fit: cover; border-radius: 50%; }
    h1 { line-height: 1.1; }
    a { color: inherit; }
  </style>
</head>
<body>
  <img class="photo" src="portrait.jpg" alt="My portrait" data-hcms-crop="1:1">
  <h1 class="name">Alex Rivera</h1>
  <p class="bio">Making small things for the web.</p>
  <a class="website" href="https://example.com">My website</a>

  <script data-rules-name="cms" data-rules-version="1" data-cms-view="inline" type="application/json">
  {
    "name": ".name",
    "bio": ".bio",
    "photo": ".photo@src",
    "website": ".website@href"
  }
  </script>
  <script src="https://clayjs.com/v1/clay.js?plugins=cms,indicator"></script>
</body>
</html>
```

The rules tag maps labels to elements. `"name": ".name"` means the name field edits the element with class `name`. `@src` edits the image source, and `@href` edits the link destination. The CMS uses the content already in those elements. `data-cms-view="inline"` tells the page to open the editor on the page instead of in the sidebar.

Save it as plain text with the exact filename `profile.htmlclay`. Double-click the file to open it through HTML Clay. If the app asks about the example folder you created, choose **Trust this folder**.

## Edit on the page

Find the floating **Edit content** control at the bottom right of the page and click it. If it is missing, see [troubleshooting](#if-something-does-not-work). Click the name or bio and type. To edit the website address, click the pencil beside the link and change the value in the popover. The visible link label stays "My website" because this example maps only its destination.

For a photo, click its edit handle and choose an image from your computer. You can reuse `portrait.jpg` to practice. The `data-hcms-crop="1:1"` attribute opens a square crop window. Confirm the crop. Keep the editor open until the progress bar disappears and the selected photo appears on the page, with no error below the upload control. A preview inside that control alone does not mean the upload has finished.

Without `data-cms-view="inline"` (or with `data-cms-view="sidebar"`), **Edit content** opens the sidebar instead and presents the same fields together in a side panel.

## Save and check the file

Check that the page shows the text, link address, and photo you intended to change. Press **Cmd+S** on macOS or **Ctrl+S** on Windows or Linux. Wait for the small **Saved** status chip at the bottom right. If it says **Couldn't save** or **Offline, not saved**, keep the tab open and [resolve the error](#if-something-does-not-work) before trying again. Do not reload while a save is failing. Closing the editor alone does not save.

Reload the page. Each change you made should remain. Reopen `profile.htmlclay` in your text editor to read the saved file, discarding any stale editor buffer without saving it. Find the text you changed. It is now part of the HTML, alongside the rules and the page's styles.

The editing popovers and sidebar are temporary controls. They are excluded from the saved page. Uploaded photos go into the host's uploads folder (`~/htmlclay/uploads/assets-profile/` on HTML Clay), and the HTML links them as `/_/uploads/assets-profile/...`, which keeps working when the page is renamed or moved. To take the page somewhere else with its photos, use Export as zip.

To enable autosave, first save any browser edits and wait for **Saved**. Close the browser tab and reopen the latest file in your text editor. Change its opening tag to `<html lang="en" autosave>`, save, then open the file through HTML Clay again. Subsequent edits save after a short pause. HTML Clay must still be running; autosave does not replace the save host.

## Use your own page

For the same local workflow, make a copy of your page with the `.htmlclay` extension in a folder inside your home folder. Keep its local images, stylesheets, and other assets at the relative paths the page uses. Add the ClayJS script and a rules tag that points at the content you want to edit. Open the copy through HTML Clay and trust its folder when asked. If the page already loads ClayJS, add `cms` to that script's plugin list rather than adding a second loader.

Start with a few fields and check each one. A rule for a heading should point at the heading, not a wrapper containing the whole page. For repeating links, cards, or gallery items, use a list rule from the [reference](https://clayjs.com/docs/plugins/cms/#lists).

An AI coding agent can help write the rules. The reference includes a [configuration prompt](https://clayjs.com/docs/plugins/cms/#configure-with-a-coding-agent). Review the result and try the fields before handing the page to someone else.

## If something does not work

- **No Edit content button:** confirm the filename ends in `.htmlclay` and open it through HTML Clay from your file manager. Then check the rules tag and ClayJS script. Opening the file directly as `file://` does not provide the saving connection.
- **A linked page opens read-only:** open its `.htmlclay` file from your file manager. To let linked `.htmlclay` pages in this example folder open editable too, use **Trusted Folders** in HTML Clay's menu and choose **Trust a Folder**. Copy any file outside your home folder inside it, along with its assets, before opening it through the app.
- **The page says the app is out of date:** update HTML Clay to **1.9.0 or newer**, then reopen the file through the app. Changing the rules or forcing edit mode does not repair an older host's saving connection.
- **Edit content opens the sidebar instead of editing on the page:** check that the rules tag has `data-cms-view="inline"`. Without that attribute, the page opens the sidebar.
- **A field is missing or points at the wrong content:** check its selector against the actual HTML. The sidebar can also expose fields that have no visible target on the page.
- **The photo does not load:** confirm `portrait.jpg` sits beside `profile.htmlclay`. After an upload, keep the generated asset folder beside the page too.
- **An upload fails:** resolve the error shown below the upload control, then choose the file again.
- **A save fails:** keep the page open with your edits intact. Confirm HTML Clay is still running, then try Cmd+S or Ctrl+S again. Follow any permission or conflict message shown by the host; reloading is not a fix for an unsaved change.

## The reference

Hyperclay CMS creates editing controls from rules that describe content in an HTML page. The page remains the content store. The CMS reads values from its elements and writes edits back to them.

For a complete first example, use the [CMS guide](https://clayjs.com/docs/plugins/cms/). This page explains the model, rule syntax, controls, and JavaScript API.

Content: [Rules](#rules), [Rich text](#rich-text), [Lists](#lists), [Views](#inline-and-sidebar-views), [Field controls](#field-controls), [Page style](#page-style-colors-fonts-and-layouts), [Templates](#custom-form-templates), [Images](#images-and-cropping).

Integration: [Load the CMS](#load-the-cms), [JavaScript API](#javascript-api), [Open options](#open-options), [Events](#events), [Saving](#saving-and-ownership), [Coding-agent prompt](#configure-with-a-coding-agent), [Hosted AI](https://hyperclay.com/docs/hosted-ai/).

## A content sidebar

**Setup is two tags.** Check *"A content sidebar"* in the [configurator](https://clayjs.com#configure) (or add `?plugins=cms` yourself), then tell it what's content with a rules tag:

```html
<script src="https://clayjs.com/v1/clay.js?plugins=cms"></script>
<script data-rules-name="cms" data-rules-version="1" type="application/json">
{
  "title":    ".page-title",
  "intro":    "p.intro",
  "avatar":   "img.avatar@src",
  "tags":     "ul.tags li[]",
  "products": [".product", { "name": ".name", "price": ".price" }]
}
</script>
```

Each entry is a field: a name, then the selector it edits. `@src` becomes an image-upload control, `li[]` a reorderable list, and the `products` shape a list of cards. Open the sidebar with `?cms=true` on the URL or `clay.cms.open()` from a button. The same rules tag can carry more tokens (`data-rules-name="api cms"`) to also power the **data** plugin's JSON API, so you configure content once.

## The page, the rules, and the editor

A rules tag gives names to the content you want to edit:

```html
<h1 class="name">Alex Rivera</h1>
<p class="bio">Making small things for the web.</p>

<script data-rules-name="cms" data-rules-version="1" type="application/json">
{
  "name": ".name",
  "bio": ".bio"
}
</script>
```

The CMS reads the named elements into a temporary editing model and builds controls for those fields. Changing a control writes to the corresponding page element.

There is no separate CMS database to synchronize. The CMS controls content editing; HTML Clay or Hyperclay supplies saving and uploaded-file storage. This page covers the CMS API, not building a custom save host.

## Load the CMS

```html
<script src="https://clayjs.com/v1/clay.js?plugins=cms"></script>
```

Use a classic script tag for the loader, without `type="module"`. ClayJS exposes the CMS as `clay.cms`. Put initialization after that loader and wait for `clay.ready` before calling the CMS:

```js
clay.ready.then(() => {
  clay.cms.open({ view: 'inline' });
});
```

The script and its imported modules need a network connection unless you serve a complete local copy. Copying only `clay.js` does not include those modules. Packaging a self-hosted ClayJS distribution is outside this CMS reference.

### Edit mode and dependencies

On a page in edit mode with a `cms` rules tag, the floating **Edit content** control opens the page's declared [view](#inline-and-sidebar-views). HTML Clay **1.9.0 or newer** supplies a `savetoken` on the root of a registered document's served HTML. Opening a file inside your home folder through the app registers it; durable folder trust is not required for that file. Files outside your home folder cannot be opened through HTML Clay. ClayJS recognizes the token as edit mode and loads the editing and saving modules. Older HTML Clay versions use a token this ClayJS build no longer accepts; update the app rather than forcing edit mode.

The CMS and crop library also load outside edit mode, but RichClay, the host uploader, and saving do not. If you open a CMS session there, text elements use popovers instead of in-place RichClay editing, and image or file picks can embed data URLs. Native inputs mapped through `@value` remain native controls. See [Saving and ownership](#saving-and-ownership) for the permission boundary.

### Build compatibility

This reference targets the two-view HyperCMS 0.11.0 build. After readiness, `typeof clay.cms.currentView === 'function'` checks for its view API. An older sidebar-only bundle lacks that method. Waiting for `clay.ready` does not upgrade the bundle. The [API table](#javascript-api) describes the method's return values.

## Rules

Use `type="application/json"` and `data-rules-version="1"`. The default rule source is the `cms` token in `data-rules-name`. A tag can carry multiple space-separated tokens, such as `data-rules-name="cms api"`.

Opening a new session throws if no tag matches the requested token or its rules version is unsupported. Correct the tag before calling `open()` again.

| Rule value | Content it describes |
| --- | --- |
| `".name"` | Element text |
| `"a.website@href"` | A link destination |
| `"img.photo@src"` | An image source |
| `".price@data-cents"` | An HTML attribute |
| `"input.agree@checked"` | A checkbox's checked state |
| `"input.name@value"` | A native input's current value |
| `".bio@innerHTML"` | Markup inside an element |
| `".tag[]"` | A list of scalar values |
| `[".card", { "title": ".title" }]` | A list of objects, with selectors scoped to each card |
| `{ "name": ".name", "bio": ".bio" }` | A group of fields |

Map a text field to the element holding that text. Mapping a large wrapper as one text field can replace its child structure when you edit it. Prefer a separate rule for each piece of content.

Use unique leaf keys across the rules tree. In this build, repeating a key in different groups, such as `title` and `metadata.title`, can bind both fields to the first generated control and overwrite both targets. Use distinct keys such as `pageTitle` and `metadataTitle`.

### Rich text

An explicit `@innerHTML` rule preserves markup within the selected element. Use it when retaining formatting is part of the field's contract.

With `richText` enabled, the default, a bare scalar selector is also upgraded to `@innerHTML` when its first matching target contains child elements. An explicit `@property` or scalar-list rule ending in `[]` is never automatically upgraded.

The [sidebar and inline views](#inline-and-sidebar-views) differ inside object lists. The sidebar does not automatically upgrade those rules. Inline mode does: if any row's matching target contains markup, that child rule is upgraded for every row. For consistent behavior in both views, write `@innerHTML` explicitly on those fields.

The inline toolbar offers formatting only when the field's rule can preserve it. The [`richText` option](#open-options) disables automatic upgrading when set to `false`; it does not turn an explicit `@innerHTML` rule into plain text.

### Lists

An object list uses `[itemSelector, shape]`. Each matching element is one item, and the shape's selectors run within that item:

```html
<style>
  [cms-template] { display: none; }
</style>

<ul class="links">
  <li class="link">
    <a href="https://example.com">My projects</a>
  </li>
  <li class="link" cms-template>
    <a href="https://example.com">New link</a>
  </li>
</ul>

<script data-rules-name="cms" data-rules-version="1" type="application/json">
{
  "links": [".link", { "label": "a", "url": "a@href" }]
}
</script>
```

The editor supplies add, remove, and reorder controls. New items copy the first existing item's structure. The `cms-template` item is excluded from content data and supplies the structure when the list is empty. It belongs inside the list container.

In this example, a new link starts with an empty label and URL. The seed supplies structure, not default field values.

Hide the seed with `[cms-template]` CSS, as above, not a `hidden` attribute or inline `display: none`. Cloning removes the `cms-template` marker, so the new content item becomes visible. A copied `hidden` attribute would keep it invisible.

Without an existing item or a matching seed, adding an item fails with `EmptyListInsert` and leaves the page unchanged. Add a seed inside the list container before retrying.

## Inline and sidebar views

Both views use the same rules and editing session. A page declares its view on the rules tag:

```html
<script data-rules-name="cms" data-rules-version="1" data-cms-view="inline" type="application/json">
```

`inline` edits text where it sits and opens small popovers for everything else. `sidebar`, and also no attribute at all, opens the form in a side panel. An unknown value opens the sidebar and logs a console warning.

| View | Interaction |
| --- | --- |
| Inline | Click text to edit it in place. Handles beside images, links, and other fields open their controls in popovers. Visible lists have nearby add, remove, and move controls. |
| Sidebar | Edit the fields together in a generated form beside the page. It can expose fields without a visible target in the page layout. |

In inline mode, writable inputs, textareas, and selects mapped through `@value` keep their native controls. Text elements use RichClay, the text editor loaded by ClayJS. Images and attribute fields use popovers. Text also falls back to a popover if RichClay is unavailable or cannot attach to the target, such as a table element or a non-HTML element.

The floating bar's **editable areas** number counts field handles, not all editable content. It does not count text edited in place or native inputs. Resolved fields whose elements are hidden or too small to target get a separate message beside **Edit in the sidebar**; choose that action to reach them in the form. A rule that matches no element is unresolved, not part of that message's count; correct its selector or add the missing content.

**Hide controls** hides the inline list controls. It does not close the editing session. **Close editor** ends the session and keeps edits in the current DOM. Saving is a separate action.

The floating **Edit content** control opens and closes whichever view the page declares. It does not switch views and does not remember a view per origin. A bare `open()` uses the open session's view if one is open, and otherwise the declared view. Use `clay.cms.open({ view: 'inline' })` or `open({ view: 'sidebar' })` to override the declaration for one session.

The URL parameter `?cms=true` opens the default editor; it does not specifically select inline mode or grant saving permission. Outside edit mode, the [dependency limits](#edit-mode-and-dependencies) still apply.

### Refresh behavior

The CMS subscribes to ClayJS's document-wide mutation observer, then rereads content within [`pageRoot`](#open-options), `document.body` by default, after 100 ms without another qualifying change. The observer ignores library controls and excluded regions such as `no-watch`. Continuous changes can postpone the automatic refresh; call `clay.cms.refresh()` when your integration needs an explicit refresh.

Normal refreshes preserve the focused form field's in-progress value. Undo and redo deliberately replace that value with the reverted content.

## Field controls

The rule and the page element determine the default control. `@src` selects an image upload control, `@checked` selects a checkbox, `@innerHTML` selects a rich text control, a rule ending in `@--color` or `@--color-<name>` selects the color control, and a rule ending in `@--font` or `@--font-<name>` selects the font control. Set `data-hcms-component` on a target element to request a specific control.

| Component | Configuration |
| --- | --- |
| `textarea` | A multiline text field |
| `richtext` | A rich text control; use `@innerHTML` to retain its markup |
| `number` | A numeric input; `getData()` returns `"5"`, not `5`, for a value of five |
| `checkbox` or `toggle` | A boolean field |
| `select` or `radio` | Add space-separated values such as `data-hcms-options="low medium high"`; use a custom template for values containing spaces |
| `image` or `file` | An [upload field](#images-and-cropping) |
| `color` | A [color control](#page-style-colors-fonts-and-layouts) for a CSS custom property; inferred for `@--color` and `@--color-<name>`, and narrowed with `data-hcms-options` |
| `font` | A [font control](#page-style-colors-fonts-and-layouts) of 40 Google Fonts and system stacks; inferred for `@--font` and `@--font-<name>`, and narrowed with `data-hcms-options` |
| `variant` | A row of [layout icon tiles](#page-style-colors-fonts-and-layouts) configured with `data-hcms-options` tokens such as `"1:cols-1 2:cols-2 3:cols-3"` |
| `chips` | On a scalar list's container, a compact editable list |

A field-specific `<template data-hcms-tpl="path">` overrides the default control.

Within a repeating rule, control attributes are sampled from the first matching element, so configure corresponding elements consistently.

If a numeric field contains a value such as `$5`, it falls back to a plain text control so opening the editor does not erase that value. A `checkbox` or `toggle` on a rule other than `@checked` accepts `true`, `false`, or an empty value; other values fall back the same way. An accepted empty value reads as unchecked; the next commit writes `false` back to the mapped content, even if another field caused that commit.

### Page style: colors, fonts and layouts

These controls change how a page looks rather than what it says. They write CSS custom properties or attributes, and the page's own CSS decides what those do.

**`@--name` rules.** A rule ending in `@--name` reads and writes one CSS custom property in an element's `style` attribute and leaves every other declaration alone:

```html
<html style="--color-accent: #0284c7; --font-body: 'Inter', ui-sans-serif, system-ui, sans-serif">
<style>
  a, .button { color: var(--color-accent); }
  body { font-family: var(--font-body); }
</style>
```

```json
{ "theme": { "accent": "html@--color-accent", "body": "html@--font-body" } }
```

Names are case sensitive. Clearing a field removes the property, so the page falls back to its own CSS.

**Color** (inferred for `@--color` and `@--color-*`): a swatch grid of the full Tailwind palette (26 hues by 11 shades) plus a custom color picker. `data-hcms-options` narrows it: color ids (`sky-600`), whole hues (`sky`), whole shades (`500`), plain CSS colors, and `custom` to keep the custom picker. A narrowed list hides the custom picker unless it includes `custom`. A clear button empties the property.

**Font** (inferred for `@--font` and `@--font-*`): a list of 40 Google Fonts, each shown in its own face, plus the system stacks. `data-hcms-options` narrows it: font slugs (`inter`), groups (`sans`, `serif`, `display`, `mono`), and `system`. The CMS keeps one `<link data-hcms-fonts>` in the head that loads exactly the families the page's font fields hold, and removes it when none do.

**Variant** (never inferred; set `variant` explicitly): a row of icon tiles, one per option. Each `data-hcms-options` token is `value:icon` or a bare value. Icons: `cols-1`, `cols-2`, `cols-3`, `cols-4`, `media-left`, `media-right`, `media-top`, `list`, `cards`, `centered`, `card-flat`, `card-raised`. Choosing one writes only the attribute, so the layout lives in your CSS:

```html
<section id="features" data-layout="3" style="--color-section-bg: #f8fafc"
  data-hcms-component:data-layout="variant"
  data-hcms-options:data-layout="1:cols-1 2:cols-2 3:cols-3"
  data-hcms-options:--color-section-bg="slate-50 slate-100 sky-50 custom">
```

```css
#features { display: grid; gap: 1.5rem; }
#features[data-layout="2"] { grid-template-columns: repeat(2, 1fr); }
#features[data-layout="3"] { grid-template-columns: repeat(3, 1fr); }
```

The matching rules are `"features": { "layout": "#features@data-layout", "background": "#features@--color-section-bg" }`.

**Several controls on one element.** `data-hcms-component:<prop>`, `data-hcms-options:<prop>` and `data-hcms-crop:<prop>` apply only to the rule ending in `@<prop>`, and win over the plain attribute. A plain `data-hcms-component` or `data-hcms-options` never applies to an `@--name` rule.

**Where they show up.** In the sidebar, style fields bound to `html`, `body` or `:root` gather in a **Page style** group at the top of the form, and inside every other group and card the style fields come first. In the inline view they are never hover targets: a section's style fields open from a **Style** chip in its top left corner, a list row's from **Style** in its gear menu, and the page's from the **Page style** button in the bar.

**What a write accepts.** Saves through the JSON API and other guarded writers accept a custom property only when its value is plain: at most 200 characters; no `;`, `{`, `}`, `<`, `>` or backslash; balanced quotes and brackets; no comments or control characters; and no `url()`, `image()`, `image-set()`, `cross-fade()`, `element()`, `paint()`, `src()` or `expression()`. Writing back a value that is already there is always accepted.

### Custom form templates

Use a template when you need a control or form layout that the named components do not supply. Put it in the page's body before opening the CMS; lookup uses `data-hcms-tpl`, not its position beside a field. This overrides the sidebar control for `bio`:

```html
<template data-hcms-tpl="bio">
  <label>
    <span data-hcms-label></span>
    <textarea data-hcms-field rows="6"></textarea>
    <span class="hcms-error" hidden></span>
  </label>
</template>
```

For a field or container, lookup uses the first matching template:

1. Exact path, such as `links.0.label`.
2. Wildcard path, such as `links.*.label`.
3. Selected component or shape default.

By default, `.photo@src` selects `@image`, while a plain text field uses `@scalar`. Overriding `@scalar` does not replace an inferred image control; use a path-specific template or override `@image` for that.

| Template key | Scope |
| --- | --- |
| `links` | This list's container |
| `links.*` | Each item in this list |
| `links.*.label` | The label field within each item |
| `@scalar`, `@object` | Default plain field or field group |
| `@scalar-array`, `@object-array` | Default list containers |
| `@scalar-array-item`, `@object-array-item` | Default list items |
| `@image`, `@textarea`, and other component keys | Fields using that component |

List items have a separate lookup order:

1. Wildcard item template, such as `links.*`.
2. The item template for the component used by the list container, if it supplies one.
3. Shape default, such as `@object-array-item`.

A path-specific list container and its item template are separate overrides.

Leaf and object-list-item templates can supply their own fields. For a field group, use a slotted template with `.hcms-object-fields`; self-supplied child fields in a group template read as null and can erase the group's page content on the next commit.

For scalar-list item layouts, retain the field used by the selected item default. With the standard scalar-list default, use `input[data-hcms-field]` in a per-list template such as `tags.*`. Replacing it with a textarea there can produce null items and erase their text after an unrelated edit. To change that field type, override `@scalar-array-item` instead; this affects every scalar list using that default.

The markers below identify the controls and slots for generated fields and list items.

| Marker inside the template | Purpose |
| --- | --- |
| `data-hcms-field` | The element holding a scalar value: input, textarea, select, image, link, or contenteditable element |
| `data-hcms-field="label"` | Bind a child field by key inside an object-list-item template |
| `data-hcms-label` | Where the generated label appears |
| `data-hcms-shape="object-array"` or `"scalar-array"` | Required on a custom list root for removal confirmation and item-limit handling |
| `data-hcms-action="add"` or `"remove"` | List add or remove button |
| `data-hcms-action="move-up"` or `"move-down"` | List reorder button |
| `.hcms-object-fields` | Container for generated child fields in a slotted field-group template |
| `.hcms-card-fields` | Container for generated child fields in a slotted object-list-item template |
| `.hcms-array-items` | Direct child of the rendered list root; generated item roots must be direct children of this slot |
| `.hcms-error` | Error display slot; retain it in custom controls |

A custom list container must preserve that direct-child relationship:

```html
<template data-hcms-tpl="links">
  <section data-hcms-shape="object-array">
    <div class="hcms-array-items"></div>
    <div class="hcms-error" hidden></div>
    <button type="button" data-hcms-action="add">Add link</button>
  </section>
</template>
```

After changing a template, compare `clay.cms.api.getData()` with the existing page content before editing or saving. Unexpected null values or empty lists indicate a binding problem; fix the template first. These mistakes can commit successfully and erase existing content, so do not rely on `onError` to catch them.

On a list's `<template>`, `data-hcms-min-items` and `data-hcms-max-items` set the editor's item limits. `data-hcms-no-add`, `data-hcms-no-remove`, and `data-hcms-no-reorder` hide those operations. These configure the editor, not server permissions or validation for writes outside it.

For per-list deletion confirmation, put `data-hcms-confirm-remove` on the root element **inside** the custom array template, not on `<template>` itself. That root must also carry the matching `data-hcms-shape` from the table above.

An empty confirmation attribute uses the default prompt. A string supplies the prompt; `off`, `false`, `no`, or `0` disables it, ignoring case and surrounding whitespace for those disabling values. This overrides the session's `confirmRemove` option.

### Images and cropping

Map the source and opt into cropping on the image:

```html
<img class="photo" src="portrait.jpg" alt="Portrait" data-hcms-crop="1:1">
```

```json
{ "photo": ".photo@src" }
```

`data-hcms-crop="1:1"` requests a square crop. Use another width-to-height ratio or `free` for an unconstrained crop. An image field without that attribute does not automatically acquire cropping.

The upload destination belongs to the save host. Every host stores accepted uploads in its uploads folder, in a folder named after the document: `profile.htmlclay` uses `uploads/assets-profile/`. The returned link is the host path `/_/uploads/assets-profile/<name>`, served at every address of the host. A host without upload support may use an embedded data URL instead, increasing the HTML file's size. An upload error is not the same as a successful fallback; check the displayed result.

## JavaScript API

These methods are available on `clay.cms` after `clay.ready` resolves:

| Member | Behavior |
| --- | --- |
| `open(options)` | Open a session or switch its view; calling it for the active view is a no-op |
| `close()` | End the session without saving the page |
| `refresh()` | Refresh the active editor from the page |
| `isOpen` | Boolean getter |
| `currentView()` | `"inline"`, `"sidebar"`, or `null` when closed |
| `api.getData()` | Current form data, or `null` when closed |
| `api.setValue(path, value)` | Write a scalar leaf through the active session |
| `api.addItem(arrayPath)` | Add an item to a list |
| `api.removeItem(itemPath)` | Remove the indexed list item without asking for confirmation |

The action methods `open`, `close`, `refresh`, `setValue`, `addItem`, and `removeItem` return `undefined`, not a Promise or a success result. Observe changes and errors through the callbacks or events below.

For the [list example](#lists), `api.getData()` returns:

```json
{ "links": [{ "label": "My projects", "url": "https://example.com" }] }
```

Paths use dots and numeric indices: `links.0.url`. `setValue` accepts a scalar leaf, not an entire object or list. `setValue`, `addItem`, and `removeItem` throw synchronously when the session is closed. Invalid paths can also throw before a commit; `onError` does not catch those API-use errors.

`confirmRemove` and `data-hcms-confirm-remove` apply to the editor's remove controls, not `api.removeItem()`. A custom delete button must ask for confirmation itself if you want one. The API still honors list limits: removal stops at `data-hcms-min-items` or `data-hcms-no-remove`; addition stops at `data-hcms-max-items` or `data-hcms-no-add`. Those blocked operations return without changing the page or throwing an error.

```js
clay.ready.then(() => {
  clay.cms.open({ view: 'inline' });
  clay.cms.api.setValue('name', 'Alex R.');
  clay.cms.open({ view: 'sidebar' });
});
```

### Open options

| Option | Default | Purpose |
| --- | --- | --- |
| `view` | Existing view, otherwise the page's declared view (`sidebar` when none) | Select `"inline"` or `"sidebar"` |
| `rules` | `"cms"` | A rules token or an explicit rules object |
| `pageRoot` | `document.body` | Root for extracting and applying content |
| `richText` | `true` | Allow rich text upgrading |
| `theme` | `null` | `"light"` or `"dark"` in either view; otherwise follow the system preference |
| `confirmRemove` | Confirmation for object-list cards, none for scalar lists including chips | For editor remove controls: `true` requests confirmation, `false` skips it, and a string supplies the prompt; a list's `data-hcms-confirm-remove` overrides this option |
| `onChange(data, info)` | None | Receive changed data and its path/structural metadata |
| `onError(error)` | None | Receive commit errors |

For a successful CMS commit, `info.path` identifies the edited field or affected list/item path. `info.structural` is `true` for add, remove, and reorder operations, and `false` for scalar edits. Editing a link URL produces info such as `{ path: 'links.0.url', structural: false }`. An undo or redo that changes CMS data also calls `onChange`, with `path: ""` and `structural: false`. Undoing unrelated page content does not call it.

Sidebar presentation options:

| Option | Default | Accepted value and behavior |
| --- | --- | --- |
| `mountTo` | `document.body` | A DOM element to contain the sidebar |
| `side` | `"right"` | `"left"` or `"right"` |
| `overlay` | `false` | Boolean; overlay the page instead of shifting it |
| `showSaveButton` | `false` | Boolean; show a button carrying `trigger-save`. ClayJS handles that click in edit mode; a custom host must provide its own handler. |
| `title` | `"Page content"` | String shown in the sidebar heading |
| `eyebrow` | `"Edit"` | String shown above the heading |

Switching views carries the current session's options. Close and reopen when you need to replace the session's rules or root.

### Events

`hcms:open`, `hcms:change`, `hcms:error`, and `hcms:close` bubble to the document. Their detail includes `pageRoot` and `view`. Change events also include `data`, `path`, and `structural`; the last is true for operations such as adding or removing list items.

A view switch dispatches `hcms:close` with `reason: "switch"`, then `hcms:open` for the new view. The open event's `previous` value names the old view; it is `null` on the first open. Use these values to distinguish switching from closing and reopening.

Undo and redo that change CMS data call `onChange`, but do not dispatch `hcms:change`.

Error events include `error`. Commit error objects identify their kind in `error.name`, such as `EmptyListInsert`. Commit failures also include `attemptedData`, the form data the CMS tried to apply to the page. Inspect it when diagnosing a rejected edit; correct the cause before retrying.

Failed structural commits restore a snapshot of the affected list container's content, or non-editor page content when that container cannot be resolved. Scalar commits do not take a rollback snapshot. After an unexpected scalar failure, inspect the affected content before retrying; do not assume every failed commit is atomic.

Upload failures dispatch `hcms:error`, not `onError`. Read `event.detail.code` and `event.detail.path`; the code is not a property of the error object. The path identifies the affected field. Common codes are `too-large` for the host's size limit, `unsupported-type` for a rejected file type, and `timeout` when the result is unknown. The code comes from the uploader's result, so handle unknown codes as a generic upload failure. For the local retry procedure, see [guide troubleshooting](https://clayjs.com/docs/plugins/cms/#if-something-does-not-work).

Use these events to observe the session. They do not replace the host's save result.

## Saving and ownership

The CMS edits the DOM. ClayJS and the host handle saving. Use Cmd+S on macOS or Ctrl+S on Windows and Linux, or add the `autosave` attribute to `<html>`. Add `indicator` to the loader's plugin list for visible save feedback, as in the [guide](https://clayjs.com/docs/plugins/cms/#save-and-check-the-file). Runtime editor elements and session markers are removed by the snapshot pipeline, so they do not become the saved content.

The saved document retains the content and the configuration needed to edit it again. Uploaded assets may remain separate files. Moving the HTML alone does not move those assets.

Host permission determines who can persist changes. Showing editing controls is not an access-control boundary. The same distinction applies to collaboration: CMS fields can refresh when the page changes, but live synchronization requires a host and runtime that provide it.

## Configure with a coding agent

On Hyperclay, give an AI coding agent your HTML and this request:

> Add Hyperclay CMS rules for the human content in this page. Preserve its layout, styles, scripts, and existing content. Map content such as headings, body text, image sources, and link destinations to its elements. Load the CMS through ClayJS. Do not map navigation controls or application machinery as editable content. Use this reference: /docs/plugins/cms/. Show me the changes.

Review the rules and try each field. Generating configuration does not verify that its selectors point at the content you intended.
