Skip to main content

Overview

SkillRise uses Cloudinary for media asset management. Educators can upload course thumbnails and other media files, which are stored and optimized by Cloudinary.

Features

  • Image uploads: Course thumbnails and profile pictures
  • Automatic optimization: Cloudinary optimizes images for web delivery
  • CDN delivery: Fast global content delivery
  • Transformations: Resize, crop, and format images on-the-fly
  • Secure uploads: Signed upload requests

Environment Variables

Add these to your server/.env file:
server/.env
CLOUDINARY_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_SECRET_KEY=your_api_secret
Get your credentials from the Cloudinary Console. They’re displayed on your dashboard home page.

Setup Instructions

1

Create Cloudinary Account

  1. Go to Cloudinary
  2. Sign up for a free account
  3. Free tier includes:
    • 25GB storage
    • 25GB bandwidth/month
    • 25,000 transformations/month
2

Get API Credentials

  1. Log in to Cloudinary Console
  2. On the dashboard, you’ll see:
    • Cloud Name
    • API Key
    • API Secret
  3. Click “Reveal API Secret” to view the secret
  4. Copy all three values
3

Configure Environment Variables

Add the credentials to server/.env:
CLOUDINARY_NAME=your_cloud_name
CLOUDINARY_API_KEY=123456789012345
CLOUDINARY_SECRET_KEY=your_api_secret
4

Install Dependencies

cd server
npm install cloudinary multer

Configuration

Initialize Cloudinary in your server:
server/configs/cloudinary.js
import { v2 as cloudinary } from 'cloudinary'

const connectCloudinary = async () => {
  cloudinary.config({
    cloud_name: process.env.CLOUDINARY_NAME,
    api_key: process.env.CLOUDINARY_API_KEY,
    api_secret: process.env.CLOUDINARY_SECRET_KEY,
  })
}

export default connectCloudinary
Initialize on server start:
server/server.js
import connectCloudinary from './configs/cloudinary.js'

await connectCloudinary()

Upload Configuration

SkillRise uses Multer for handling multipart/form-data uploads:
server/configs/multer.js
import multer from 'multer'

// Use memory storage (files stored in memory as Buffer)
const storage = multer.memoryStorage()

const upload = multer({
  storage,
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB limit
  },
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp']
    
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true)
    } else {
      cb(new Error('Only JPEG, PNG, and WebP images are allowed'))
    }
  },
})

export default upload

Upload Implementation

Backend Upload Handler

server/controllers/educatorController.js
import { v2 as cloudinary } from 'cloudinary'
import upload from '../configs/multer.js'

export const uploadCourseThumbnail = async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ 
        success: false, 
        message: 'No file uploaded' 
      })
    }

    // Upload to Cloudinary
    const result = await new Promise((resolve, reject) => {
      const uploadStream = cloudinary.uploader.upload_stream(
        {
          folder: 'skillrise/course-thumbnails',
          transformation: [
            { width: 800, height: 450, crop: 'fill' },
            { quality: 'auto' },
            { fetch_format: 'auto' },
          ],
        },
        (error, result) => {
          if (error) reject(error)
          else resolve(result)
        }
      )
      uploadStream.end(req.file.buffer)
    })

    res.json({
      success: true,
      url: result.secure_url,
      publicId: result.public_id,
    })
  } catch (error) {
    console.error('Upload error:', error)
    res.status(500).json({ 
      success: false, 
      message: 'Failed to upload image' 
    })
  }
}
Register route:
server/routes/educatorRoutes.js
import upload from '../configs/multer.js'
import { uploadCourseThumbnail } from '../controllers/educatorController.js'
import { protectEducator } from '../middlewares/authMiddleware.js'

router.post(
  '/upload-thumbnail',
  protectEducator,
  upload.single('thumbnail'),
  uploadCourseThumbnail
)

Frontend Upload Component

client/src/components/ThumbnailUpload.jsx
import { useState } from 'react'

function ThumbnailUpload({ onUploadComplete }) {
  const [uploading, setUploading] = useState(false)
  const [preview, setPreview] = useState(null)

  const handleFileChange = async (e) => {
    const file = e.target.files[0]
    if (!file) return

    // Show preview
    setPreview(URL.createObjectURL(file))

    // Upload to server
    setUploading(true)
    const formData = new FormData()
    formData.append('thumbnail', file)

    try {
      const response = await fetch('/api/educator/upload-thumbnail', {
        method: 'POST',
        body: formData,
      })
      const data = await response.json()

      if (data.success) {
        onUploadComplete(data.url)
      }
    } catch (error) {
      console.error('Upload failed:', error)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div>
      <input
        type="file"
        accept="image/*"
        onChange={handleFileChange}
        disabled={uploading}
      />
      {preview && (
        <img 
          src={preview} 
          alt="Preview" 
          className="w-48 h-27 object-cover rounded" 
        />
      )}
      {uploading && <p>Uploading...</p>}
    </div>
  )
}

Image Transformations

Cloudinary supports on-the-fly transformations via URL parameters:

Responsive Images

function CourseThumbnail({ url }) {
  // Original: https://res.cloudinary.com/demo/image/upload/sample.jpg
  
  // Optimized versions:
  const thumbnail = url.replace('/upload/', '/upload/w_400,h_225,c_fill,q_auto,f_auto/')
  const card = url.replace('/upload/', '/upload/w_800,h_450,c_fill,q_auto,f_auto/')
  const hero = url.replace('/upload/', '/upload/w_1920,h_1080,c_fill,q_auto,f_auto/')

  return (
    <img
      srcSet={`
        ${thumbnail} 400w,
        ${card} 800w,
        ${hero} 1920w
      `}
      sizes="(max-width: 640px) 400px, (max-width: 1024px) 800px, 1920px"
      src={card}
      alt="Course thumbnail"
    />
  )
}

Common Transformations

TransformationURL ParameterExample
Resize widthw_400400px width
Resize heighth_300300px height
Cropc_fillFill area, crop excess
Qualityq_autoAuto quality optimization
Formatf_autoAuto format (WebP, AVIF)
Gravityg_faceFocus on faces
Radiusr_20Rounded corners (20px)

Transformation Examples

// Avatar - circular crop, 200x200
const avatarUrl = cloudinaryUrl.replace(
  '/upload/',
  '/upload/w_200,h_200,c_fill,g_face,r_max,q_auto,f_auto/'
)

// Card thumbnail - 16:9 ratio, 800x450
const cardUrl = cloudinaryUrl.replace(
  '/upload/',
  '/upload/w_800,h_450,c_fill,q_auto,f_auto/'
)

// Hero image - 1920x1080, blur background
const heroUrl = cloudinaryUrl.replace(
  '/upload/',
  '/upload/w_1920,h_1080,c_fill,e_blur:300,q_auto,f_auto/'
)

Organize Assets with Folders

Organize uploads by type:
const uploadOptions = {
  folder: 'skillrise/course-thumbnails',    // Course thumbnails
  folder: 'skillrise/user-avatars',         // User profile pictures
  folder: 'skillrise/course-content',       // Course materials
}
View folders in Cloudinary Console → Media Library.

Delete Assets

Delete old images when updating:
import { v2 as cloudinary } from 'cloudinary'

export const deleteImage = async (publicId) => {
  try {
    await cloudinary.uploader.destroy(publicId)
    console.log(`Deleted image: ${publicId}`)
  } catch (error) {
    console.error('Delete failed:', error)
  }
}

// Usage:
await deleteImage('skillrise/course-thumbnails/abc123')

Signed Uploads (Advanced)

For direct browser-to-Cloudinary uploads (bypassing your server):
server/routes/educatorRoutes.js
import { v2 as cloudinary } from 'cloudinary'

router.get('/upload-signature', protectEducator, (req, res) => {
  const timestamp = Math.round(new Date().getTime() / 1000)
  const signature = cloudinary.utils.api_sign_request(
    {
      timestamp,
      folder: 'skillrise/course-thumbnails',
    },
    process.env.CLOUDINARY_SECRET_KEY
  )

  res.json({
    timestamp,
    signature,
    cloudName: process.env.CLOUDINARY_NAME,
    apiKey: process.env.CLOUDINARY_API_KEY,
  })
})
Frontend:
const { timestamp, signature, cloudName, apiKey } = await fetchSignature()

const formData = new FormData()
formData.append('file', file)
formData.append('timestamp', timestamp)
formData.append('signature', signature)
formData.append('api_key', apiKey)
formData.append('folder', 'skillrise/course-thumbnails')

const response = await fetch(
  `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`,
  { method: 'POST', body: formData }
)

File Size Limits

Free Tier Limits

  • File size: 10MB per file
  • Video length: 100MB (not applicable for SkillRise images)
  • Monthly bandwidth: 25GB
Set reasonable limits in Multer config:
const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 10 * 1024 * 1024,        // 10MB
    files: 1,                          // 1 file per request
  },
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp']
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true)
    } else {
      cb(new Error('Invalid file type'))
    }
  },
})

Common Issues

  • Verify CLOUDINARY_API_KEY and CLOUDINARY_SECRET_KEY are correct
  • Check that you’ve copied the values from the correct cloud name
  • Ensure there are no extra spaces or quotes in .env file
  • Verify the URL starts with https://res.cloudinary.com/
  • Check browser console for CORS errors
  • Ensure the image was uploaded successfully (check Cloudinary Media Library)
  • Check Multer fileSize limit (default 10MB)
  • Verify Cloudinary account limits
  • Compress images before uploading
  • Ensure transformation params are in the correct format
  • Check for typos in parameter names
  • Verify transformations are placed after /upload/ in the URL

Best Practices

Optimize Images

Always use q_auto,f_auto for automatic quality and format optimization.

Use Folders

Organize assets by type (thumbnails, avatars, content) for easier management.

Set Limits

Enforce file size and type limits to prevent abuse and storage bloat.

Delete Old Files

Remove old images when updating to save storage and bandwidth.

Resources