<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wc="https://wilhelm.codes/ns/feed/1.0"><channel><title>Wilhelm Codes · Performance</title><link>https://wilhelm.codes/tags/performance/</link><description>Coder and platform engineer. Slinger of nibbles, bits and bytes. Over 25 years of professional experience as a software engineer. Love making glowing rectangles go "beep-boop".</description><generator>Hugo</generator><language>en-US</language><managingEditor>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</managingEditor><webMaster>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</webMaster><lastBuildDate>Fri, 11 Sep 2026 01:09:12 +1000</lastBuildDate><atom:link href="https://wilhelm.codes/tags/performance/index.xml" rel="self" type="application/rss+xml"/><item><title>Page loads had been creeping up again, so I went looking. Turns out …</title><link>https://wilhelm.codes/bits/update-putting-the-site-on-a-diet/</link><pubDate>Fri, 11 Sep 2026 01:09:12 +1000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/bits/update-putting-the-site-on-a-diet/</guid><category>wilhelm.codes</category><category>hugo</category><category>performance</category><wc:kind>bit</wc:kind><wc:bit>update</wc:bit><wc:untitled>true</wc:untitled><description>Page loads had been creeping up again, so I went looking. Turns out I&amp;rsquo;d been quietly serving enormous screenshots. One post was pulling 5.1 MB of PNGs, and a single one of those was a 1.96 MB app screenshot sitting in a column 736 pixels wide. Oof.
The fix was mostly plumbing. My img shortcode was handing the raw src straight to the &amp;lt;img&amp;gt; tag, so Hugo never got a look in. Everything now goes through one partial that resizes to the width it actually displays at, re-encodes to WebP and emits a srcset, plus width and height so the layout stops jumping around while things load. Markdown images get the same treatment through a render hook. The awkward bit: images under static/ aren&amp;rsquo;t resources, so Hugo can&amp;rsquo;t touch them. A second mount in hugo.toml fixes that without breaking any existing URLs.</description><content:encoded><![CDATA[<p>Page loads had been creeping up again, so I went looking. Turns out I&rsquo;d been quietly serving <em>enormous</em> screenshots. One post was pulling 5.1 MB of PNGs, and a single one of those was a 1.96 MB app screenshot sitting in a column 736 pixels wide. Oof.</p>
<p>The fix was mostly plumbing. My <code>img</code> shortcode was handing the raw <code>src</code> straight to the <code>&lt;img&gt;</code> tag, so Hugo never got a look in. Everything now goes through one partial that resizes to the width it actually displays at, re-encodes to WebP and emits a <code>srcset</code>, plus <code>width</code> and <code>height</code> so the layout stops jumping around while things load. Markdown images get the same treatment through a render hook. The awkward bit: images under <code>static/</code> aren&rsquo;t resources, so Hugo can&rsquo;t touch them. A second mount in <code>hugo.toml</code> fixes that without breaking any existing URLs.</p>
<p>While I was in there I dealt with the JavaScript. Every page was carrying all seven inline scripts, even the 90-odd pages with no code blocks, no zoomable images and no locked series titles. Those three now load only where they actually do something. The two that genuinely run everywhere moved into one small fingerprinted bundle, so your browser fetches it once for the whole visit instead of re-reading it on every page. I also finally turned on HTML minification, which I&rsquo;d apparently never bothered with.</p>
<p>Where it landed:</p>
<table>
	<thead>
			<tr>
					<th>what</th>
					<th>before</th>
					<th>after</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Images the site serves</td>
					<td>12.3 MB</td>
					<td>1.5 MB</td>
			</tr>
			<tr>
					<td>HTML over the wire, mean per page</td>
					<td>10,236 B</td>
					<td>6,671 B</td>
			</tr>
			<tr>
					<td>Worst offending post, first paint</td>
					<td>5.1 MB</td>
					<td>112 KB</td>
			</tr>
	</tbody>
</table>
<p>That last one is about 46 times smaller. Nothing changed visually, which was the whole point. The sparks still spark and the locked titles still scramble.</p>
<p>One last silly thing. I added a new short code to style &ldquo;Plant Smart&rdquo; as &ldquo;<span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span>&rdquo;. Fun!</p>
]]></content:encoded></item><item><title>Why I Rebuilt Plant Smart</title><link>https://wilhelm.codes/blog/why-i-rebuilt-plant-smart/</link><pubDate>Sun, 30 Aug 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/why-i-rebuilt-plant-smart/</guid><category>hugo</category><category>plantsm.art</category><category>woodpecker</category><category>performance</category><wc:kind>post</wc:kind><description>Back in June I wrote a little blurb that said, in its entirety, &amp;ldquo;I should update Plant🌱Smart.&amp;rdquo; Bolder and more visionary words have never been written. So bold, in fact, I went right back to demolishing this blog instead and let the poor thing sit for another two months.
Well, I finally did the thing.</description><content:encoded><![CDATA[<p>Back in June I wrote <a href="https://wilhelm.codes/bits/blurb-i-should-update-plantsmart/">a little blurb</a> that said, in its entirety, &ldquo;I should update <span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span>.&rdquo; Bolder and more visionary words have never been written. So bold, in fact, I went right back to <a href="https://wilhelm.codes/blog/some-long-overdue-demolition/">demolishing this blog</a> instead and let the poor thing sit for another two months.</p>
<p>Well, I finally did the thing.</p>
<p>Three and a half years ago I wrote <a href="https://wilhelm.codes/blog/why-i-built-plant-smart/">Why I Built <span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span></a>. A free and searchable database of plants that are toxic to pets. I built it over a holiday break to answer a simple question, &ldquo;Can I bring this plant home without endangering the fluffy little gremlin that runs our household?&rdquo; It was also my excuse to learn SvelteKit, TypeScript, TailwindCSS and Vite, because my frontend peers wouldn&rsquo;t shut the hell up about them.</p>
<p>The site then chugged along quietly and untouched for years, which sounds like a compliment to its architecture and very much is not one to the maintainer.</p>
<h2 id="the-toolchain-aged-like-fine-milk">
  <a class="heading-link" href="#the-toolchain-aged-like-fine-milk">The toolchain aged like fine milk.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The thing no one really thinks about when they build something like this with shiny new frontend tools is that it doesn&rsquo;t rust, it <em>curdles</em>. When I finally came back to give <span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span> a fresh coat of paint, the build no longer ran. Not &ldquo;a few deprecation warnings&rdquo; didn&rsquo;t run. It didn&rsquo;t run at all. Two years of an unattended <code>package.json</code> had fermented into something of a cheese-like consistency.</p>
<p>My original requirements list from 2022 said the site must be fast, static, ( almost ) free to run and open with its data. Nowhere did it say &ldquo;must still compile in two years&rdquo;.</p>
<p>So, I decided to spend a few weeks off and on to throw out the toolchain and give it a much-needed refresh. Just like this site, Hugo now builds the entire thing. There&rsquo;s a single hand-written stylesheet and a few small scripts of delicious vanilla JavaScript. No <code>npm install</code>, because there is no longer anything to install. If I disappear for another two years ( likely ), the site will build exactly the same the day I come back.</p>
<h2 id="first-things-first-absolute-parity">
  <a class="heading-link" href="#first-things-first-absolute-parity">First things first, absolute parity.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The rebuild had one rule that saved me from myself: the first pass had to be a 1:1 replica of the old site. It needed the same pages, filters, URLs and quirks. The pass was meant to be boring on purpose. I had to resist the temptation of doing both the replatforming and redesigning all in one go.</p>
<p>My primary constraint was the plant data. <span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span>&rsquo;s whole gimmick is what I called a &ldquo;dumb API&rdquo;. The entire primary database lives in <a href="https://plantsm.art/api/plants.json">one JSON file</a> with other, smaller, supplemental files you could use as lookup tables. Since this was all hosted on Cloudflare&rsquo;s CDN it was effectively free to use for others without keys or annoying rate limits. The old derivations were originally done by a pile of Go magefiles. The new site had to reproduce them exactly, down to the deliberately unsorted first-appearance ordering, or every downstream consumer would notice.</p>
<p>Hugo, it turns out, can do all of it. A content adapter reads the database and births every plant page directly with no intermediate files. A handful of templates re-implement the old Go derivation logic and regenerate the API on every build. I diffed the generated files against the originals and they came back value-identical on the first honest attempt.</p>
<p>Then came my favourite commit of the whole project:</p>
<div class="code-block">
  <pre tabindex="0"><code>retire the sveltekit stack: remove src, magefiles and the node toolchain
566 files changed, 112 insertions(+), 9905 deletions(-)</code></pre>
</div>
<p>I deleted nearly ten thousand lines and the site did more than before. This is every developer&rsquo;s <em>favourite</em> kind of commit.</p>
<h2 id="i-made-the-site-cute-on-purpose">
  <a class="heading-link" href="#i-made-the-site-cute-on-purpose">I made the site cute on purpose.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>With the replica shipped, the redesign could actually be fun. The old site was fine but it was very much &ldquo;2022 Tailwind starter&rdquo; vibe. A lot of cool greys, hard shadows and gradients on anything that stood still. Meanwhile the logo, a randomised squishy little pastel potted plant with a face, deserved better surroundings.</p>
<p>So, I set out to make the logo the design brief. Cream paper instead of white with warm espresso ink instead of a dull grey. Sage and moss greens with honey and clay accents were all lifted straight from the icon set. Every shadow is tinted warm because plain black shadows on cream look like stickers. The corners got a bit rounder and the controls became pills. The whole thing is set in <a href="https://fonts.google.com/specimen/Fredoka">Fredoka</a>, a rounded display face that weighs about <code>30kB</code> which I thought fit perfectly with the cute little mascot.</p>
<p>Here are a few before and after shots of the main content pages, with a bonus shot of the new pill navigation that smaller screens finally got:</p>
<div class="strip">
  
  <button type="button" class="zoom" aria-label="Zoom image: The homepage before the redesign.">
      <img
    src="https://wilhelm.codes/blog/why-i-rebuilt-plant-smart/home-before_hu_c134c356ab255f31.webp"
    srcset="/blog/why-i-rebuilt-plant-smart/home-before_hu_c134c356ab255f31.webp 1x, /blog/why-i-rebuilt-plant-smart/home-before_hu_fd4c4f2261904a3.webp 2x"
    
    width="337"
    height="224"
    alt="The homepage before the redesign."
    loading="lazy"
    decoding="async"
    data-full="/blog/why-i-rebuilt-plant-smart/home-before_hu_bd376468f433ee37.webp"
    
    
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: The homepage as it currently stands.">
      <img
    src="https://wilhelm.codes/blog/why-i-rebuilt-plant-smart/home-after_hu_7bf504f00e2d4b7b.webp"
    srcset="/blog/why-i-rebuilt-plant-smart/home-after_hu_7bf504f00e2d4b7b.webp 1x, /blog/why-i-rebuilt-plant-smart/home-after_hu_18e93a68df4a3045.webp 2x"
    
    width="326"
    height="224"
    alt="The homepage as it currently stands."
    loading="lazy"
    decoding="async"
    data-full="/blog/why-i-rebuilt-plant-smart/home-after_hu_e3c3f0257d1fdaa7.webp"
    
    
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: The plant details page before the redesign.">
      <img
    src="https://wilhelm.codes/blog/why-i-rebuilt-plant-smart/details-before_hu_778178bb15ddf6e2.webp"
    srcset="/blog/why-i-rebuilt-plant-smart/details-before_hu_778178bb15ddf6e2.webp 1x, /blog/why-i-rebuilt-plant-smart/details-before_hu_47cac0840311163a.webp 2x"
    
    width="235"
    height="224"
    alt="The plant details page before the redesign."
    loading="lazy"
    decoding="async"
    data-full="/blog/why-i-rebuilt-plant-smart/details-before_hu_a7d36e9ec06698d9.webp"
    
    
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: The plant details page as it currently stands.">
      <img
    src="https://wilhelm.codes/blog/why-i-rebuilt-plant-smart/details-after_hu_be1325861b1ce8de.webp"
    srcset="/blog/why-i-rebuilt-plant-smart/details-after_hu_be1325861b1ce8de.webp 1x, /blog/why-i-rebuilt-plant-smart/details-after_hu_5bc6a433aa239b54.webp 2x"
    
    width="140"
    height="224"
    alt="The plant details page as it currently stands."
    loading="lazy"
    decoding="async"
    data-full="/blog/why-i-rebuilt-plant-smart/details-after_hu_672f659284e5d144.webp"
    
    
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: The new pill navigation for mobile users.">
      <img
    src="https://wilhelm.codes/blog/why-i-rebuilt-plant-smart/mobile-pill-navigation_hu_292dde7b92e8dcc8.webp"
    srcset="/blog/why-i-rebuilt-plant-smart/mobile-pill-navigation_hu_292dde7b92e8dcc8.webp 1x, /blog/why-i-rebuilt-plant-smart/mobile-pill-navigation_hu_18b41e7ae3eacdf6.webp 2x"
    
    width="102"
    height="224"
    alt="The new pill navigation for mobile users."
    loading="lazy"
    decoding="async"
    data-full="/blog/why-i-rebuilt-plant-smart/mobile-pill-navigation_hu_9d3bd352027c355a.webp"
    
    
  />
  
    </button>

</div>

<h2 id="the-data-enrichments-will-continue-until-morella-improves">
  <a class="heading-link" href="#the-data-enrichments-will-continue-until-morella-improves">The data enrichments will continue until &ldquo;Morella&rdquo; improves.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Get it? No? Ok, I&rsquo;ll see myself out.</p>
<p>The database had severity ratings for every plant, computed from symptom groupings and the old site displayed them precisely nowhere. Now, every card has a severity pill rated from Mild to Critical. The pills are also clickable filters on the main page&rsquo;s plant cards. Search happens as you type and if you fumble the Latin a bit, a little <a href="https://en.wikipedia.org/wiki/Levenshtein_distance">Levenshtein</a> pass offers &ldquo;Did you mean <em>Lilium</em>?&rdquo;. I thought this was a little bit more helpful than what the old site did after literally joking about spelling mistakes in its empty state.</p>
<p>A few other mentionable updates:</p>
<ul>
<li>A <a href="https://plantsm.art/safe/">safe plants list</a>, because the site only ever told you what <em>not</em> to buy. Every entry has to clear the ASPCA non-toxic list and be absent from my own toxic database before it&rsquo;s allowed in.</li>
<li>A <a href="https://plantsm.art/symptoms/">symptom finder</a> that allows you to perform a plant search backwards. You can click on multiple symptoms and it&rsquo;ll filter accordingly.</li>
<li>An emergency strip on every plant page with the poison-control hotlines. Unfortunately, for now, these are US numbers. Localising them by region is on the roadmap.</li>
<li>Wikipedia descriptions for nearly every plant credited under CC BY-SA.</li>
<li>Around 3,000 common-name redirects, so <code>/plant/peace-lily/</code> finally lands somewhere useful.</li>
<li>Because it made sense, the whole thing is now a PWA and works completely offline. So, now you can use the site in a greenhouse with no signal, which is precisely where this site is most likely to get used.</li>
</ul>
<figure class="pull-quote">
  <span class="pull-quote-mark" aria-hidden="true"><span class="icon "><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M108 72v72H40a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h60a8 8 0 0 1 8 8m108-8h-60a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h68V72a8 8 0 0 0-8-8" opacity=".2"/><path d="M100 56H40a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80H40V72h60Zm116-80h-60a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80h-60V72h60Z"/></g></svg></span></span>
  <blockquote>
    <p>My friends can no longer accuse me of trying to teach people how to poison their neighbour's pets.</p>
  </blockquote>
  <figcaption>
    <cite>J'Accuse...!</cite>
  </figcaption>
</figure>

<p>Once I started auditing the data properly, I started to see a few additional gaps. There were several plants with multiple entries under different, but similar, names. I deduped and merged the symptom data and also updated the severity groupings to make more sense.</p>
<p>I also quickly realised that merging <code>seizure</code> into <code>seizures</code> would have silently promoted over a hundred plants to &ldquo;Critical&rdquo;. The severity groups treated those two spellings as <em>different levels</em>. The little cleaner script I threw together caught it during a dry run. The script grew a rule about never merging across severity levels as a result. The plants that genuinely deserved promotion got one. For instance <a href="https://plantsm.art/plant/ageratina-altissima/">white snakeroot</a>, the plant that killed Lincoln&rsquo;s mother, was somehow rated &ldquo;Severe&rdquo;. The script properly re-rated it to &ldquo;Critical&rdquo;.</p>
<p>The enrichment didn&rsquo;t stop there. I sourced proper taxonomy for every record via GBIF, per-plant toxicity principles with a glossary and little world maps showing their native origins.</p>
<h2 id="there-was-also-quite-a-bit-of-fat-to-trim">
  <a class="heading-link" href="#there-was-also-quite-a-bit-of-fat-to-trim">There was also quite a bit of fat to trim.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Somewhere in all this I noticed every page weighed over <code>200kB</code>. This included pages that were basically just copy. The culprits were painfully self-inflicted. There was an <code>85kB</code> inline script shipping all ten animated logo SVGs to every single page. The same animal icon inlined seventy times on the front page. Oh, and let&rsquo;s not forget my personal favourite, the attributions page carrying <code>149kB</code> of pure <em>indentation</em> that a template loop had quietly emitted. I was shipping whitespace at scale.</p>
<p>The fixes were old-school and very satisfying. A single fingerprinted and cached JS bundle, SVG sprites so each drawing only ships once and one well-placed whitespace trim yielded the following results:</p>
<table>
	<thead>
			<tr>
					<th>page</th>
					<th>before</th>
					<th>after</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>home</td>
					<td>247 KB</td>
					<td>56 KB</td>
			</tr>
			<tr>
					<td>attributions</td>
					<td>238 KB</td>
					<td>24 KB</td>
			</tr>
			<tr>
					<td>safe plants</td>
					<td>270 KB</td>
					<td>33 KB</td>
			</tr>
			<tr>
					<td>plant pages</td>
					<td>114 KB</td>
					<td>36 KB</td>
			</tr>
	</tbody>
</table>
<p>No framework was harmed, or indeed present, in the making of this facelift.</p>
<h2 id="we-ship-from-the-homelab-now">
  <a class="heading-link" href="#we-ship-from-the-homelab-now">We ship from the homelab now.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The last box to tick from the original post was the bittersweet one. In 2022 I wanted the project &ldquo;open-source and community-driven&rdquo; on GitHub. In practice, the community contribution was me, at midnight, fixing my own typos. So, the repository followed this blog into my homelab using the exact same moving parts as outlined in <a href="https://wilhelm.codes/blog/my-blog-now-ships-from-my-homelab/">My Blog Now Ships From My Homelab!</a>. It just made sense after this site&rsquo;s migration. The <em>data</em> remains as open as ever through the API, which was always the part worth opening. That will never change.</p>
<p>While migrating I had a small panic about the 315MB of plant photos sitting in my working tree, imagining every CI build hauling them across the network. Turns out my 2023 self had already gitignored them and the entire repository clones at 7.5MB. Every once in a blue moon past me does something right. I&rsquo;ve decided to let sleeping dogs lie and not investigate what else he buried in there.</p>
<h2 id="was-the-juice-worth-the-squeeze">
  <a class="heading-link" href="#was-the-juice-worth-the-squeeze">Was the juice worth the squeeze?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Very much so. The site is faster, cuter and finally says the most important thing it knows on every card: <em>how</em> dangerous is this plant, not just <em>whether</em>. It builds from nothing with a single binary and deploys itself when I push. The data is cleaner and more robust than it has ever been and enriched well past what 2022 me had the patience for.</p>
<p>But the honest win is the same one as the blog demolition. The whole thing is now boring to operate and boring is what lets a hobby project survive its maintainer&rsquo;s attention span.</p>
<p>If you have a plant-curious pet, <a href="https://plantsm.art"><span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span></a> is free, the <a href="https://plantsm.art/api/">API</a> is free and the <a href="https://plantsm.art/safe/">safe list</a> might even let you buy something nice for yourself. And if four years from now there&rsquo;s a third post in this series, may it be shorter than this one.</p>
<figure class="pull-quote">
  <span class="pull-quote-mark" aria-hidden="true"><span class="icon "><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M108 72v72H40a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h60a8 8 0 0 1 8 8m108-8h-60a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h68V72a8 8 0 0 0-8-8" opacity=".2"/><path d="M100 56H40a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80H40V72h60Zm116-80h-60a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80h-60V72h60Z"/></g></svg></span></span>
  <blockquote>
    <p>The data enrichments will continue until "Morella" improves.</p>
  </blockquote>
  <figcaption>
    <cite>C'mon, this was a GREAT pun!</cite>
  </figcaption>
</figure>

<p>Anyways, adios&hellip;</p>]]></content:encoded></item><item><title>Some Long Overdue Demolition</title><link>https://wilhelm.codes/blog/some-long-overdue-demolition/</link><pubDate>Sun, 23 Aug 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/some-long-overdue-demolition/</guid><category>hugo</category><category>css</category><category>performance</category><category>wilhelm.codes</category><wc:kind>post</wc:kind><description>Two months ago I wrote Some Long Overdue Housekeeping. A post about giving this blog a proper facelift. I untangled my Tailwind setup, modernised Hugo and taught it to drive the compiler natively and generally felt very pleased with the state of things.
So, of course just weeks later I have this burning desire to nuke it all from orbit!</description><content:encoded><![CDATA[<p>Two months ago I wrote <a href="https://wilhelm.codes/blog/some-long-overdue-housekeeping/">Some Long Overdue Housekeeping</a>. A post about giving this blog a proper facelift. I untangled my Tailwind setup, modernised Hugo and taught it to drive the compiler natively and generally felt very pleased with the state of things.</p>
<p>So, of course just weeks later I have this burning desire to nuke it all from orbit!</p>
<figure class="pull-quote">
  <span class="pull-quote-mark" aria-hidden="true"><span class="icon "><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M108 72v72H40a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h60a8 8 0 0 1 8 8m108-8h-60a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h68V72a8 8 0 0 0-8-8" opacity=".2"/><path d="M100 56H40a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80H40V72h60Zm116-80h-60a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80h-60V72h60Z"/></g></svg></span></span>
  <blockquote>
    <p>This means no more standalone Tailwind config, no committed stylesheet, no <code>node_modules</code> in <code>static/</code> and no build toolchain leaking onto the live site. Very cool!</p>
  </blockquote>
  <figcaption>
    <cite>me, in June, admiring a thing I have since bulldozed</cite>
  </figcaption>
</figure>

<p>To be clear, the June cleanup wasn&rsquo;t <em>wrong</em>. The pipeline was genuinely tidy. But, a tidy pipeline is still a pipeline and somewhere between then and now I started asking a more annoying question: why does a personal blog, a pile of static text, need a JavaScript toolchain at all? Why do I need to buy into the Tailwind framework when I knew full well how to write vanilla CSS with my own hands? Every <code>npm install</code> was a little tax that I&rsquo;d eventually have to pay with interest the next time I wanted to update the site&rsquo;s dependencies. I felt as if I wasn&rsquo;t maintaining a blog. I was maintaining the machinery that produces a blog and the machinery had some very strong opinions.</p>
<p>So, I did a hard reset with a <em>very</em> satisfying amount of deleting.</p>
<h2 id="the-demolition-">
  <a class="heading-link" href="#the-demolition-">The demolition &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Here&rsquo;s the bill for the redesign commit:</p>
<div class="code-block">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">230 files changed, 3951 insertions(+), 33365 deletions(-)</span></span></code></pre></div>
</div>
<p>That&rsquo;s roughly a 10-to-1 ratio of deleted to written. Which, as a software engineer, is my favourite kind of ratio. I happily added the following the to chopping block:</p>
<ul>
<li><strong>Tailwind v4 and the typography plugin.</strong> Replaced by one hand-written CSS file. The whole thing gzips to under <code>~10kB</code> with syntax highlighting included.</li>
<li><strong>The entire Node toolchain.</strong> The <code>package.json</code>, lockfile, Prettier and its plugins. There is no <code>npm install</code> step anymore because there is no npm anymore.</li>
<li><strong>Fuse.js and the ⌘K search palette.</strong> Nobody was searching this blog. I checked. It was me. I was nobody.</li>
<li><strong>Two self-hosted webfonts.</strong> More on this in a second, because it&rsquo;s the biggest single win.</li>
<li><strong>The changelog page.</strong> This pulled from the GitHub events API and shipped with an <code>18,016</code>-line JSON fixture for local dev. Eighteen <em>thousand</em> lines of someone else&rsquo;s API response, committed to my repo. So, a page barely anybody visited could render in development. I mean, I added it back becase <em>I</em> missed it, but it&rsquo;s <em>much</em> more efficient now.</li>
<li><strong>68 of the 79 icon SVGs.</strong> A 390-line drawer menu and a taxonomy I&rsquo;d stopped using.</li>
</ul>
<p>The layouts went from <code>2,868</code> lines of template across <code>48</code> files to about <code>800</code> lines across <code>21</code>. Not because I got cleverer, but because most of that code was written for the joy of it. Which, look, is a fine reason to build something. It&rsquo;s just a bad reason to keep serving it to strangers. And with all that complexity and cruft came what felt like a lot of brittleness.</p>
<h2 id="the-fonts-had-to-go-too">
  <a class="heading-link" href="#the-fonts-had-to-go-too">The fonts had to go too.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>This one hurt a little. I like Inter and Rubik. Together they were <code>449kB</code> of <code>woff2</code> riding along on every first visit. Which made them, by an enormous margin, the heaviest thing on the site.</p>
<p>So, I dropped them and switched to the system font stack which, to my surprise, actually looked <em>better</em> on the site. The actual typeface was doing very little. Your operating system already ships a perfectly good set. All lovingly hinted by people with far more patience and experience than myself. Best of all it costs zero bytes to use!</p>
<figure class="pull-quote">
  <span class="pull-quote-mark" aria-hidden="true"><span class="icon "><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M108 72v72H40a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h60a8 8 0 0 1 8 8m108-8h-60a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h68V72a8 8 0 0 0-8-8" opacity=".2"/><path d="M100 56H40a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80H40V72h60Zm116-80h-60a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h60v8a32 32 0 0 1-32 32a8 8 0 0 0 0 16a48.05 48.05 0 0 0 48-48V72a16 16 0 0 0-16-16m0 80h-60V72h60Z"/></g></svg></span></span>
  <blockquote>
    <p>By Grabthar's hammer... What a savings.</p>
  </blockquote>
  <figcaption>
    <cite>Dr. Lazarus from Galaxy Quest</cite>
  </figcaption>
</figure>

<h2 id="hugo-finally-home-alone">
  <a class="heading-link" href="#hugo-finally-home-alone">Hugo, finally home alone.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Thanks to all the trimmed fat, the build process is effectively just <code>hugo</code>. No more extended binary, SCSS, image processing or defining <code>NODE_VERSION</code> in Cloudflare Pages required. Fewer dependencies mean fewer moving parts and that&rsquo;s always an improvement.</p>
<h2 id="numbers-or-it-didnt-happen">
  <a class="heading-link" href="#numbers-or-it-didnt-happen">Numbers, or it didn&rsquo;t happen.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Right after the deploy I proudly measured the new site and announced it was &ldquo;under <code>100kB</code>&rdquo;. Which would have been true if the production site were served by my dev server, uncompressed, with a <code>40kB</code> livereload script attached. I made the mistake of measuring the network stats on my local development server. <em>Womp, womp</em>.</p>
<p>The real numbers, from the live site with Cloudflare&rsquo;s <code>brotli</code> doing its thing:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Before</th>
					<th>After</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Cold home page</td>
					<td><code>~750kB</code></td>
					<td><code>~24kB</code></td>
			</tr>
			<tr>
					<td>Webfonts</td>
					<td><code>449kB</code></td>
					<td><code>0kB</code></td>
			</tr>
			<tr>
					<td>Stylesheet ( compressed )</td>
					<td>it&rsquo;s complicated</td>
					<td>under <code>~10kB</code></td>
			</tr>
			<tr>
					<td><code>npm install</code></td>
					<td>required</td>
					<td>there is nothing to install</td>
			</tr>
	</tbody>
</table>
<p>The single largest asset on the home page is now the favicon. <code>7.5kB</code>; roughly a third of the total weight. I think you&rsquo;re allowed to stop optimising when your largest single asset is your <code>favicon.ico</code>. It&rsquo;s an image of my gorgeous mug. It stays. I&rsquo;m quite attached to it.</p>
<h2 id="an-ambitious-first-pass">
  <a class="heading-link" href="#an-ambitious-first-pass">An ambitious first pass.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>In retrospect, I wanted to make everything simpler and unintentionally over-compensated in the opposite direction. I stripped out too much and was now staring at a sad little blog with zero personality.</p>
<p>So, the second half of the job was building the personality back and making my selections a bit more deliberate. One thing led to another and I think I&rsquo;ve produced my finest work yet on a personal blog. It&rsquo;s small and lightening fast. I&rsquo;m not sure where else I can optimise without drastically affecting the overall vibe of the design; the law of diminishing returns&hellip; returns.</p>
<p>The thing about now having such a light frame is adding to it stops feeling like a burden, or something future Wilhelm will have to pay. For example, here are the biggest things I&rsquo;ve added and they <em>barely</em> increased the overall &ldquo;weight&rdquo; of the design.</p>
<h3 id="series">
  <a class="heading-link" href="#series">Series<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>A proper taxonomy for multi-part writing. Built because I&rsquo;ve been wanting to write a Cyberpunk-themed set of tutorials for writing advanced HCL in Go. The series will be about eight posts long and outside of sharing a tag, I had no way to string them together. Series posts wear a stacked-sheet card, a segmented progress meter that is absolutely <em>not</em> a cyberdeck RAM readout and a panel showing every part in the run, including the ones that don&rsquo;t exist yet.</p>
<p>The unpublished parts of a series ship as glyph noise and decode, left to right, when you point at them. The real titles sit in <code>aria-label</code> the whole time, so a screen reader is never handed the static. The browser tab pulls the same trick on every page load, because a title bar is just a very small marquee nobody was using.</p>
<p>Try moving your mouse pointer over some of the &ldquo;encrypted&rdquo; titles on the new <a href="https://wilhelm.codes/series">dedicated</a> page.</p>
<h3 id="tired-leds">
  <a class="heading-link" href="#tired-leds">Tired LEDs<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>The lit chips in the series meters, static leaders in the main page&rsquo;s hero section and a scattered handful of cells in the activity graph flicker like hardware that has seen some things. Their animation clocks share no common factors, so the pattern never visibly repeats. The same no-RNG randomness as the heading glitch. On page load the whole graph boots like an old panel, dark, then a sputter per cell, then steady stream of blinking boxes. Reminiscent of the old monochrome screens and beige boxes of my youth.</p>
<p>Christ, I&rsquo;m getting old.</p>
<h3 id="changelog-resurrected">
  <a class="heading-link" href="#changelog-resurrected">Changelog, resurrected<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Yes, the one from the butcher&rsquo;s block above. However, instead of hitting the associated Github repository directly at build time using a PAT, the new one doesn&rsquo;t call anyone. Commit data is sourced <em>directly</em> from Cloudflare Pages&rsquo; shallow clone it checks out during build time. There&rsquo;s a simple 20-or-so-line shell script reads <code>git log</code> and Hugo renders the result. <code>HEAD</code> gets a lit node that flickers, <a href="https://wilhelm.codes/changelog">because why the hell not</a>?</p>
<h3 id="image-shortcodes">
  <a class="heading-link" href="#image-shortcodes">Image shortcodes<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Something I&rsquo;ve been meaning to add is support for various ways to display images. Each image can now be clicked-to-zoom with attribution in the following forms:</p>
<ul>
<li>Masonry.</li>
<li>A scrollable, horizontal display.</li>
<li>Last, but not least, the typicla single image display.</li>
</ul>
<p>There&rsquo;s actually a sneaky, super-secret page I use to test all my shortcodes and typography stylings where you can <a href="https://wilhelm.codes/blog/markdown-elements/">see everything for yourself</a>.</p>
<p>Even with all these complex additions, it all adds up to a few kilobytes of CSS and inline script. Which is to say the entire second wave of features weighs an order of magnitude less than one of the webfonts I deleted.</p>
<h2 id="my-new-favourite-fidget-toy">
  <a class="heading-link" href="#my-new-favourite-fidget-toy">My new favourite fidget toy.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The part I didn&rsquo;t expect was to glue my eyeballs to the Firefox throttle menu under its network tools panel.</p>
<p>I flipped it to a 2G profile just for shits and giggles - call it a generous <code>50kb</code> per second - and did the napkin math. The old site&rsquo;s <code>750kB</code> works out to between <code>60</code> to <code>90</code> seconds of staring at a blank viewport. The new site lands in about four seconds on a connection profile most of the industry stopped thinking about a decade ago.</p>
<p>I have reloaded this page on simulated garbage networks far more times than any reasonable person should, and it is satisfying <em>every single time</em>. The page just&hellip; appears. Like a website. Remember plain&rsquo;ole websites?</p>
<p>None of this is novel, I know. The &ldquo;your blog doesn&rsquo;t need <code>2MB</code> of JavaScript&rdquo; essay has been written a hundred times. Usually on a blog serving <code>2MB</code> of JavaScript. I&rsquo;m not even the hundred-and-first person to do this. But, there&rsquo;s a difference between nodding along to the idea and watching your own site load over a simulated tin-can connection before you can finish blinking.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The funny thing about the June post is that it ended with me feeling like the blog was finally sorted. And it was! For the architecture it had. It just turned out the architecture itself was the thing that needed to go.</p>
<p>The site is now boring in the best possible way. It builds with one command, it has no dependencies to rot behind my back and the whole design fits in one stylesheet I can actually hold in my head ( not really ). Fewer moving parts, fewer things to forget, fewer surprises the next time I touch it in six months. Best of all, it looks fucking great!</p>
<p>Set-and-forget, but this time with almost nothing left to forget. &hellip; Right?</p>]]></content:encoded></item></channel></rss>