Cloudinary

Integrate Cloudinary with Strapi to streamline media management, optimize performance, and deliver assets globally with ease through Cloudinary’s powerful CDN and transformation tools.

Cloudinary

What Is Cloudinary?

Cloudinary is a cloud-based platform that provides solutions for managing and optimizing media assets like images and videos. Cloudinary offers a comprehensive set of tools for image and video uploading, storage, transformation, and delivery.

Cloudinary's services allow developers to automate tasks like resizing, cropping, and applying effects, making it easier to manage media in web and mobile applications. The platform also integrates with various content management systems (CMS) and frameworks. With its advanced optimization capabilities, Cloudinary helps reduce the load time of media-rich websites, ensuring faster performance and improved user experience.

Why Integrate Cloudinary with Strapi

Integrating Cloudinary with Strapi creates a powerful technical foundation with several key benefits:

  • Automated optimization: Cloudinary automatically handles file formats, device-specific sizing, and intelligent cropping. While traditional methods to optimize images with PHP require significant effort, Cloudinary automates the process, saving valuable development time.
  • Performance boost: Global CDN network reduces latency for users worldwide.
  • Resource efficiency: Offloads media handling, freeing server resources for core application functions.
  • Workflow improvements: Strapi's admin interface, part of this leading open-source CMS, works seamlessly with Cloudinary's media tools, enhancing efficiency and enabling multi-platform publishing.
  • Unlimited scalability: Removes local storage constraints as your media library grows.
  • Technical flexibility: Combines Strapi's headless CMS capabilities, including Strapi API integrations, with Cloudinary's robust media APIs.
  • Simplified development: Provides ready-to-use CDN links, eliminating manual optimization steps.

Cloudinary’s integration with Strapi excels for content-rich applications like e-commerce platforms, media-intensive blogs, and complex web applications. For instance, in e-commerce, leveraging AI tools for eCommerce alongside this integration can significantly enhance user experiences.

How to Integrate Cloudinary with Strapi

In this section, we will walk you through the steps required to integrate Cloudinary with your Strapi project.

Prerequisites

Before starting to integrate Cloudinary with Strapi, make sure you have:

  • Node.js version 14 or higher installed
  • NPM or Yarn package manager
  • A Cloudinary account (you can start with a free tier)
  • A Strapi application (version 4 or later)

Setting Up Your Cloudinary Account

  1. Sign up for a Cloudinary account at Cloudinary's website.
  2. Confirm your email address via the verification link sent to you.
  3. Once logged in, you'll be redirected to your account management dashboard.
  4. Locate your API credentials (Cloud Name, API Key, and API Secret) in the dashboard. You'll need these for the integration.

Installing the Cloudinary Provider in Strapi

To integrate Cloudinary with your Strapi project, you need to install the Cloudinary provider package:

npm install @strapi/provider-upload-cloudinary

Or if you prefer using Yarn:

yarn add @strapi/provider-upload-cloudinary

Configuring Environment Variables

Create or edit the .env file at the root of your Strapi project and add the following variables:

CLOUDINARY_NAME=your_cloud_name
CLOUDINARY_KEY=your_api_key
CLOUDINARY_SECRET=your_api_secret

Replace the placeholder values with your actual Cloudinary credentials.

Setting Up Strapi Upload Provider to Integrate with Cloudinary

Create or edit the ./config/plugins.js file in your Strapi project and add the following configuration:

module.exports = ({ env }) => ({
  upload: {
    config: {
      provider: 'cloudinary',
      providerOptions: {
        cloud_name: env('CLOUDINARY_NAME'),
        api_key: env('CLOUDINARY_KEY'),
        api_secret: env('CLOUDINARY_SECRET'),
      },
      actionOptions: {
        upload: {},
        delete: {},
      },
    },
  },
});

This configuration tells Strapi to use Cloudinary as the upload provider instead of the default local storage.

Testing the Integration

1. Restart your Strapi server to apply the changes:

npm run develop

Or with Yarn:

yarn develop

2. Access your Strapi admin panel (typically at http://localhost:1337/admin).
3. Navigate to the Media Library in the sidebar menu.
4. Upload a test image by clicking "Add new assets" and selecting a file from your computer.
5. Verify that the upload was successful:

  • Check for a 200 status code in your terminal.
  • Confirm that the image appears in both your Strapi Media Library and your Cloudinary dashboard.

If you encounter any issues with image previews in the Strapi admin panel, you may need to configure CORS settings in your Cloudinary account:

  1. Go to the Delivery or Access Control section in your Cloudinary dashboard settings.
  2. Add your Strapi domain to the allowed CORS origins.

Additionally, explore new Strapi features like live preview and on-the-fly relations to enhance your development experience.

Let's examine a practical implementation of integrating Cloudinary with Strapi through a media gallery application. Consider a photo-sharing platform where users upload, browse, and share high-quality images. Using Strapi as the content backend and integrating Cloudinary for media processing creates a robust technical foundation that scales effectively.

Example Code Implementation

1. Strapi Content Type Configuration (Gallery Collection)

Create a content type for photo galleries in Strapi:

// Path: src/api/gallery/content-types/gallery/schema.json
{
  "kind": "collectionType",
  "collectionName": "galleries",
  "info": {
    "singularName": "gallery",
    "pluralName": "galleries",
    "displayName": "Photo Gallery",
    "description": "A collection of curated images"
  },
  "options": {
    "draftAndPublish": true
  },
  "attributes": {
    "title": {
      "type": "string",
      "required": true
    },
    "description": {
      "type": "text"
    },
    "photos": {
      "type": "media",
      "multiple": true,
      "required": true,
      "allowedTypes": ["images"]
    },
    "category": {
      "type": "enumeration",
      "enum": ["nature", "architecture", "people", "travel", "abstract"],
      "default": "nature"
    },
    "slug": {
      "type": "uid",
      "targetField": "title"
    },
    "featured": {
      "type": "boolean",
      "default": false
    }
  }
}

2. Frontend Upload Component (React)

This component handles image uploads to Strapi, which then uses Cloudinary for storage:

// ImageUploader.jsx
import React, { useState } from 'react';
import axios from 'axios';

const ImageUploader = ({ galleryId, onUploadComplete }) => {
  const [files, setFiles] = useState([]);
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  const handleFileChange = (e) => {
    setFiles(Array.from(e.target.files));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (files.length === 0) return;

    setUploading(true);
    setProgress(0);
    
    // Create FormData object
    const formData = new FormData();
    files.forEach(file => {
      formData.append('files', file);
    });
    
    // If uploading to a specific gallery
    formData.append('ref', 'api::gallery.gallery');
    formData.append('refId', galleryId);
    formData.append('field', 'photos');

    try {
      const response = await axios.post(`${process.env.REACT_APP_STRAPI_URL}/api/upload`, formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: `Bearer ${localStorage.getItem('token')}`,
        },
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          setProgress(percentCompleted);
        },
      });
      
      onUploadComplete(response.data);
      setFiles([]);
      setUploading(false);
    } catch (error) {
      console.error('Upload failed:', error);
      setUploading(false);
    }
  };

  return (
    <div className="uploader-container">
      <form onSubmit={handleSubmit}>
        <div className="file-input-wrapper">
          <input 
            type="file" 
            multiple 
            accept="image/*" 
            onChange={handleFileChange} 
            disabled={uploading}
          />
          <label>
            {files.length > 0 
              ? `${files.length} file(s) selected` 
              : 'Choose images to upload'}
          </label>
        </div>
        
        {files.length > 0 && (
          <div className="selected-files">
            <h4>Selected Files:</h4>
            <ul>
              {files.map((file, index) => (
                <li key={index}>{file.name} ({Math.round(file.size / 1024)} KB)</li>
              ))}
            </ul>
          </div>
        )}
        
        {uploading && (
          <div className="progress-bar">
            <div 
              className="progress" 
              style={{ width: `${progress}%` }}
            ></div>
            <span>{progress}%</span>
          </div>
        )}
        
        <button 
          type="submit" 
          disabled={files.length === 0 || uploading}
          className="upload-button"
        >
          {uploading ? 'Uploading...' : 'Upload Images'}
        </button>
      </form>
    </div>
  );
};

export default ImageUploader;

3. Displaying Responsive Images with Cloudinary Transformations

Leverage Cloudinary URLs for on-the-fly image transformations:

// ResponsiveGallery.jsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';

const ResponsiveGallery = ({ galleryId }) => {
  const [images, setImages] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchGalleryImages = async () => {
      try {
        const response = await axios.get(
          `${process.env.REACT_APP_STRAPI_URL}/api/galleries/${galleryId}?populate=photos`
        );
        
        if (response.data.data.attributes.photos.data) {
          setImages(response.data.data.attributes.photos.data);
        }
        setLoading(false);
      } catch (error) {
        console.error('Failed to fetch gallery images:', error);
        setLoading(false);
      }
    };

    fetchGalleryImages();
  }, [galleryId]);

  // Function to transform Cloudinary URLs for responsive images
  const getResponsiveImageUrl = (url, width, height, options = {}) => {
    // Extract the base Cloudinary URL
    if (!url || !url.includes('cloudinary')) return url;
    
    // Parse the existing URL to maintain the cloud name and other parameters
    const urlParts = url.split('/upload/');
    if (urlParts.length !== 2) return url;
    
    // Construct transformation string
    let transformation = `c_fill,w_${width},h_${height}`;
    
    // Add quality parameter if specified
    if (options.quality) {
      transformation += `,q_${options.quality}`;
    }
    
    // Add format parameter if specified
    if (options.format) {
      transformation += `,f_${options.format}`;
    }
    
    // Return the transformed URL
    return `${urlParts[0]}/upload/${transformation}/${urlParts[1]}`;
  };

  if (loading) {
    return <div className="loading">Loading gallery...</div>;
  }

  if (images.length === 0) {
    return <div className="no-images">No images found in this gallery</div>;
  }

  return (
    <div className="responsive-gallery">
      <div className="gallery-grid">
        {images.map((image) => {
          const imageUrl = image.attributes.url;
          
          return (
            <div key={image.id} className="gallery-item">
              <picture>
                {/* Mobile devices */}
                <source 
                  media="(max-width: 640px)" 
                  srcSet={getResponsiveImageUrl(imageUrl, 300, 300, { quality: 'auto', format: 'webp' })}
                />
                
                {/* Tablets */}
                <source 
                  media="(max-width: 1024px)" 
                  srcSet={getResponsiveImageUrl(imageUrl, 500, 500, { quality: 'auto', format: 'webp' })}
                />
                
                {/* Desktops */}
                <source 
                  srcSet={getResponsiveImageUrl(imageUrl, 800, 800, { quality: 'auto', format: 'webp' })}
                />
                
                {/* Fallback image */}
                <img 
                  src={getResponsiveImageUrl(imageUrl, 500, 500)}
                  alt={image.attributes.alternativeText || 'Gallery image'} 
                  loading="lazy"
                  className="gallery-image"
                />
              </picture>
              {image.attributes.caption && (
                <div className="image-caption">{image.attributes.caption}</div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default ResponsiveGallery;

4. Server-Side Processing for Album Creation

// Path: src/api/gallery/controllers/gallery.js
'use strict';

/**
 * Custom controller for gallery management
 */

const { createCoreController } = require('@strapi/strapi').factories;

module.exports = createCoreController('api::gallery.gallery', ({ strapi }) => ({
  // Custom create method with metadata extraction
  async create(ctx) {
    try {
      // Extract and validate the request body
      const { title, description, category } = ctx.request.body;
      
      // Create a gallery entry first
      const gallery = await strapi.entityService.create('api::gallery.gallery', {
        data: {
          title,
          description,
          category,
          slug: title.toLowerCase().replace(/\s+/g, '-'),
          publishedAt: new Date(),
        },
      });
      
      return { data: gallery };
    } catch (error) {
      ctx.body = error;
      return ctx.badRequest(`Gallery creation failed: ${error.message}`);
    }
  },
  
  // Custom method to fetch galleries with optimized images
  async findFeatured(ctx) {
    try {
      const galleries = await strapi.entityService.findMany('api::gallery.gallery', {
        filters: { featured: true },
        populate: ['photos'],
        sort: { createdAt: 'desc' },
        limit: 6,
      });
      
      // Transform the response to include optimized preview URLs
      const transformed = galleries.map(gallery => {
        const { id, title, description, slug, photos } = gallery;
        
        // Get cover image (first image or default)
        const coverImage = photos && photos.length > 0 
          ? photos[0] 
          : null;
          
        // Get Cloudinary-optimized thumbnail URL if available
        let thumbnailUrl = null;
        if (coverImage && coverImage.url && coverImage.url.includes('cloudinary')) {
          const urlParts = coverImage.url.split('/upload/');
          thumbnailUrl = `${urlParts[0]}/upload/c_fill,w_400,h_300/${urlParts[1]}`;
        }
        
        return {
          id,
          title,
          description,
          slug,
          photoCount: photos ? photos.length : 0,
          thumbnailUrl
        };
      });
      
      return { data: transformed };
    } catch (error) {
      return ctx.badRequest(`Failed to fetch featured galleries: ${error.message}`);
    }
  }
}));

Strapi Open Office Hours

If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.

For more details, visit the Strapi documentation and the Cloudinary documentation.

Frequently Asked Questions

Before integrating Cloudinary with Strapi, ensure you have Node.js version 14 or higher, NPM or Yarn package manager, a Cloudinary account, and a Strapi application version 4 or later.

To set up your Cloudinary account, sign up at Cloudinary's website, verify your email address, and locate your API credentials (Cloud Name, API Key, and API Secret) in your dashboard. These credentials are necessary for the integration with Strapi.

Install the Cloudinary provider package in your Strapi project using NPM (npm install @strapi/provider-upload-cloudinary) or Yarn (yarn add @strapi/provider-upload-cloudinary).

Create or edit the .env file at the root of your Strapi project and add your Cloudinary credentials (CLOUDINARY_NAME, CLOUDINARY_KEY, CLOUDINARY_SECRET) to it, replacing the placeholder values with your actual Cloudinary credentials.

To use Cloudinary as the upload provider, create or edit the ./config/plugins.js file in your Strapi project with the configuration that specifies Cloudinary as the provider and includes your Cloudinary credentials and options for upload and delete actions.

Advanced configuration options include setting custom folder structures in Cloudinary for organized asset management, specifying transformation options for automatic image processing, and configuring security settings to load assets from Cloudinary's domain securely.

For personalized guidance, participate in Strapi Open Office Hours for direct access to technical experts, or connect with the Strapi community through forums, Discord, and GitHub Issues for technical discussions and problem-solving.