Show navigationHide navigation
Rich Text Field
The richtext field type stores content as companion .mdx files (MDX — Markdown with JSX). It extends standard Markdown with embeddable CMS blocks: images from the media library, references to other entries, conditional content branches, template variables, and custom React components.
Quick start#
typescript// cms/octocms.config.ts { body: { label: 'Body', format: 'richtext', }, }
This gives you a full WYSIWYG editor with headings, lists, bold/italic, tables, code blocks, links, and images. Content is stored in a companion .mdx file alongside the entry JSON (e.g. post-123.body.mdx).
Embeds#
Enable embeddable content types via the richtext.embeds config:
typescriptbody: { label: 'Body', format: 'richtext', richtext: { embeds: { images: true, references: { collections: ['post', 'author'], display: 'both' }, conditions: true, variables: ['user.firstName', 'site.name'], components: { CallToAction: { label: 'Call to Action', kind: 'block', props: [ { name: 'text', label: 'Button text', type: 'string', required: true }, { name: 'url', label: 'Link URL', type: 'url', required: true }, ], }, }, }, }, },
Image embeds (embeds.images)#
When enabled, adds a toolbar button and slash command to insert images from the media library. Stored as <CmsImage mediaId="uuid" /> in MDX. Resolved by query() to a ResolvedImageField object (same shape as the image field type).
Reference embeds (embeds.references)#
Embed other content entries inline or as blocks. Configure which collections are allowed and whether references appear 'inline', 'block', or 'both'.
Stored as <CmsRef id="post-456.json" display="block" />. Resolved by query() to full entry objects (deep resolution, same as reference fields).
Condition embeds (embeds.conditions)#
Insert conditional (A/B) content blocks with named branches:
mdx<CmsCondition field="promo"> <CmsBranch key="control">Standard pricing.</CmsBranch> <CmsBranch key="test">**50% off** this week!</CmsBranch> </CmsCondition>
At render time, pass a conditions map to select which branch to display.
Template variables (embeds.variables)#
Insert dynamic placeholders that are substituted at render time. Only variable names listed in the config array are available in the editor.
Stored as <CmsVar name="user.firstName" />. Rendered by passing a variables map to RichTextContent.
Custom components (embeds.components)#
Define arbitrary React components that editors can insert. Each component has:
label— display name in the editorkind—'inline'(within text) or'block'(own line)props— typed prop definitions (string,number,boolean,url,image,select)
Stored as standard JSX: <CallToAction text="Sign up" url="/signup" />.
Toolbar customization#
Control which standard toolbar buttons appear:
typescriptbody: { label: 'Body', format: 'richtext', richtext: { toolbar: { formatting: true, // Bold, italic, underline (default: true) headings: true, // Block type selector (default: true) lists: true, // Ordered/unordered lists (default: true) code: true, // Inline code toggle (default: true) codeBlock: true, // Code block insertion (default: true) links: true, // Link creation (default: true) tables: true, // Table insertion (default: true) thematicBreak: true, // Horizontal rule (default: true) images: true, // Standard markdown images (default: true) undoRedo: true, // Undo/redo buttons (default: true) }, }, },
Set any key to false to hide that toolbar section. Omitted keys default to true.
Slash commands#
Type / in the editor to open the insert menu. It lists all available block types and embed types (filtered by your richtext config). Type to filter, arrow keys to navigate, Enter to select, Escape to close.
Storage format#
Rich text content is stored as companion .mdx files alongside the entry JSON:
cms/content/post/ post-123.json # structured fields post-123.body.mdx # rich text content
Standard Markdown is valid MDX, so the format is backward-compatible. Embeds are stored as JSX tags.
Reading richtext on the public site (Server Components)#
Use query() like any other field. For richtext fields, query() reads the companion .mdx file, parses it to a RichTextDocument AST, and resolves embedded images and references.
tsximport { query } from 'octocms/query'; import RichTextContent from 'octocms/components/public/RichTextContent'; import { CallToAction } from '@/components/CallToAction'; 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> <RichTextContent document={post.fields.body} components={{ CallToAction }} variables={{ 'user.firstName': 'Guest' }} conditions={{ promo: 'control' }} /> </article> ); }
RichTextContent props#
| Prop | Type | Description |
|---|---|---|
document | RichTextDocument | null | The AST from query(). |
components | Record<string, ComponentType> | Map custom component names to React components. |
variables | Record<string, string> | Variable substitutions for <CmsVar> nodes. Unmatched variables render as {name}. |
conditions | Record<string, string> | Map condition field names to the branch key to render. Unmatched conditions render nothing. |
className | string | Optional wrapper class name. |
Custom component rendering#
Custom components receive their configured props plus optional children:
tsxfunction CallToAction({ text, url }: { text: string; url: string }) { return ( <a href={url} className="btn btn-primary"> {text} </a> ); }
Reference rendering#
By default, embedded references render with a fallback display (type label + title). To customize, pass a component keyed by collection name:
tsx<RichTextContent document={post.fields.body} components={{ post: PostCard, author: AuthorBadge }} />
Reference components receive { entry, display } where display is 'inline' or 'block'.
Reading richtext on the client (CSR)#
If you need client-side rendering, the RichTextContent component works in client components too — it's a pure React component, no server-only imports:
tsx'use client'; import RichTextContent from 'octocms/components/public/RichTextContent'; export function RichTextPreview({ document }) { return <RichTextContent document={document} />; }
RichTextDocument AST shape#
The AST returned by query() for richtext fields:
typescripttype RichTextDocument = { type: 'doc'; content: RichTextNode[] }; type RichTextNode = | { type: 'paragraph'; children: RichTextNode[] } | { type: 'heading'; level: 1|2|3|4|5|6; children: RichTextNode[] } | { type: 'blockquote'; children: RichTextNode[] } | { type: 'list'; ordered: boolean; children: RichTextNode[] } | { type: 'listItem'; children: RichTextNode[] } | { type: 'text'; value: string; marks?: ('bold'|'italic'|'underline'|'code')[] } | { type: 'link'; url: string; children: RichTextNode[] } | { type: 'code'; lang?: string; value: string } | { type: 'thematicBreak' } | { type: 'break' } | { type: 'image'; image: ResolvedImageField } | { type: 'reference'; entry: unknown; display: 'inline' | 'block' } | { type: 'variable'; name: string } | { type: 'component'; name: string; props: Record<string, unknown>; children?: RichTextNode[] } | { type: 'condition'; field: string; branches: Record<string, RichTextDocument> };
Migrating from markdown to richtext#
- Change the field format in
cms/octocms.config.tsfrom'markdown'to'richtext'. - Run the migration script to rename companion files:
bashnpx tsx scripts/migrate-markdown-to-richtext.ts
The script finds all .md companion files for fields that are now richtext and renames them to .mdx. Plain Markdown is valid MDX, so content is preserved without transformation.
Options#
| Flag | Description |
|---|---|
--dry-run | Show what would be renamed without making changes. |
--collection <name> | Only process a specific collection. |
--field <name> | Only process a specific field name. |
- Run
npm run types:genandnpm run docs:gento update generated files. - Update public pages to use
RichTextContentinstead ofreact-markdown/MarkdownContent.
Comparison with markdown#
| Feature | markdown | richtext |
|---|---|---|
| Storage | .md companion | .mdx companion |
| Editor | Basic MDXEditor toolbar | Full toolbar + slash commands + embed buttons |
| Embeds | None | Images, references, conditions, variables, custom components |
| Query return type | string (raw markdown) | RichTextDocument (AST with resolved embeds) |
| Public rendering | react-markdown + rehype-sanitize | RichTextContent (AST → React) |
| Toolbar customization | No | Yes — per-field toolbar config |