Skip to content

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 DB

Step-by-step

  1. Upload — Multer receives the file in memory (10 MB limit)
  2. Processing — Sharp converts the image to WebP format for optimal compression
  3. Delivery — Image is uploaded to Cloudflare Images via their API
  4. Storage — The Cloudflare URL is stored in the database (on the Activity or Media model)

Upload Endpoints

MethodEndpointDescription
POST/uploadSingle file upload
POST/upload/multipleMultiple file upload
POST/upload/localLocal storage upload (fallback)
POST/upload/cloudflareDirect 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}/public

Variants 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

MethodEndpointDescription
GET/media-libraryList all media
POST/media-libraryAdd to library
DELETE/media-library/:idDelete from library
GET/media-library/entity/:type/:idGet 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.

Built with VitePress