Files
mypage/components/blog/markdown-renderer.tsx
RJ a2bd055879 🖼️ added images support
- Should investigate how to resize the image from .md specs
2025-11-21 16:15:15 +02:00

202 lines
6.8 KiB
TypeScript

'use client'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeRaw from 'rehype-raw'
import { OptimizedImage } from './OptimizedImage'
import { CodeBlock } from './code-block'
import Link from 'next/link'
interface MarkdownRendererProps {
content: string
className?: string
}
export default function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
return (
<div className={`prose prose-invert prose-zinc max-w-none ${className}`}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
img: ({ node, src, alt, title, ...props }) => {
if (!src || typeof src !== 'string') return null
const isExternal = src.startsWith('http://') || src.startsWith('https://')
if (isExternal) {
return (
<img
src={src}
alt={alt || ''}
title={title}
className="rounded-lg border border-zinc-800"
{...props}
/>
)
}
// Ensure absolute path for Next Image
const absoluteSrc = src.startsWith('/') ? src : `/${src}`
const titleStr = typeof title === 'string' ? title : ''
const [altText, caption] = titleStr?.includes('|')
? titleStr.split('|').map(s => s.trim())
: [alt, undefined]
const url = new URL(absoluteSrc, 'http://localhost')
const width = url.searchParams.get('w') ? parseInt(url.searchParams.get('w')!) : 800
const height = url.searchParams.get('h') ? parseInt(url.searchParams.get('h')!) : 600
const cleanSrc = absoluteSrc.split('?')[0]
return (
<OptimizedImage
src={cleanSrc}
alt={altText || alt || ''}
caption={caption}
width={width}
height={height}
/>
)
},
code: ({ node, className, children, ...props }) => {
const inline = !className && typeof children === 'string' && !children.includes('\n')
const match = /language-(\w+)/.exec(className || '')
const language = match ? match[1] : ''
if (inline) {
return (
<code
className="rounded bg-zinc-900 px-1.5 py-0.5 text-sm text-emerald-400"
{...props}
>
{children}
</code>
)
}
return <CodeBlock code={String(children).replace(/\n$/, '')} language={language} />
},
a: ({ node, href, children, ...props }) => {
if (!href) return <a {...props}>{children}</a>
const isExternal = href.startsWith('http://') || href.startsWith('https://')
const isAnchor = href.startsWith('#')
if (isExternal) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-emerald-400 hover:text-emerald-300 inline-flex items-center gap-1"
{...props}
>
{children}
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
)
}
if (isAnchor) {
return (
<a href={href} className="text-emerald-400 hover:text-emerald-300" {...props}>
{children}
</a>
)
}
return (
<Link href={href} className="text-emerald-400 hover:text-emerald-300" {...props}>
{children}
</Link>
)
},
h1: ({ node, children, ...props }) => {
const text = String(children)
const id = text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
return (
<h1 id={id} className="text-3xl font-bold text-zinc-100 mt-8 mb-4" {...props}>
{children}
</h1>
)
},
h2: ({ node, children, ...props }) => {
const text = String(children)
const id = text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
return (
<h2 id={id} className="text-2xl font-bold text-zinc-100 mt-6 mb-3" {...props}>
{children}
</h2>
)
},
h3: ({ node, children, ...props }) => {
const text = String(children)
const id = text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
return (
<h3 id={id} className="text-xl font-bold text-zinc-100 mt-4 mb-2" {...props}>
{children}
</h3>
)
},
ul: ({ node, children, ...props }) => (
<ul className="list-disc list-inside space-y-2 text-zinc-300" {...props}>
{children}
</ul>
),
ol: ({ node, children, ...props }) => (
<ol className="list-decimal list-inside space-y-2 text-zinc-300" {...props}>
{children}
</ol>
),
blockquote: ({ node, children, ...props }) => (
<blockquote
className="border-l-4 border-emerald-500 pl-4 italic text-zinc-400"
{...props}
>
{children}
</blockquote>
),
table: ({ node, children, ...props }) => (
<div className="overflow-x-auto my-6">
<table className="min-w-full border border-zinc-800" {...props}>
{children}
</table>
</div>
),
th: ({ node, children, ...props }) => (
<th
className="bg-zinc-900 px-4 py-2 text-left font-bold text-zinc-100 border border-zinc-800"
{...props}
>
{children}
</th>
),
td: ({ node, children, ...props }) => (
<td className="px-4 py-2 text-zinc-300 border border-zinc-800" {...props}>
{children}
</td>
),
}}
>
{content}
</ReactMarkdown>
</div>
)
}