ML.
KB/dev-life/Adding Mermaid Diagram Support to a Next.js Blog

Adding Mermaid Diagram Support to a Next.js Blog

·4 min read·dev-life

This article is mostly written by Claude Code

Background

Diagrams are essential for explaining architectures and flows in a technical blog. Rather than opening an image editor every time, I wanted to write diagrams as text directly inside Markdown.

graph LR
    A[Markdown 작성] --> B[Mermaid 코드 블록]
    B --> C[빌드 타임 SVG 변환]
    C --> D[블로그에 렌더링]

Tech Stack

The content pipeline for this blog looks like this:

flowchart TD
    MDX[".mdx 파일"] --> CL["Contentlayer2"]
    CL --> RP["remark/rehype 플러그인 체인"]
    RP --> |remarkGfm| GFM["GitHub Flavored Markdown"]
    RP --> |remarkMath| MATH["수식 처리"]
    RP --> |rehypePrismPlus| CODE["코드 하이라이팅"]
    RP --> |rehypeMermaid| MERMAID["Mermaid → SVG"]
    GFM & MATH & CODE & MERMAID --> NEXT["Next.js 정적 페이지"]

Why rehype-mermaid?

There are two main approaches to rendering Mermaid diagrams:

ApproachProsCons
Client-side rendering (mermaid.js)No Playwright required~800 KB increase in JS bundle
Build-time rendering (rehype-mermaid)No client-side JS, SVG inlined directlyRequires Playwright + Chromium

Since this blog already uses a rehype plugin chain, rehype-mermaid is a natural fit.

Installation

yarn add rehype-mermaid playwright
npx playwright install chromium

rehype-mermaid uses mermaid-isomorphic under the hood, which runs mermaid.js inside a headless browser to produce SVG output. This is necessary because mermaid.js itself depends on browser DOM APIs.

sequenceDiagram
    participant B as 빌드 프로세스
    participant P as Playwright (Chromium)
    participant M as mermaid.js

    B->>P: mermaid 코드 블록 전달
    P->>M: 브라우저 내에서 실행
    M-->>P: SVG 반환
    P-->>B: SVG를 MDX에 삽입

The Key: Conditional Loading

The challenge lies in CI environments. Installing Playwright Chromium on every build — even when no post contains a Mermaid diagram — wastes build time unnecessarily.

Solution: Scan the post files at build time and load the plugin only when a ```mermaid block is actually found.

flowchart TD
    START["빌드 시작"] --> SCAN["data/posts/*.mdx 스캔"]
    SCAN --> CHECK{"mermaid 블록 존재?"}
    CHECK -->|Yes| LOAD["rehype-mermaid 동적 import"]
    LOAD --> RENDER["SVG 변환"]
    CHECK -->|No| SKIP["플러그인 스킵"]
    SKIP --> BUILD["Next.js 빌드"]
    RENDER --> BUILD

Core Code in contentlayer.config.ts

import { readFileSync, readdirSync } from 'fs'

function hasMermaidBlocks(): boolean {
  const postsDir = path.join(root, 'data', 'posts')
  try {
    return readdirSync(postsDir).some((file) => {
      if (!file.endsWith('.mdx')) return false
      return readFileSync(path.join(postsDir, file), 'utf-8').includes('```mermaid')
    })
  } catch {
    return false
  }
}

// Lazy-load: contentlayer's esbuild targets es2020,
// which doesn't support top-level await, so we wrap it in a function
function lazyRehypeMermaid() {
  let plugin: any = null
  return () => {
    return async (tree: any, file: any) => {
      if (!plugin) {
        const mod = await import('rehype-mermaid')
        plugin = mod.default({ strategy: 'inline-svg' })
      }
      return plugin(tree, file)
    }
  }
}

const useMermaid = hasMermaidBlocks()

// Conditionally append to the rehypePlugins array
rehypePlugins: [
  // ... existing plugins
  ...(useMermaid ? [lazyRehypeMermaid()] : []),
  [rehypePrismPlus, { defaultLanguage: 'js', ignoreMissing: true }],
]

GitHub Actions CI

- name: Check for mermaid diagrams
  id: check-mermaid
  run: |
    if grep -r '```mermaid' data/posts/ >/dev/null 2>&1; then
      echo "found=true" >> $GITHUB_OUTPUT
    else
      echo "found=false" >> $GITHUB_OUTPUT
    fi
- name: Get Playwright version
  if: steps.check-mermaid.outputs.found == 'true'
  id: playwright-version
  run: echo "version=$(npx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Cache Playwright browsers
  if: steps.check-mermaid.outputs.found == 'true'
  uses: actions/cache@v4
  id: playwright-cache
  with:
    path: ~/.cache/ms-playwright
    key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright Chromium (for mermaid rendering)
  if: steps.check-mermaid.outputs.found == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
  run: npx playwright install --with-deps chromium
- name: Install Playwright deps only (cached browsers)
  if: steps.check-mermaid.outputs.found == 'true' && steps.playwright-cache.outputs.cache-hit == 'true'
  run: npx playwright install-deps chromium

Results

ScenarioCI Behavior
No mermaid blocksPlaywright skipped — no change in build time
mermaid blocks present (cache miss)Full Playwright + Chromium install (+30–40 seconds)
mermaid blocks present (cache hit)Browser binary (112 MB) served from cache, only OS deps installed (~5s)

With this setup, simply writing a ```mermaid code block in any post is enough — SVG diagrams are generated automatically at build time.

● KBdev-life·2026-04-13-adding-mermaid-to-nextjs-blog4 min read