Sanity#176
Conversation
… recommended posts, Changing styles to one unified format, Bug fixes
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis PR enhances the blog section UI/UX by introducing interactive author links, linkified URLs in post text, featured post sections in category pages, improved post filtering with resolved author parameters, and post ordering changes with caching adjustments. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@frontend/app/`[locale]/blog/category/[category]/page.tsx:
- Around line 80-146: The current logic always treats posts[0] as featuredPost
then only renders it if featuredPost.mainImage exists, which drops the first
post from the grid when it has no image; update the selection so featuredPost is
the first post in posts with a mainImage (e.g., find index via posts.findIndex(p
=> p.mainImage)), set restPosts to posts with that featured item removed (if
found) or to the full posts array when no featured post exists, and keep the
featuredDate logic tied to the chosen featuredPost; ensure BlogCategoryGrid
receives restPosts (or posts when no featured found) and adjust any rendering
guards that assume featuredPost is posts[0].
🧹 Nitpick comments (5)
frontend/client.ts (1)
6-6: Consider the performance implications of disabling CDN globally.Setting
useCdn: falseat the client level affects all Sanity queries across the application. While this ensures fresh data, it increases latency and API usage for every request.Given that the blog page already uses
.withConfig({ useCdn: false })to override this per-query, consider keepinguseCdn: trueas the default and only disabling it for specific queries that require real-time data. This would improve performance for pages that can tolerate slight staleness.frontend/app/[locale]/blog/page.tsx (1)
7-7: Redundant caching opt-outs.Both
export const revalidate = 0andnoStore()are used to disable caching. While this works, it's redundant—either one alone is sufficient. Consider using onlynoStore()since it's the more explicit approach for dynamic data fetching.Also applies to: 28-28
frontend/components/blog/BlogCategoryGrid.tsx (1)
9-15: Consider memoizing the empty callback to avoid unnecessary re-renders.The inline
() => {}creates a new function reference on each render. IfBlogGridor its children are memoized, this could cause unnecessary re-renders.Suggested optimization
'use client'; +import { useCallback } from 'react'; import BlogGrid from '@/components/blog/BlogGrid'; import type { Post } from '@/components/blog/BlogFilters'; +const noop = () => {}; + export function BlogCategoryGrid({ posts }: { posts: Post[] }) { if (!posts.length) return null; return ( <BlogGrid posts={posts} - onAuthorSelect={() => {}} + onAuthorSelect={noop} disableHoverColor /> ); }frontend/components/blog/BlogFilters.tsx (1)
132-140:clearAllleaves thesearchparam behind.If the intent is to reset all filters, consider removing
searchas well—or rename the action to reflect partial clearing.🔧 Optional tweak
const params = new URLSearchParams(searchParams?.toString() || ''); params.delete('author'); params.delete('category'); +params.delete('search'); const nextPath = params.toString() ? `${pathname}?${params}` : pathname; router.replace(nextPath);frontend/app/[locale]/blog/[slug]/PostDetails.tsx (1)
48-67: Avoid statefulRegExp.testwith a global regex.
test()on a global regex carrieslastIndexstate, which can behave inconsistently across parts. Safer to use a non-global check.🔧 Safer URL check
function linkifyText(text: string) { const urlRegex = /(https?:\/\/[^\s]+)/g; const parts = text.split(urlRegex); return parts.map((part, index) => { if (!part) return null; - if (urlRegex.test(part)) { + const isUrl = /^https?:\/\/\S+$/.test(part); + if (isUrl) { return ( <a key={`link-${index}`}
| const featuredPost = posts[0]; | ||
| const restPosts = posts.slice(1); | ||
| const featuredDate = featuredPost?.publishedAt | ||
| ? new Intl.DateTimeFormat(locale, { | ||
| day: '2-digit', | ||
| month: '2-digit', | ||
| year: 'numeric', | ||
| }).format(new Date(featuredPost.publishedAt)) | ||
| : ''; | ||
|
|
||
| return ( | ||
| <main className="max-w-6xl mx-auto px-6 py-12"> | ||
| <h1 className="text-4xl font-bold mb-4 text-center"> | ||
| {displayTitle} | ||
| {categoryTitle} | ||
| </h1> | ||
| {featuredPost?.mainImage && ( | ||
| <section className="mt-10"> | ||
| <article className="group relative overflow-hidden rounded-3xl bg-white dark:bg-black"> | ||
| <div className="h-[320px] w-full overflow-hidden sm:h-[380px] md:h-[450px] lg:h-[618px] max-h-[65vh]"> | ||
| <Image | ||
| src={featuredPost.mainImage} | ||
| alt={featuredPost.title} | ||
| width={1400} | ||
| height={800} | ||
| className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]" | ||
| priority={false} | ||
| /> | ||
| </div> | ||
| <div className="pointer-events-none absolute inset-x-0 bottom-0 h-56 bg-gradient-to-t from-white/95 via-white/70 to-transparent dark:from-black/90 dark:via-black/60 sm:h-64" /> | ||
| <div className="absolute inset-x-0 bottom-0 p-6 sm:p-8"> | ||
| {featuredPost.categories?.[0] && ( | ||
| <div className="text-sm font-medium text-gray-900 dark:text-gray-100"> | ||
| {featuredPost.categories[0]} | ||
| </div> | ||
| )} | ||
| <h2 className="mt-2 text-3xl font-semibold text-gray-900 transition group-hover:text-[var(--accent-primary)] dark:text-white dark:group-hover:text-[var(--accent-primary)] sm:text-4xl"> | ||
| {featuredPost.title} | ||
| </h2> | ||
| <div className="mt-3 flex items-center gap-3 text-sm text-gray-800 dark:text-gray-200"> | ||
| {featuredPost.author?.image && ( | ||
| <Image | ||
| src={featuredPost.author.image} | ||
| alt={featuredPost.author.name || 'Author'} | ||
| width={28} | ||
| height={28} | ||
| className="h-7 w-7 rounded-full object-cover" | ||
| /> | ||
| )} | ||
| {featuredPost.author?.name && ( | ||
| <span>{featuredPost.author.name}</span> | ||
| )} | ||
| {featuredPost.author?.name && featuredDate && <span>·</span>} | ||
| {featuredDate && <span>{featuredDate}</span>} | ||
| </div> | ||
| <Link | ||
| href={`/blog/${featuredPost.slug.current}`} | ||
| className="absolute bottom-6 right-6 inline-flex h-11 w-11 items-center justify-center rounded-full bg-[var(--accent-primary)] text-white opacity-0 transition group-hover:opacity-100 hover:brightness-110" | ||
| aria-label={featuredPost.title} | ||
| > | ||
| <span aria-hidden="true">↗</span> | ||
| </Link> | ||
| </div> | ||
| </article> | ||
| </section> | ||
| )} | ||
| <div className="mt-12"> | ||
| <BlogCategoryGrid posts={posts} /> | ||
| <BlogCategoryGrid posts={restPosts} /> |
There was a problem hiding this comment.
Avoid dropping the first post when it lacks an image.
featuredPost is always posts[0], but the featured section renders only when mainImage exists. If the first post lacks an image, it’s excluded from both the featured section and the grid.
✅ Suggested fix (select first post with an image, otherwise keep full list)
-const featuredPost = posts[0];
-const restPosts = posts.slice(1);
-const featuredDate = featuredPost?.publishedAt
+const featuredPost = posts.find(post => post.mainImage);
+const restPosts = featuredPost
+ ? posts.filter(post => post._id !== featuredPost._id)
+ : posts;
+const featuredDate = featuredPost?.publishedAt
? new Intl.DateTimeFormat(locale, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).format(new Date(featuredPost.publishedAt))
: '';
...
-{featuredPost?.mainImage && (
+{featuredPost && (📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const featuredPost = posts[0]; | |
| const restPosts = posts.slice(1); | |
| const featuredDate = featuredPost?.publishedAt | |
| ? new Intl.DateTimeFormat(locale, { | |
| day: '2-digit', | |
| month: '2-digit', | |
| year: 'numeric', | |
| }).format(new Date(featuredPost.publishedAt)) | |
| : ''; | |
| return ( | |
| <main className="max-w-6xl mx-auto px-6 py-12"> | |
| <h1 className="text-4xl font-bold mb-4 text-center"> | |
| {displayTitle} | |
| {categoryTitle} | |
| </h1> | |
| {featuredPost?.mainImage && ( | |
| <section className="mt-10"> | |
| <article className="group relative overflow-hidden rounded-3xl bg-white dark:bg-black"> | |
| <div className="h-[320px] w-full overflow-hidden sm:h-[380px] md:h-[450px] lg:h-[618px] max-h-[65vh]"> | |
| <Image | |
| src={featuredPost.mainImage} | |
| alt={featuredPost.title} | |
| width={1400} | |
| height={800} | |
| className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]" | |
| priority={false} | |
| /> | |
| </div> | |
| <div className="pointer-events-none absolute inset-x-0 bottom-0 h-56 bg-gradient-to-t from-white/95 via-white/70 to-transparent dark:from-black/90 dark:via-black/60 sm:h-64" /> | |
| <div className="absolute inset-x-0 bottom-0 p-6 sm:p-8"> | |
| {featuredPost.categories?.[0] && ( | |
| <div className="text-sm font-medium text-gray-900 dark:text-gray-100"> | |
| {featuredPost.categories[0]} | |
| </div> | |
| )} | |
| <h2 className="mt-2 text-3xl font-semibold text-gray-900 transition group-hover:text-[var(--accent-primary)] dark:text-white dark:group-hover:text-[var(--accent-primary)] sm:text-4xl"> | |
| {featuredPost.title} | |
| </h2> | |
| <div className="mt-3 flex items-center gap-3 text-sm text-gray-800 dark:text-gray-200"> | |
| {featuredPost.author?.image && ( | |
| <Image | |
| src={featuredPost.author.image} | |
| alt={featuredPost.author.name || 'Author'} | |
| width={28} | |
| height={28} | |
| className="h-7 w-7 rounded-full object-cover" | |
| /> | |
| )} | |
| {featuredPost.author?.name && ( | |
| <span>{featuredPost.author.name}</span> | |
| )} | |
| {featuredPost.author?.name && featuredDate && <span>·</span>} | |
| {featuredDate && <span>{featuredDate}</span>} | |
| </div> | |
| <Link | |
| href={`/blog/${featuredPost.slug.current}`} | |
| className="absolute bottom-6 right-6 inline-flex h-11 w-11 items-center justify-center rounded-full bg-[var(--accent-primary)] text-white opacity-0 transition group-hover:opacity-100 hover:brightness-110" | |
| aria-label={featuredPost.title} | |
| > | |
| <span aria-hidden="true">↗</span> | |
| </Link> | |
| </div> | |
| </article> | |
| </section> | |
| )} | |
| <div className="mt-12"> | |
| <BlogCategoryGrid posts={posts} /> | |
| <BlogCategoryGrid posts={restPosts} /> | |
| const featuredPost = posts.find(post => post.mainImage); | |
| const restPosts = featuredPost | |
| ? posts.filter(post => post._id !== featuredPost._id) | |
| : posts; | |
| const featuredDate = featuredPost?.publishedAt | |
| ? new Intl.DateTimeFormat(locale, { | |
| day: '2-digit', | |
| month: '2-digit', | |
| year: 'numeric', | |
| }).format(new Date(featuredPost.publishedAt)) | |
| : ''; | |
| return ( | |
| <main className="max-w-6xl mx-auto px-6 py-12"> | |
| <h1 className="text-4xl font-bold mb-4 text-center"> | |
| {categoryTitle} | |
| </h1> | |
| {featuredPost && ( | |
| <section className="mt-10"> | |
| <article className="group relative overflow-hidden rounded-3xl bg-white dark:bg-black"> | |
| <div className="h-[320px] w-full overflow-hidden sm:h-[380px] md:h-[450px] lg:h-[618px] max-h-[65vh]"> | |
| <Image | |
| src={featuredPost.mainImage} | |
| alt={featuredPost.title} | |
| width={1400} | |
| height={800} | |
| className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]" | |
| priority={false} | |
| /> | |
| </div> | |
| <div className="pointer-events-none absolute inset-x-0 bottom-0 h-56 bg-gradient-to-t from-white/95 via-white/70 to-transparent dark:from-black/90 dark:via-black/60 sm:h-64" /> | |
| <div className="absolute inset-x-0 bottom-0 p-6 sm:p-8"> | |
| {featuredPost.categories?.[0] && ( | |
| <div className="text-sm font-medium text-gray-900 dark:text-gray-100"> | |
| {featuredPost.categories[0]} | |
| </div> | |
| )} | |
| <h2 className="mt-2 text-3xl font-semibold text-gray-900 transition group-hover:text-[var(--accent-primary)] dark:text-white dark:group-hover:text-[var(--accent-primary)] sm:text-4xl"> | |
| {featuredPost.title} | |
| </h2> | |
| <div className="mt-3 flex items-center gap-3 text-sm text-gray-800 dark:text-gray-200"> | |
| {featuredPost.author?.image && ( | |
| <Image | |
| src={featuredPost.author.image} | |
| alt={featuredPost.author.name || 'Author'} | |
| width={28} | |
| height={28} | |
| className="h-7 w-7 rounded-full object-cover" | |
| /> | |
| )} | |
| {featuredPost.author?.name && ( | |
| <span>{featuredPost.author.name}</span> | |
| )} | |
| {featuredPost.author?.name && featuredDate && <span>·</span>} | |
| {featuredDate && <span>{featuredDate}</span>} | |
| </div> | |
| <Link | |
| href={`/blog/${featuredPost.slug.current}`} | |
| className="absolute bottom-6 right-6 inline-flex h-11 w-11 items-center justify-center rounded-full bg-[var(--accent-primary)] text-white opacity-0 transition group-hover:opacity-100 hover:brightness-110" | |
| aria-label={featuredPost.title} | |
| > | |
| <span aria-hidden="true">↗</span> | |
| </Link> | |
| </div> | |
| </article> | |
| </section> | |
| )} | |
| <div className="mt-12"> | |
| <BlogCategoryGrid posts={restPosts} /> |
🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/blog/category/[category]/page.tsx around lines 80 -
146, The current logic always treats posts[0] as featuredPost then only renders
it if featuredPost.mainImage exists, which drops the first post from the grid
when it has no image; update the selection so featuredPost is the first post in
posts with a mainImage (e.g., find index via posts.findIndex(p => p.mainImage)),
set restPosts to posts with that featured item removed (if found) or to the full
posts array when no featured post exists, and keep the featuredDate logic tied
to the chosen featuredPost; ensure BlogCategoryGrid receives restPosts (or posts
when no featured found) and adjust any rendering guards that assume featuredPost
is posts[0].
Adding brend colours
Adding main posts to blog and category pages
Fix for the text rendering on the post details page
changing UI in general
Summary by CodeRabbit
Release Notes
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.