How to build a modern blog system from scratch
With React + Vite + Cloudflare workers / Pages (worker-rs) + D1 + R2 + OIDC
TL;DR:
I wanted a personal blog that was free, highly customizable, and technically advanced, so I built my own using React + Vite + WASM Workers + D1 + R2 + Keycloak OIDC, and encountered a lot of problems along the way.
1. Why am I building this blog?
As a young developer, I was always attracted by cool blogs built by other developers, so I decided to make a blog on my own. I guess 100 other existing solutions can build a blog in one click and enjoy my life. But I just don't want my blog to look the same as the other developers' or non-developers' blogs. So I just built it myself.
2. The reason I chose this tech stack
2.0 The very high-level reason:
For me, one very natural logic is: if I decide to build something on my own and don't use an existing solution, it must be in some way better than the existing solution, or else I should just use the existing solution.
That's the underlying logic of why I decided to build this with a serverless solution. A common traditional WordPress way will require a server to deploy. If I use AWS (EC2, S3, RDS), for example, it should be around 8$ - 20$ each month. Whereas if I use the Cloudflare solution (Workers, R2, D1), I doubt there will be a day that I go beyond the free tier.
2.1 But why are you using Rust and WASM then?
The TL;DR is I just hate myself.
The long story was the first time I worked with Cloudflare Workers. I was working on a budgeted project with a non-profit organization. And I don't really have a concept of how generous the free tier of Cloudflare was back then. If I use Typescript, I was worried about the possibility that I would go beyond the free tier. Even though later I realized that it was not possible. That early project pushed me into using Rust and WASM for Cloudflare Workers, and the habit stuck. But what is different from the earlier project is that I use the axum style router. It makes it easier to use middleware.
2.2 Where does the OIDC come from?
From my self-hosted server
There is a long story behind that as well. Basically, I have a homelab equipped with a bunch of CNCF services. My initial intention with that homelab was just to use cheap Intel Xeon E5 processors to avoid giving 200$ to AWS each month. And it just evolves into a self-hosted Kubernetes cluster with all these expensive Sapphire Rapids processors. My Keycloak was deployed there. Since it is already there, I just decided to use it in my blog's admin system. I can talk more about my homelab in my future blog.
3.Problems I ran into
Needless to explain, when someone decides to build a blog with this weird but powerful tech stack, it will become a flood of problems.
3.1 How do I store tags without MongoDB?
In fact, it is not impossible to use MongoDB as MongoDB Atlas provides an HTTP endpoint, plus Cloudflare made some durable object freeze connection magic recently. But back then, I thought a MongoDB database required a keep-alive TCP connection. And the entire idea of serverless is to be stateless, so I decided to follow the best practice and use D1 instead of MongoDB. But now I have to deal with the issue of using a relational database to store non-relational data.
I end up having to build my database schema like below:
CREATE TABLE posts (
_id INTEGER PRIMARY KEY,
title TEXT,
excerpt TEXT,
date TEXT,
category TEXT,
cover_image TEXT,
content TEXT,
word_count INTEGER,
read_time INTEGER
);
CREATE TABLE tags (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
color_class TEXT
);
CREATE TABLE post_tags (
post_id TEXT,
tag_id INTEGER,
FOREIGN KEY(post_id) REFERENCES posts(_id),
FOREIGN KEY(tag_id) REFERENCES tags(_id)
);
And a bad thing is that Cloudflare D1 doesn't have the triggerer thing, so I have to maintain the consistency of the three tables and deal with key constraints. The problem gets even worse since I somehow decided to use Rust to code this entire thing. And even worse, there is not a good enough ORM for Cloudflare D1 yet, so I have to write SQL myself.
Although it's a rather troublesome task, it doesn't actually require a lot of technical expertise; it just takes more time to write code and debug.
3.2 Real Challenge: OIDC
Even though I had gone through this a couple of times, I still find it quite confusing. The idea of OIDC does not sound too hard: it's just backend verifying the token provided by the user, and if it is a bad token, redirect the user to the SSO provider. After user login, they are redirected to a specified URL and get the token, so they can use the token to unlock the backend.
I have gone through different combinations of frontend and backend with this. The very first attempt was a Discord bot frontend and a Gin backend. The second attempt was a Flutter Dart frontend and Cloudflare Workers backend. This one is about React Frontend and Cloudflare Workers backend. Therefore, the backend wasn't too big a challenge for me since I could just reuse my old code. For the React part, AI recommended a keycloak-js lib to me. It turned out that some hero before me tried to integrate JS and Keycloak. Therefore, I just designed an AuthProvider and AuthContext and copied the official example.
Also, one thing that stuck with me for a while is that Keycloak's "aud" or audiences does not automatically equal the client's name. I went through a couple of rounds of debugging and fixed it by adding a mapper to the client scope.