π Set Up a CDN for Your Product Images with Cloudflare R2 + Workers + Vercel
Managing a structured media repo (like product images in category folders) and serving them via CDN can boost load times, improve SEO, and simplify frontend integration. In this guide, weβll walk you through:
Goal: Store images like
/tablets/paracetamol.png
in Cloudflare R2 and serve them fromhttps://cdn.mppharmaceuticals.com/tablets/paracetamol.png
.
β Step 1: Organize Your Media Repository
Structure your media folder locally like this:
media/
βββ tablets/
β βββ paracetamol.png
β βββ ibuprofen.png
βββ syrups/
β βββ coughx.png
βββ injections/
βββ insulin.png
Keep file and folder names lowercase and URL-friendly (e.g. hyphens instead of spaces).
β Step 2: Set Up Cloudflare R2
1. Go to Cloudflare R2
- Select your account and go to R2.
- Click "Create Bucket"
- Bucket name:
media
- Bucket name:
2. Enable Public Access
- Inside the bucket β Settings β Enable βPublic accessβ.
β Step 3: Upload Files to R2
You can upload manually or use the AWS SDK if automating:
Option A: Manually
- Go into the
media
bucket β Upload folders liketablets
,syrups
, etc.
Option B: Programmatically via Node.js
Install AWS SDK for R2:
bun add @aws-sdk/client-s3
Example upload script:
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readdirSync, readFileSync } from "fs";
import path from "path";
const client = new S3Client({
region: "auto",
endpoint: "https://<your-account-id>.r2.cloudflarestorage.com",
credentials: {
accessKeyId: "YOUR_R2_ACCESS_KEY",
secretAccessKey: "YOUR_R2_SECRET_KEY",
},
});
const uploadDir = (dir: string, prefix = "") => {
const files = readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
const key = path.join(prefix, file.name);
if (file.isDirectory()) {
uploadDir(fullPath, key);
} else {
const Body = readFileSync(fullPath);
client.send(
new PutObjectCommand({
Bucket: "media",
Key: key,
Body,
ContentType: "image/png",
})
).then(() => console.log(`Uploaded: ${key}`));
}
}
};
uploadDir("media");
β Step 4: Set Up CDN Using Cloudflare Workers
1. Create a New Worker
Go to Cloudflare Dashboard β Workers & Pages β Create Worker
Paste this code:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
const object = await env.MEDIA_BUCKET.get(key);
if (!object || !object.body) {
return new Response("Not found", { status: 404 });
}
return new Response(object.body, {
headers: {
"Content-Type": object.httpMetadata?.contentType || "image/png",
"Cache-Control": "public, max-age=31536000",
},
});
},
};
2. Bind Your R2 Bucket to the Worker
- In the Worker dashboard β Settings β Environment Variables β R2 Bindings
- Binding name:
MEDIA_BUCKET
- Bucket name:
media
- Binding name:
3. Set Route for Custom Domain
A. Set up a Subdomain
- In Cloudflare DNS, add:
- Type: CNAME
- Name:
cdn
- Target:
workers.dev
(or the Worker subdomain)
B. Add Custom Domain to Worker
- Go to your Worker β Triggers β Custom Domains
- Add:
cdn.mppharmaceuticals.com
Cloudflare will guide you to verify DNS and SSL.
β Step 5: Use the CDN in Vercel or Anywhere
In your Vercel (Next.js) app:
<Image
src="https://cdn.mppharmaceuticals.com/tablets/paracetamol.png"
width={500}
height={500}
alt="Paracetamol"
/>
Also update your next.config.js
:
module.exports = {
images: {
domains: ['cdn.mppharmaceuticals.com'],
},
};
π§ͺ Final Test
Try visiting:
https://cdn.mppharmaceuticals.com/syrups/coughx.png
β You should see the image served from R2 with full CDN caching.
β¨ Optional: Add Image Optimization
To support on-the-fly resizing:
- Use Cloudflare Image Resizing inside Workers
- Or process query parameters (e.g.
?width=400
) via Sharp