Show navigationHide navigation

Markdown

Rich markdown editor powered by MDXEditor. Use for long-form content like blog posts, documentation articles, and changelogs.

Usage#

typescript
body: { label: 'Body', format: 'markdown' },

Options#

OptionTypeDefaultDescription
requiredbooleanfalseMust be non-empty after trimming
hintstringHelper text below the field

The editor toolbar includes bold / italic / underline, link, insert image, lists, and block type (heading levels, quote, etc.).

Inline images#

Click the image button in the toolbar to open the Insert image dialog. It hosts the same FormImageField component the entry editor uses for image-format fields, so the visual + interaction model is identical:

  • Upload new image — opens the per-file upload queue (Title + "Generate blur placeholder" toggle). After upload, each file is inserted at the cursor in order.
  • Select existing image — opens a compact media browser (search, folder filter, grid/list view) backed by the shared media list. Pick an asset and it's inserted with its Title as the alt text.

The dialog respects mediaAllowedFormats — files with disallowed extensions are skipped with a destructive toast.

The inserted markdown looks like:

![bamboo-grove](/media/abc-123.jpg)

Editing & deleting inline images#

Hovering an inserted image reveals a small floating toolbar with two icons:

  • Pencil (Edit) — opens an alt-text editor seeded with the current value. Save updates the alt; the image source is locked (to swap the image, delete it and insert a new one).
  • Trash (Delete) — opens a confirmation dialog before removing the image from the markdown body. The asset stays in the media library; you can re-insert it any time.

Drag-and-drop and paste-image still work and bypass the dialog entirely — they're routed through the package's imageUploadHandler, which uploads via the same uploadMedia action and inserts the /media/<uuid>.<ext> URL directly. This makes "drop a screenshot into the body" feel natural without an extra dialog hop.

This all works on the public site out of the box: MarkdownContent renders the syntax via react-markdown, and /media/<uuid>.<ext> is served by the media proxy route (src/app/media/[...slug]/route.ts) — so images committed to GitHub after deploy still resolve on Vercel's immutable filesystem.

Storage#

Markdown content is not stored inside the entry JSON. Instead, it is saved as a companion .md file alongside the entry:

cms/content/post/
  post-abc123.json          # structured fields (title, slug, etc.)
  post-abc123.body.md       # markdown content for the "body" field

Naming convention: {collection}-{id}.{fieldName}.md

If a collection has multiple markdown fields, each gets its own companion file:

post-abc123.body.md
post-abc123.summary.md

Example: Blog with posts#

Schema:

typescript
// cms/octocms.config.ts
post: {
  label: 'Post',
  hasMany: true,
  fields: {
    title: { label: 'Title', format: 'string', entryTitle: true, required: true },
    slug: { label: 'Slug', format: 'slug', required: true },
    publishedAt: { label: 'Published', format: 'datetime', dateOnly: true },
    body: { label: 'Body', format: 'markdown', required: true },
  },
},

Query + render:

tsx
// src/app/blog/[slug]/page.tsx
import { query } from 'cms/__generated__/query';
import { MarkdownContent } from 'octocms/components/public';

export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await query('post').filter({ slug: params.slug }).first();
  if (!post) return null;

  return (
    <article>
      <h1>{post.fields.title}</h1>
      <MarkdownContent content={post.fields.body} />
    </article>
  );
}

MarkdownContent renders markdown to React using react-markdown + remark-gfm (tables, strikethrough, task lists) + rehype-sanitize (XSS protection).

Query result#

query() automatically reads the companion .md file and returns the raw markdown string in the field value. No manual file loading needed.

typescript
const post = await query('post').first();
post.fields.body; // "# Hello\n\nThis is **bold** text."

Comparison with richtext#

markdownrichtext
Storage.md companion file.mdx companion file
EditorBasic toolbarFull toolbar + slash commands + embeds
EmbedsNoneImages, references, conditions, variables, custom components
Query outputRaw markdown stringRichTextDocument AST
Public renderingMarkdownContent (react-markdown)RichTextContent (AST → React)

For content with embedded images, references, or custom components, use richtext instead.