Building the Shopify → Cloudinary Flow

From image sync to Next.js display—the complete implementation guide

CloudinaryShopifyImage SyncNext.jsImplementation
5 min read

Implementation Overview

The flow for delivering Shopify product images through Cloudinary consists of three main steps:

  1. Image Sync: Upload Shopify images to Cloudinary
  2. URL Transformation: Convert Shopify image URLs to Cloudinary URLs
  3. Frontend Display: Show optimized images in Next.js
Image Sync Flow
Shopify (Product Creation)

Product create/update

Webhook
Sync Process (Lambda etc)

Extract image URLs

Upload API
Cloudinary (Storage)

Transform & store

Image Delivery Flow
Customer's Browser

Image request

CDN access
Cloudinary CDN

Return transformed image

Fast delivery
Fast Display

Show optimized image

Step 1: Image Sync

Automatically upload to Cloudinary when products are created or updated in Shopify.

Shopify Webhook Configuration:

  • products/create - On product creation
  • products/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

Webhook Sync
First Load Speed◎ Fast
Implementation Effort△ More work
Operations Effort○ Less
Recommendation★★★
Fetch Type
First Load Speed△ Slower
Implementation Effort◎ Minimal
Operations Effort◎ Almost none
Recommendation★★
Hybrid
First Load Speed○ Medium
Implementation Effort△ More work
Operations Effort△ Medium
Recommendation★★

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.

Related Topics