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 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.
Integrating Cloudinary with Strapi creates a powerful technical foundation with several key benefits:
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.
In this section, we will walk you through the steps required to integrate Cloudinary with your Strapi project.
Before starting to integrate Cloudinary with Strapi, make sure you have:
To integrate Cloudinary with your Strapi project, you need to install the Cloudinary provider package:
npm install @strapi/provider-upload-cloudinaryOr if you prefer using Yarn:
yarn add @strapi/provider-upload-cloudinaryCreate 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_secretReplace the placeholder values with your actual Cloudinary credentials.
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.
1. Restart your Strapi server to apply the changes:
npm run developOr with Yarn:
yarn develop2. 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:
If you encounter any issues with image previews in the Strapi admin panel, you may need to configure CORS settings in your Cloudinary account:
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.
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}`);
}
}
}));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.
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.