Plug in your Markdown library
One function connects Content Blocks to react-markdown, marked, markdown-it, or remark. Step-by-step for each.
Content Blocks is not a Markdown renderer. It parses the block layer (:::callout,
::tab, pipe rows) and hands every prose segment — paragraphs between blocks, and
Markdown bodies inside blocks — to one function you provide:
- React: the
renderMarkdownprop → returns aReactNode - Static HTML: the
renderMarkdownoption → returns an HTML string
If you don't provide it, the built-in default only escapes text and wraps paragraphs
in <p> tags. **bold**, links, and lists will show as literal characters. Wiring
your Markdown library is step one of any real setup.
What your library sees (and never sees)
your markdown file what reaches YOUR renderer
───────────────────── ──────────────────────────
Some intro **prose**. → "Some intro **prose**."
← blocks never reach it
:::callout{type="tip"}
Inline *formatting* too. → "Inline *formatting* too."
:::Your library is only ever called with plain Markdown strings. It never sees :::
lines, props, or pipe rows — no plugins or escaping rules needed on its side.
React + react-markdown
Step 1 — install:
pnpm add react-markdownStep 2 — create one component (style the elements however your app does):
// components/markdown.tsx
import ReactMarkdown from 'react-markdown'
export function Markdown({ source }: { source: string }) {
return <ReactMarkdown>{source}</ReactMarkdown>
}Step 3 — pass it once:
import { ContentBlocks } from '@contentbit/react'
import { Markdown } from '@/components/markdown'
<ContentBlocks
document={result.document}
renderMarkdown={(md) => <Markdown source={md} />}
/>Done. Every prose segment and every block body now renders through react-markdown. (This site's playground uses exactly this setup.)
React + an existing remark/MDX pipeline
If your app already renders Markdown (a blog, a CMS front end), reuse that component —
the contract is just (md: string) => ReactNode:
<ContentBlocks document={doc} renderMarkdown={(md) => <YourExistingMarkdown source={md} />} />Static HTML + marked
Step 1 — install:
pnpm add marked dompurifyStep 2 — wire it:
import { renderToHtml } from '@contentbit/html'
import DOMPurify from 'dompurify'
import { marked } from 'marked'
const html = renderToHtml(result.document, {
renderMarkdown: (md) => DOMPurify.sanitize(marked.parse(md, { async: false })),
})Why DOMPurify: renderMarkdown output is inserted as-is (it has to be — it's
HTML). marked passes raw HTML in the source through by default, so sanitize unless
every author is trusted.
Static HTML + markdown-it
pnpm add markdown-itimport { renderToHtml } from '@contentbit/html'
import MarkdownIt from 'markdown-it'
const md = new MarkdownIt() // html: false by default — raw HTML stays escaped
const html = renderToHtml(result.document, {
renderMarkdown: (src) => md.render(src),
})markdown-it's default (html: false) already refuses raw HTML, so no sanitizer is
needed for that vector.
Static HTML + unified/remark
pnpm add unified remark-parse remark-rehype rehype-stringifyimport { renderToHtml } from '@contentbit/html'
import rehypeStringify from 'rehype-stringify'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import { unified } from 'unified'
const processor = unified().use(remarkParse).use(remarkRehype).use(rehypeStringify)
const html = renderToHtml(result.document, {
renderMarkdown: (md) => String(processor.processSync(md)),
})Plain Markdown target
renderToMarkdown needs nothing — its output is Markdown:
import { renderToMarkdown } from '@contentbit/core'
import { genericMarkdownRenderers } from '@contentbit/blocks'
const md = renderToMarkdown(result.document, { renderers: genericMarkdownRenderers })Gotchas
- The default is intentionally minimal. Escaped text +
<p>paragraphs, nothing else. If formatting "doesn't work", you haven't wired this page yet. - One function covers everything. Prose between blocks and Markdown bodies inside
blocks (
callout,tab,faq-item) all flow through the samerenderMarkdown. - Security split: Content Blocks escapes everything it renders (props, rows,
titles). Whatever your
renderMarkdownreturns is your responsibility — see the marked note above. - Headings inside prose work normally — but keep document structure (H1/H2) in your page chrome, not in block bodies.