Skip to content

Commit bc37755

Browse files
committed
feat(mdx): add mermaid diagram support
Adds rehype-mermaid (pre-mermaid strategy) to the MDX rehype chain before Shiki, and renders the resulting pre.mermaid blocks with a client-side Mermaid component that follows the site's theme. Fixes: #7540
1 parent 27ec25e commit bc37755

6 files changed

Lines changed: 983 additions & 0 deletions

File tree

apps/site/components/MDX/CodeBox/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { getLanguageDisplayName } from '@node-core/rehype-shiki';
22

33
import CodeBox from '#site/components/Common/CodeBox';
4+
import Mermaid from '#site/components/MDX/Mermaid';
45

56
import type { FC, HTMLAttributes } from 'react';
67

78
const MDXCodeBox: FC<HTMLAttributes<HTMLElement>> = ({
89
children: code,
910
className,
1011
}) => {
12+
// Mermaid diagrams arrive as `<pre class="mermaid">` (see rehype-mermaid),
13+
// so we render them as diagrams instead of code boxes
14+
if (className?.split(' ').includes('mermaid')) {
15+
return <Mermaid>{String(code)}</Mermaid>;
16+
}
17+
1118
const matches = className?.match(/language-(?<language>[a-zA-Z]+)/);
1219
const language = matches?.groups?.language ?? '';
1320

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.mermaid {
2+
display: flex;
3+
justify-content: center;
4+
margin: 1rem 0;
5+
}
6+
7+
.mermaid svg {
8+
height: auto;
9+
max-width: 100%;
10+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client';
2+
3+
import { useTheme } from 'next-themes';
4+
import { useEffect, useId, useRef } from 'react';
5+
6+
import type { FC } from 'react';
7+
8+
import styles from './index.module.css';
9+
10+
11+
type MermaidProps = {
12+
children: string;
13+
};
14+
15+
const Mermaid: FC<MermaidProps> = ({ children }) => {
16+
const containerRef = useRef<HTMLDivElement>(null);
17+
const { resolvedTheme } = useTheme();
18+
const reactId = useId().replace(/:/g, '-');
19+
20+
useEffect(() => {
21+
let cancelled = false;
22+
23+
const renderDiagram = async () => {
24+
try {
25+
// Mermaid is heavy, so we only load it on the client when needed
26+
const { default: mermaid } = await import('mermaid');
27+
28+
mermaid.initialize({
29+
startOnLoad: false,
30+
securityLevel: 'strict',
31+
theme: resolvedTheme === 'dark' ? 'dark' : 'default',
32+
});
33+
34+
const { svg, bindFunctions } = await mermaid.render(
35+
`mermaid-${reactId}`,
36+
String(children).trim()
37+
);
38+
39+
if (!cancelled && containerRef.current) {
40+
containerRef.current.innerHTML = svg;
41+
bindFunctions?.(containerRef.current);
42+
}
43+
} catch {
44+
// On failure (chunk load or invalid diagram), keep the source readable
45+
if (!cancelled && containerRef.current) {
46+
const fallback = document.createElement('pre');
47+
fallback.textContent = String(children);
48+
containerRef.current.replaceChildren(fallback);
49+
}
50+
}
51+
};
52+
53+
renderDiagram();
54+
55+
return () => {
56+
cancelled = true;
57+
};
58+
}, [children, resolvedTheme, reactId]);
59+
60+
return <div ref={containerRef} className={styles.mermaid} />;
61+
};
62+
63+
export default Mermaid;

apps/site/mdx/plugins.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { shikiOptions } from '#platform/shiki.mjs';
44
import rehypeShikiji from '@node-core/rehype-shiki/plugin';
55
import remarkHeadings from '@vcarl/remark-headings';
66
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
7+
import rehypeMermaid from 'rehype-mermaid';
78
import rehypeSlug from 'rehype-slug';
89
import remarkGfm from 'remark-gfm';
910
import readingTime from 'remark-reading-time';
@@ -21,6 +22,9 @@ export const rehypePlugins = [
2122
rehypeSlug,
2223
// Automatically add anchor links to headings (H1, ...)
2324
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
25+
// Transforms ```mermaid code blocks into renderable diagrams;
26+
// must run before Shiki so they are not highlighted as plain code
27+
[rehypeMermaid, { strategy: 'pre-mermaid' }],
2428
// Transforms sequential code elements into code tabs and
2529
// adds our syntax highlighter (Shikiji) to Codeboxes
2630
() => singletonShiki,

apps/site/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"github-slugger": "~2.0.0",
4747
"gray-matter": "~4.0.3",
4848
"mdast-util-to-string": "^4.0.0",
49+
"mermaid": "^11.16.1",
4950
"next": "catalog:",
5051
"next-intl": "~4.13.4",
5152
"next-themes": "~0.4.6",
@@ -54,6 +55,7 @@
5455
"react-dom": "^19.2.8",
5556
"reading-time": "~1.5.0",
5657
"rehype-autolink-headings": "~7.1.0",
58+
"rehype-mermaid": "^3.0.0",
5759
"rehype-slug": "~6.0.0",
5860
"remark-gfm": "~4.0.1",
5961
"remark-reading-time": "~2.1.0",

0 commit comments

Comments
 (0)