feat(mdx): add mermaid diagram support - #9107
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR adds Mermaid diagram rendering support to the site’s MDX pipeline by transforming ```mermaid fenced blocks into a renderable form during MDX compilation and then rendering them client-side at runtime.
Changes:
- Added
rehype-mermaidto the MDX rehype plugin chain (before Shiki) and addedmermaid/rehype-mermaiddependencies. - Introduced a new
MermaidMDX component that lazy-loads Mermaid on the client and re-renders on theme changes. - Updated the
MDXCodeBox(preoverride) to route Mermaid blocks to the new Mermaid renderer instead of the standard code box.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks new dependencies pulled in by mermaid and rehype-mermaid. |
| apps/site/package.json | Adds mermaid and rehype-mermaid runtime dependencies. |
| apps/site/mdx/plugins.mjs | Inserts rehype-mermaid before Shiki to avoid Mermaid blocks being highlighted as plain code. |
| apps/site/components/MDX/Mermaid/index.tsx | New client component that loads Mermaid lazily and renders diagrams (with theme support). |
| apps/site/components/MDX/Mermaid/index.module.css | Basic layout constraints for Mermaid-rendered SVG output. |
| apps/site/components/MDX/CodeBox/index.tsx | Detects Mermaid <pre class="mermaid"> blocks and renders the Mermaid component instead of CodeBox. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const renderDiagram = async () => { | ||
| // Mermaid is heavy, so we only load it on the client when needed | ||
| const { default: mermaid } = await import('mermaid'); | ||
|
|
||
| mermaid.initialize({ | ||
| startOnLoad: false, | ||
| theme: resolvedTheme === 'dark' ? 'dark' : 'default', | ||
| }); | ||
|
|
||
| try { | ||
| const { svg } = await mermaid.render( | ||
| `mermaid-${reactId}`, | ||
| String(children).trim() | ||
| ); | ||
|
|
||
| if (!cancelled && containerRef.current) { | ||
| containerRef.current.innerHTML = svg; | ||
| } | ||
| } catch { | ||
| // If the diagram source is invalid, fall back to showing the source | ||
| if (!cancelled && containerRef.current) { | ||
| containerRef.current.textContent = String(children); | ||
| } | ||
| } | ||
| }; |
4b6eca2 to
bc37755
Compare
|
Addressed the Copilot review on the Mermaid component (head
|
| import rehypeShikiji from '@node-core/rehype-shiki/plugin'; | ||
| import remarkHeadings from '@vcarl/remark-headings'; | ||
| import rehypeAutolinkHeadings from 'rehype-autolink-headings'; | ||
| import rehypeMermaid from 'rehype-mermaid'; |
There was a problem hiding this comment.
Doesn't this dramatically slow down builds since it requires initializing a whole browser instance?
There was a problem hiding this comment.
No — with strategy: 'pre-mermaid' no browser is ever started at build time. Verified in the dependency source:
createMermaidRenderer()only creates a lazybrowserPromise(browserPromise ||= getBrowser(...)inside the returned render function inmermaid-isomorphic) — nothing launches at plugin setup.rehype-mermaidnever calls that render function forpre-mermaid; it only rewrites the AST (<pre><code class="language-mermaid">→<pre class="mermaid">). The package comments this exact path as "No need to start a browser in this case."- Playwright (peer dep) is only exercised by the
inline-svg/img-*strategies.
So build cost is a single AST walk per document, and rendering happens client-side — with the mermaid library itself lazy-loaded via dynamic import(), so it stays out of the initial bundle too.
Happy to switch to inline-svg if the team prefers zero client-side JS — that's the trade-off (build-time Chromium vs. client rendering).
There was a problem hiding this comment.
Can you do some benchmarks and compare building site previously and after? + adding an example storybook with mermaid content so we can render/test?
There was a problem hiding this comment.
both done. stories are in packages/ui-components/src/MDX/Mermaid/index.stories.tsx (flowchart, sequence, dark, invalid-source). for the benchmark i compiled 25 real blog posts through the production chain, with vs without the plugin, 3 warmed runs — 5.62 vs 5.19 ms/doc, so no measurable build cost (pre-mermaid is ast-only, no browser involved).
| [rehypeAutolinkHeadings, { behavior: 'wrap' }], | ||
| // Transforms ```mermaid code blocks into renderable diagrams; | ||
| // must run before Shiki so they are not highlighted as plain code | ||
| [rehypeMermaid, { strategy: 'pre-mermaid' }], |
There was a problem hiding this comment.
If we're using rehype-mermaid, do we need to add mermaid support on code boxes? Or what exactly this rehype plugin does?
There was a problem hiding this comment.
it's pretty minimal — the plugin just rewrites ```mermaid fenced blocks into
at compile time. MDXCodeBox routes those to the mermaid component instead of a code box, and regular code blocks are untouched. so no, no extra codebox support needed.
| const { resolvedTheme } = useTheme(); | ||
| const reactId = useId().replace(/:/g, '-'); | ||
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
So we're doing mermaid rendering solely on the client-side? Is this what we'd like to have, @avivkeller?
BTW, @ashrees could you extract this effect into a dedicated hook, so we can unit test the Mermaid specific API within an unit testable hook?
There was a problem hiding this comment.
I saw some people made browserless versions of Mermaid, e.g.:
https://github.com/1jehuang/mermaid-rs-renderer
There was a problem hiding this comment.
I can write bindings for it?
There was a problem hiding this comment.
Looks like they already exist at https://www.npmjs.com/package/@mermanjs/web
There was a problem hiding this comment.
yes, client-side only. the effect is extracted into useMermaid now (packages/ui-components/src/hooks/useMermaid.ts) with an injectable loader so tests can stub the wasm renderer — unit tests live in test/useMermaid.test.jsx, 2/2 passing locally. if you'd rather have build-time rendering (inline-svg) instead, happy to explore that.
There was a problem hiding this comment.
done — rendering now goes through @mermanjs/web (wasm), so the 500kb mermaid.js is out of the client bundle entirely.
bc37755 to
99db225
Compare
|
Updated the PR per the review (head @ovflowd's asks:
Renderer switch (@avivkeller): rendering now uses What the rehype plugin does (clarifying question): it only rewrites The component lives in |
Adds rehype-mermaid (pre-mermaid strategy) to the MDX rehype chain before Shiki. Diagrams render client-side through a dedicated useMermaid hook (@node-core/ui-components) backed by the @mermanjs/web WASM renderer — no browser at build time and no heavy mermaid.js bundle. - useMermaid hook with injectable loader + node:test unit tests (2/2 pass) - Mermaid component with light/dark themes and <pre> source fallback - Storybook stories (flowchart, sequence, dark, invalid source) - Benchmarked against 25 real blog docs: no measurable compile-time impact (within run variance) Fixes: nodejs#7540
99db225 to
4459c03
Compare
avivkeller
left a comment
There was a problem hiding this comment.
Can't we make a remark plugin to do the transform?
|
@ashrees Please familiarize yourself with Node.js's AI Policy, and review the code you write. Your comments do not match the actual code, which makes it seem as though you don't understand what you've written. If that's the case, it's okay, but be transparent. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9107 +/- ##
==========================================
+ Coverage 86.05% 86.21% +0.15%
==========================================
Files 86 87 +1
Lines 6046 6100 +54
Branches 358 361 +3
==========================================
+ Hits 5203 5259 +56
+ Misses 839 837 -2
Partials 4 4 ☔ View full report in Codecov by Harness. |
📦 Build Size ComparisonSummary
Changes🔄 Modified Routes (4)
|
Adds Mermaid diagram support to the MDX pipeline, as requested in #7540 (blessed there by @AugustinMauroy, with
rehype-mermaidsuggested by @flakey5).How it works
rehype-mermaid(strategypre-mermaid) is added to the rehype chain inapps/site/mdx/plugins.mjs, positioned before@node-core/rehype-shikiso```mermaidfenced blocks become<pre class="mermaid">instead of being syntax-highlighted as plain code.MDXCodeBox(thepreMDX override) detects themermaidclass and renders a newMermaidclient component instead of a code box.Mermaidcomponent lazy-loads themermaidlibrary on the client (keeping it out of the initial bundle) and re-renders on theme change vianext-themes(default/darkthemes).Testing
```mermaidblocks emit<pre class="mermaid">with the diagram source intact, while regular code blocks still go through Shiki highlighting.eslintandstylelintpass on all changed files.Happy to switch to a build-time strategy (
inline-svg) if preferred — I chose client-side rendering to avoid adding a heavy build-time dependency (Playwright/Chromium).Fixes #7540