Skip to content

Inline Editing

data-caret is the wedge. This page covers every shape it takes.

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" }
}
}

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.

There’s no client-side hydration of CMS content. A response-rewriting middleware reads stored entries and replaces template defaults at render time, before HTML hits the browser.

VisitorWhat they see
Public visitorStored content (or template defaults if no edit exists)
Editor (with session cookie)Same content, plus inline editor JS that turns clicks into edits
Visitor with JS disabledLatest stored content — rewriting happened server-side
<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.

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'}
/>