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';
import React, { useState } from 'react';
import Image from 'next/image';
import { useCartStore, redirectToCheckout } from '@/hooks/use-shopify-cart';
import { Button } from '@/components/ui/button';
import { Loader } from '@/components/ui/loader';
@@ -171,9 +172,11 @@ const CartDrawer: React.FC = () => {
{/* Product Image */}
<div className="w-16 h-16 bg-zinc-100 overflow-hidden shrink-0">
{image ? (
<img
<Image
src={image}
alt={item.merchandise.product.title}
width={64}
height={64}
className="w-full h-full object-cover"
/>
) : (
+5 -2
View File
@@ -1,5 +1,6 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
interface CollectionImage {
url: string;
@@ -27,10 +28,12 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection }) => {
{/* Collection Image */}
<div className="relative aspect-square overflow-hidden">
{collection.image ? (
<img
<Image
src={collection.image.url}
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">
+4 -1
View File
@@ -1,6 +1,7 @@
'use client';
import React, { useState } from 'react';
import Image from 'next/image';
import { RiImageLine } from '@remixicon/react';
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">
<div className="h-16 w-16 shrink-0 overflow-hidden bg-zinc-100">
{node.variant?.image ? (
<img
<Image
src={node.variant.image.url}
alt={node.variant.image.altText || node.title}
width={64}
height={64}
className="h-full w-full object-cover"
/>
) : (
+5 -2
View File
@@ -1,5 +1,6 @@
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { truncate } from '@/lib/utils';
interface ProductImage {
@@ -68,10 +69,12 @@ const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
{/* Product Image */}
<div className="relative aspect-square overflow-hidden">
{firstImage ? (
<img
<Image
src={firstImage.url}
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">
@@ -1,12 +1,15 @@
'use client';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { RiCloseLine } from '@remixicon/react';
import { Button } from '@/components/ui/button';
interface ProductImage {
url: string;
altText?: string | null;
width?: number | null;
height?: number | null;
}
interface ProductDetailGalleryProps {
@@ -136,11 +139,14 @@ const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({
isSingle ? 'sm:col-span-2' : ''
}`}
>
<img
<Image
src={image.url}
alt={image.altText || 'Product image'}
fill
sizes="(min-width: 1024px) 30vw, (min-width: 640px) 50vw, 100vw"
priority={index === 0}
draggable={false}
className="w-full h-full object-cover select-none"
className="object-cover select-none"
/>
</button>
))}
@@ -173,11 +179,19 @@ const ProductDetailGallery: React.FC<ProductDetailGalleryProps> = ({
onClick={close}
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}
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()}
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
+4 -1
View File
@@ -1,6 +1,7 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import {
CommandDialog,
@@ -162,9 +163,11 @@ const SearchDialog: React.FC = () => {
>
<div className="h-14 w-14 shrink-0 overflow-hidden bg-zinc-100">
{product.featuredImage ? (
<img
<Image
src={product.featuredImage.url}
alt={product.featuredImage.altText || product.title}
width={56}
height={56}
className="h-full w-full object-cover"
/>
) : (
+2 -2
View File
@@ -4,15 +4,15 @@ export default {
ignoreBuildErrors: true,
},
images: {
unoptimized: true,
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
// Covers cdn.shopify.com plus any other Shopify-hosted image subdomain.
{
protocol: 'https',
hostname: 'cdn.shopify.com',
hostname: '**.shopify.com',
},
{
protocol: 'https',