:::contentbit
How-to guides

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 to one function you provide:

  • paragraphs between blocks
  • Markdown bodies inside blocks
  • Markdown strings stored in parsed block data

React returns a ReactNode. Astro returns an HTML string or promise. If you do not provide this function, the built-in default only escapes text and wraps paragraphs in <p> tags. **bold**, links, and lists will show as literal characters.

For the bigger picture, see Rendering model.

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-markdown

Step 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 or content front end), reuse that component — the contract is just (md: string) => ReactNode:

<ContentBlocks document={doc} renderMarkdown={(md) => <YourExistingMarkdown source={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 same renderMarkdown. Astro block components receive that function as ctx.renderMarkdown.
  • Security split: Content Blocks escapes everything it renders (props, rows, titles). Whatever your renderMarkdown returns is your responsibility.
  • Headings inside prose work normally — but keep document structure (H1/H2) in your page chrome, not in block bodies.

On this page