From 23ee1a2da2cc0a96fcd14967a095acd80844ebdf Mon Sep 17 00:00:00 2001 From: Viktor Svertoka Date: Mon, 23 Feb 2026 22:37:32 +0200 Subject: [PATCH] fix(blog,qa): restore Sanity images on Vercel and strengthen Git tab accent --- .../app/[locale]/blog/[slug]/PostDetails.tsx | 9 +++++++++ .../blog/category/[category]/page.tsx | 7 +++++++ frontend/components/blog/BlogCard.tsx | 5 +++++ frontend/components/blog/BlogFilters.tsx | 7 +++++++ frontend/data/categoryStyles.ts | 6 +++--- frontend/lib/blog/image.ts | 20 +++++++++++++++++++ frontend/next.config.ts | 2 +- 7 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 frontend/lib/blog/image.ts diff --git a/frontend/app/[locale]/blog/[slug]/PostDetails.tsx b/frontend/app/[locale]/blog/[slug]/PostDetails.tsx index 733b2723..d1f9ab79 100644 --- a/frontend/app/[locale]/blog/[slug]/PostDetails.tsx +++ b/frontend/app/[locale]/blog/[slug]/PostDetails.tsx @@ -7,6 +7,7 @@ import { client } from '@/client'; import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground'; import { Link } from '@/i18n/routing'; import { formatBlogDate } from '@/lib/blog/date'; +import { shouldBypassImageOptimization } from '@/lib/blog/image'; export const revalidate = 0; @@ -285,6 +286,7 @@ function renderPortableText( alt={postTitle || 'Post image'} width={1200} height={800} + unoptimized={shouldBypassImageOptimization(block.url)} sizes="100vw" className="my-6 h-auto w-full rounded-xl border border-gray-200" /> @@ -577,6 +579,7 @@ export default async function PostDetails({ src={post.mainImage} alt={post.title || 'Post image'} fill + unoptimized={shouldBypassImageOptimization(post.mainImage)} className="object-contain" /> @@ -618,6 +621,9 @@ export default async function PostDetails({ src={item.mainImage} alt={item.title || 'Post image'} fill + unoptimized={shouldBypassImageOptimization( + item.mainImage + )} className="object-cover transition-transform duration-300 group-hover:scale-[1.03]" /> @@ -640,6 +646,9 @@ export default async function PostDetails({ src={item.author.image} alt={item.author.name || 'Author'} fill + unoptimized={shouldBypassImageOptimization( + item.author.image + )} className="object-cover" /> diff --git a/frontend/app/[locale]/blog/category/[category]/page.tsx b/frontend/app/[locale]/blog/category/[category]/page.tsx index ea470ee9..4746431e 100644 --- a/frontend/app/[locale]/blog/category/[category]/page.tsx +++ b/frontend/app/[locale]/blog/category/[category]/page.tsx @@ -9,6 +9,7 @@ import { FeaturedPostCtaButton } from '@/components/blog/FeaturedPostCtaButton'; import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground'; import { Link } from '@/i18n/routing'; import { formatBlogDate } from '@/lib/blog/date'; +import { shouldBypassImageOptimization } from '@/lib/blog/image'; export const revalidate = 0; @@ -121,6 +122,9 @@ export default async function BlogCategoryPage({ alt={featuredPost.title} width={1400} height={800} + unoptimized={shouldBypassImageOptimization( + featuredPost.mainImage + )} className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]" priority={false} /> @@ -142,6 +146,9 @@ export default async function BlogCategoryPage({ alt={featuredPost.author.name || 'Author'} width={28} height={28} + unoptimized={shouldBypassImageOptimization( + featuredPost.author.image + )} className="h-7 w-7 rounded-full object-cover" /> )} diff --git a/frontend/components/blog/BlogCard.tsx b/frontend/components/blog/BlogCard.tsx index b145a1c4..fb07afb7 100644 --- a/frontend/components/blog/BlogCard.tsx +++ b/frontend/components/blog/BlogCard.tsx @@ -6,6 +6,7 @@ import { useTranslations } from 'next-intl'; import { useMemo } from 'react'; import { formatBlogDate } from '@/lib/blog/date'; +import { shouldBypassImageOptimization } from '@/lib/blog/image'; import type { Author, @@ -69,6 +70,7 @@ export default function BlogCard({ src={post.mainImage} alt={post.title} fill + unoptimized={shouldBypassImageOptimization(post.mainImage)} className="scale-[1.03] object-cover brightness-95 contrast-110 transition-transform duration-300 group-hover:scale-[1.06]" priority={false} /> @@ -109,6 +111,9 @@ export default function BlogCard({ src={post.author.image} alt={post.author.name || 'Author'} fill + unoptimized={shouldBypassImageOptimization( + post.author.image + )} className="object-cover" /> diff --git a/frontend/components/blog/BlogFilters.tsx b/frontend/components/blog/BlogFilters.tsx index 7daa3399..49bbd879 100644 --- a/frontend/components/blog/BlogFilters.tsx +++ b/frontend/components/blog/BlogFilters.tsx @@ -22,6 +22,7 @@ import { BlogPagination } from '@/components/blog/BlogPagination'; import { usePathname, useRouter } from '@/i18n/routing'; import { Link } from '@/i18n/routing'; import { formatBlogDate } from '@/lib/blog/date'; +import { shouldBypassImageOptimization } from '@/lib/blog/image'; export type PortableTextSpan = { _type: 'span'; @@ -492,6 +493,9 @@ export default function BlogFilters({ src={featuredPost.mainImage} alt={featuredPost.title} fill + unoptimized={shouldBypassImageOptimization( + featuredPost.mainImage + )} sizes="(max-width: 640px) 100vw, (max-width: 1024px) 60vw, 720px" className="scale-[1.02] object-cover transition-transform duration-300 group-hover:scale-[1.05]" priority @@ -565,6 +569,9 @@ export default function BlogFilters({ src={selectedAuthorData.image} alt={selectedAuthorData.name || t('author')} fill + unoptimized={shouldBypassImageOptimization( + selectedAuthorData.image + )} className="object-cover" /> diff --git a/frontend/data/categoryStyles.ts b/frontend/data/categoryStyles.ts index c10705ff..02a81d69 100644 --- a/frontend/data/categoryStyles.ts +++ b/frontend/data/categoryStyles.ts @@ -20,9 +20,9 @@ export const categoryTabStyles = { git: { icon: '/icons/git.svg', color: - 'group-hover:border-[#F05032]/50 group-hover:bg-[#F05032]/10 data-[state=active]:border-[#F05032]/50 data-[state=active]:bg-[#F05032]/10', - glow: 'bg-[#F05032]', - accent: '#F05032', + 'group-hover:border-[#C1121F]/50 group-hover:bg-[#C1121F]/10 data-[state=active]:border-[#C1121F]/50 data-[state=active]:bg-[#C1121F]/10', + glow: 'bg-[#C1121F]', + accent: '#C1121F', }, html: { icon: '/icons/html5.svg', diff --git a/frontend/lib/blog/image.ts b/frontend/lib/blog/image.ts new file mode 100644 index 00000000..7d8fbb08 --- /dev/null +++ b/frontend/lib/blog/image.ts @@ -0,0 +1,20 @@ +export function isSanityAssetUrl(url?: string | null): boolean { + if (!url) return false; + + try { + const parsed = new URL(url); + return ( + parsed.protocol === 'https:' && + parsed.hostname === 'cdn.sanity.io' && + (parsed.pathname.startsWith('/images/') || + parsed.pathname.startsWith('/files/')) + ); + } catch { + return false; + } +} + +export function shouldBypassImageOptimization(url?: string | null): boolean { + // Hotfix for Vercel image optimizer failures on some Sanity assets. + return isSanityAssetUrl(url); +} diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 17e95474..017c8b8c 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -10,7 +10,7 @@ const nextConfig: NextConfig = { { protocol: 'https', hostname: 'cdn.sanity.io', - pathname: '/images/**', + pathname: '/**', }, { protocol: 'https',