Inline Editing
data-caret is the wedge. This page covers every shape it takes.
The binding triple
Section titled “The binding triple”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.
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" } }}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="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.
<img data-caret="..."> opens an upload dialog on click and swaps src after the upload.
<img data-caret="hero.image" src="/img/hero.jpg" alt="Hero" width="1200" height="630"/>The default localUploads provider writes files to public/uploads/<hash>.<ext> and stores the resulting URL in the field. With Cloudflare R2, files go to your bucket and the public URL is recorded.
Editors can update alt text by clicking the image and editing the alt field in the upload dialog.
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”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.
| Visitor | What they see |
|---|---|
| Public visitor | Stored 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 disabled | Latest stored content — rewriting happened server-side |
Disabling the editor on specific subtrees
Section titled “Disabling the editor on specific subtrees”<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.
How the editor loads
Section titled “How the editor loads”Order of operations:
- Bootstrap checks for any
data-caretelement. If none, exits. - Calls
GET /api/cms/auth/sessionto check for a logged-in editor. - If authenticated, injects
editor.cssandeditor.jsfrom/__caret/.
Editor assets are versioned with ?v=<timestamp> to bust cache after redeploys.
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”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'}/>