Old Hugo site
I originally built my site to have a place to share my own projects, mainly keyboard related back then, but as the content was made for that time period, I noticed it didn’t age well and certain sections became outdated. The old site also had some dynamic pages based on tags, but I had forgotten to build templates for those, so essentially there were broken pages (though they were hard to find).
I did, however, like the base layout of my old site, having every page work in a similar manner and the content be block-based for presenting information in a flexible format.
Things that broke down over the years
When I originally released the Hugo site, everything was working just fine, but as we all know, services can sometimes stop working and replacements need to be found, but I never got to fixing these as they were not always small things:
Forestry CMS 💀
I really liked Forestry, and it provided a free tier that allowed me to make quick content changes on the fly. Forestry was discontinued at one point, and the team moved on to work on Tina CMS, a very similar CMS but with its own tweaks. I would have migrated over, but Tina required the repo to be on GitHub, so I never got around to doing the migration, as I would have needed to redo the deployment pipeline as well.
Netlify Image Transforms 🪦
I used the service to adjust my images into a more optimized format so that I could focus on content and let the service downscale the images depending on the block they were used in. This service, however, was ended in favor of Netlify Image CDN, another similar service that worked in a slightly different way. Since my site was still fully functional, I never got around to implementing it.
Deprecated text content 👵
When I released the site, the content was tailored for that moment in my life. As the years went on, I noticed that the content on the non-blog pages no longer reflected me in the same way, as my priorities had changed quite a bit. Lesson learned: when making non-blog content, keep in mind how it will age and how to make it cause less work for you in the future.
Goals with new site
When planning anything, having goals makes it clear what you want to achieve. As this was a project I did for myself, I knew I had set the bar quite high, and in the end I had to take some shortcuts to reach the goal, as one cannot keep working on a single project forever.
- Super fast site (static)
- Tina.io support
- Qwik support (nice to have)
- Lighthouse score 100 in all categories
- Previously mentioned issues resolved
- Slightly modernized visuals (using Tailwind)
- Dark/Light switcher
- Modernized and more mature content
- Use a more up-to-date profile photo
I knew these were possible to achieve but there was definitely work to be done.
Hunt for the optimal stack
I had been interested in trying out a JS framework called Qwik, as it promises dynamic components without hydration, which could also help me get a better performance score. Qwik itself, however, is only meant for the client side, and I needed a framework to handle all the routing and data mapping. That’s when I stumbled upon Qwik City.
Qwik City focuses on the server side of things like pages, routing, and data handling. I gave this a shot and quickly got stuff to show up, but when I tried to get it to work together with TinaCMS, I was out of luck. Qwik City is still (at the time of writing) a quite new framework and doesn’t offer much flexibility, so I was forced to look at other alternatives, and that’s when Astro looked promising 👀
QwikQwik CityBuilder.io?
Builder.io, the main supporters of Qwik, provide a CMS experience that’s a little Figma-like. You can visually create your own site and use AI tools to generate specific elements in Qwik. This sounded too good to be true, and I gave it a shot. Unfortunately, at least when I tried it, the results were not that impressive, and the AI was only capable of making very simple elements. I also found the UX a tad confusing, as you are simultaneously editing component layouts and text content.
I quickly gave up on this because I did not want my editing experience to be so clunky. I didn’t need millions of tools to create elements; I wanted simplicity and limited editability, and if I needed something more, I could develop it myself.
Astro + TinaCMS
Astro is a framework meant to be very adaptive with various stacks, allowing you to mix, for example, React and Vue on a single page without hassle. The challenge was that I needed TinaCMS to edit and create files in a structure that could be read by Astro as the page structure. I started with pure .md files, and after some tweaking, I actually had the base structure working 🎉
AstroFinal piece of the puzzle 🧩
Around the same time as I got the base Astro + TinaCMS working, a new package called @qwikdev/astro was released, bringing Qwik support to Astro. I was one of the earliest users of this package and helped out with bug reports. My plan was to use it on my site, but as you might have noticed, making this site took quite a while, so I can’t call myself an early adopter of the package 😅. I had to switch from .md to .mdx to fulfill my needs, but that was luckily no problem for TinaCMS nor Astro 👍
This was it! TinaCMS for handling content, Astro for handling all routing and base logic, and Qwik for all components in the frontend. My stack was decided and PoC was done; now it was time to get to work and actually build the site!
Full Stack for clarity
- Astro
- TinaCMS
- Qwik
- Tailwind 4.0
- Netlify
Content structure & .mdx parsing challenge
The way I wanted to handle content was block-based, each block having its own specific content fields (whether text, selected values, or media), and then the block is rendered with its own structure. By making blocks work after each other in any order, I created a flexible system that is super fast to add content to, and you can be sure the visuals always look right, as you are never touching the HTML structure.
This approach is similar to WordPress ACF Flexible Content/Blocks or only using Gutenberg patterns. When I create content, I don’t want to think about the technical stuff, just do the content and stay as much in the flow as possible.
The Parsing Challenge
TinaCMS supports adding custom elements in its .mdx content. Often, if that were vanilla JS, it would be easy to parse, but since we were using Qwik components, we needed a parser that understood the JSX components were supposed to be handled as Qwik components, not vanilla JS. This caused a lot of issues…
I tried many workarounds, and AI tools couldn’t come up with a solution either, as they hallucinated wrong answers. Fixing one issue would cause two new ones.
**In the end, I decided that the parser should, instead of trying to separate out the JSX components, turn every element into JSX with its own props. **That turned out to be the temporary (read: permanent-ish) solution that worked. This way, I could have classic HTML but also render Qwik components like buttons without an issue. It wasn’t perfect, but in this situation, I was happy with the solution until I found something better. Also, since it was good enough, I could move on to other things and potentially fix this in its own MDX-render function later.
Image Optimization
Let’s be realistic, on most websites the thing that slows the site down is unoptimized images. A single unoptimized image can easily be, at worst, the size of several fully optimized sites with images. Google Lighthouse is also pretty aggressive at pinpointing the potential bandwidth savings with smaller images and better formats. I didn’t want to optimize images manually, so everything needed to be done either in the build process or by an external service, and I ended up choosing the latter.
Netlify Image CDN
Similar to my old site, I utilize the tools Netlify offers and decided to use Netlify Image CDN for my image optimizations. The CDN also provides the possibility to create a blurhash, similar to what services like Wolt use.
My optimized solution
I had a vision to have my images initially blurred, and when nothing else is blocking loading, we load the necessary sized optimized images. As we have loaded the small blurred images and scaled them up to size on the page, we can inspect their rendered size and use that info to load a optimized resolution version from Netlify Image CDN. Contained images are loaded more naturally, but all cover images are scaled in one way or another to cut away pixels which would not be anyways shown. I've also set some breakpoints that retrigger the loading of the images so that we don't get very zoomed goofy images at any point.
Yes, it does require JS to run, but I feel that in my use case (and by being lazy) srcset would never be optimized enough that I could use that. As the layout is dynamic, images are scaled in different ways and setting up srcset to work better would require setting it up in a different way for each image-render position, yet it would not be perfect, when my solution is again more flexible and lives with the moment.
I don't load the perfect optimized resolution image ever as I round up the needed images by 25px so that I won't cause a AWS/GCP outage 😂
Caveats & my two cents
Making this image component was far from easy, as I wanted to do it using a pure <img> tag with object-fit instead of CSS background images. I ran into issues with scaling, weird behavior with the object-fit rule, and a game of whack-a-mole trying to get the images to work in cover vs contained. In the end, I got it working reasonably well, but I feel it can still be optimized further. At some point, however, you just need to settle for “ok” to eventually finish a project instead of endlessly trying to resolve a minor detail.
Header
I wanted to keep the Header similar to my old site, having the logo and a hamburger menu, but also adding in a theme-switcher. I'm a heavy user of dark themes wherever possible, but I know that there are some physopaths who like burning their retinas with light themes (jk).
On my old site, I had a sidebar that animated in, but as I scrolled through Siteinspire for inspiration, I came across a fullscreen menu with the site owner’s image next to the links. I thought that was a clean way of showing the links while still reminding the reader who is behind the content.
I also added some micro-animations to the icons in the header. Most users probably won’t notice them, but I felt there should be something more than just a fade 💁.
Theme switcher
When implementing a theme switcher, you need to think about the best way for it to make a change. You could replace your whole content with something different, but I chose to use CSS variables to decide the colors for all elements. This way, we can use the prefers-color-scheme CSS media feature to detect user preference and apply the right CSS variables. If a user wants to switch the theme, we need to store the change somewhere, so I used localStorage to save the setting.
Content
Since one of my goals was to update the content, I actually made the drastic change and removed some pages:
- Keyboard services (ain't got time for that)
- Keyboard (base hobby page)
And the pages that got a major rework:
- Frontpage
- Development (Now Skills & Experience)
- FMKC (directing traffic to https://fmkc.fi)
As I aim for a perfect Lighthouse score, every image needs an alt text. Since I hadn’t added them previously too all images, that took a while to resolve...
Redirections were also made for the changed URLs to avoid any SEO penalties.
Additionally, when I released this site, I added a few new blog posts to give visitors more reasons to explore the new content, not just because the site is new.
Ecological impact
There has lately been quite a lot of talk on LinkedIn about the ecological impact of websites, and various companies have tried to claim their sites are eco-friendly, but I beg to differ. Many companies still rely on non-optimized systems that save time and money and keep production costs down, but these are precisely the sites that would benefit most from going static, as they tend to have larger user bases.
I want to use my site as a demonstration on how one can still make a fast and usable site with reduced carbon footprint. Both my new and old site scored a 92% on the Website Carbon Calculator. This translates to just 0.09g of emissions per visit, an A+ rating, making them more ecological than 92% of all global websites. You should tho takes scores from this site with a grain of salt.
Despite my new and old site are built in different stacks, the new site is much more optimized. By building a static site, avoiding unnecessary caching layers, relying on native fonts, using Qwik to load only what’s needed, and optimizing images heavily, this site is a example on how to deliver excellent performance while keeping ecological impact in mind.
Website Carbon CalculatorCutting corners
This project probably has the worst possible person chosen to judge the site and suggest improvements, and that's me.. Being somewhat of a perfectionist, I’ve many times had to push myself to stop improving things and just “ship it.” Luckily, I can improve this site further in the future, as I had to make compromises with certain features.
TinaCMS Serverless hosting
One of my goals was to edit content like in ForestryCMS, on the go and anywhere. TinaCMS offers a self-hosted option, but setting this up is no easy feat. I tried getting it running on Netlify functions without success, and TinaCMS members tried to help, but with no results, one even saying after lots of debugging “use Vercel" 😂
I did try Vercel but faced similar issues. Since I wanted to use Netlify Image CDN, I decided to give up and settle on plan B: always host TinaCMS locally to make changes (or edit .mdx files on GitHub directly).
Content parsing issues
This is probably the first thing I'll revisit and see if I can resolve. Most tags work without issues but I noticed that if I do a list inside a list, a nested list, my parser breaks and renders the whole list in a very weird way with some HTML tags rendered as cleartext. I had to make some adjustments to my older blogposts because of this. No big deal, I can live with this, but it would be nice to get fixed 😶
Visual design
Despite having a sharp eye for pixel-perfect alignment, I wouldn’t want to be solely a web designer. I like clear visuals without distractions and have come to like my current layout, as it follows a pattern, is simple, and doesn’t confuse users. The site could perhaps look better in some ways, maybe by adjusting gradients or colors, but I worked to get it to a point I’m currently satisfied with and may revisit the design in depth later.
Summary
Final Lighthouse Score: PERFECT
The result of all this work:
- Desktop: 100/100/100/100
- Mobile: 100/100/100/100
At first I only got 99 in performance, but once I switched over to a ignito window without chrome plugins, I was able to get a perfect score! Lighthouse seems to complain about images, even though all are optimized via Netlify. You shouldn’t focus solely on Lighthouse scores, but they give a pretty good indication of how the site performs. I'm satisfied with my score 🎉
If you’re interested in a site that performs at a similar level, get in touch and we’ll see what we can do!
Get in touchPhase two?
As you’ve noticed, I’m happy with my site, but there are still some issues to address. At the time of writing, I’ve already made a long list of To-Dos to make the site simpler, faster, better, and more future-proof. I might even write another blog post on this phase two update if there are enough things to cover.








