Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,29 @@ src/content/posts/YYYY-MM-DD-slug/
└── image.png # Co-located assets (optional)
```


## Mermaid Diagram (MDX Component)

다이어그램을 이미지 파일 대신 코드로 관리하려면 MDX에서 `Mermaid` 컴포넌트를 import해서 사용하세요.

```mdx
---
title: "diagram post"
---

import Mermaid from "../../../components/Mermaid.astro";

<Mermaid
chart={`
flowchart TD
A[Write Mermaid code in MDX] --> B[Build Astro]
B --> C[Diagram renders in post]
`}
/>
```

> `mermaid`는 npm dependency로 관리되며, Astro 번들 단계에서 처리됩니다.

## License

MIT
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"astro-og-canvas": "^0.10.1",
"canvaskit-wasm": "^0.40.0",
"tailwindcss": "^4.1.18",
"unist-util-visit": "^5.0.0"
"unist-util-visit": "^5.0.0",
"mermaid": "^11.12.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
Expand Down
79 changes: 79 additions & 0 deletions src/components/Mermaid.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
interface Props {
chart: string;
class?: string;
}

const { chart, class: className = "" } = Astro.props;
---

<div class={`mermaid-diagram ${className}`.trim()} data-mermaid-root>
<pre class="mermaid" data-mermaid-source={chart}>{chart}</pre>
</div>

<script type="module">
import mermaid from "mermaid";

const getTheme = () => (document.documentElement.classList.contains("dark") ? "dark" : "default");
let initializedTheme = "";

const ensureInitialized = () => {
const theme = getTheme();
if (initializedTheme === theme) return;
mermaid.initialize({
startOnLoad: false,
securityLevel: "strict",
theme,
});
initializedTheme = theme;
};

const resetRenderedCharts = () => {
const rendered = document.querySelectorAll("[data-mermaid-root][data-mermaid-rendered] .mermaid");
rendered.forEach((node) => {
if (!(node instanceof HTMLElement)) return;
const source = node.dataset.mermaidSource;
if (!source) return;
node.removeAttribute("data-processed");
node.innerHTML = source;

Choose a reason for hiding this comment

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

P1 Badge Use textContent when resetting Mermaid source

Resetting the chart with node.innerHTML = source re-parses Mermaid text as HTML during theme changes, which corrupts valid diagram syntax containing <... sequences (for example B-->|x<y|C gets truncated by the HTML parser) and can cause re-rendered diagrams to break or render incorrectly after toggling dark/light mode. This restore path should write plain text (textContent) so the original Mermaid source is preserved exactly.

Useful? React with 👍 / 👎.

});
document
.querySelectorAll("[data-mermaid-root][data-mermaid-rendered]")
.forEach((container) => container.removeAttribute("data-mermaid-rendered"));
};

const renderMermaid = async () => {
ensureInitialized();
const containers = document.querySelectorAll("[data-mermaid-root]:not([data-mermaid-rendered])");
if (!containers.length) return;

const nodes = [];
containers.forEach((container) => {
const target = container.querySelector(".mermaid");
if (target instanceof Element) nodes.push(target);
});

if (!nodes.length) return;
await mermaid.run({ nodes });
containers.forEach((container) => container.setAttribute("data-mermaid-rendered", "true"));
};

renderMermaid();

let themeObserverQueued = false;
const observer = new MutationObserver(() => {
if (themeObserverQueued) return;
themeObserverQueued = true;
requestAnimationFrame(() => {
themeObserverQueued = false;
if (initializedTheme === getTheme()) return;
resetRenderedCharts();
renderMermaid();
});
});

observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
</script>