Skip to content

Markdown body editing

CaretCMS can edit prose from Astro content collections without moving that content into a separate database. An editor clicks the rendered paragraph or heading, makes a change on the page, and publishes it back to the original .md file.

Install @caretcms/core@0.5.0 and use an Astro collection containing ordinary .md files in a flat directory such as src/content/blog/. This guide assumes Astro already loads that collection through src/content.config.ts. If you have not set up content collections, use the editorial example as a starting point.

Use the Markdown storage adapter. CaretCMS selects it automatically when it finds collections under src/content/, or you can set it yourself:

astro.config.mjs
import { defineConfig } from 'astro/config';
import caret, { markdownStorage } from '@caretcms/core';
export default defineConfig({
integrations: [
caret({
storage: markdownStorage({ contentRoot: './src/content' }),
}),
],
});

Markdown body editing is enabled by default. Set bodyEditing: false in caret() if you only want frontmatter fields to be editable.

Render the entry with Astro’s normal content collection API:

src/pages/blog/[...slug].astro
---
import { getCollection, getEntry, render } from 'astro:content';
// For a static route. On a server-rendered route, getStaticPaths is unnecessary.
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({ params: { slug: post.id } }));
}
const post = await getEntry('blog', Astro.params.slug);
if (!post) return Astro.redirect('/404');
const { Content } = await render(post);
---
<article class="post-body">
<Content />
</article>

CaretCMS adds its editing markers while Astro renders the Markdown. You do not add data-caret to each prose block.

  1. Sign in and click a marked paragraph, heading, list item, or supported paragraph inside a blockquote.
  2. Edit the text or use the inline formatting toolbar.
  3. Leave the field to save it as a private draft. The .md file has not changed yet.
  4. Use Publish to write the draft to the source file.
  5. Commit and deploy the source change, or use CaretCMS commit-on-publish and a rebuild webhook.

For a statically built page, visitors see the published change after the next build. A server-rendered route that actually reads the changed source at request time can show it on the next request. Astro’s build-time getCollection() / getEntry() data is not made live simply by setting output: 'server'; rebuild when the content was bundled at build time. See Live collections for request-time structured fields.

Source block Editable
Paragraph Yes
# ATX heading Yes
First paragraph in a list item Yes, when its source fits on one line
Paragraph inside a blockquote Yes, when its source fits on one line
Setext heading No
Table or code block No
Raw HTML block No
MDX prose, JSX, or a component island No

Nested blocks stay read-only when their source spans more than one line. This prevents an edit from removing list indentation or blockquote markers.

The toolbar can create:

  • bold and emphasis;
  • links.

Existing inline code round-trips safely, but the toolbar does not provide an inline-code toolbar button. Shift+Enter can create a line break only in a top-level paragraph; headings and nested list/quote blocks reject it so their source structure remains intact.

CaretCMS escapes Markdown punctuation in normal text. It rejects markup outside this small set instead of guessing how to convert it.

Editing a link rewrites it as an inline Markdown link. Link titles are not kept, and a reference-style link becomes an inline link with the same text and URL.

Consecutive supported top-level paragraphs share an editing region:

Action Result
Enter Split at the caret or add a paragraph at the end
Backspace at the start Merge with the previous paragraph
Delete at the end Merge the next paragraph
Shift+Enter Insert a hard line break within a supported top-level paragraph
Paste Insert plain text; blank lines separate paragraphs
Undo / redo Use the browser’s editing history until the page is reloaded

Headings, lists, quotes, code, HTML, tables and unsupported source patterns form boundaries; this is not an arbitrary block editor. Nested list/quote blocks keep single-block editing. Empty paragraphs disappear when published because Markdown has no empty paragraph node.

A network save failure retains edits in the page. Focus and leave the region to retry. On a source conflict, copy unsaved work before reloading. These structural interactions have Chromium coverage; Firefox, Safari and mobile input methods need additional validation before relying on identical behavior.

Editor previews wrap paragraph groups in div[data-caret-md-sources]. Style prose descendants rather than requiring paragraphs to be direct children of an article. Public rendering uses normal Markdown HTML.

See Frontmatter for multiline YAML fields and which source bytes are preserved when an entry is saved.

Each prose draft stores the source range and a hash of the text that was edited. CaretCMS checks that range when saving the block and checks every drafted range again before Publish. If the source changed before the block save, the editor reverts that field and asks for a reload so Astro can generate fresh stamps. If it changed after a draft was saved, Publish returns a conflict, leaves the file and draft untouched, and lets the editor reload and reapply the intent.

When an entry contains several edited blocks, CaretCMS checks all of them before writing. One stale block stops the whole entry from publishing.

CaretCMS uses Astro 7’s Markdown processor when it is active and falls back to a Remark plugin for Astro 6 or another unified-based processor. Both paths produce the same editing markers and use the same source checks when publishing.

The release test suite covers both Astro versions, accented characters, emoji, Windows line endings, files without a final newline, concurrent edits, stale source ranges, history restoration, and exact preservation of unedited text.