
Going static, kinda
Making my blog static - and hosting the server side components in cloudflare.
There are so few posts on this blog that I’ve decided to make it static, just plain old HTML on some random web server.
Well, not really, but with the number of posts I could do that and it would be workable. I have, however, moved this site away from Umbraco and to a static site generator setup. Not because Umbraco is bad, quite the opposite if I require a CMS, I would still go for Umbraco. However, for this blog I don’t need a full-blown CMS, instead, I’m going to keep it simple.
Picking a framework
To choose the framework I wanted to use, I asked my friend ChatGPT to give me a list of different static site frameworks, their pros and cons, with a few constraints. Mostly, it needs to be able to use TypeScript, have components and, most importantly, run on Cloudflare Pages.
It came back with this table
| Framework | TypeScript | Components | Static output | Cloudflare Pages | Best use |
|---|---|---|---|---|---|
| Astro | Excellent | Astro + React/Vue/Svelte | Excellent | Excellent | Blogs, portfolios, community and business sites |
| SvelteKit | Excellent | Svelte | Excellent with adapter | Very good | Interactive sites and lightweight applications |
| Nuxt | Excellent | Vue | Excellent | Very good | Vue-based websites and applications |
| React + Vite | Excellent | React | Client-side SPA | Excellent | Dashboards and browser applications |
| Next.js static export | Excellent | React | Supported with restrictions | Usable | Existing React/Next projects |
| Eleventy | Possible | Template-oriented | Excellent | Excellent | Simple sites, but weaker component ergonomics |
| Hugo | No native TypeScript. | Go templates | Excellent | Excellent | Huge content sites, but not a TS component workflow |
Astro jumped out of that table mostly because it aligned with my use case the most and seems to be quite flexible if I want to add more complex things later on.
It was then up to Copilot to migrate my blog into an Astro-based website, migrate the content to Markdown files and implement a few styling updates I’ve had on my mind, like adding some colour and making a few things more… stylish.
This was the easy part, as I did not want to lose the functionality I already had on the site, which is mostly based on server-side things. So the next step was how to move server-side components to Cloudflare, and also, why Cloudflare.
Why Cloudflare
While I think the move from a CMS to a static site generator might be more understandable, the move from server to serverless might need some more explanation. It’s mostly based on two wants: no longer having to manage anything for this site and wanting to learn about using Cloudflare’s serverless stack.
While not wanting to manage anything is definitely a win for this site, it’s also the least important one, as I run a Kubernetes cluster at home (maybe more on that later this year, if I make another post). The Cloudflare stack, however, has been on my mind for a while, both because they offer it for free with limited use and because I’ve been hearing good things about it and want to see what kind of solutions can be made with it.
And since my site had a few server-side parts to it, mostly the search and the ActivityPub integration, I wanted to move those to this Cloudflare stack.
Serverless server-side
There are two parts that were server-side on my blog: search and ActivityPub. I moved both over to Cloudflare with different implementations, and both migrations were done with assistance from Copilot.
Search
The search field on the site is definitely the simpler one, and thus this was the first one I migrated. While it is a static site, there’s one part that still gets dynamically rendered on the client side of the application: search results. The goal was not to have any server-side components that render content, so instead I created a TypeScript-based implementation in Astro that could fetch JSON from an endpoint and use that to render items on a page.
And since there’s no backend that can dynamically generate content, I could make the indexing of the site for searching quite static as well. Thankfully, Astro has the ability to create “API” endpoints; in this case, it generates the responses for these endpoints at compile time and serves them statically. I use this both for the RSS feed as well as the search index, which is just a JSON object served on this site.
So, with the search index generated at compile time and the search results rendered on the client side, the search functionality only requires one last core component: actually searching. I’ve solved this with a simple TypeScript-based search implementation that can search through the JSON object and return results. This script is running in a Cloudflare Worker, which is a serverless function that can be called from the web. While it would be possible to have the search functionality run on the client side, I wanted to use this as a test case for Cloudflare Workers and thus have the search functionality run on the serverless stack.
Not only was this a good, simple use case to test Cloudflare Workers, it was also a good, simple entry point to help me build the CI pipeline in GitHub, both deploying the site to Pages for main and preview builds and deploying the Cloudflare Worker to Cloudflare.
ActivityPub
The ActivityPub implementation was a bit more complex, as it required a few more things to be implemented. I started by letting Copilot reverse-engineer the specs based on my C# implementation and blog posts. Since I still find the documentation a bit lacking, I wanted to be sure that all the discovery work I put into my previous implementation was not lost in this migration.
The next step was to see how I could fit all these components into the Cloudflare stack, especially since it’s not a full server and there are some different limitations and options to work with. In the end, I came to the conclusion to use a variety of Cloudflare services to implement the ActivityPub protocol.
I created a second Worker that would handle all ActivityPub-related compute. On top of that, I chose to create a D1 database to store the ActivityPub data, a KV store to store the public and private keys for signing and verifying requests, and a queue for handling async activities. The queue is used to handle the delivery of activities to other actors, as this can be a time-consuming process and I don’t want to block the main Worker from handling requests. This also makes it possible to break the processing up into smaller pieces, which is very helpful for staying within the 10 ms CPU time that comes with the free Cloudflare Workers plan.
Let’s run through it component by component, starting with the D1 database. This is used to store all the ActivityPub data, including actors, activities, objects and more. The D1 database is a relational database that can be queried with SQL, which makes it easy to store and retrieve data. I need to store some data because, to post a post to feeds, we need to know which inboxes to send the activity to, and to know which inboxes to send to, I need to know which actors are following us, and to know which actors are following us, I need to store that data somewhere.
I also need to store which posts I have already “posted” and which posts we need to post, because I don’t want to post the same post multiple times. And since the site is static, I can’t do it with an on-publish hook like I did in the C# implementation. Instead, I created a sync process that runs at deploy time on the Cloudflare Worker, checks for new posts and adds them to the D1 database. This is done by fetching the RSS feed and checking for new posts. And when a new post is stored, the process of sending it to the followers is also started. This is done by adding the post to the queue, which will be processed by the Worker and send the post to the followers.
That mostly also explains the queue. It’s used to handle the delivery of activities to other actors, as this can be a time-consuming process and I don’t want to block the main Worker from handling requests. But besides handling new posts, it is also used to handle all other incoming activities.
Finally, the KV store is used to store the public and private keys for signing and verifying requests. We don’t really have many keys to store (since we only have one actor), but it is a good place to store them, as it is a key-value store and we can easily retrieve the keys when we need them. This is mostly just because I wanted to use KV for my own learning purposes.
Implementing ActivityPub, again
After deciding on the architecture of the ActivityPub implementation, I started to implement the Worker. This was done with the help of Copilot and a lot of reading of the ActivityPub specs. I mostly let Copilot generate the code based on my previous implementation and the specs, and made some changes to keep it within the coding standards I wanted.
On top of that, I rewrote the test set I have in my C# implementation into a TypeScript-based test set that can be run with Vitest. This was a great help in testing the implementation and making sure it was working as expected. And while you do have to refollow this blog to get the new posts, as I could not be bothered to write an actual database migration and wanted to change the handle of this blog, it does seem to work, except that, for whatever reason, the logo does not seem to load on Mastodon.