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:
1npm install @strapi/provider-upload-cloudinary
Or if you prefer using Yarn:
1yarn 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:
1CLOUDINARY_NAME=your_cloud_name
2CLOUDINARY_KEY=your_api_key
3CLOUDINARY_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:
1module.exports = ({ env }) => ({
2 upload: {
3 config: {
4 provider: 'cloudinary',
5 providerOptions: {
6 cloud_name: env('CLOUDINARY_NAME'),
7 api_key: env('CLOUDINARY_KEY'),
8 api_secret: env('CLOUDINARY_SECRET'),
9 },
10 actionOptions: {
11 upload: {},
12 delete: {},
13 },
14 },
15 },
16});
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:
1npm run develop
Or with Yarn:
1yarn 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:
- 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:
1// Path: src/api/gallery/content-types/gallery/schema.json
2{
3 "kind": "collectionType",
4 "collectionName": "galleries",
5 "info": {
6 "singularName": "gallery",
7 "pluralName": "galleries",
8 "displayName": "Photo Gallery",
9 "description": "A collection of curated images"
10 },
11 "options": {
12 "draftAndPublish": true
13 },
14 "attributes": {
15 "title": {
16 "type": "string",
17 "required": true
18 },
19 "description": {
20 "type": "text"
21 },
22 "photos": {
23 "type": "media",
24 "multiple": true,
25 "required": true,
26 "allowedTypes": ["images"]
27 },
28 "category": {
29 "type": "enumeration",
30 "enum": ["nature", "architecture", "people", "travel", "abstract"],
31 "default": "nature"
32 },
33 "slug": {
34 "type": "uid",
35 "targetField": "title"
36 },
37 "featured": {
38 "type": "boolean",
39 "default": false
40 }
41 }
42}
2. Frontend Upload Component (React)
This component handles image uploads to Strapi, which then uses Cloudinary for storage:
1// ImageUploader.jsx
2import React, { useState } from 'react';
3import axios from 'axios';
4
5const ImageUploader = ({ galleryId, onUploadComplete }) => {
6 const [files, setFiles] = useState([]);
7 const [uploading, setUploading] = useState(false);
8 const [progress, setProgress] = useState(0);
9
10 const handleFileChange = (e) => {
11 setFiles(Array.from(e.target.files));
12 };
13
14 const handleSubmit = async (e) => {
15 e.preventDefault();
16 if (files.length === 0) return;
17
18 setUploading(true);
19 setProgress(0);
20
21 // Create FormData object
22 const formData = new FormData();
23 files.forEach(file => {
24 formData.append('files', file);
25 });
26
27 // If uploading to a specific gallery
28 formData.append('ref', 'api::gallery.gallery');
29 formData.append('refId', galleryId);
30 formData.append('field', 'photos');
31
32 try {
33 const response = await axios.post(`${process.env.REACT_APP_STRAPI_URL}/api/upload`, formData, {
34 headers: {
35 'Content-Type': 'multipart/form-data',
36 Authorization: `Bearer ${localStorage.getItem('token')}`,
37 },
38 onUploadProgress: (progressEvent) => {
39 const percentCompleted = Math.round(
40 (progressEvent.loaded * 100) / progressEvent.total
41 );
42 setProgress(percentCompleted);
43 },
44 });
45
46 onUploadComplete(response.data);
47 setFiles([]);
48 setUploading(false);
49 } catch (error) {
50 console.error('Upload failed:', error);
51 setUploading(false);
52 }
53 };
54
55 return (
56 <div className="uploader-container">
57 <form onSubmit={handleSubmit}>
58 <div className="file-input-wrapper">
59 <input
60 type="file"
61 multiple
62 accept="image/*"
63 onChange={handleFileChange}
64 disabled={uploading}
65 />
66 <label>
67 {files.length > 0
68 ? `${files.length} file(s) selected`
69 : 'Choose images to upload'}
70 </label>
71 </div>
72
73 {files.length > 0 && (
74 <div className="selected-files">
75 <h4>Selected Files:</h4>
76 <ul>
77 {files.map((file, index) => (
78 <li key={index}>{file.name} ({Math.round(file.size / 1024)} KB)</li>
79 ))}
80 </ul>
81 </div>
82 )}
83
84 {uploading && (
85 <div className="progress-bar">
86 <div
87 className="progress"
88 style={{ width: `${progress}%` }}
89 ></div>
90 <span>{progress}%</span>
91 </div>
92 )}
93
94 <button
95 type="submit"
96 disabled={files.length === 0 || uploading}
97 className="upload-button"
98 >
99 {uploading ? 'Uploading...' : 'Upload Images'}
100 </button>
101 </form>
102 </div>
103 );
104};
105
106export default ImageUploader;
3. Displaying Responsive Images with Cloudinary Transformations
Leverage Cloudinary URLs for on-the-fly image transformations:
1// ResponsiveGallery.jsx
2import React, { useEffect, useState } from 'react';
3import axios from 'axios';
4
5const ResponsiveGallery = ({ galleryId }) => {
6 const [images, setImages] = useState([]);
7 const [loading, setLoading] = useState(true);
8
9 useEffect(() => {
10 const fetchGalleryImages = async () => {
11 try {
12 const response = await axios.get(
13 `${process.env.REACT_APP_STRAPI_URL}/api/galleries/${galleryId}?populate=photos`
14 );
15
16 if (response.data.data.attributes.photos.data) {
17 setImages(response.data.data.attributes.photos.data);
18 }
19 setLoading(false);
20 } catch (error) {
21 console.error('Failed to fetch gallery images:', error);
22 setLoading(false);
23 }
24 };
25
26 fetchGalleryImages();
27 }, [galleryId]);
28
29 // Function to transform Cloudinary URLs for responsive images
30 const getResponsiveImageUrl = (url, width, height, options = {}) => {
31 // Extract the base Cloudinary URL
32 if (!url || !url.includes('cloudinary')) return url;
33
34 // Parse the existing URL to maintain the cloud name and other parameters
35 const urlParts = url.split('/upload/');
36 if (urlParts.length !== 2) return url;
37
38 // Construct transformation string
39 let transformation = `c_fill,w_${width},h_${height}`;
40
41 // Add quality parameter if specified
42 if (options.quality) {
43 transformation += `,q_${options.quality}`;
44 }
45
46 // Add format parameter if specified
47 if (options.format) {
48 transformation += `,f_${options.format}`;
49 }
50
51 // Return the transformed URL
52 return `${urlParts[0]}/upload/${transformation}/${urlParts[1]}`;
53 };
54
55 if (loading) {
56 return <div className="loading">Loading gallery...</div>;
57 }
58
59 if (images.length === 0) {
60 return <div className="no-images">No images found in this gallery</div>;
61 }
62
63 return (
64 <div className="responsive-gallery">
65 <div className="gallery-grid">
66 {images.map((image) => {
67 const imageUrl = image.attributes.url;
68
69 return (
70 <div key={image.id} className="gallery-item">
71 <picture>
72 {/* Mobile devices */}
73 <source
74 media="(max-width: 640px)"
75 srcSet={getResponsiveImageUrl(imageUrl, 300, 300, { quality: 'auto', format: 'webp' })}
76 />
77
78 {/* Tablets */}
79 <source
80 media="(max-width: 1024px)"
81 srcSet={getResponsiveImageUrl(imageUrl, 500, 500, { quality: 'auto', format: 'webp' })}
82 />
83
84 {/* Desktops */}
85 <source
86 srcSet={getResponsiveImageUrl(imageUrl, 800, 800, { quality: 'auto', format: 'webp' })}
87 />
88
89 {/* Fallback image */}
90 <img
91 src={getResponsiveImageUrl(imageUrl, 500, 500)}
92 alt={image.attributes.alternativeText || 'Gallery image'}
93 loading="lazy"
94 className="gallery-image"
95 />
96 </picture>
97 {image.attributes.caption && (
98 <div className="image-caption">{image.attributes.caption}</div>
99 )}
100 </div>
101 );
102 })}
103 </div>
104 </div>
105 );
106};
107
108export default ResponsiveGallery;
4. Server-Side Processing for Album Creation
1// Path: src/api/gallery/controllers/gallery.js
2'use strict';
3
4/**
5 * Custom controller for gallery management
6 */
7
8const { createCoreController } = require('@strapi/strapi').factories;
9
10module.exports = createCoreController('api::gallery.gallery', ({ strapi }) => ({
11 // Custom create method with metadata extraction
12 async create(ctx) {
13 try {
14 // Extract and validate the request body
15 const { title, description, category } = ctx.request.body;
16
17 // Create a gallery entry first
18 const gallery = await strapi.entityService.create('api::gallery.gallery', {
19 data: {
20 title,
21 description,
22 category,
23 slug: title.toLowerCase().replace(/\s+/g, '-'),
24 publishedAt: new Date(),
25 },
26 });
27
28 return { data: gallery };
29 } catch (error) {
30 ctx.body = error;
31 return ctx.badRequest(`Gallery creation failed: ${error.message}`);
32 }
33 },
34
35 // Custom method to fetch galleries with optimized images
36 async findFeatured(ctx) {
37 try {
38 const galleries = await strapi.entityService.findMany('api::gallery.gallery', {
39 filters: { featured: true },
40 populate: ['photos'],
41 sort: { createdAt: 'desc' },
42 limit: 6,
43 });
44
45 // Transform the response to include optimized preview URLs
46 const transformed = galleries.map(gallery => {
47 const { id, title, description, slug, photos } = gallery;
48
49 // Get cover image (first image or default)
50 const coverImage = photos && photos.length > 0
51 ? photos[0]
52 : null;
53
54 // Get Cloudinary-optimized thumbnail URL if available
55 let thumbnailUrl = null;
56 if (coverImage && coverImage.url && coverImage.url.includes('cloudinary')) {
57 const urlParts = coverImage.url.split('/upload/');
58 thumbnailUrl = `${urlParts[0]}/upload/c_fill,w_400,h_300/${urlParts[1]}`;
59 }
60
61 return {
62 id,
63 title,
64 description,
65 slug,
66 photoCount: photos ? photos.length : 0,
67 thumbnailUrl
68 };
69 });
70
71 return { data: transformed };
72 } catch (error) {
73 return ctx.badRequest(`Failed to fetch featured galleries: ${error.message}`);
74 }
75 }
76}));
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.