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().

The binding triple
Section titled “The binding triple”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.
Three ways to write a binding
Section titled “Three ways to write a binding”<h1 data-caret="pages::home::headline">Welcome</h1>Use this for one-off bindings that don’t share a parent.
<main data-caret-scope="pages::home"> <h1 data-caret="headline">Welcome</h1> <p data-caret="intro">Tagline...</p></main>data-caret-scope="collection::id" on a parent shortens every child to just the field name. Cleanest pattern for pages.
<section data-caret-scope="site::global"> <h2 data-caret="company.name">Acme Inc.</h2> <p data-caret="company.tagline">Quality since 1923</p></section>Dot-paths in the field portion (company.name) resolve to nested objects in the stored JSON.
The nested-object form maps to:
{ "id": "global", "data": { "company": { "name": "Acme Inc.", "tagline": "Quality since 1923" } }}Binding in a loop with bindEntry
Section titled “Binding in a loop with bindEntry”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.
Rich text (data-caret-rich)
Section titled “Rich text (data-caret-rich)”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.
Markdown prose
Section titled “Markdown prose”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.
Editable element types
Section titled “Editable element types”Any text-bearing element (h1–h6, 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.
<img data-caret="..."> opens an upload dialog on click and swaps src after the upload.
<img data-caret="pages::home::hero.image" src="/img/hero.jpg" alt="Hero" width="1200" height="630"/>The default localUploads provider writes files to public/uploads/ using a timestamp and sanitized filename and stores the resulting URL in the field. With Cloudflare R2, files go to your bucket and the public URL is recorded.
The image picker replaces the URL. Model alt text as a separate Studio field (see below).
Wrap a list of repeating sections with data-caret-section-composer and each section becomes reorderable, toggleable, and removable from the editor:
<div data-caret-scope="pages::home" data-caret-section-composer="sections"> <section data-caret-section="hero">…</section> <section data-caret-section="features">…</section> <section data-caret-section="cta">…</section></div>Stored as an ordered array of section names with per-section enabled flags.
What renders, and when
Section titled “What renders, and when”Response-rewriting middleware reads stored entries and replaces template defaults at render time, before HTML hits the browser.
At astro build, CaretCMS bakes stored content into generated HTML. Production has no middleware — rebuild after publish. See Static delivery.
| 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) |
Choosing what is editable
Section titled “Choosing what is editable”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.
How the editor loads
Section titled “How the editor loads”Order of operations:
- Checks for
data-caret,data-caret-md, or encoded editable text. - Calls
GET /api/cms/auth/sessionto check for an authenticated editor. - When both conditions are met, injects
editor.cssandeditor.jsfrom/__caret/.
Editor assets are versioned with ?v=<timestamp> to bust cache after redeploys.
Signed in, but nothing to edit?
Section titled “Signed in, but nothing to edit?”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 and security
Section titled “Field paths and security”Field paths support dot-notation (hero.title, meta.og.image) but reject prototype-pollution attempts:
Image alt text
Section titled “Image alt text”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.