From 678c803408e8a46c983774209051cd9f21ae881d Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sat, 21 Feb 2026 18:53:10 +0100 Subject: [PATCH] feat(blog): show random fallback articles in post footer navigation instead of blank spaces --- app/[locale]/blog/[slug]/page.tsx | 22 ++++++++++++------ app/[locale]/blog/page.tsx | 36 ++++++++++++++---------------- components/blog/PostNavigation.tsx | 32 +++++++++++++++----------- lib/blog.ts | 29 ++++++++++++++++++++---- lib/imgproxy-loader.ts | 21 +++++++++++++++-- 5 files changed, 95 insertions(+), 45 deletions(-) diff --git a/app/[locale]/blog/[slug]/page.tsx b/app/[locale]/blog/[slug]/page.tsx index 0bd41e50..81e18cca 100644 --- a/app/[locale]/blog/[slug]/page.tsx +++ b/app/[locale]/blog/[slug]/page.tsx @@ -54,7 +54,7 @@ export default async function BlogPost({ params }: BlogPostProps) { const { locale, slug } = await params; setRequestLocale(locale); const post = await getPostBySlug(slug, locale); - const { prev, next } = await getAdjacentPosts(slug, locale); + const { prev, next, isPrevRandom, isNextRandom } = await getAdjacentPosts(slug, locale); if (!post) { notFound(); @@ -70,11 +70,7 @@ export default async function BlogPost({ params }: BlogPostProps) { category={post.frontmatter.category} readingTime={getReadingTime(post.content)} /> - {(new Date(post.frontmatter.date) > new Date() || post.frontmatter.public === false) && ( -
- Preview (Not visible in production) -
- )} + {/* Featured Image Header */} {post.frontmatter.featuredImage ? (
@@ -114,6 +110,12 @@ export default async function BlogPost({ params }: BlogPostProps) { {getReadingTime(post.content)} min read + {(new Date(post.frontmatter.date) > new Date() || post.frontmatter.public === false) && ( + <> + + Draft Preview + + )}
@@ -142,6 +144,12 @@ export default async function BlogPost({ params }: BlogPostProps) { {getReadingTime(post.content)} min read + {(new Date(post.frontmatter.date) > new Date() || post.frontmatter.public === false) && ( + <> + + Draft Preview + + )} @@ -173,7 +181,7 @@ export default async function BlogPost({ params }: BlogPostProps) { {/* Post Navigation */}
- +
{/* Back to blog link */} diff --git a/app/[locale]/blog/page.tsx b/app/[locale]/blog/page.tsx index bb9d3804..01e00e69 100644 --- a/app/[locale]/blog/page.tsx +++ b/app/[locale]/blog/page.tsx @@ -62,7 +62,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) { {featuredPost && featuredPost.frontmatter.featuredImage && ( <> {featuredPost.frontmatter.title} new Date() || featuredPost.frontmatter.public === false) && ( - - Preview + + Draft Preview )} @@ -160,7 +160,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) { {post.frontmatter.featuredImage && (
{post.frontmatter.title} )} - {(new Date(post.frontmatter.date) > new Date() || - post.frontmatter.public === false) && ( - - Preview - - )} +
)}
-
- {new Date(post.frontmatter.date).toLocaleDateString(locale, { - year: 'numeric', - month: 'long', - day: 'numeric', - })} +
+ + {new Date(post.frontmatter.date).toLocaleDateString(locale, { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + + {(new Date(post.frontmatter.date) > new Date() || + post.frontmatter.public === false) && ( + Draft + )}

{post.frontmatter.title} diff --git a/components/blog/PostNavigation.tsx b/components/blog/PostNavigation.tsx index 9fadcaee..f7d7eed5 100644 --- a/components/blog/PostNavigation.tsx +++ b/components/blog/PostNavigation.tsx @@ -5,43 +5,47 @@ import { PostMdx } from '@/lib/blog'; interface PostNavigationProps { prev: PostMdx | null; next: PostMdx | null; + isPrevRandom?: boolean; + isNextRandom?: boolean; locale: string; } -export default function PostNavigation({ prev, next, locale }: PostNavigationProps) { +export default function PostNavigation({ prev, next, isPrevRandom, isNextRandom, locale }: PostNavigationProps) { if (!prev && !next) return null; return (
{/* Previous Post (Older) */} {prev ? ( - {/* Background Image */} {prev.frontmatter.featuredImage ? ( -
) : (
)} - + {/* Overlay */}
- + {/* Content */}
- {locale === 'de' ? 'Vorheriger Beitrag' : 'Previous Post'} + {isPrevRandom + ? (locale === 'de' ? 'Weiterer Artikel' : 'More Article') + : (locale === 'de' ? 'Vorheriger Beitrag' : 'Previous Post')}

{prev.frontmatter.title}

- + {/* Arrow Icon */}
@@ -55,33 +59,35 @@ export default function PostNavigation({ prev, next, locale }: PostNavigationPro {/* Next Post (Newer) */} {next ? ( - {/* Background Image */} {next.frontmatter.featuredImage ? ( -
) : (
)} - + {/* Overlay */}
- + {/* Content */}
- {locale === 'de' ? 'Nächster Beitrag' : 'Next Post'} + {isNextRandom + ? (locale === 'de' ? 'Weiterer Artikel' : 'More Article') + : (locale === 'de' ? 'Nächster Beitrag' : 'Next Post')}

{next.frontmatter.title}

- + {/* Arrow Icon */}
diff --git a/lib/blog.ts b/lib/blog.ts index 8ec1e8c6..8a15bbac 100644 --- a/lib/blog.ts +++ b/lib/blog.ts @@ -109,7 +109,7 @@ export async function getAllPostsMetadata(locale: string): Promise { +): Promise<{ prev: PostMdx | null; next: PostMdx | null; isPrevRandom?: boolean; isNextRandom?: boolean }> { const posts = await getAllPosts(locale); const currentIndex = posts.findIndex((post) => post.slug === slug); @@ -120,10 +120,31 @@ export async function getAdjacentPosts( // Posts are sorted by date descending (newest first) // So "next" post (newer) is at index - 1 // And "previous" post (older) is at index + 1 - const next = currentIndex > 0 ? posts[currentIndex - 1] : null; - const prev = currentIndex < posts.length - 1 ? posts[currentIndex + 1] : null; + let next = currentIndex > 0 ? posts[currentIndex - 1] : null; + let prev = currentIndex < posts.length - 1 ? posts[currentIndex + 1] : null; - return { prev, next }; + let isNextRandom = false; + let isPrevRandom = false; + + const getRandomPost = (excludeSlugs: string[]) => { + const available = posts.filter(p => !excludeSlugs.includes(p.slug)); + if (available.length === 0) return null; + return available[Math.floor(Math.random() * available.length)]; + }; + + // If there's no next post (we are at the newest post), show a random post instead + if (!next && posts.length > 2) { + next = getRandomPost([slug, prev?.slug].filter(Boolean) as string[]); + isNextRandom = true; + } + + // If there's no previous post (we are at the oldest post), show a random post instead + if (!prev && posts.length > 2) { + prev = getRandomPost([slug, next?.slug].filter(Boolean) as string[]); + isPrevRandom = true; + } + + return { prev, next, isPrevRandom, isNextRandom }; } export function getReadingTime(content: string): number { diff --git a/lib/imgproxy-loader.ts b/lib/imgproxy-loader.ts index 0b23fd81..041f58e4 100644 --- a/lib/imgproxy-loader.ts +++ b/lib/imgproxy-loader.ts @@ -23,11 +23,28 @@ export default function imgproxyLoader({ return src; } + // Check if src contains custom gravity query parameter + let gravity = 'sm'; // Use smart gravity (content-aware) by default + let cleanSrc = src; + + try { + // Dummy base needed for relative URLs + const url = new URL(src, 'http://localhost'); + const customGravity = url.searchParams.get('gravity'); + if (customGravity) { + gravity = customGravity; + url.searchParams.delete('gravity'); + cleanSrc = src.startsWith('http') ? url.href : url.pathname + url.search; + } + } catch (e) { + // Fallback if parsing fails + } + // We use the width provided by Next.js for responsive images // Height is set to 0 to maintain aspect ratio - return getImgproxyUrl(src, { + return getImgproxyUrl(cleanSrc, { width, resizing_type: 'fit', - gravity: 'sm', // Use smart gravity (content-aware) instead of face detection (requires ML/Pro) + gravity, }); }