I used Cloudflare Workers and R2 as HTML generating service. It was so easy!
Tags: webdev, javascript
Published: 2024-04-26 23:40:15


This month, I added a HTML generate feature in my web app with Cloudflare Workers and R2.

I was very surprised at workflow of them, it's so easy!

I will explain why and how I implemented it.


## About my web app

[CoraPic](https://cora-pic.com/en/) is a easy meme generator web app.
You can create a meme just in 10 seconds only on your browser.


![example of CoraPic](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ik4ph8y6mjuhbgnnxvbz.gif)

(this meme is [here](https://cora-pic.com/en/meme-generator/love-triangle))


This web app is just hosted on AWS with Amplify.
It didn't have any API server, because all image processing is done with Canvas API on browser.

But...
I wanted to make it easy to share on any social media without downloading image.
Easy sharing feature will help app to be more popular.

That's why I decided to add a feature to generate a HTML file.

## Why Cloudflare? Even though I'm using AWS

I choose Cloudflare because of the price.

Cloudflare Workers have [free plan](https://developers.cloudflare.com/workers/platform/limits/#worker-limits).
It's enough for me.


![Cloudflare Workers free limit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gnt5g09lrnahhotodqrc.png)


About Cloudflare R2, they also offers free capacity:


![Cloudflare R2 pricing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pqnfvxbwk8tztivr6lb8.png)

What I love the most about　[R2 pricing](https://developers.cloudflare.com/r2/pricing/) is, "Zero egress fee"!
It means we don't have to pay for **data transfer fee**.


![Screenshot of Cloudflare R2 official site](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vuw3fo2d6z59x4vi84pd.png)

Even if we used AWS' CDN service CloudFront, we have to pay the cost of data transfer.

But using Cloudflare R2 let us release from this cost!

It's super cool for me.
I already have $4 / month cost for AWS Amplify, mainly because of the data transfer fee, even my app only has 10k PV / month.

I don't want to pay such a stupid fee anymore.

So I decided to use Cloudflare Workers and R2.

(I definitely have to quit AWS Amplify if I want to reduce the cost. I know.)

![diagram of data transfer fee](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iit9nvn5t4ndbgr5cbqx.png)

## The architecture of the HTML generator

Here is the architecture:

![diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k7khte69r1o2pjdx8k70.png)

As I mentioned, my web app creates the meme image on the browser.
So, what I had to do is:

- Save the image on server
- Generate a HTML file with the image URL
- Enable HTML file to be accessed from the browser

By saving HTML file and enabling public access to R2 bucket, we don't have to care how to serve it.

I used Cloudflare Workers to:
- receive the image data from the browser
- save the image on R2
- generate a HTML file with the image URL
- return the HTML access URL to the browser



## About Worker's code

I used [hono](https://hono.dev/) to implement worker.

They have [Cloudflare Workers tutrial](https://hono.dev/getting-started/cloudflare-workers) in their document.
It covers how to write code and deploy it with [Wrangler](https://developers.cloudflare.com/workers/wrangler/).

My main code is like [this](https://github.com/Spice-Z/cora-pic-g/blob/37e6300d53383df594afd492d561958ea636a44e/src/gen.ts#L20C1-L32C78):

```ts
export async function genHTML({ base64, title, lang }: GenHtmlArgs, bucket: R2Bucket, bucketPreviewUrl: string): Promise<string> {
	const base64Image = base64.split(';base64,').pop();
	if (!base64Image) {
		throw new Error('Invalid base64 image');
	}
	const imageType = detectType(base64Image);
	if (!imageType) {
		throw new Error('Invalid image type');
	}
	// Convert base64 to binary
	const imageBinary = Uint8Array.from(atob(base64Image), (c) => c.charCodeAt(0));
	const uuid = randomUUID();
	const imageKey = `gen/${uuid}.${imageType.suffix}`;
	try {
		// Save image
		await bucket.put(imageKey, imageBinary, { httpMetadata: { contentType: imageType.mimeType } });
	} catch (e) {
		throw new Error('Error saving image');
	}
	const imageSrc = `${bucketPreviewUrl}/${imageKey}`;

 // generate html from template with saved image
	const html = genFromTemplate({ imageSrc, title, lang });
	const key = `gen/${uuid}.html`;
	// Save html
	await bucket.put(key, html, { httpMetadata: { contentType: 'text/html' } });
	return key;
}
```

You can read all of my source code in [GitHub](https://github.com/Spice-Z/cora-pic-g) if you are interested in.

Anyway, it was very easy.

## About other trivial matters

I had to setup the R2 bucket.
It was very straightforward and Cloudflare offers good [getting started document](https://developers.cloudflare.com/r2/get-started/).

Also, I moved my domain (cora-pic.com) from [Amazon Route 53](https://aws.amazon.com/route53/) to [Cloudflare Registrar](https://www.cloudflare.com/products/registrar/) to use custom domain for Worker and R2.

Both of them have document about domain transfer.
We can follow these document to move the domain.

- [Transferring a domain from Amazon Route 53 to another registrar](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-transfer-from-route-53.html)
- [Transfer your domain to Cloudflare](https://developers.cloudflare.com/registrar/get-started/transfer-domain-to-cloudflare/)


## That's it !

[CoraPic](https://cora-pic.com/en) has a good feature now!

I'm so happy if you use this feature!


![Thank you for reading!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ejztaijatu6fc2tk26j.jpeg)