Skip to main content
Once your media is stored in Cloudinary via Payload CMS, you can leverage Cloudinary’s powerful transformation APIs to deliver optimized images, videos, and PDFs to your users.

Getting Started

All Cloudinary URLs follow this pattern:
https://res.cloudinary.com/{cloud_name}/{resource_type}/upload/{transformations}/{public_id}.{format}
  • cloud_name: Your Cloudinary cloud name from process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME
  • resource_type: image, video, or raw
  • transformations: Optional transformations (resize, crop, quality, etc.)
  • public_id: From media.cloudinary.public_id
  • format: From media.cloudinary.format

Basic Image Component

Here’s a simple React component that displays an image from Cloudinary:
const CloudinaryImage = ({ media }) => {
  if (!media?.cloudinary) return null;

  const { public_id, format } = media.cloudinary;
  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;

  return (
    <img
      src={`https://res.cloudinary.com/${cloudName}/image/upload/${public_id}.${format}`}
      alt={media.alt || media.filename}
      loading="lazy"
    />
  );
};

Responsive Images with Transformations

Use Cloudinary transformations to deliver optimized images at different screen sizes. This example is from README.md:465-491:
const CloudinaryImage = ({ media }) => {
  if (!media?.cloudinary) return null;

  const { public_id, format } = media.cloudinary;
  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;

  // You can use Cloudinary transformations in the URL
  return (
    <picture>
      <source
        media="(max-width: 640px)"
        srcSet={`https://res.cloudinary.com/${cloudName}/image/upload/w_400,c_limit,q_auto,f_auto/${public_id}.${format}`}
      />
      <source
        media="(max-width: 1024px)"
        srcSet={`https://res.cloudinary.com/${cloudName}/image/upload/w_800,c_limit,q_auto,f_auto/${public_id}.${format}`}
      />
      <img
        src={`https://res.cloudinary.com/${cloudName}/image/upload/q_auto,f_auto/${public_id}.${format}`}
        alt={media.alt || media.filename}
        loading="lazy"
      />
    </picture>
  );
};

Common Transformations

// Width only (maintain aspect ratio)
w_400

// Height only
h_300

// Width and height
w_400,h_300

// Example URL
`https://res.cloudinary.com/${cloudName}/image/upload/w_400,h_300/${public_id}.${format}`

Next.js Image Component

Integrate with Next.js Image component for automatic optimization:
import Image from 'next/image';

const CloudinaryNextImage = ({ media, width = 800, height = 600 }) => {
  if (!media?.cloudinary) return null;

  const { public_id, format } = media.cloudinary;
  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;

  // Next.js Image loader for Cloudinary
  const cloudinaryLoader = ({ src, width, quality }) => {
    const params = [`w_${width}`, `q_${quality || 'auto'}`, 'f_auto'];
    return `https://res.cloudinary.com/${cloudName}/image/upload/${params.join(',')}/${src}`;
  };

  return (
    <Image
      loader={cloudinaryLoader}
      src={`${public_id}.${format}`}
      alt={media.alt || media.filename}
      width={width}
      height={height}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
  );
};

Global Cloudinary Loader

Add a global loader to your next.config.js:
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './cloudinary-loader.js',
  },
};
Create cloudinary-loader.js:
export default function cloudinaryLoader({ src, width, quality }) {
  const params = [`w_${width}`, `q_${quality || 'auto'}`, 'f_auto', 'c_limit'];
  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
  return `https://res.cloudinary.com/${cloudName}/image/upload/${params.join(',')}/${src}`;
}

PDF Viewer Component

Display PDFs with page navigation and thumbnails. This example is from README.md:199-241:
const PDFViewer = ({ media }) => {
  if (!media?.cloudinary || media.cloudinary.format !== 'pdf') {
    return null;
  }

  const { public_id, pages, selected_page } = media.cloudinary;
  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
  const page = selected_page || 1;

  return (
    <div className="pdf-viewer">
      <h2>{media.filename}</h2>

      {/* Display the selected page as a thumbnail */}
      <a href={`https://res.cloudinary.com/${cloudName}/image/upload/${public_id}.pdf`} target="_blank">
        <img
          src={`https://res.cloudinary.com/${cloudName}/image/upload/pg_${page},w_300,h_400,c_fill,q_auto,f_jpg/${public_id}.pdf`}
          alt={`PDF Page ${page}`}
        />
      </a>

      {/* Page navigation if there are multiple pages */}
      {pages > 1 && (
        <div className="pdf-pages">
          <p>Page {page} of {pages}</p>

          {/* Thumbnail grid of all pages */}
          <div className="page-thumbnails">
            {Array.from({ length: pages }).map((_, i) => (
              <img
                key={i}
                src={`https://res.cloudinary.com/${cloudName}/image/upload/pg_${i + 1},w_100,h_130,c_fill,q_auto,f_jpg/${public_id}.pdf`}
                alt={`Page ${i + 1}`}
                className={i + 1 === page ? 'active' : ''}
              />
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

PDF Transformations

Cloudinary provides special transformations for PDFs:
// Convert specific page to image
pg_1  // First page
pg_3  // Third page

// Example: Get page 2 as JPEG thumbnail
`https://res.cloudinary.com/${cloudName}/image/upload/pg_2,w_200,h_260,c_fill,q_auto,f_jpg/${public_id}.pdf`

// Full PDF download
`https://res.cloudinary.com/${cloudName}/image/upload/${public_id}.pdf`

Video Player Component

Embed Cloudinary videos with transformations:
const CloudinaryVideo = ({ media }) => {
  if (!media?.cloudinary || media.cloudinary.resource_type !== 'video') {
    return null;
  }

  const { public_id, format } = media.cloudinary;
  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;

  return (
    <video
      controls
      poster={`https://res.cloudinary.com/${cloudName}/video/upload/so_0,w_800,h_450,c_fill,q_auto/${public_id}.jpg`}
    >
      <source
        src={`https://res.cloudinary.com/${cloudName}/video/upload/q_auto/${public_id}.${format}`}
        type={`video/${format}`}
      />
      Your browser does not support the video tag.
    </video>
  );
};

Video Transformations

// Auto quality and format
q_auto,f_auto

// Resize video
w_800,h_450,c_fill

// Generate thumbnail from specific time (seconds offset)
so_2.5  // 2.5 seconds into the video

// Example poster image URL
`https://res.cloudinary.com/${cloudName}/video/upload/so_2.5,w_800,h_450,c_fill,q_auto,f_jpg/${public_id}.jpg`

TypeScript Types

For better type safety, use the media document types:
import type { PayloadDocument } from 'payload-cloudinary';

interface MediaDocument extends PayloadDocument {
  id: string;
  filename: string;
  alt?: string;
  caption?: string;
  cloudinary: {
    public_id: string;
    format: string;
    resource_type: 'image' | 'video' | 'raw';
    width?: number;
    height?: number;
    duration?: number;
    pages?: number;
    selected_page?: number;
  };
}

const CloudinaryImage: React.FC<{ media: MediaDocument }> = ({ media }) => {
  // Component implementation
};

Reusable Hooks

Create custom hooks for common operations:
import { useMemo } from 'react';

function useCloudinaryUrl(
  media: MediaDocument | null,
  transformations: string = ''
): string | null {
  return useMemo(() => {
    if (!media?.cloudinary) return null;
    
    const { public_id, format, resource_type } = media.cloudinary;
    const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
    
    const baseUrl = `https://res.cloudinary.com/${cloudName}/${resource_type}/upload`;
    const params = transformations ? `${transformations}/` : '';
    
    return `${baseUrl}/${params}${public_id}.${format}`;
  }, [media, transformations]);
}

// Usage
function MyComponent({ media }) {
  const thumbnailUrl = useCloudinaryUrl(media, 'w_400,h_300,c_fill,q_auto');
  const fullUrl = useCloudinaryUrl(media, 'q_auto,f_auto');
  
  return (
    <div>
      <img src={thumbnailUrl} alt="Thumbnail" />
      <a href={fullUrl}>View Full Size</a>
    </div>
  );
}

Advanced: Signed URLs

For private media, generate signed URLs:
import crypto from 'crypto';

function generateSignedUrl(
  publicId: string,
  transformations: string = '',
  expiresAt: number = Math.floor(Date.now() / 1000) + 3600 // 1 hour
): string {
  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
  const apiSecret = process.env.CLOUDINARY_API_SECRET;
  
  // Build the string to sign
  const toSign = transformations
    ? `${transformations}/${publicId}${expiresAt}`
    : `${publicId}${expiresAt}`;
  
  // Generate signature
  const signature = crypto
    .createHash('sha256')
    .update(toSign + apiSecret)
    .digest('hex')
    .substring(0, 8);
  
  return `https://res.cloudinary.com/${cloudName}/image/upload/s--${signature}--/${transformations}/${publicId}`;
}
Never expose your CLOUDINARY_API_SECRET to the client. Generate signed URLs server-side.

Performance Best Practices

1

Use Auto Quality

Always include q_auto to let Cloudinary optimize quality based on content and bandwidth
2

Use Auto Format

Include f_auto to serve modern formats (WebP, AVIF) to supported browsers
3

Lazy Load Images

Add loading="lazy" to images below the fold
4

Responsive Images

Use srcSet or Next.js Image component with sizes prop for responsive delivery
5

Cache Assets

Cloudinary URLs are cached at the CDN level. Avoid changing transformations frequently

Next Steps

Cloudinary Docs

Explore all available transformations

Troubleshooting

Common issues and solutions

Build docs developers (and LLMs) love