Skip to content

feat(mdx): add mermaid diagram support - #9107

Open
ashrees wants to merge 1 commit into
nodejs:mainfrom
ashrees:feat/mermaid-support
Open

feat(mdx): add mermaid diagram support#9107
ashrees wants to merge 1 commit into
nodejs:mainfrom
ashrees:feat/mermaid-support

Conversation

@ashrees

@ashrees ashrees commented Aug 14, 2026

Copy link
Copy Markdown

Adds Mermaid diagram support to the MDX pipeline, as requested in #7540.

How it works

  • apps/site/mdx/plugins.mjs: rehype-mermaid (strategy pre-mermaid) runs in the rehype chain before @node-core/rehype-shiki, so ```mermaid fenced blocks become <pre class="mermaid"> instead of being syntax-highlighted as plain code. This strategy is AST-only and never starts a browser at build time (verified against the dependency source and benchmarked, see below).
  • apps/site/components/MDX/CodeBox (the pre MDX override) routes <pre class="mermaid"> to the Mermaid component; all other code blocks render as before.
  • The Mermaid component lives in @node-core/ui-components (packages/ui-components/src/MDX/Mermaid) and renders diagrams client-side through a dedicated useMermaid hook (packages/ui-components/src/hooks/useMermaid.ts) backed by the @mermanjs/web WASM renderer. The hook lazy-loads the renderer only when a diagram is on the page, and accepts an injectable loader so unit tests can stub the WASM module.
  • The site-level wrapper (apps/site/components/MDX/Mermaid) maps the next-themes resolved theme onto the component (light/dark host theme).
  • Invalid diagram sources fall back to rendering the raw source in a <pre>.

Testing

  • packages/ui-components/src/hooks/__test__/useMermaid.test.jsx: 2/2 pass (node:test + testing-library, stubbed loader).
  • Storybook stories at packages/ui-components/src/MDX/Mermaid/index.stories.tsx (flowchart, sequence, dark, invalid source).
  • Benchmark: compiled 25 real blog posts through the production rehype chain, with vs without the plugin, 3 warmed runs: 5.62 vs 5.19 ms/doc, no measurable build-time impact.
  • eslint/stylelint pass on all changed files.

Fixes #7540

@ashrees
ashrees requested a review from a team as a code owner August 14, 2026 20:19
Copilot AI lite review requested due to automatic review settings August 14, 2026 20:19
@ashrees
ashrees requested a review from a team as a code owner August 14, 2026 20:19
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodejs-org Ready Ready Preview Aug 18, 2026 4:24am

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-mermaid to the MDX rehype plugin chain (before Shiki) and added mermaid/rehype-mermaid dependencies.
  • Introduced a new Mermaid MDX component that lazy-loads Mermaid on the client and re-renders on theme changes.
  • Updated the MDXCodeBox (pre override) 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.

Comment on lines +23 to +47
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);
}
}
};
@ashrees
ashrees force-pushed the feat/mermaid-support branch from 4b6eca2 to bc37755 Compare August 14, 2026 23:45
@ashrees

ashrees commented Aug 14, 2026

Copy link
Copy Markdown
Author

Addressed the Copilot review on the Mermaid component (head bc37755):

  • the dynamic import('mermaid') now sits inside the try block, so a failed chunk load falls back gracefully
  • securityLevel: 'strict' is set on mermaid.initialize
  • bindFunctions from mermaid.render() is invoked, enabling diagram interactions
  • the error fallback now renders the source inside a <pre> to preserve whitespace

Comment thread apps/site/mdx/plugins.mjs
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';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this dramatically slow down builds since it requires initializing a whole browser instance?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No — with strategy: 'pre-mermaid' no browser is ever started at build time. Verified in the dependency source:

  • createMermaidRenderer() only creates a lazy browserPromise (browserPromise ||= getBrowser(...) inside the returned render function in mermaid-isomorphic) — nothing launches at plugin setup.
  • rehype-mermaid never calls that render function for pre-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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do some benchmarks and compare building site previously and after? + adding an example storybook with mermaid content so we can render/test?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please provide a table with the benchmark results?

Comment thread apps/site/mdx/plugins.mjs
[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' }],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're using rehype-mermaid, do we need to add mermaid support on code boxes? Or what exactly this rehype plugin does?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why we need a mermaid UI component?

const { resolvedTheme } = useTheme();
const reactId = useId().replace(/:/g, '-');

useEffect(() => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw some people made browserless versions of Mermaid, e.g.:
https://github.com/1jehuang/mermaid-rs-renderer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can write bindings for it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define bindings? 👀

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like they already exist at https://www.npmjs.com/package/@mermanjs/web

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks nice, @ashrees can you update your PR?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done — rendering now goes through @mermanjs/web (wasm), so the 500kb mermaid.js is out of the client bundle entirely.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, is the UI component piece still needed for @mermanjs/web? Why we need rehype mermaid and this at the same time?

@ashrees
ashrees force-pushed the feat/mermaid-support branch from bc37755 to 99db225 Compare August 18, 2026 04:14
@ashrees

ashrees commented Aug 18, 2026

Copy link
Copy Markdown
Author

Updated the PR per the review (head 99db225):

@ovflowd's asks:

  1. Dedicated, unit-testable hook — the effect now lives in useMermaid (packages/ui-components/src/hooks/useMermaid.ts) with an injectable loader for test stubbing. Unit tests in __test__/useMermaid.test.jsx (node:test + testing-library): 2/2 pass.
  2. Storybook storypackages/ui-components/src/MDX/Mermaid/index.stories.tsx with flowchart, sequence, dark-theme, and invalid-source variants.
  3. Benchmarks — compiled 25 real blog posts through the production rehype chain (slug → autolink → shiki), with vs. without the plugin, 3 warmed runs: 5.62ms/doc vs 5.19ms/doc — no measurable build-time impact (delta within run variance), because pre-mermaid is AST-only and never starts a browser.

Renderer switch (@avivkeller): rendering now uses @mermanjs/web (Merman WASM) instead of mermaid.js — so even the client-side path drops the ~500KB JS dependency. Theme-aware (light/dark via next-themes in the site wrapper), invalid sources fall back to a <pre> with the raw source.

What the rehype plugin does (clarifying question): it only rewrites ```mermaid fenced blocks into <pre class="mermaid"> at compile time; the MDXCodeBox override then routes those to the Mermaid component. Code boxes are otherwise untouched — no separate codebox support needed.

The component lives in @node-core/ui-components (alongside the other MDX components) with a thin next-themes wrapper in apps/site.

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

@avivkeller avivkeller left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we make a remark plugin to do the transform?

@avivkeller

Copy link
Copy Markdown
Member

@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

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.14815% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 86.21%. Comparing base (707dd00) to head (4459c03).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/ui-components/src/hooks/useMermaid.ts 98.14% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Build Size Comparison

Summary

Metric Value
Old Total First Load JS 7.06 MB
New Total First Load JS 7.06 MB
Delta 4.36 KB (+0.06%)

Changes

🔄 Modified Routes (4)
Route Old First Load JS New First Load JS Delta
/[locale] 1.66 MB 1.66 MB 📈 1.09 KB (+0.06%)
/[locale]/[...path] 1.66 MB 1.66 MB 📈 1.09 KB (+0.06%)
/[locale]/blog/[...path] 1.66 MB 1.66 MB 📈 1.09 KB (+0.06%)
/[locale]/download/archive/[version] 1.66 MB 1.66 MB 📈 1.09 KB (+0.06%)

@ashrees

ashrees commented Aug 18, 2026

Copy link
Copy Markdown
Author

i've updated the pr body so it matches the current implementation exactly. the mismatch was on me. the pr went through several fast iterations (copilot review, the mermaid.js to wasm switch, then the hook extraction) and i didn't keep the description and comments in sync with the branch.

on the policy point: yes, i used ai assistance while drafting, and i've read the ai policy. i take full responsibility for every line. i reviewed and understand the code, verified it locally (unit tests 2/2, lint clean, benchmarks in the thread above), and i'm glad to walk through any part of it in review.

@ovflowd

ovflowd commented Aug 18, 2026

Copy link
Copy Markdown
Member

@ashrees several CI steps are failing, please give them an eye and fix it.

// Mermaid diagrams arrive as `<pre class="mermaid">` (see rehype-mermaid),
// so we render them as diagrams instead of code boxes
if (className?.split(' ').includes('mermaid')) {
return <Mermaid>{String(code)}</Mermaid>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think our codebox component should have anything to do with Mermaid. If Mermaid renders a pre then it shouldn't. It is ambiguous that we have two transformations and I wonder if this is the right path. As asked on other comments could you describe why

  • We need a rehype plugin that transforms the mermaid markdown code snippets into a pre and then another UI component that then grabs that pre element and renders as mermaid? Wouldn't it be more efficient if only one pass is done? Is that possible? Or at the very least it'd be good to remove the dependency of the Mermaid component within the MDX CodeBox component. Point being, that the CodeBox component should have no logic that goes to Mermaid path. And I believe that can be easily fixed on our remark package within this monorepo, so that you early on catch that this is a Mermaid and easily point to the right component as we do for code boxes 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mermaid diagram support

4 participants