Rendering Markdown
The value of a RichText or Markdown field is a Markdown string. When you render such a value yourself — in a Custom Preview Template, the preview output of a Custom Editor Component, or a Custom Field Type — the string is used as is, so text like **bold** appears verbatim unless you convert it to HTML.
Overview
Sveltia CMS offers two ways to do that. Both are available as soon as the CMS is loaded, whether you use the CDN build or the npm package, so no additional dependency is necessary:
CMS.renderRichText()renders the value into a DOM element you provide, exactly like the built-in preview pane — custom editor components, images and sanitization included. Use it whenever you have an element to render into.markedandDOMPurifyare the parser and sanitizer the CMS uses internally, exposed on thewindowobject. Use them when you need an HTML string, such as for a React component.
Using renderRichText
The CMS.renderRichText() method renders a Markdown string into a DOM element you provide, using the same pipeline as the preview pane of a RichText field:
- Custom editor components are rendered with their own
toPreviewoutput, including components nested in the value, recursively - Markdown is parsed into HTML, with a single line break becoming a
<br> - Code blocks are syntax-highlighted
- Internal image paths are replaced with blob URLs, so uploaded images appear in the preview
- The resulting HTML is sanitized
It takes the target element, the Markdown string and an optional options object, and returns a function that removes the rendered content and destroys any component previews within it:
const destroy = CMS.renderRichText(element, markdown, options);The content is rendered asynchronously, so the target element can still be detached from the document when you call the method — for example, an element created in toPreview that the CMS inserts into the preview pane afterwards.
The options object accepts the following property:
fieldConfig— RichText field options to be applied, such aseditor_componentsto restrict the available components andsanitize_previewto disable sanitization. The output is sanitized by default, regardless of thefield_defaultsconfiguration.
The method is designed for an editor component whose toPreview returns a DOM element, where the value of a nested RichText field would otherwise be displayed verbatim. Call the returned destroy function once the CMS dispatches the Unmount event on the element, so that the nested previews are destroyed along with your component:
import { registerEditorComponent } from '@sveltia/cms';
import { mount, unmount } from 'svelte';
import Warning from '$lib/components/Warning.svelte';
registerEditorComponent({
id: 'warning',
label: 'Warning',
icon: 'warning',
fields: [{ name: 'body', label: 'Body', widget: 'richtext' }],
pattern: /<Warning>\s*(?<body>[\s\S]*?)\s*<\/Warning>/,
toBlock: ({ body = '' }) => `<Warning>\n\n${body}\n\n</Warning>`,
toPreview: ({ body = '' }) => {
const element = document.createElement('div');
const component = mount(Warning, { target: element, props: { body } });
element.addEventListener('Unmount', () => unmount(component), { once: true });
return element;
},
});CMS.registerEditorComponent({
id: 'warning',
label: 'Warning',
icon: 'warning',
fields: [{ name: 'body', label: 'Body', widget: 'richtext' }],
pattern: /<Warning>\s*(?<body>[\s\S]*?)\s*<\/Warning>/,
toBlock: ({ body = '' }) => `<Warning>\n\n${body}\n\n</Warning>`,
toPreview: ({ body = '' }) => {
const element = document.createElement('div');
const destroy = CMS.renderRichText(element, body);
element.className = 'bg-red-100';
element.addEventListener('Unmount', destroy, { once: true });
return element;
},
});In the Svelte example, the component itself calls the method with an attachment, which runs the returned destroy function automatically when the component is unmounted:
<script>
import { renderRichText } from '@sveltia/cms';
let { body } = $props();
</script>
<div class="bg-red-100" {@attach (element) => renderRichText(element, body)}></div>Because the nested value goes through the same component matching as the field itself, a component can contain other components, or even another instance of itself, as long as the pattern of each component can match its own block within the parent’s. Any nested component preview is rendered in place, whether it returns a string, a DOM element or a React element.
Raw values for nested fields
The value of a nested RichText field is passed to toPreview as is, including the syntax of any component within it. It’s never partially rendered, regardless of the order in which components are registered, so you can always pass it to renderRichText or process it yourself.
Using marked and DOMPurify
If you need an HTML string rather than a rendered element — for example, to pass it to a React component’s dangerouslySetInnerHTML prop — Sveltia CMS exposes the two libraries it uses internally, so you don’t need to add a dependency of your own:
marked— The Marked parser, which converts a Markdown string to an HTML stringDOMPurify— The DOMPurify sanitizer, which strips scripts and other dangerous markup from an HTML string
These are available on the window object when Sveltia CMS is loaded, whether you use the CDN build or the npm package. No additional imports are necessary to use them.
const html = DOMPurify.sanitize(marked.parse(markdown));The CMS renders the preview pane with the breaks option enabled, meaning a single line break becomes a <br>. Pass the same option if you want your output to match:
const html = DOMPurify.sanitize(marked.parse(markdown, { breaks: true }));Note that, unlike renderRichText, this approach doesn’t render custom editor components or resolve internal image paths; the value is converted as plain Markdown.
Security Risk
Always sanitize the HTML before inserting it into the DOM, as the examples above do. Markdown allows raw HTML, so skipping the sanitizer can expose your CMS to cross-site scripting (XSS) attacks if untrusted users have access to the CMS, especially when using Open Authoring, because entries can be written by anybody.
Shared parser instance
marked is the very parser the CMS uses to render the preview pane, so any extension you add with marked.use() also changes how the CMS itself renders Markdown. Prefer passing options to marked.parse() for one-off customization.