📄 a couple updates
All checks were successful
Build and Deploy Next.js Blog to Production / 🔍 Code Quality Checks (push) Successful in 17s
Build and Deploy Next.js Blog to Production / 🏗️ Build and Push Docker Image (push) Successful in 30s
Build and Deploy Next.js Blog to Production / 🚀 Deploy to Production (push) Successful in 48s
All checks were successful
Build and Deploy Next.js Blog to Production / 🔍 Code Quality Checks (push) Successful in 17s
Build and Deploy Next.js Blog to Production / 🏗️ Build and Push Docker Image (push) Successful in 30s
Build and Deploy Next.js Blog to Production / 🚀 Deploy to Production (push) Successful in 48s
This commit was merged in pull request #9.
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user