📄 a couple updates

This commit is contained in:
RJ
2025-12-02 17:57:31 +02:00
committed by Rares J
parent a1fbd5e374
commit 072320ed73
17 changed files with 335 additions and 294 deletions

View File

@@ -27,38 +27,44 @@ export function OptimizedImage({
if (hasError) {
return (
<div className="my-8 rounded-lg border border-zinc-800 bg-zinc-900/50 p-8 text-center">
<p className="text-zinc-400">Failed to load image</p>
{caption && <p className="mt-2 text-sm text-zinc-500">{caption}</p>}
</div>
<span className="block my-8 rounded-lg border border-zinc-800 bg-zinc-900/50 p-8 text-center">
<span className="block text-zinc-400">Failed to load image</span>
{caption && <span className="block mt-2 text-sm text-zinc-500">{caption}</span>}
</span>
)
}
return (
<figure className={`my-8 ${className}`}>
<div className="relative overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/50">
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
style={{ maxWidth: '100%', height: 'auto' }}
className={`transition-opacity duration-300 ${isLoading ? 'opacity-0' : 'opacity-100'}`}
onLoad={() => setIsLoading(false)}
onError={() => setHasError(true)}
placeholder="blur"
blurDataURL="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300'%3E%3Crect width='400' height='300' fill='%2318181b'/%3E%3C/svg%3E"
/>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-emerald-500 border-t-transparent" />
</div>
)}
</div>
{caption && (
<figcaption className="mt-3 text-center text-sm text-zinc-400">{caption}</figcaption>
const imageElement = (
<span className="block relative overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/50">
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
style={{ maxWidth: '100%', height: 'auto' }}
className={`transition-opacity duration-300 ${isLoading ? 'opacity-0' : 'opacity-100'}`}
onLoad={() => setIsLoading(false)}
onError={() => setHasError(true)}
placeholder="blur"
blurDataURL="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300'%3E%3Crect width='400' height='300' fill='%2318181b'/%3E%3C/svg%3E"
/>
{isLoading && (
<span className="absolute inset-0 flex items-center justify-center">
<span className="block h-8 w-8 animate-spin rounded-full border-2 border-emerald-500 border-t-transparent" />
</span>
)}
</figure>
</span>
)
// Always use <span> to avoid invalid HTML nesting in <p> tags
// This prevents hydration mismatches between server and client
return (
<span className={`block my-8 ${className}`}>
{imageElement}
{caption && (
<span className="block mt-3 text-center text-sm text-zinc-400">{caption}</span>
)}
</span>
)
}

View File

@@ -18,17 +18,50 @@ export default function MarkdownRenderer({ content, className = '' }: MarkdownRe
<div className={`prose prose-invert prose-zinc max-w-none ${className}`}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, [rehypeSanitize, {
tagNames: ['p', 'a', 'img', 'code', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'blockquote', 'table', 'thead', 'tbody', 'tr', 'th', 'td',
'strong', 'em', 'del', 'br', 'hr', 'div', 'span'],
attributes: {
a: ['href', 'rel', 'target'],
img: ['src', 'alt', 'title', 'width', 'height'],
code: ['className'],
'*': ['className', 'id']
}
}]]}
rehypePlugins={[
rehypeRaw,
[
rehypeSanitize,
{
tagNames: [
'p',
'a',
'img',
'code',
'pre',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'ul',
'ol',
'li',
'blockquote',
'table',
'thead',
'tbody',
'tr',
'th',
'td',
'strong',
'em',
'del',
'br',
'hr',
'div',
'span',
],
attributes: {
a: ['href', 'rel', 'target'],
img: ['src', 'alt', 'title', 'width', 'height'],
code: ['className'],
'*': ['className', 'id'],
},
},
],
]}
components={{
img: ({ node, src, alt, title, ...props }) => {
if (!src || typeof src !== 'string') return null
@@ -55,19 +88,19 @@ export default function MarkdownRenderer({ content, className = '' }: MarkdownRe
: [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 width = url.searchParams.get('w') ? parseInt(url.searchParams.get('w')!) : null
const height = url.searchParams.get('h') ? parseInt(url.searchParams.get('h')!) : null
const cleanSrc = absoluteSrc.split('?')[0]
return (
<OptimizedImage
src={cleanSrc}
alt={altText || alt || ''}
caption={caption}
width={width}
height={height}
/>
)
const imageProps = {
src: cleanSrc,
alt: altText || alt || '',
caption: caption,
...(width && { width }),
...(height && { height }),
}
return <OptimizedImage {...imageProps} />
},
code: ({ node, className, children, ...props }) => {
const inline = !className && typeof children === 'string' && !children.includes('\n')

View File

@@ -52,7 +52,13 @@ export function Navbar() {
>
[ABOUT]
</Link>
<ThemeToggle />
<Link
href="/blog"
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer"
>
[BLOG]
</Link>
<ThemeToggle />
</div>
</div>
</div>