Skip to content

Commit 4fc3558

Browse files
committed
feat(blog): add Mermaid chart support and enhance code block styling
- Introduced MermaidChart component for rendering diagrams in MDX posts. - Updated MdxComponents to use MermaidChart and added a new MdxTbody component. - Integrated rehype-prism-plus for improved syntax highlighting in code blocks. - Modified global styles for better code block appearance and background consistency.
1 parent 21edb0c commit 4fc3558

8 files changed

Lines changed: 1215 additions & 31 deletions

File tree

app/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
--tw-prose-captions: #9ca3af;
2121
--tw-prose-code: #a7f950;
2222
--tw-prose-pre-code: #e5e7eb;
23-
--tw-prose-pre-bg: #111827;
23+
--tw-prose-pre-bg: var(--color-background-card);
2424
--tw-prose-th-borders: #374151;
2525
--tw-prose-td-borders: #1f2937;
2626
}

components/landing-page/blog/BlogPostDetails.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import BlogCard from './BlogCard';
1010
import AuthLoadingState from '@/components/auth/AuthLoadingState';
1111
import { useRouter } from 'next/navigation';
1212
import { useTransition, useCallback } from 'react';
13+
import './prism-theme.css';
1314

1415
interface BlogPostDetailsProps {
1516
post: MdxBlogPost & { content: React.ReactElement };
@@ -96,7 +97,7 @@ const BlogPostDetails: React.FC<BlogPostDetailsProps> = ({
9697
{(isNavigating || isPending) && (
9798
<AuthLoadingState message='Loading article...' />
9899
)}
99-
<div className='bg-background-main-bg relative z-10 mx-auto min-h-screen max-w-[1440px] justify-start space-y-[23px] px-5 py-5 text-white md:space-y-[80px] md:px-[50px] md:py-16 lg:px-[100px]'>
100+
<div className='relative z-10 mx-auto min-h-screen max-w-[1440px] justify-start space-y-[23px] px-5 py-5 text-white md:space-y-[80px] md:px-[50px] md:py-16 lg:px-[100px]'>
100101
<div className='relative flex flex-col lg:flex-row'>
101102
<div className='flex-1'>
102103
<div className='max-w-4xl py-6 sm:py-8'>
Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import React from 'react';
22
import { Badge } from '@/components/ui/badge';
3-
4-
// ---------------------------------------------------------------------------
5-
// Code block with syntax highlighting via <pre><code>
6-
// ---------------------------------------------------------------------------
3+
import MermaidChart from './MermaidChart';
74
function MdxCode({
85
children,
96
className,
@@ -14,7 +11,7 @@ function MdxCode({
1411
if (!isBlock) {
1512
return (
1613
<code
17-
className='rounded bg-[#1a1a1a] px-1.5 py-0.5 font-mono text-sm text-[#a7f950]'
14+
className='bg-muted text-primary rounded px-1.5 py-0.5 font-mono text-sm'
1815
{...props}
1916
>
2017
{children}
@@ -31,7 +28,7 @@ function MdxCode({
3128
function MdxPre({ children, ...props }: React.ComponentProps<'pre'>) {
3229
return (
3330
<pre
34-
className='my-6 overflow-x-auto rounded-lg border border-[#2b2b2b] bg-[#111827] p-4 text-sm leading-relaxed'
31+
className='bg-background-card my-6 overflow-x-auto rounded-lg border border-gray-900 p-4 text-sm leading-relaxed'
3532
{...props}
3633
>
3734
{children}
@@ -44,9 +41,9 @@ function MdxPre({ children, ...props }: React.ComponentProps<'pre'>) {
4441
// ---------------------------------------------------------------------------
4542
function MdxTable({ children, ...props }: React.ComponentProps<'table'>) {
4643
return (
47-
<div className='my-6 w-full overflow-x-auto rounded-lg border border-[#2b2b2b]'>
44+
<div className='my-6 w-full overflow-x-auto rounded-lg border border-gray-900'>
4845
<table
49-
className='w-full border-collapse text-sm text-[#d1d5db]'
46+
className='text-muted-foreground w-full border-collapse text-sm'
5047
{...props}
5148
>
5249
{children}
@@ -57,7 +54,7 @@ function MdxTable({ children, ...props }: React.ComponentProps<'table'>) {
5754

5855
function MdxThead({ children, ...props }: React.ComponentProps<'thead'>) {
5956
return (
60-
<thead className='bg-[#1a1a1a] text-[#ffffff]' {...props}>
57+
<thead className='bg-section text-foreground' {...props}>
6158
{children}
6259
</thead>
6360
);
@@ -66,7 +63,7 @@ function MdxThead({ children, ...props }: React.ComponentProps<'thead'>) {
6663
function MdxTh({ children, ...props }: React.ComponentProps<'th'>) {
6764
return (
6865
<th
69-
className='border-b border-[#2b2b2b] px-4 py-3 text-left font-semibold'
66+
className='border-b border-gray-900 px-4 py-3 text-left font-semibold'
7067
{...props}
7168
>
7269
{children}
@@ -76,37 +73,27 @@ function MdxTh({ children, ...props }: React.ComponentProps<'th'>) {
7673

7774
function MdxTd({ children, ...props }: React.ComponentProps<'td'>) {
7875
return (
79-
<td className='border-b border-[#2b2b2b] px-4 py-3' {...props}>
76+
<td className='border-b border-gray-900 px-4 py-3' {...props}>
8077
{children}
8178
</td>
8279
);
8380
}
8481

8582
function MdxTr({ children, ...props }: React.ComponentProps<'tr'>) {
8683
return (
87-
<tr className='transition-colors hover:bg-[#1a1a1a]/50' {...props}>
84+
<tr className='hover:bg-section/50 transition-colors' {...props}>
8885
{children}
8986
</tr>
9087
);
9188
}
9289

90+
function MdxTbody({ children, ...props }: React.ComponentProps<'tbody'>) {
91+
return <tbody {...props}>{children}</tbody>;
92+
}
93+
9394
// ---------------------------------------------------------------------------
94-
// Mermaid diagram — renders the raw text inside a styled container.
95-
// Full client-side rendering would require a 'use client' wrapper + mermaid.js;
96-
// for now we render a readable fallback with the diagram source.
95+
// Mermaid diagram — client component that renders with mermaid.js
9796
// ---------------------------------------------------------------------------
98-
function Mermaid({ children }: { children?: React.ReactNode }) {
99-
return (
100-
<div className='my-6 rounded-lg border border-[#2b2b2b] bg-[#111827] p-4'>
101-
<p className='mb-2 text-xs font-semibold tracking-wider text-[#6b7280] uppercase'>
102-
Diagram
103-
</p>
104-
<pre className='font-mono text-sm whitespace-pre-wrap text-[#d1d5db]'>
105-
{children}
106-
</pre>
107-
</div>
108-
);
109-
}
11097

11198
// ---------------------------------------------------------------------------
11299
// Exported component map — passed to compileMDX
@@ -117,10 +104,11 @@ export const mdxComponents = {
117104
code: MdxCode,
118105
table: MdxTable,
119106
thead: MdxThead,
107+
tbody: MdxTbody,
120108
th: MdxTh,
121109
td: MdxTd,
122110
tr: MdxTr,
123111
// Named components usable inside .mdx files as <Badge> / <Mermaid>
124112
Badge,
125-
Mermaid,
113+
Mermaid: MermaidChart,
126114
};
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
'use client';
2+
3+
import React, { useEffect, useId, useState } from 'react';
4+
5+
function getTextContent(children: React.ReactNode): string {
6+
if (typeof children === 'string') return children.trim();
7+
if (Array.isArray(children))
8+
return children.map(getTextContent).join('').trim();
9+
if (children && typeof children === 'object' && 'props' in children) {
10+
return getTextContent(
11+
(children as React.ReactElement<{ children?: React.ReactNode }>).props
12+
.children
13+
);
14+
}
15+
return String(children ?? '').trim();
16+
}
17+
18+
interface MermaidChartProps {
19+
children?: React.ReactNode;
20+
}
21+
22+
const MermaidChart: React.FC<MermaidChartProps> = ({ children }) => {
23+
const id = useId().replace(/:/g, '');
24+
const containerId = `mermaid-${id}`;
25+
const [error, setError] = useState<string | null>(null);
26+
const [svg, setSvg] = useState<string | null>(null);
27+
28+
const source = getTextContent(children);
29+
30+
useEffect(() => {
31+
if (!source) return;
32+
33+
let cancelled = false;
34+
35+
const run = async () => {
36+
try {
37+
const mermaid = (await import('mermaid')).default;
38+
mermaid.initialize({
39+
startOnLoad: false,
40+
theme: 'dark',
41+
securityLevel: 'loose',
42+
});
43+
const { svg: rendered } = await mermaid.render(containerId, source);
44+
if (!cancelled) {
45+
setError(null);
46+
setSvg(rendered);
47+
}
48+
} catch (err) {
49+
if (!cancelled) {
50+
setError(
51+
err instanceof Error ? err.message : 'Mermaid render failed'
52+
);
53+
setSvg(null);
54+
}
55+
}
56+
};
57+
58+
run();
59+
return () => {
60+
cancelled = true;
61+
};
62+
}, [source, containerId]);
63+
64+
if (error) {
65+
return (
66+
<div className='border-warning-300/30 bg-warning-50/10 dark:border-warning-400/30 dark:bg-warning-900/10 my-6 rounded-lg border p-4'>
67+
<p className='text-warning-600 dark:text-warning-400 mb-2 text-xs font-semibold tracking-wider uppercase'>
68+
Diagram (render failed)
69+
</p>
70+
<pre className='text-muted-foreground font-mono text-sm whitespace-pre-wrap'>
71+
{source}
72+
</pre>
73+
<p className='text-warning-600 dark:text-warning-400 mt-2 text-xs'>
74+
{error}
75+
</p>
76+
</div>
77+
);
78+
}
79+
80+
if (svg) {
81+
return (
82+
<div
83+
className='bg-background-card my-6 flex justify-center rounded-lg border border-gray-900 p-4'
84+
dangerouslySetInnerHTML={{ __html: svg }}
85+
/>
86+
);
87+
}
88+
89+
return (
90+
<div className='bg-background-card my-6 rounded-lg border border-gray-900 p-4'>
91+
<p className='mb-2 text-xs font-semibold tracking-wider text-gray-600 uppercase'>
92+
Diagram
93+
</p>
94+
<pre className='text-muted-foreground font-mono text-sm whitespace-pre-wrap'>
95+
{source}
96+
</pre>
97+
</div>
98+
);
99+
};
100+
101+
export default MermaidChart;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* IDE-style syntax highlighting for MDX code blocks (rehype-prism-plus / refractor) */
2+
.prose pre[class*='language-'] {
3+
background: var(--color-background-card);
4+
border: 1px solid var(--color-gray-900);
5+
}
6+
7+
.prose pre[class*='language-'] code {
8+
background: transparent;
9+
padding: 0;
10+
font-size: 0.875rem;
11+
line-height: 1.625;
12+
}
13+
14+
/* Token colors — IDE-like dark theme using design system */
15+
.prose .token.comment,
16+
.prose .token.prolog,
17+
.prose .token.doctype,
18+
.prose .token.cdata {
19+
color: var(--color-gray-600);
20+
}
21+
22+
.prose .token.punctuation {
23+
color: var(--color-gray-500);
24+
}
25+
26+
.prose .token.property,
27+
.prose .token.tag,
28+
.prose .token.boolean,
29+
.prose .token.number,
30+
.prose .token.constant,
31+
.prose .token.symbol {
32+
color: #d4a574;
33+
}
34+
35+
.prose .token.selector,
36+
.prose .token.attr-name,
37+
.prose .token.string,
38+
.prose .token.char,
39+
.prose .token.builtin {
40+
color: #7ec699;
41+
}
42+
43+
.prose .token.operator,
44+
.prose .token.entity,
45+
.prose .token.url,
46+
.prose .token.variable {
47+
color: var(--color-gray-300);
48+
}
49+
50+
.prose .token.atrule,
51+
.prose .token.keyword {
52+
color: var(--color-primary);
53+
}
54+
55+
.prose .token.attr-value,
56+
.prose .token.regex,
57+
.prose .token.important {
58+
color: #7ec699;
59+
}
60+
61+
.prose .token.function,
62+
.prose .token.class-name {
63+
color: #ddb6f2;
64+
}
65+
66+
.prose .token.deleted {
67+
color: #e26d6d;
68+
}
69+
70+
.prose .token.inserted {
71+
color: #7ec699;
72+
}
73+
74+
/* Line numbers (if enabled via rehype-prism-plus meta) */
75+
.prose .code-line.line-number::before {
76+
color: var(--color-gray-700);
77+
}
78+
79+
.prose .code-highlight .highlight-line {
80+
background: color-mix(in srgb, var(--color-section) 60%, transparent);
81+
}

lib/mdx.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import fs from 'fs';
22
import path from 'path';
33
import matter from 'gray-matter';
44
import { compileMDX } from 'next-mdx-remote/rsc';
5+
import remarkGfm from 'remark-gfm';
6+
import rehypePrism from 'rehype-prism-plus';
57
import type { ReactElement } from 'react';
68
import { mdxComponents } from '@/components/landing-page/blog/MdxComponents';
79

@@ -82,7 +84,13 @@ export async function getBlogPostBySlug(
8284
const { content } = await compileMDX({
8385
source: mdxSource,
8486
components: mdxComponents,
85-
options: { parseFrontmatter: false },
87+
options: {
88+
parseFrontmatter: false,
89+
mdxOptions: {
90+
remarkPlugins: [remarkGfm],
91+
rehypePlugins: [rehypePrism],
92+
},
93+
},
8694
});
8795

8896
return {

0 commit comments

Comments
 (0)