My First Post

Ok, not really my first post, but rather the first one on my new site.

· 13 min read · 2552 words

Finally, I have my own website again 🥳.

I have been thinking about self-hosting content for a while now. Reasons include:

  • I started my own company. At this time, it is only a small consulting company selling a single product (my time). Nevertheless, every self-respecting company should obviously have a website.
  • I sold myself to the idea of spending some time building this also by thinking of it as building my “personal brand” (I know, I know, but still…).
  • I mostly just like the idea of owning my content, and having sources for what I write.

I can now easily write and edit posts in Markdown, and run a single command to push changes to the site. I aim to write here quite frequently. Many posts will have myself as the target audience.

The remainder of this particular post is just a bit of rambling about the process I went through building this site, going from the framework that I use to generate the site to the conversion of my existing content — Medium posts — to Markdown.

Framework

TL;DR, I made this site with Astro.

I am not a frontend developer. I therefore spent some time figuring out which framework to use to render my Markdown file into properly styled HTML.

Over twenty years ago, I started creating “server-side rendered” static websites using PHP. I am not old, but old enough to have positioned elements exclusively using HTML <table> elements. Later, I learned about AJAX (very hip at the time), jQuery (which I believe is still around), and eventually moved on to creating Single-Page Applications (SPAs) using Vue.js, first using rather complicated Webpack configurations and later on using Vite. Most of this just for my own little projects, such as applications to turn on and off the Christmas tree lights, or to change the country from which Netflix thought I was connecting from. Anyway, the evolution from static to dynamic felt like a natural progression.

Back to the future screenshot illustrating that the past is also the future What if the past… is also the future? Source: Back to the Future

So, it was somewhat unexpected to me that static content and server-side rendering is cool again. Perhaps the progression only existed in my mind. SPAs, Vue and Vite are alive and well, but static sites are too, each serving a different purpose (web site <> web app). It’s not just a return to the past either, as static is not what it used to be — we do now use fancy generators to generate and optimize the static content, and the static generator and server-side rendering component are now nicely integrated.

Static is simple and cheap to host, and I like simple and cheap. But boy, there are indeed a lot of static site generator frameworks out there. I briefly played around with a number of them, and decided on one for no particularly good reasons. No attempt at objectivity and “architecting” here, just picking the one that felt right.

  • Jekyll. Quite popular, but usually referred to as the “established value”, and as the slower alternative to Hugo. I think I was looking for something more “hip”.
  • Hugo. I thought this was going to be my pick going in. I spend some part of every workday complaining about Golang templates (and Helm), though. Writing them in my spare time would be inconsistent.
  • Svelte and SvelteKit. This is overcomplicated for my needs. I wanted something that focused on simplicity and static rendering, Svelte seemed to focus on server-side rendering.
  • Nue.js. It promises simplicity and speed, but turned out to be a bit too much on the bleeding edge for me. I looked at this some time ago though. The lack of documentation and community support was a turn-off.
  • 11ty. I quite liked this. I am not sure anymore why I did not pick this one. I guess I just liked something else more.
  • Streamlit. Yes but no. This is quite a useful tool and used a lot in my industry, but there are too many reasons to not use it for this use case to list.
  • Alpine.js. I really like this and even used it to create a small webapp at a client, but it’s not the tool for this use case. This is a nice client-side rendering framework, not a static site generator.

I decided to use AstroJS, as it quickly made sense to me (“clicked”). It is modern, popular and seems to be universally loved. The “distance” between what I put in and what comes out (HTML, CSS, JavaScript) is small, which I like. Astro allows me to very flexibly render static content, and for now, this allows me to do everything what I want without requiring a server-side rendering component, making the website as cheap and portable as it can be.

Styling

TL;DR, I did not use a template, and wrote the CSS myself with the help of ChatGPT and Claude.

I figured I would just pick a nice template and be done with it. Unfortunately, there was no template that I liked enough. Most were too simple, lacking the functionality that I was looking for. Other templates were quite feature-rich, but it seemed like using them would result in me “owning” a lot of cruft, code that I did not understand or need. Again, this reminded me too much of Cookiecutter templates and work.

I ended up creating a site from scratch, admittedly with quite some help from ChatGPT and Claude. I have no immediate plans to open-source my code (or content, for that matter).

CSS is written plainly. I don’t use any pre-processors like less or sass or styling frameworks like Tailwind or Bootstrap. I used to use GIMP to manually create GIF images and manually positioned them using CSS2 just to get rounded corners. Basically, I consider people who think current CSS is “low-level” to be a bit spoiled 😉.

Functionality

Most of the actual functionality is pretty basic, and part of a basic Astro setup

There were only these pieces of functionality that took some effort to get right:

  • Posts are identified by an ID, which is part of the URL. This allows me to rename posts without breaking links, while still having the post’s title be part of the URL.
  • To keep the blog fully pre-rendered, I statically generate a JSON file mapping post IDs to full post slugs. I added JavaScript redirection logic using this JSON on the default HTTP 404 “Page Not Found” page.

Hosting

I host the site on Cloudflare Workers. I thought I was going to use Cloudflare Pages, but apparently Cloudflare now recommends the use of Workers for basically everything, and changed Worker pricing to be free for static content.

Cloudflare Workers are a no-brainer to me, although there are of course many other great options. I quite like Cloudflare and its approach to things. Working mostly with AWS professionally, the simplicity of Cloudflare is so refreshing; no regions, transparent pricing, generous free tiers. In fact, Workers are infinitely free when serving static content. They are fast, and allow for server-side rendering if I ever need it. There’s also no lock-in whatsoever. I can easily move to another provider if I need to.

I did not currently connect Cloudflare to a Git repository, as it does not support Bitbucket, and I use Bitbucket for all my personal projects. I use Bitbucket for historical reasons: it used to be the only game in town for free, private repositories.

Configuration of my Worker was as simple as creating the worker (named welwit) and adding a single wrangler.toml configuration file to the root of my project:

name = "welwit"
compatibility_date = "2025-09-25"

[assets]
directory = "./dist"
not_found_handling = "404-page"

I can publish a new version of the site simply by running

npm run build && wrangler deploy

Building the site currently takes two seconds, uploads happen incrementally in seconds. (Global) deployment of the new version takes less than 30 seconds.

The above would publish the site to the welwit worker at welwit.<my-org-name>.workers.dev. As my welw.it domain was already hosted at Cloudflare, using the welw.it domain instead was just a simple single-click configuration change. My entire site currently has 33 posts and consists of 168 assets (images, CSS, JavaScript, HTML, …). Free-tier Cloudflare Workers allow up to 20000 assets for free.

Conversion of Medium posts

TL;DR: Converting Medium posts properly is hard. I used Cursor and Claude to convert my Medium posts to extended Markdown.

I have been using Medium for a few years now, and published a number of posts there. I wanted to import my content to my own website. Unfortunately but also unsurprisingly, Medium does not make this easy for you. While it does offer an export feature, it formats exported blog posts as hard-to-read HTML. Exports do not include images, instead linking to Medium’s CDN.

I looked for tools that could help me with this task:

While these worked for the most part, none of them gave me quite the output I was looking for.

I ended up using Cursor and claude-4-sonnet (in agentic thinking mode) to perform the conversion. I can’t really say that it was a breeze, but it did the job, and I am certain that I would not have been able to do it as well or as quickly without AI assistance.

  • I do not have an “Ultimate” subscription, just the 20 EUR/month “Pro” subscription, and conversion of just 30 odd posts burned through my entire monthly token budget.
  • Other models, including GPT-5, did not work as well. I tried using code-supernova as it was free at the time I was doing the conversion. It made too many mistakes.
  • I had to craft a somewhat elaborate rule before it worked the way I wanted it to.
  • Conversion took several minutes per post.
  • Even after having converted many posts without problems, some new conversions would contain errors without any obvious reason. I had to quickly validate each post to make sure that the conversion was correct. There are probably mistakes left.

When encountering a nice use case for them like this, I still think LLMs are a bit like magic. Besides the relatively straight-forward HTML-to-Markdown conversion, it also:

  • Extracts the title, subtitle, id, tags, and kicker, and inserts it as metadata into my post’s front matter. Front matter is metadata at the front of the Markdown file that I use in my Astro template to render the post. Example:
---
title: Authorizing AWS Principals on Azure
subtitle:
  How to delegate trust from Entra to AWS IAM through Cognito, authorizing Azure
  actions without needing long-lived credentials.
kicker: Use AWS IAM user- or session credentials to access Azure resources
id: 2a9353a3f97f
thumbnail: './thumbnail.png'
tags: []
draft: false
---
  • Recognizes Medium-specific formatting features such as “small quote” and “large quote” and converts them to HTML components for which I defined CSS styles. Some files are generated as MDX files, not plain Markdown.
  • Downloads all images to my post’s directory; images are not included in the export from Medium.
  • Recognizes code blocks inserted as GitHub Gists. It retrieves the Gist’s contents as Markdown code blocks and annotates them with the correct language. I actually forgot about this at first, but Claude did not.
  • Recognizes self-referencing links, i.e. when Medium “cards” link to my own blog posts. It then converts those to a custom Astro component that renders based on information in the linked post’s front matter.

What’s next?

I still plan to improve some aspects of the site itself:

  • Some formatting improvements, especially on mobile.
  • Add functionality for zooming images (lightbox).
  • Display post tags in the post itself, in the archive, and allow listing of posts by tag.
  • Add some SEO metadata.
  • Allow single-click copying of code blocks.
  • Add anchor links to headings.
  • Add table of contents component. Technically doable, but I am not yet sure what I want this to look like. TOC on top, or TOC next to the article?
  • Add search functionality. Even if no-one else ever reads this blog, I want to be able to find my own posts.
  • Add an RSS feed.
  • Add a comments section and perhaps “clapping” or “liking” functionality. Claps motivate me. Obviously, this means adding something dynamic to my static site, which is probably best done by using a third-party service like Disqus.
  • Add a way to track visits to the site. Similar remark; I do not want to embed Google Analytics or similar services though. Maybe I can just enable Cloudflare Web Analytics.
  • Add email subscription functionality.
  • Add backlinking functionality (this post is linked to from these other posts).
  • Add “similar posts” functionality.
  • Add support for Mermaid diagrams or Kroki.
  • Add support for math (probably MathJax or KaTeX).
  • Add some styling for printing or PDF export.

It may take some time before I get to some of these, if I get to them at all. Some I will add as I have a need for them. I suspect that I will wait at least until my Cursor credit balance resets 😅.

Content-wise, I plan to continue writing as I did, but also to start writing different types of posts.

  • Continue writing the types of posts as I have done for the past few years. Those are usually a bit longer-form and usually technical stories, proper “content” that I write with some care and also post on my client’s — Dataminded’s — blog.
  • Start writing shorter-form or less polished posts, about things that I am working on, just learned, or read and found interesting. This post falls into this category.

Maybe I will even visually indicate which category a post belongs to by styling them a bit differently.

I will continue to cross-post most content to Medium, but plan to mark the post on my own website as the original source, aka the canonical URL.