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-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 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 samerenderMarkdown. Astro block components receive that function asctx.renderMarkdown. - Security split: Content Blocks escapes everything it renders (props, rows,
titles). Whatever your
renderMarkdownreturns is your responsibility. - Headings inside prose work normally — but keep document structure (H1/H2) in your page chrome, not in block bodies.