These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
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.
Keep in touch with the latest Strapi and Cloudinary updates
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
- Sign up for a Cloudinary account at Cloudinary's website.
- Confirm your email address via the verification link sent to you.
- Once logged in, you'll be redirected to your account management dashboard.
- 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-cloudinaryOr if you prefer using Yarn:
yarn add @strapi/provider-upload-cloudinaryConfiguring 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_secretReplace 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 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:
- 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:
- Go to the Delivery or Access Control section in your Cloudinary dashboard settings.
- 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.
Keep in touch with the latest Strapi and Cloudinary updates
Project Example: How to Integrate Cloudinary with Strapi in a Media Gallery Application
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.