Render Shopify imagery through next/image

Turn the optimizer back on by dropping images.unoptimized, and widen the
Shopify remote pattern to **.shopify.com so any image subdomain resolves.
With the optimizer live, remotePatterns is now actually enforced.

Convert the seven Shopify CDN call sites: the product/collection cards and
the gallery carousel size with fill + sizes, while the cart, order and
search thumbnails carry explicit dimensions. The avatar, markdown,
attachment, assistant and logo images stay plain <img> — they take
blob:/data: or arbitrary hosts that next/image cannot process.

The lightbox needs w-auto h-auto: the width and height attributes
next/image requires make both axes definite, so max-w/max-h clamp them
independently instead of preserving the ratio. object-contain masked that,
but the stretched element covered the overlay and ate the backdrop clicks
that close the dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uSQQNWrmiYDRW9L9VnsGC
This commit is contained in:
Rami Bitar
2026-08-08 17:48:19 -04:00
co-authored by Claude Opus 5
parent 58742d5d00
commit 82c9626c75
7 changed files with 56 additions and 27 deletions
+4 -1
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import Image from 'next/image';
import { useCartStore, redirectToCheckout } from '@/hooks/use-shopify-cart'; import { useCartStore, redirectToCheckout } from '@/hooks/use-shopify-cart';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Loader } from '@/components/ui/loader'; import { Loader } from '@/components/ui/loader';
@@ -171,9 +172,11 @@ const CartDrawer: React.FC = () => {
{/* Product Image */} {/* Product Image */}
<div className="w-16 h-16 bg-zinc-100 overflow-hidden shrink-0"> <div className="w-16 h-16 bg-zinc-100 overflow-hidden shrink-0">
{image ? ( {image ? (
<img <Image
src={image} src={image}
alt={item.merchandise.product.title} alt={item.merchandise.product.title}
width={64}
height={64}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
+5 -2
View File
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image';
interface CollectionImage { interface CollectionImage {
url: string; url: string;
@@ -27,10 +28,12 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection }) => {
{/* Collection Image */} {/* Collection Image */}
<div className="relative aspect-square overflow-hidden"> <div className="relative aspect-square overflow-hidden">
{collection.image ? ( {collection.image ? (
<img <Image
src={collection.image.url} src={collection.image.url}
alt={collection.image.altText || collection.title} alt={collection.image.altText || collection.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-[1.04]" fill
sizes="(min-width: 1024px) 25vw, 50vw"
className="object-cover transition-transform duration-500 group-hover:scale-[1.04]"
/> />
) : ( ) : (
<div className="absolute inset-0 flex items-center justify-center text-zinc-300"> <div className="absolute inset-0 flex items-center justify-center text-zinc-300">
+4 -1
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import Image from 'next/image';
import { RiImageLine } from '@remixicon/react'; import { RiImageLine } from '@remixicon/react';
import type { Customer, CustomerOrder } from '@/services/shopify/customer'; import type { Customer, CustomerOrder } from '@/services/shopify/customer';
@@ -123,9 +124,11 @@ const OrderHistory: React.FC<OrderHistoryProps> = ({ customer }) => {
<li key={index} className="flex items-start gap-x-3"> <li key={index} className="flex items-start gap-x-3">
<div className="h-16 w-16 shrink-0 overflow-hidden bg-zinc-100"> <div className="h-16 w-16 shrink-0 overflow-hidden bg-zinc-100">
{node.variant?.image ? ( {node.variant?.image ? (
<img <Image
src={node.variant.image.url} src={node.variant.image.url}
alt={node.variant.image.altText || node.title} alt={node.variant.image.altText || node.title}
width={64}
height={64}
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />
) : ( ) : (
+5 -2
View File
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image';
import { truncate } from '@/lib/utils'; import { truncate } from '@/lib/utils';
interface ProductImage { interface ProductImage {
@@ -68,10 +69,12 @@ const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
{/* Product Image */} {/* Product Image */}
<div className="relative aspect-square overflow-hidden"> <div className="relative aspect-square overflow-hidden">
{firstImage ? ( {firstImage ? (
<img <Image
src={firstImage.url} src={firstImage.url}
alt={firstImage.altText || product.title} alt={firstImage.altText || product.title}
className="w-full h-full object-contain transition-transform duration-500 group-hover:scale-[1.04]" fill
sizes="(min-width: 1280px) 20vw, (min-width: 1024px) 25vw, (min-width: 640px) 33vw, 50vw"
className="object-contain transition-transform duration-500 group-hover:scale-[1.04]"
/> />
) : ( ) : (
<div className="absolute inset-0 flex items-center justify-center text-zinc-300"> <div className="absolute inset-0 flex items-center justify-center text-zinc-300">
@@ -1,12 +1,15 @@
'use client'; 'use client';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { RiCloseLine } from '@remixicon/react'; import { RiCloseLine } from '@remixicon/react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
interface ProductImage { interface ProductImage {
url: string; url: string;
altText?: string | null; altText?: string | null;
width?: number | null;
height?: number | null;
} }
interface ProductDetailGalleryProps { interface ProductDetailGalleryProps {
@@ -136,11 +139,14 @@ const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({
isSingle ? 'sm:col-span-2' : '' isSingle ? 'sm:col-span-2' : ''
}`} }`}
> >
<img <Image
src={image.url} src={image.url}
alt={image.altText || 'Product image'} alt={image.altText || 'Product image'}
fill
sizes="(min-width: 1024px) 30vw, (min-width: 640px) 50vw, 100vw"
priority={index === 0}
draggable={false} draggable={false}
className="w-full h-full object-cover select-none" className="object-cover select-none"
/> />
</button> </button>
))} ))}
@@ -173,11 +179,19 @@ const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({
onClick={close} onClick={close}
className="fixed inset-0 z-100 flex items-center justify-center bg-foreground/20 backdrop-blur-md p-6 md:p-10" className="fixed inset-0 z-100 flex items-center justify-center bg-foreground/20 backdrop-blur-md p-6 md:p-10"
> >
<img <Image
src={zoomedImage.url} src={zoomedImage.url}
alt={zoomedImage.altText || 'Product image'} alt={zoomedImage.altText || 'Product image'}
// Shopify gives us the intrinsic size; the square fallback only
// reserves space until CSS scales it down to fit the overlay.
width={zoomedImage.width || 1600}
height={zoomedImage.height || 1600}
sizes="100vw"
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
className="max-h-full max-w-full object-contain" // w/h-auto keeps the box at the image's own ratio; without it the
// width+height attributes make both axes definite and the element
// stretches to the overlay, swallowing backdrop clicks that close it.
className="max-h-full max-w-full w-auto h-auto object-contain"
/> />
<Button <Button
+4 -1
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { import {
CommandDialog, CommandDialog,
@@ -162,9 +163,11 @@ const SearchDialog: React.FC = () => {
> >
<div className="h-14 w-14 shrink-0 overflow-hidden bg-zinc-100"> <div className="h-14 w-14 shrink-0 overflow-hidden bg-zinc-100">
{product.featuredImage ? ( {product.featuredImage ? (
<img <Image
src={product.featuredImage.url} src={product.featuredImage.url}
alt={product.featuredImage.altText || product.title} alt={product.featuredImage.altText || product.title}
width={56}
height={56}
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />
) : ( ) : (
+16 -16
View File
@@ -3,23 +3,23 @@ export default {
typescript: { typescript: {
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
images: { images: {
unoptimized: true,
remotePatterns: [ remotePatterns: [
{ {
protocol: 'https', protocol: 'https',
hostname: 'images.unsplash.com', hostname: 'images.unsplash.com',
}, },
{ // Covers cdn.shopify.com plus any other Shopify-hosted image subdomain.
protocol: 'https', {
hostname: 'cdn.shopify.com', protocol: 'https',
}, hostname: '**.shopify.com',
{ },
protocol: 'https', {
hostname: '**.frontend.co', protocol: 'https',
}, hostname: '**.frontend.co',
], },
}, ],
},
experimental: { experimental: {
reactDebugChannel: false reactDebugChannel: false
}, },