Render Markdown with Components from a CMS or Database

Parse Markdown with components at runtime, store the JSON document, and render it in Vue, React, Svelte, Angular, or HTML with no build step.

Parse the Markdown on the server when an editor saves it, and store the resulting MarkdownDocument as JSON. Any client then renders that JSON with <MarkdownDocument>, with no parser in the browser and no build step.

Content from a CMS or a database changes after you deploy. A compiler such as MDX turns content into code at build time, so new content needs a new build. Comark parses at runtime into plain data. See Comark vs MDX for a full comparison.

Parse and store the document

A MarkdownDocument is a JSON-safe object: { nodes, frontmatter, meta }. Parse once when the content changes, then store or cache the result. Sanitize before you store, so unsafe content never reaches the database:

server/save-page.ts
import { createMarkdownParser } from 'comark'
import security from 'comark/plugins/security'

const parse = createMarkdownParser({
  plugins: [security({ blockedTags: ['script', 'iframe', 'embed', 'form', 'base', 'meta', 'link', 'style', 'object'] })],
})

export async function savePage(slug: string, markdown: string) {
  const document = await parse(markdown)
  // `db` stands for your database client
  await db.pages.upsert({ slug, markdown, document: JSON.stringify(document) })
}

This example keeps the source Markdown for the editor and the document for rendering. Your API then returns the stored document.

Render the document on any client

<MarkdownDocument> renders a parsed document without a parse step. Map component tags to your own components with the components prop:

<script setup lang="ts">
import { MarkdownDocument } from '@comark/vue'
import Alert from './components/Alert.vue'

const document = await fetch('/api/pages/pricing').then(res => res.json())
</script>

<template>
  <MarkdownDocument :value="document" :components="{ alert: Alert }" />
</template>

Svelte and Angular use the same pattern with <MarkdownDocument> and <comark-markdown-document>. The document format doesn't depend on a framework, so one stored document serves every client.

Pass runtime data

Content often refers to values that change per request, for example the user name or a price. Pass them in the data prop, and reference them with a : prop in the Markdown:

Welcome back, :badge{:label="data.user.name"}!
<MarkdownDocument :value="document" :data="{ user: { name: 'Ada' } }" />

To insert a value directly into text, add the binding plugin. It adds the {{ data.user.name || guest }} syntax. See data binding for the full scope.

Turn the document back into Markdown

renderMarkdown() converts a document into a Markdown string, with frontmatter and component syntax. Use it when you edit the document in code, or when you keep only the document:

server/edit-page.ts
import { renderMarkdown } from 'comark/render'

// `row` is the stored page from your database
const document = JSON.parse(row.document)
document.frontmatter.title = 'New pricing'

const markdown = await renderMarkdown(document)
await savePage(row.slug, markdown)

The output can differ from the original formatting. For example, a component with more than three attributes moves them to a YAML block. See the maxInlineAttributes option.

FAQ

Next steps

© 2026 Vercel, Inc.