This website

October 5th
Thumbnail
Visit project page ↗

I made this website from scratch! Because why would you set up a basic WordPress website in minutes when you can spend weeks writing something that does pretty much the same? On multiple occasions, I was unable to answer that question. But I did end up learning a lot.

Stack

  • The backend is written in Rust
    • It uses the axum web framework
    • sqlx for interacting with a Postgres database
    • Amazon's aws-sdk-s3 crate for storing these markdown files and thumbnail images
  • The frontend is written using basic templates
    • It uses the askama templating engine
    • I used tailwindcss to write as little CSS as necessary
    • For improved interactivity, I tried htmx for the first time
  • It uses some external services
    • A self-hosted Keycloak instance for OpenID Connect authentication
    • Cloudflare's R2 for S3 compatible object storage
      • For local testing, a Docker Compose setup spins up MinIO instead
    • A PostgreSQL database
    • Velero for backing up the database from time to time
  • It runs on my dual-node k3s cluster
    • GitHub Actions automatically builds Docker images for new versions
    • A Helm chart references those images to describe a complete deployment of the application
    • fleet automatically deploys that helm chart, along with self-hosted instances of the external services above

Lessons learned

Overall, I'm happy I tried this stack. A few days in, I was even convinced that using simple templates in combination with HTMX was far superior to using a frontend framework for the majority of websites. After a while, however, it started showing it's limitations.

It's not visible for the majority of the readers (assuming that at least one other person is going to be reading this), but this website has a full-fledged content management system built-in. One useful feature for me would be to have a preview button that renders the content I've written so far as markdown, so I can verify it looks as intended before I publish it. But due to templates being completely server-side, the most straightforward way to implement this would be to use HTMX to send a request to an endpoint that converts the markdown written so far to html. That's not necessarily a problem for the one person writing content for this website, but it is something that could easily be done client-side. Doing this manually with JavaScript in your template, however, is cumbersome without some npm-like setup.

Another drawback is that the frontend and backend become closely tied together. Some endpoints return full HTML pages, others only reply partial HTML fragments for HTMX to insert into an already loaded full page. In many cases you even require both types of endpoints for the same data, for example when you want to support deeplinks or users that disable JavaScript. Many frontend frameworks come with built-in solutions to these problems, but with this stack it's a manual process. In hindsight, I'm not a big fan of editing API endpoints to change the look of the frontend.

Update February 17, 2025

Another practical limitation of using templates and a backend language other than JavaScript or TypeScript, is that you have to host it yourself. It's not a problem to host it along with the other services in my cluster, but my cluster only runs in my house; someone on the other side of the world will have to wait a lot longer for their page to load. In contrast, JAMstack applications can be hosted by services like Vercel or Cloudflare Pages, which run instances of the frontend nearby the users accessing them. Not only does this reduce loading times, Cloudflare pages is also free for up to 100k requests per day (and the traffic of this website is nowhere near that), and makes it extremely easy to deploy straight from your Git repository. This sounds like an ad, but it isn't. I'm just a satisfied customer that didn't pay a dime.

Stack updates!

  • The frontend is now written in Svelte
    • I also updated the look, using Atom's One Dark colors instead of default Tailwind colors
  • Speaking of Tailwind, the new look required quite a bit of custom configuration, and the new Tailwind CSS 4.0 made that so much easier
  • The frontend is now hosted through Cloudflare Pages
  • The backend now generates OpenAPI specifications with utoipa, which the frontend uses to automatically generate its API client
    • I also looked at prost and tonic for a Protocol Buffers + gRPC approach instead, but decided against it because OpenAPI can generate API endpoints and clients with more complete validation out-of-the-box, due to supporting things like required fields. This is an anti-pattern in ProtoBuf and other interface definition languages - and rightfully so - but because this project has exactly one producer and one consumer, complete client generation is preferable to guaranteed backward and forward compatibility.

New lessons learned

To start, the new setup with separate backend and frontend applications communicating with JSON is significantly simpler than the HTMX approach before, which is ironic because I tried the templates + HTMX stack initially because I thought it would be simpler. However, manually making sure every page of your website can load standalone, while also improving interactivity by dynamically loading some parts of the page, quickly leads to many different API endpoints that return different HTML variations of the same data, even in a small app such as this one.

For example, when a user submitted the comment form that used to be at the bottom of this page, HTMX would replace that form with the HTML returned from the backend (or reload the page if JavaScript is not enabled). The endpoint returned either the that same form but with an error message in it, or the same form with the submitted comment below it, so that it would appear above all the other comments. When combined with a few other requirements, such as replacing the form with a link if the user isn't logged in, you can imagine how convoluted the backend can get from one simple interactive component.

Interactive components are the reason frontend frameworks exist, and they do a great job making it simple to create them. Also, look at that juicy animation when you click on the 'Contact' button in the navigation menu above! HTMX could never do that. It was too wide for mobile devices though, in that case you get a boring dropdown instead.