Guide

Advanced Rich-Text

Mermaid diagrams and custom templates in a rich-text field, plus one gotcha about the parser.

Beyond basic markdown (bold, italic, links, lists, blockquotes, all rendered automatically through <TinaMarkdown>), nuxt-tina also supports Mermaid diagrams in a rich-text field.

Mermaid diagrams

Add 'mermaid' to the rich-text field's toolbar:

{
  type: 'rich-text',
  name: 'body',
  isBody: true,
  overrides: {
    toolbar: ['heading', 'link', 'bold', 'italic', 'mermaid', 'codeBlock', /* ... */],
  },
}

TinaMarkdown doesn't render Mermaid diagrams by default. mermaid is a large dependency, so it's opt-in:

npm install mermaid
<TinaMarkdown :content="post.body" :components="{ 'tina-mermaid': TinaMermaid }" />

TinaMermaid is auto-imported by the module, so no explicit import is needed. With the components prop wired up, a mermaid code block renders as an SVG diagram through mermaid.render() (dynamically imported only when a diagram is actually mounted, so it never adds to your bundle unless used). Without the prop, mermaid code blocks render as plain code.

Custom templates

A rich-text field can define its own templates (Tina's {{ shortcode }} syntax or MDX-style embeds):

{
  type: 'rich-text',
  name: 'body',
  isBody: true,
  templates: [
    {
      name: 'callout',
      label: 'Callout',
      match: { start: '{{', end: '}}' },
      fields: [
        { name: '_value', type: 'string', label: 'Text', required: true },
      ],
    },
  ],
}

nuxt-tina doesn't ship a component for any specific custom template: every one renders through <TinaMarkdown> as a generic wrapper: <div data-tina-type="callout">...</div>. Style or target it with that attribute in your own CSS. Unlike Mermaid, there's currently no components-prop hook to swap a named custom template for your own Vue component: extending that coverage is on the roadmap, tracked in the adapter's own source (comark-adapter.ts).

⚠️ Gotcha: templates aren't parsed when format: 'md'

A collection with format: 'md' automatically gets parser: { type: 'markdown' } on its rich-text fields, and that parser mode entirely skips the template-matching logic (templates[].match) inside @tinacms/mdx. Without an override, {{ callout "..." }} just becomes plain text, not a parsed node.

Fix: override the field's parser explicitly:

{
  type: 'rich-text',
  name: 'body',
  isBody: true,
  parser: {
    type: 'mdx', // force the path that actually preprocesses templates
  },
  templates: [ /* ... */ ],
}

This applies to both custom shortcodes ({{ }}) and real JSX-style MDX embeds (<Cta ... />).

What a custom template looks like in the AST

If you want to build your own render adapter, a custom template node shows up in the AST as:

{
  "type": "mdxJsxFlowElement",
  "name": "callout",
  "props": { "_value": "This is a callout." },
  "children": [{ "type": "text", "text": "" }]
}

comark-adapter.ts already handles this shape: look at the mdxJsxFlowElement/mdxJsxTextElement case in that file for the pattern if you're extending it.

Copyright © 2026