fix(blog): restore TOC, list styling, and dynamic OG images
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 2m51s
Build & Deploy / 🏗️ Build (push) Successful in 5m32s
Build & Deploy / 🚀 Deploy (push) Successful in 16s
Build & Deploy / 🔔 Notify (push) Successful in 1s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 5m12s

This commit reapplies fixes directly to main after reverting an accidental feature branch merge.

chore(release): bump version to 2.2.6
This commit is contained in:
2026-03-01 13:18:24 +01:00
parent 4adf547265
commit 1bb0efc85b
6 changed files with 161 additions and 29 deletions

View File

@@ -8,6 +8,20 @@ export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png';
export const runtime = 'nodejs';
async function fetchImageAsBase64(url: string) {
try {
const res = await fetch(url);
if (!res.ok) return undefined;
const arrayBuffer = await res.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const contentType = res.headers.get('content-type') || 'image/jpeg';
return `data:${contentType};base64,${buffer.toString('base64')}`;
} catch (error) {
console.error('Failed to fetch OG image:', url, error);
return undefined;
}
}
export default async function Image({
params,
}: {
@@ -32,12 +46,19 @@ export default async function Image({
: `${SITE_URL}${post.frontmatter.featuredImage}`
: undefined;
// Fetch image explicitly and convert to base64 because Satori sometimes struggles
// fetching remote URLs directly inside ImageResponse correctly in various environments.
let base64Image: string | undefined = undefined;
if (featuredImage) {
base64Image = await fetchImageAsBase64(featuredImage);
}
return new ImageResponse(
<OGImageTemplate
title={post.frontmatter.title}
description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'}
image={featuredImage}
image={base64Image || featuredImage}
/>,
{
...OG_IMAGE_SIZE,

View File

@@ -1,12 +1,18 @@
import { notFound, redirect } from 'next/navigation';
import JsonLd from '@/components/JsonLd';
import { SITE_URL } from '@/lib/schema';
import { getPostBySlug, getAdjacentPosts, getReadingTime } from '@/lib/blog';
import {
getPostBySlug,
getAdjacentPosts,
getReadingTime,
extractLexicalHeadings,
} from '@/lib/blog';
import { Metadata } from 'next';
import Link from 'next/link';
import Image from 'next/image';
import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents';
import { Heading } from '@/components/ui';
import { setRequestLocale } from 'next-intl/server';
import BlogEngagementTracker from '@/components/analytics/BlogEngagementTracker';
@@ -67,6 +73,10 @@ export default async function BlogPost({ params }: BlogPostProps) {
const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(post.slug, locale);
// Convert Lexical content into a plain string to estimate reading time roughly
// Extract headings for TOC
const headings = extractLexicalHeadings(post.content?.root || post.content);
// Convert Lexical content into a plain string to estimate reading time roughly
const rawTextContent = JSON.stringify(post.content);
@@ -88,7 +98,6 @@ export default async function BlogPost({ params }: BlogPostProps) {
alt={post.frontmatter.title}
fill
priority
quality={90}
className="object-cover"
sizes="100vw"
style={{
@@ -232,10 +241,10 @@ export default async function BlogPost({ params }: BlogPostProps) {
</div>
</div>
{/* Right Column: Sticky Sidebar - Temporarily Hidden without ToC */}
{/* Right Column: Sticky Sidebar - TOC */}
<aside className="sticky-narrative-sidebar hidden lg:block">
<div className="space-y-12">
{/* Future Payload Table of Contents Implementation */}
<div className="space-y-12 lg:sticky lg:top-32">
<TableOfContents headings={headings} locale={locale} />
</div>
</aside>
</div>