Skip to content

Inline editing

After installing CaretCMS, add data-caret to connect an element to a stored field. Signed-in editors can then change it on the page. This guide covers explicit bindings; for values passed through components, see editable().

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

A binding is the address of a field. It has three parts:

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, stegaClean } from '@caretcms/core';
import { getLiveCollection } from 'astro:content';
const { entries, error } = await getLiveCollection('gallery');
if (error) throw error;
---
<ul>
{entries.map((item) => {
const bind = bindEntry({ collection: 'gallery', id: item.id });
return (
<li>
<img {...bind('src')} src={stegaClean(item.data.src)} alt={stegaClean(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="pages::home::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="pages::home::headline">Click me</h1>

Click outside or press Cmd/Ctrl+S to save. Escape cancels and reverts. Enter is not a save shortcut for these fields. If another editor saved the same entry first, your save can return a revision conflict. Keep your edits visible and compare them with the latest content before choosing how to proceed.

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) Their active draft overlay where configured, with Edit/Preview interaction modes
Visitor with JS disabled Latest published content (server: rewritten at request time; static: baked at build)

Only bind fields intended for editing. There is no supported data-caret-disable subtree switch. To disable the inline interface globally, set enableInlineEditor: false; Studio and authenticated API routes remain available.

In the editor, Preview temporarily stops editing interactions. Edit restores them. Tools groups secondary controls, including the content map. The Studio drawer can move sides, expand, and close. Section controls keep common actions nearby and less frequent actions in More.

Order of operations:

  1. Checks for data-caret, data-caret-md, or encoded editable text.
  2. Calls GET /api/cms/auth/session to check for an authenticated editor.
  3. When both conditions are met, 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:

Add an alt string to your collection schema so editors can change it in Studio. It is a separate field; uploading an image does not generate or edit alt text. Render both values explicitly and clean live-loader attribute values:

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

Use descriptive alt text for meaningful images and an empty string for purely decorative images. See Live collection attributes.