Image Handling
MyEasyGuide uses Cloudflare Images for image delivery and Sharp for server-side image processing.
Image Upload Pipeline
User uploads image → Multer (memory) → Sharp (WebP) → Cloudflare Images → URL stored in DBStep-by-step
- Upload — Multer receives the file in memory (10 MB limit)
- Processing — Sharp converts the image to WebP format for optimal compression
- Delivery — Image is uploaded to Cloudflare Images via their API
- Storage — The Cloudflare URL is stored in the database (on the Activity or Media model)
Upload Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /upload | Single file upload |
| POST | /upload/multiple | Multiple file upload |
| POST | /upload/local | Local storage upload (fallback) |
| POST | /upload/cloudflare | Direct Cloudflare upload |
Response
json
{
"url": "https://imagedelivery.net/{hash}/{imageId}/public",
"filename": "everest-trek.jpg"
}Cloudflare Images
Configuration
bash
CF_ACCOUNT_ID=your-cloudflare-account-id
CF_API_TOKEN=your-cloudflare-api-token
CF_ACCOUNT_HASH=your-account-hash
IMAGE_BASE_URL=https://imagedelivery.net/{hash}URL Format
https://imagedelivery.net/{CF_ACCOUNT_HASH}/{imageId}/publicVariants can be specified for different sizes (the /public variant is used by default).
Frontend Configuration
The frontend's next.config.ts includes Cloudflare Images in the remote patterns:
typescript
images: {
remotePatterns: [
{
protocol: "https",
hostname: "imagedelivery.net",
},
{
protocol: "https",
hostname: "ik.imagekit.io", // ImageKit (secondary)
},
],
},Sharp Processing
File: src/lib/sharp.ts
Sharp processes uploaded images before they are sent to Cloudflare:
typescript
import sharp from "sharp";
export async function toWebP(buffer: Buffer): Promise<Buffer> {
return sharp(buffer)
.webp({ quality: 80 })
.toBuffer();
}Benefits:
- ~30% file size reduction compared to JPEG
- Modern format with broad browser support
- Maintains quality at lower file sizes
Local Storage
As a fallback, images can be stored locally:
- Directory:
public/uploads/ - URL:
{UPLOADS_BASE_URL}/uploads/{filename} - Served via: Express static file middleware
v1Router.use(
"/uploads",
express.static(path.join(process.cwd(), "public", "uploads"))
);Media Library
The Media model tracks uploaded files:
prisma
model Media {
id String @id @default(cuid())
url String // Cloudflare or local URL
filename String
mimeType String?
fileSize Int?
category MediaCategory @default(UNRELATED) // UNRELATED or SUPPLIER_DOC
relatedId String? // Polymorphic reference to entity
}Media Library Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /media-library | List all media |
| POST | /media-library | Add to library |
| DELETE | /media-library/:id | Delete from library |
| GET | /media-library/entity/:type/:id | Get images for entity |
Activity Images
Activities store images as a String[] field — an array of Cloudflare URLs:
prisma
model Activity {
...
images String[] @default([])
...
}These images are rendered in the activity gallery on the frontend.