Implementation Overview
The flow for delivering Shopify product images through Cloudinary consists of three main steps:
- Image Sync: Upload Shopify images to Cloudinary
- URL Transformation: Convert Shopify image URLs to Cloudinary URLs
- Frontend Display: Show optimized images in Next.js
Product create/update
Extract image URLs
Transform & store
Image request
Return transformed image
Show optimized image
Step 1: Image Sync
Method A: Webhook Auto-Sync (Recommended)
Automatically upload to Cloudinary when products are created or updated in Shopify.
Shopify Webhook Configuration:
products/create- On product creationproducts/update- On product update
Sync Flow:
1. Product registered in Shopify
2. Webhook fires → POST to your endpoint
3. Extract image URLs from product data
4. Upload images via Cloudinary Upload API
5. Save Cloudinary URLs somewhere (see below)
Sample Code (Conceptual):
// Webhook receiving endpoint
async function handleProductWebhook(shopifyProduct: ShopifyProduct) {
for (const image of shopifyProduct.images) {
// Upload from Shopify image URL to Cloudinary
const result = await cloudinary.uploader.upload(image.src, {
folder: `shopify/${shopifyProduct.id}`,
public_id: image.id.toString(),
overwrite: true,
});
// Save Cloudinary URL (metafield, DB, etc.)
await saveCloudinaryUrl(shopifyProduct.id, image.id, result.secure_url);
}
}
Method B: Fetch Type (On-Demand)
No pre-sync—Cloudinary fetches from Shopify on request.
Cloudinary Fetch URL Format:
https://res.cloudinary.com/your-cloud/image/fetch/w_400,f_auto,q_auto/https://cdn.shopify.com/...
Specifying a Shopify image URL after /fetch/ makes Cloudinary fetch, transform, and cache that image.
Pros:
- No pre-sync needed
- Simple implementation
Cons:
- First request is slow (includes Shopify fetch)
- Needs re-fetch if Shopify image URL changes
Step 2: URL Transformation Logic
Create a utility to convert Shopify image URLs to Cloudinary URLs for frontend use.
For Upload Type
// Generate Cloudinary URL from Shopify product ID and image ID
function getCloudinaryUrl(
productId: string,
imageId: string,
options: {
width?: number;
height?: number;
format?: 'auto' | 'webp' | 'avif';
quality?: 'auto' | number;
} = {}
): string {
const { width, height, format = 'auto', quality = 'auto' } = options;
const transforms: string[] = [];
if (width) transforms.push(`w_${width}`);
if (height) transforms.push(`h_${height}`);
transforms.push(`f_${format}`);
transforms.push(`q_${quality}`);
const transformStr = transforms.join(',');
return `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/${transformStr}/shopify/${productId}/${imageId}`;
}
// Usage
const imageUrl = getCloudinaryUrl('12345', '67890', {
width: 400,
format: 'auto',
quality: 'auto',
});
// → https://res.cloudinary.com/your-cloud/image/upload/w_400,f_auto,q_auto/shopify/12345/67890
For Fetch Type
function getCloudinaryFetchUrl(
shopifyImageUrl: string,
options: {
width?: number;
format?: 'auto' | 'webp';
quality?: 'auto' | number;
} = {}
): string {
const { width, format = 'auto', quality = 'auto' } = options;
const transforms: string[] = [];
if (width) transforms.push(`w_${width}`);
transforms.push(`f_${format}`);
transforms.push(`q_${quality}`);
const transformStr = transforms.join(',');
return `https://res.cloudinary.com/${CLOUD_NAME}/image/fetch/${transformStr}/${encodeURIComponent(shopifyImageUrl)}`;
}
Step 3: Display in Next.js
Custom Image Component
Create a custom component combining Next.js Image component with Cloudinary URLs.
// components/CloudinaryImage.tsx
import Image from 'next/image';
interface CloudinaryImageProps {
productId: string;
imageId: string;
alt: string;
width: number;
height: number;
priority?: boolean;
}
// Cloudinary loader
const cloudinaryLoader = ({ src, width, quality }: {
src: string;
width: number;
quality?: number;
}) => {
const q = quality || 'auto';
// src expected in productId/imageId format
return `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/w_${width},f_auto,q_${q}/${src}`;
};
export function CloudinaryImage({
productId,
imageId,
alt,
width,
height,
priority = false,
}: CloudinaryImageProps) {
return (
<Image
loader={cloudinaryLoader}
src={`shopify/${productId}/${imageId}`}
alt={alt}
width={width}
height={height}
priority={priority}
/>
);
}
next.config.js Configuration
Allow the Cloudinary domain.
// next.config.js
module.exports = {
images: {
domains: ['res.cloudinary.com'],
// Or when using a loader
loader: 'custom',
loaderFile: './lib/cloudinary-loader.ts',
},
};
Architecture Pattern Comparison
| Pattern | First Load Speed | Implementation Effort | Operations Effort | Recommendation |
|---|---|---|---|---|
| Webhook Sync | ◎ Fast | △ More work | ○ Less | ★★★ |
| Fetch Type | △ Slower | ◎ Minimal | ◎ Almost none | ★★ |
| Hybrid | ○ Medium | △ More work | △ Medium | ★★ |
Recommended: Webhook Sync
For serious EC sites, webhook sync is recommended.
- Fast even on first access
- Images guaranteed to exist in Cloudinary
- Unaffected by Shopify image URL changes
Quick Start: Fetch Type
For testing or small-scale sites, fetch type is easy to start with.
Implementation Tips
1. Use Placeholder Images
Cloudinary easily generates Low Quality Image Placeholders (LQIP).
w_20,f_auto,q_auto,e_blur:500
This gets a 20px blurred image to show while the main image loads.
2. Responsive Support
Use srcset to specify multiple sizes.
const sizes = [320, 640, 960, 1280];
const srcSet = sizes
.map(size => `${getCloudinaryUrl(productId, imageId, { width: size })} ${size}w`)
.join(', ');
3. Error Handling
Prepare fallbacks for Cloudinary errors.
<Image
src={cloudinaryUrl}
onError={(e) => {
// Fallback to original Shopify image
e.currentTarget.src = shopifyOriginalUrl;
}}
alt={alt}
/>
Summary
Building a Cloudinary image delivery flow provides:
- Server Load: Reduced to zero
- Costs: Predictable and optimizable
- Display Speed: Accelerated via global CDN
- Transform Features: Access to advanced image processing
If you're struggling with image delivery in headless EC, Cloudinary is a strong option. Try starting with fetch type to see the benefits, then migrate to webhook sync as needed.