- Cloudinary: Primary image storage and transformation service
- Handles image transformations, optimization, and CDN delivery
- Used for all public-facing images
- Active Storage: Database tracking for image attachments
- Manages relationships between images and models
- Handles local file storage during upload process
- Service Worker: Offline caching and progressive loading
- Caches Cloudinary URLs and responses
- Manages offline fallback images
- IndexedDB: Offline image upload queue
- Stores pending uploads when offline
- Manages local image previews
app/
├── javascript/
│ ├── components/
│ │ ├── images/
│ │ │ ├── LazyImage.tsx # Progressive loading component
│ │ │ │ # Uses: Cloudinary + Service Worker
│ │ │ │ # Purpose: Optimized image loading with fallbacks
│ │ │ │
│ │ │ └── ImageUploader.tsx # Drag & drop upload component
│ │ │ # Uses: Active Storage + Cloudinary + IndexedDB
│ │ │ # Purpose: Handles file uploads with offline support
│ │ │
│ │ └── common/
│ │ └── Avatar.tsx # Reusable avatar component
│ │ # Uses: Cloudinary + Service Worker
│ │ # Purpose: Displays user avatars with caching
│ │
│ ├── services/
│ │ ├── imageUpload.ts # Image upload and transformation service
│ │ │ # Uses: Cloudinary API
│ │ │ # Purpose: Handles image transformations and uploads
│ │ │
│ │ └── offlineImageService.ts # Offline storage handling
│ │ # Uses: IndexedDB
│ │ # Purpose: Manages offline image queue
│ │
│ └── service-worker.ts # Service worker for caching
│ # Uses: Service Worker + Cloudinary Cache
│ # Purpose: Offline image availability
│
└── services/
├── image_service.rb # Server-side image processing
│ # Uses: Cloudinary API
│ # Purpose: Server-side image optimizations
│
└── default_image_service.rb # Default image management
# Uses: Cloudinary + Active Storage
# Purpose: Manages fallback images
Location: app/javascript/components/images/LazyImage.tsx
Technology Stack:
- Primary: Cloudinary (image delivery and transformations)
- Secondary: Service Worker (offline caching)
- Supporting: React (UI rendering)
Purpose:
- Progressive image loading with placeholders
- Automatic error handling
- Optimized image loading with caching
- Responsive image delivery
Implementation Notes:
import React, { useState, useEffect } from 'react'
// Uses Cloudinary URL generation
import { generateImageUrl } from '../../services/imageUpload'
interface Props {
publicId: string // Cloudinary public ID
width?: number // Cloudinary transformation parameter
height?: number // Cloudinary transformation parameter
quality?: string // Cloudinary quality setting
alt?: string
className?: string
onLoad?: () => void
}
const LazyImage: React.FC<Props> = ({
publicId, // Cloudinary resource identifier
width,
height,
quality = 'auto', // Cloudinary auto-quality
alt = '',
className = '',
onLoad
}) => {
const [loaded, setLoaded] = useState(false)
const [error, setError] = useState(false)
useEffect(() => {
const img = new Image()
img.src = generateImageUrl(publicId, { width, height, quality })
img.onload = () => {
setLoaded(true)
onLoad?.()
}
img.onerror = () => {
setError(true)
}
}, [publicId, width, height, quality, onLoad])
return (
<div className={`lazy-image ${loaded ? 'loaded' : ''} ${className}`}>
<img
src={generateImageUrl(publicId, { width, height, quality })}
alt={alt}
style={{ opacity: loaded ? 1 : 0 }}
/>
{!loaded && <div className="image-placeholder" />}
</div>
)
}
Location: app/javascript/components/images/ImageUploader.tsx
Technology Stack:
- Primary: Active Storage (initial upload handling)
- Secondary: Cloudinary (final storage and optimization)
- Supporting:
- IndexedDB (offline storage)
- React Dropzone (file input)
- Service Worker (cache management)
Purpose:
- Handle file uploads with drag & drop
- Manage offline uploads
- Process and optimize images
- Provide upload status feedback
Implementation Notes:
import React, { useState, useCallback, useEffect } from 'react'
import { useDropzone } from 'react-dropzone'
// Uses both Cloudinary and Active Storage
import { uploadImage } from '../../services/imageUpload'
// Uses IndexedDB for offline storage
import { offlineImageService } from '../../services/offlineImageService'
const ImageUploader: React.FC<Props> = ({ type, onUpload }) => {
const onDrop = useCallback(async (acceptedFiles: File[]) => {
const file = acceptedFiles[0]
if (!navigator.onLine) {
// Uses IndexedDB for offline storage
const localUrl = await offlineImageService.storePendingUpload(file, type)
onUpload(localUrl)
} else {
// Uses Active Storage for upload, then Cloudinary for storage
const response = await uploadImage(file, type)
onUpload(response.url) // Cloudinary URL
}
}, [type, onUpload])
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
{/* Drag & drop UI */}
</div>
)
}
Location: app/javascript/services/offlineImageService.ts
Technology Stack:
- Primary: IndexedDB (client-side storage)
- Supporting:
- URL.createObjectURL (local preview)
- Promise-based async handling
Purpose:
- Store pending uploads during offline mode
- Manage upload queue
- Handle local image previews
- Sync with server when online
Implementation Notes:
import { openDB } from 'idb'
// Pure IndexedDB implementation for offline storage
class OfflineImageService {
private db: Promise<IDBPDatabase>
constructor() {
this.db = this.initDB()
}
// Stores files locally until online
async storePendingUpload(file: File, type: 'avatar' | 'gallery'): Promise<string> {
const db = await this.db
const localUrl = URL.createObjectURL(file)
await db.add('pending-uploads', {
file,
type,
localUrl,
timestamp: Date.now()
})
return localUrl
}
async processPendingUploads(uploadFn: (file: File, type: string) => Promise<string>) {
const db = await this.db
const pending = await db.getAll('pending-uploads')
for (const upload of pending) {
await uploadFn(upload.file, upload.type)
await db.delete('pending-uploads', upload.id)
URL.revokeObjectURL(upload.localUrl)
}
}
}
Location: app/services/image_service.rb
Technology Stack:
- Primary: Cloudinary API (image processing)
- Secondary: Ruby/Rails (service layer)
- Supporting: Active Storage (temporary storage)
Purpose:
- Optimize images for different use cases
- Generate multiple image variants
- Apply face detection for avatars
- Ensure consistent image quality
Implementation Notes:
class ImageService
# Direct Cloudinary API usage for image optimization
def self.optimize_avatar(image)
Cloudinary::Uploader.upload(
image,
folder: 'avatars',
transformation: [
# Cloudinary-specific transformations
{ width: 500, height: 500, crop: :fill, gravity: :face },
{ quality: 'auto:good', fetch_format: :auto }
],
eager: [
# Cloudinary eager transformations for different sizes
{ width: 100, height: 100, crop: :thumb, gravity: :face },
{ width: 300, height: 300, crop: :thumb, gravity: :face }
]
)
end
def self.optimize_gallery_image(image)
Cloudinary::Uploader.upload(
image,
folder: 'gallery',
transformation: [
{ width: 1200, height: 1200, crop: :limit },
{ quality: 'auto:good', fetch_format: :auto }
]
)
end
end
Location: app/services/default_image_service.rb
Technology Stack:
- Primary: Cloudinary (image storage)
- Secondary: Active Storage (local file handling)
- Supporting: SVG (default image format)
Purpose:
- Provide fallback images
- Ensure default assets are available
- Manage image transformations
- Handle missing image scenarios
Implementation Notes:
class DefaultImageService
# Maps local files to Cloudinary public IDs
DEFAULT_IMAGES = {
avatar: {
url: 'app/assets/images/default-avatar.svg', # Local file
public_id: 'defaults/avatar' # Cloudinary ID
},
profile: {
url: 'app/assets/images/default-profile.svg', # Local file
public_id: 'defaults/profile' # Cloudinary ID
}
}
def self.ensure_default_images
DEFAULT_IMAGES.each do |type, image_data|
ensure_default_image(type, image_data)
end
end
private
def self.upload_default_image(type, image_data)
Cloudinary::Uploader.upload(
image_data[:url],
public_id: image_data[:public_id],
overwrite: true,
transformation: default_image_transformations(type)
)
end
end
Location: app/models/user.rb
Technology Stack:
- Primary: Active Storage (attachment management)
- Secondary: Cloudinary (image storage)
- Supporting: Rails URL helpers
Purpose:
- Manage user image associations
- Handle image attachment logic
- Provide fallback mechanisms
- Generate appropriate URLs
Implementation Notes:
class User < ApplicationRecord
# Active Storage attachment
has_one_attached :avatar
def avatar_url
if avatar.attached?
# Uses Active Storage URL helper
Rails.application.routes.url_helpers.url_for(avatar)
else
# Falls back to Cloudinary default image
DefaultImageService.get_default_image_url(:avatar)
end
end
def profile_image_url
if profile_image.attached?
Rails.application.routes.url_helpers.url_for(profile_image)
else
DefaultImageService.get_default_image_url(:profile)
end
end
end
# Technology Stack: Active Storage → Cloudinary → Service Worker
class ProfileImageUploader
def upload(file)
# 1. Active Storage: Initial upload
attachment = user.avatar.attach(file)
# 2. Cloudinary: Process and store
cloudinary_url = ImageService.optimize_avatar(attachment)
# 3. Service Worker: Cache for offline
cache_image(cloudinary_url)
# 4. IndexedDB: Store metadata
store_image_metadata(cloudinary_url)
end
end
// Technology Stack: Service Worker → IndexedDB → Cloudinary
const ImageDisplay: React.FC<Props> = ({ imageId }) => {
const [imageUrl, setImageUrl] = useState<string>()
useEffect(() => {
// 1. Try Service Worker cache
const cached = await caches.match(`/images/${imageId}`)
if (cached) return setImageUrl(cached)
// 2. Try IndexedDB
const stored = await offlineImageService.getImage(imageId)
if (stored) return setImageUrl(stored)
// 3. Fallback to Cloudinary
const cloudinaryUrl = generateImageUrl(imageId)
setImageUrl(cloudinaryUrl)
}, [imageId])
return <LazyImage src={imageUrl} />
}
- Automatic format conversion (WebP, AVIF)
- Face detection for avatars
- Responsive image sizing
- Quality optimization
- CDN delivery
- Database tracking of attachments
- Model associations
- Local file handling
- Temporary storage during upload
- Offline image caching
- Network request interception
- Cache management
- Fallback image serving
- Offline file storage
- Upload queue management
- Local URL generation
- Automatic sync when online
- Automatic format selection based on browser support
- Dynamic quality adjustment
- Responsive image generation
- Metadata stripping
- Background job processing
- Variant caching
- Database indexing
- Blob deduplication
- Selective caching strategies
- Cache size management
- Network-first for fresh content
- Cache-first for static assets
- Batch processing of pending uploads
- Automatic cleanup of old entries
- Memory-efficient blob handling
- Upload retry management
Location: app/javascript/service-worker.ts
const IMAGE_CACHE_NAME = 'cloudinary-images-v1'
self.addEventListener('fetch', (event: FetchEvent) => {
if (event.request.url.includes('res.cloudinary.com')) {
event.respondWith(
caches.open(IMAGE_CACHE_NAME).then((cache) => {
return cache.match(event.request).then((response) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone())
return networkResponse
})
return response || fetchPromise
})
})
)
}
})