Skip to content

Inline editing

data-caret is the one attribute that makes an element editable. This page covers every shape it takes.

CaretCMS inline editor — click to edit, swap images, open Studio

Every editable field resolves to three things:

collection :: id :: field
Part Description Example
collection Group of entries pages, site, products
id Entry within the collection home, global, widget-x
field Property name on the entry data headline, hero.title

You can write all three on a single element, or split them across a parent scope and child fields.

<h1 data-caret="pages::home::headline">Welcome</h1>

Use this for one-off bindings that don’t share a parent.

The nested-object form maps to:

{
"id": "global",
"data": {
"company": { "name": "Acme Inc.", "tagline": "Quality since 1923" }
}
}

When you render a list and can’t hand-write a data-caret string per item, use the bindEntry helper. It takes a { collection, id } and returns a function that produces the attributes for any field — spread the result onto the element.

---
import { bindEntry } from '@caretcms/core';
import { getLiveCollection } from 'astro:content';
const { entries } = await getLiveCollection('gallery');
---
<ul>
{entries.map((item) => {
const bind = bindEntry({ collection: 'gallery', id: item.id });
return (
<li>
<img {...bind('src')} src={item.data.src} alt={item.data.alt} />
<figcaption {...bind('caption')}>{item.data.caption}</figcaption>
</li>
);
})}
</ul>

bind('caption') expands to data-caret="gallery::<id>::caption". Pass { rich: true } (bind('body', { rich: true })) to add data-caret-rich and allow formatted text instead of plain text.

For headings or paragraphs with inline formatting (<strong>, <em>, links, …), use data-caret-rich:

<p data-caret="body" data-caret-rich>
Ship <strong>faster</strong> with CaretCMS.
</p>

The editor shows a floating format toolbar on text selection. Only sanitizer-safe inline tags round-trip. To keep CSS classes on spans, add them to allowedClasses:

caret({ allowedClasses: { span: ['gold'] } })

Caretize --rich-class can tag styled blocks and print the exact allowedClasses snippet you need.

Pages rendered from .md content collections do not need hand-written data-caret attributes on every paragraph. With markdownStorage(), CaretCMS marks supported headings, paragraphs, list items, and blockquotes as Astro renders them. Editors can change the prose on the page and publish it back to the source file.

See Markdown body editing for setup, supported formatting, and the safety checks used before CaretCMS changes a source file.

Any text-bearing element (h1h6, p, span, div, li, a, button) becomes contenteditable when an editor session is active.

<h1 data-caret="headline">Click me</h1>

Pressing Enter saves. Clicking outside saves. Escape cancels and reverts. The save uses optimistic locking — if someone else saved between your read and write, the request fails with 409 Conflict and you see a toast.

Response-rewriting middleware reads stored entries and replaces template defaults at render time, before HTML hits the browser.

Visitor What they see
Public visitor Published content (or template defaults if no edit exists)
Editor (with session cookie) Draft overlay when preview is on; otherwise published + inline editor
Visitor with JS disabled Latest published content (server: rewritten at request time; static: baked at build)
<footer data-caret-disable>
<p>© 2026 — not editable</p>
</footer>

To disable globally, set enableInlineEditor: false in caret(). The admin UI and API still work; only the click-to-edit hydration is skipped.

Order of operations:

  1. Bootstrap checks for any data-caret element. If none, exits.
  2. Calls GET /api/cms/auth/session to check for a logged-in editor.
  3. If authenticated, injects editor.css and editor.js from /__caret/.

Editor assets are versioned with ?v=<timestamp> to bust cache after redeploys.

If you’re logged in and land on a live page that has no data-caret or data-caret-md bindings, CaretCMS shows a small “Signed in · no editable fields on this page” hint pointing you at the next step. The hint is editor-only, comes from the server with no extra client request, and never appears inside Studio or on API/asset routes.

Field paths support dot-notation (hero.title, meta.og.image) but reject prototype-pollution attempts:

Editors can update alt text from the upload dialog. Stored as a sibling field — by convention <field>.alt:

---
import { getLiveEntry } from 'astro:content';
const { entry: home } = await getLiveEntry('pages', 'home');
---
<img
data-caret="hero.image"
src={home?.data.hero?.image ?? '/img/hero.jpg'}
alt={home?.data.hero?.alt ?? 'Hero'}
/>