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
PartDescriptionExample
collectionGroup of entriespages, site, products
idEntry within the collectionhome, global, widget-x
fieldProperty name on the entry dataheadline, 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.

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.

VisitorWhat they see
Public visitorPublished 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 disabledLatest 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 bindings yet, CaretCMS shows a small “Signed in · no editable fields on this page” hint pointing you at the next step — so an un-annotated page reads as “nothing to edit here” instead of looking broken. The hint is editor-only (anonymous visitors never see it), comes from the server with no extra client request, and never appears inside the 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'}
/>