---
title: "Render Markdown with Components from a CMS or Database"
description: "Parse Markdown with components at runtime, store the JSON document, and render it in Vue, React, Svelte, Angular, or HTML with no build step."
canonical_url: "https://comark.dev/use-cases/cms-runtime-content"
---
# 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](https://comark.dev/compare/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:

```typescript [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:

::code-group
```vue [Vue]
<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>
```

```tsx [React]
import { MarkdownDocument } from '@comark/react'
import Alert from './components/Alert'

export default async function PricingPage() {
  const document = await fetch('https://cms.example.com/api/pages/pricing').then(res => res.json())
  return <MarkdownDocument value={document} components={{ alert: Alert }} />
}
```

```typescript [HTML]
import { renderHtmlFromDocument } from '@comark/html'

const document = await fetch('https://cms.example.com/api/pages/pricing').then(res => res.json())
const html = await renderHtmlFromDocument(document)
```
::

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:

```mdc
Welcome back, :badge{:label="data.user.name"}!
```

```vue
<MarkdownDocument :value="document" :data="{ user: { name: 'Ada' } }" />
```

To insert a value directly into text, add the [binding plugin](https://comark.dev/plugins/built-in/binding). It adds the `{{ data.user.name || guest }}` syntax. See [data binding](https://comark.dev/syntax/components#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:

```typescript [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`](https://comark.dev/reference/render#options-maxinlineattributes) option.

## FAQ

::accordion
  :::accordion-item{label="Does the client need the parser?"}
  No. `<MarkdownDocument>` only renders. The `<Markdown>` component imports the parser, so use it only when you parse in the browser.
  :::

  :::accordion-item{label="Can authors use components the app doesn't have?"}
  An unregistered tag renders as a plain element with the same name. To limit the tags in stored content, use the `allowedTags` or `blockedTags` options of the [security plugin](https://comark.dev/plugins/built-in/security).
  :::

  :::accordion-item{label="How do I update a live page without a reload?"}
  The renderers can subscribe to a context and apply patches by key. See [live documents](https://comark.dev/rendering/vue#live-documents).
  :::
::

## Next steps

- [Document model](https://comark.dev/getting-started/document-model)
- [Render API reference](https://comark.dev/reference/render)
- [Migrate from MDX](https://comark.dev/kb/migration-from-mdx)
- [Build a docs site or blog](https://comark.dev/use-cases/docs-and-blogs)

---

- [Document model](https://comark.dev/getting-started/document-model)
- [Render API](https://comark.dev/reference/render)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
