Documentation Index Fetch the complete documentation index at: https://mintlify.com/pv-pushkarverma/SkillRise/llms.txt
Use this file to discover all available pages before exploring further.
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:
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
Create Cloudinary Account
Go to Cloudinary
Sign up for a free account
Free tier includes:
25GB storage
25GB bandwidth/month
25,000 transformations/month
Get API Credentials
Log in to Cloudinary Console
On the dashboard, you’ll see:
Cloud Name
API Key
API Secret
Click “Reveal API Secret” to view the secret
Copy all three values
Configure Environment Variables
Add the credentials to server/.env: CLOUDINARY_NAME=your_cloud_name
CLOUDINARY_API_KEY=123456789012345
CLOUDINARY_SECRET_KEY=your_api_secret
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:
import connectCloudinary from './configs/cloudinary.js'
await connectCloudinary ()
Upload Configuration
SkillRise uses Multer for handling multipart/form-data uploads:
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 >
)
}
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"
/>
)
}
Transformation URL Parameter Example Resize width w_400400px width Resize height h_300300px height Crop c_fillFill area, crop excess Quality q_autoAuto quality optimization Format f_autoAuto format (WebP, AVIF) Gravity g_faceFocus on faces Radius r_20Rounded corners (20px)
// 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
Recommended Limits
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
Upload fails with 'Invalid API key'
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)
File upload returns 413 Payload Too Large
Check Multer fileSize limit (default 10MB)
Verify Cloudinary account limits
Compress images before uploading
Transformations not working
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
Cloudinary Docs Official Cloudinary documentation
Node.js SDK Node.js SDK reference
Image Transformations Transformation guide
Upload Widget Upload widget for direct uploads