<?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 · Hugo</title><link>https://wilhelm.codes/tags/hugo/</link><description>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/hugo/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>What Good is a Number You Can't Call?</title><link>https://wilhelm.codes/blog/what-good-is-a-number-you-cant-call/</link><pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/what-good-is-a-number-you-cant-call/</guid><category>plantsm.art</category><category>api</category><category>updates</category><category>hugo</category><wc:kind>post</wc:kind><description>This one&amp;rsquo;s relatively short. I feel like I&amp;rsquo;ve been posting about Plant🌱Smart a bit too much lately. But I&amp;rsquo;ve made a small change to how the site displays emergency information and it matters enough to warrant its own post. So, here we go.</description><content:encoded><![CDATA[<p>This one&rsquo;s relatively short. I feel like I&rsquo;ve been posting about <span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span> a bit too much lately. But I&rsquo;ve made a small change to how the site displays emergency information and it matters enough to warrant its own post. So, here we go.</p>
<h2 id="some-background">
  <a class="heading-link" href="#some-background">Some background.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>For three years every plant page on <span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span> ended with the same two phone numbers and both of them were for the US. That never really sat right with me. I even flagged it as roadmap work when I <a href="https://wilhelm.codes/blog/why-i-rebuilt-plant-smart/">rebuilt the site</a>, then left it sitting there. The site&rsquo;s whole reason for existing is for the moment your cat eats something and you need an answer now. For most of the people who land there from a search engine, I was answering with a number they can&rsquo;t dial. So, I recently sat down to address that gap, assuming I&rsquo;d spend a few minutes filling in a table of hotlines and be done with it.</p>
<h2 id="every-single-country-had-a-speed-bump">
  <a class="heading-link" href="#every-single-country-had-a-speed-bump">Every single country had a speed bump.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<ul>
<li>Sweden&rsquo;s poison centre stopped answering questions about animals in July 2018 because the human call volume got too high.</li>
<li>The Dutch line only takes calls from vets, so if you&rsquo;re a pet owner in Rotterdam, the correct advice is &ldquo;ring your vet and let them ring the line&rdquo;, which is not a phone number at all.</li>
<li>Belgium answers animal calls only during the day and says outright on its own site that it isn&rsquo;t funded for animal work.</li>
<li>The Australian helpline I&rsquo;d have sworn was free actually starts at <code>$75</code>. This is a bit embarrassing because I live here and <em>should</em> know this.</li>
<li>The UK&rsquo;s fee changes depending on the time of day you call.</li>
<li>The ASPCA doesn&rsquo;t publish its consultation fee anywhere on its own page, so the figure everyone quotes online comes from third parties.</li>
</ul>
<p>The list goes on.</p>
<p>I&rsquo;d have got at least half of these wrong from memory, which is a genuinely unsettling thing to realise about emergency information.</p>
<p>Once again, I had to collate a bunch of disparate data and create a shape to support it before munging it all together. Every entry in the hotlines list now cites the operator&rsquo;s page it came from and the date I read it which is the <a href="https://wilhelm.codes/blog/every-record-now-has-a-source/">same rule</a> I put on the plant records in the last post. A Python script holds the whole list and nags me when an entry goes over a year old and it runs as part of the automated deployment. So, the build gives me a big&rsquo;ole bonk on the nose whenever the list goes stale.</p>
<h2 id="the-boring-technical-stuff">
  <a class="heading-link" href="#the-boring-technical-stuff">The boring technical stuff.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The engineering ended up being the easy half and mostly a matter of deciding what fails safely. Hugo writes every country into the HTML as a partial. A small script then lifts the relevant one out and folds the rest away. It guesses in order: a country you&rsquo;ve pinned, then the location filter if it names exactly one country, then your browser locale.</p>
<p>It never asks permission for anything, because a permission prompt at 2am is the last thing you need while you&rsquo;re panicking about your pet. And, personally, I <em>hate</em> browser prompts asking for location tracking. Hard pass. If the script doesn&rsquo;t run, or if the service worker hands you a stale page, you get all countries instead of the wrong 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>Too many numbers costs you a few seconds. The wrong number costs you the call.</p>
  </blockquote>
  <figcaption>
    <cite>It me.</cite>
  </figcaption>
</figure>

<p>There&rsquo;s also a new <a href="https://plantsm.art/emergency/">emergency</a> page for the first five minutes before you get through: what to do, what not to do and what they&rsquo;ll ask you. The guidance follows the ASPCA and the Pet Poison Helpline which is also cited and linked. After all, the one thing I&rsquo;m definitely not qualified to write is clinical advice.</p>
<h2 id="another-free-gift-the-hotlines-api">
  <a class="heading-link" href="#another-free-gift-the-hotlines-api">Another free gift: the hotlines API.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>There&rsquo;s really no obvious centralised place online that collates all this information into a single data set. I&rsquo;ve at least done the work of bootstrapping the handful of most common countries that have helplines and the list will definitely grow over time. So, I&rsquo;ve exposed a new &ldquo;hotlines&rdquo; <a href="https://plantsm.art/api/hotlines.json">API endpoint</a> with its own documented shape.</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">JSON</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;countries&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// ... more countries ...
</span></span></span><span class="line"><span class="cl">        <span class="nt">&#34;US&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;checked&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-09-10&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;United States&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;services&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;audience&#34;</span><span class="p">:</span> <span class="s2">&#34;owner&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;display&#34;</span><span class="p">:</span> <span class="s2">&#34;(888) 426-4435&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;fee&#34;</span><span class="p">:</span> <span class="s2">&#34;A consultation fee applies&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;hours&#34;</span><span class="p">:</span> <span class="s2">&#34;24 hours, every day&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;org&#34;</span><span class="p">:</span> <span class="s2">&#34;ASPCA Animal Poison Control Center&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;https://www.aspca.org/pet-care/animal-poison-control&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;tel&#34;</span><span class="p">:</span> <span class="s2">&#34;+18884264435&#34;</span>
</span></span><span class="line"><span class="cl">                <span class="p">},</span>
</span></span><span class="line"><span class="cl">                <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;audience&#34;</span><span class="p">:</span> <span class="s2">&#34;owner&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;display&#34;</span><span class="p">:</span> <span class="s2">&#34;(855) 764-7661&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;fee&#34;</span><span class="p">:</span> <span class="s2">&#34;$89 for each incident&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;hours&#34;</span><span class="p">:</span> <span class="s2">&#34;24 hours, every day&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;org&#34;</span><span class="p">:</span> <span class="s2">&#34;Pet Poison Helpline&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;https://www.petpoisonhelpline.com/about/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="nt">&#34;tel&#34;</span><span class="p">:</span> <span class="s2">&#34;+18557647661&#34;</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;notice&#34;</span><span class="p">:</span> <span class="s2">&#34;Emergency telephone numbers for animal poisoning. Each country cites the operator page the entry came from and the date a person read it. Services close and numbers change, so check an entry before you rely on it. Coverage is not global. A country in this file with an empty services list has been checked and has no service an owner can call. A country that is absent has not been checked.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;updated&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-09-10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>I felt wrapping it in an <a href="https://www.progress.com/blogs/envelope-design-pattern">envelope</a> was the correct approach here. It leads with the notice and the date I last changed anything. A <code>.countries</code> object follows, keyed by ISO 3166-1 alpha-2 code. At the very least, it gives me room to change a country&rsquo;s metadata later without breaking the top-level shape.</p>
<p>The <code>audience</code> field on each service is what carries the Dutch problem into the data. It reads either <code>owner</code> or <code>vet</code>. A <code>vet</code> entry means the number exists but you can&rsquo;t ring it yourself, so the entry tells you to phone your vet and let them make the call.</p>
<p>Anyway, along with some stylistic changes and code cleanup, that&rsquo;s pretty much all there is to it for this update. I&rsquo;ll be giving <span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span> a rest for a bit while all the changes settle in. This gives me some time to work on other fun things!</p>
<p>Enjoy, nerds!</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>My Blog Now Ships From My Homelab!</title><link>https://wilhelm.codes/blog/my-blog-now-ships-from-my-homelab/</link><pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/my-blog-now-ships-from-my-homelab/</guid><category>hugo</category><category>woodpecker</category><category>forgejo</category><category>cloudflare</category><category>homelab</category><wc:kind>post</wc:kind><description>If you&amp;rsquo;ve given GitHub&amp;rsquo;s status page a quick glance recently, you&amp;rsquo;ll immediately see that it&amp;rsquo;s been going through it. Every outage is a small reminder that maybe we shouldn&amp;rsquo;t rely too much on single points of failure we can&amp;rsquo;t directly control.
So, having been bitten several times by intermittent outages and service disruptions, I figured a small personal win would be to migrate the repository for this site to the homelab. Now, it lives on my own Forgejo forge and the builds run on my own Woodpecker cluster. The only things that leave the network are static build artefacts that get pushed up to Cloudflare Pages.
The switch was surprisingly straightforward and took less than 30 minutes overall.</description><content:encoded><![CDATA[<p>If you&rsquo;ve given GitHub&rsquo;s status page a quick glance recently, you&rsquo;ll immediately see that it&rsquo;s been <em>going through it</em>. Every outage is a small reminder that maybe we shouldn&rsquo;t rely too much on single points of failure we can&rsquo;t directly control.</p>
<p>So, having been bitten several times by intermittent outages and service disruptions, I figured a small personal win would be to migrate the repository for <em>this</em> site to the homelab. Now, it lives on my own <a href="https://forgejo.org">Forgejo</a> forge and the builds run on my own <a href="https://woodpecker-ci.org">Woodpecker</a> cluster. The only things that leave the network are static build artefacts that get pushed up to Cloudflare Pages.</p>
<p>The switch was surprisingly straightforward and took less than 30 minutes overall.</p>
<p>Before anyone reaches for the comment box, no, you don&rsquo;t need a homelab for any of this. Forgejo and a Woodpecker cluster ( server and at least one agent ) will happily run on a spare mini PC, an old laptop, that Raspberry Pi you bought with the best of intentions or even as a single Docker Compose file running on the machine you use every day.</p>
<h2 id="how-complex-can-this-possibly-be">
  <a class="heading-link" href="#how-complex-can-this-possibly-be">How complex can this possibly be?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The first useful thing I did was ask myself what GitHub was <em>actually</em> doing for me when it comes to managing this site. After some mulling about, I could only really list the following:</p>
<ul>
<li>Hosting a git repository.</li>
<li>Hosting the comment threads, via <a href="https://giscus.app">giscus</a> Discussions.</li>
</ul>
<p>That&rsquo;s pretty much it. I don&rsquo;t use Actions and GitHub alone never built this site. <a href="https://pages.cloudflare.com">Cloudflare Pages</a> watched the <code>main</code> branch and did the building itself. This is something I covered back in <a href="https://wilhelm.codes/blog/my-blog-publishing-setup/">My Blog Publishing Setup</a>. GitHub&rsquo;s one job was holding a bunch of markdown files.</p>
<p>That&rsquo;s a strange thing to accept as a single point of failure.</p>
<h2 id="the-new-shape-of-things">
  <a class="heading-link" href="#the-new-shape-of-things">The new shape of things.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The replacement looks like this:</p>
<ul>
<li>I push to Forgejo, which lives inside my homelab network.</li>
<li>Forgejo fires off a message to the Woodpecker Server over a webhook. Also inside the network.</li>
<li>The Woodpecker Server assigns the job to an available Woodpecker Agent, which clones the repo, builds the site with Hugo and pushes the result to Cloudflare Pages with <a href="https://developers.cloudflare.com/workers/wrangler/">wrangler</a>.</li>
<li>Cloudflare serves the files at the edge, exactly as before.</li>
</ul>
<p>I just added support for mermaid diagrams, so of course I&rsquo;m not going to let the opportunity to show it off slide:</p>
<figure class="diagram"><svg id="d-702390838d2d" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="max-width: 865px; background-color: transparent;" viewBox="-50 -10 865 421" role="graphics-document document" aria-roledescription="sequence"><g><rect x="615" y="335" fill="#eaeaea" stroke="#666" width="150" height="65" name="C" rx="3" ry="3" class="actor actor-bottom"/><text x="690" y="367.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="690" dy="0">Pages</tspan></text></g><g><rect x="400" y="335" fill="#eaeaea" stroke="#666" width="150" height="65" name="W" rx="3" ry="3" class="actor actor-bottom"/><text x="475" y="367.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="475" dy="0">Woodpecker</tspan></text></g><g><rect x="200" y="335" fill="#eaeaea" stroke="#666" width="150" height="65" name="F" rx="3" ry="3" class="actor actor-bottom"/><text x="275" y="367.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="275" dy="0">Forgejo</tspan></text></g><g><rect x="0" y="335" fill="#eaeaea" stroke="#666" width="150" height="65" name="D" rx="3" ry="3" class="actor actor-bottom"/><text x="75" y="367.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="75" dy="0">The desk</tspan></text></g><g><line id="actor3" x1="690" y1="65" x2="690" y2="335" class="actor-line 200" stroke-width="0.5px" stroke="#999" name="C" data-et="life-line" data-id="C"/><g id="root-3" data-et="participant" data-type="participant" data-id="C"><rect x="615" y="0" fill="#eaeaea" stroke="#666" width="150" height="65" name="C" rx="3" ry="3" class="actor actor-top"/><text x="690" y="32.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="690" dy="0">Pages</tspan></text></g></g><g><line id="actor2" x1="475" y1="65" x2="475" y2="335" class="actor-line 200" stroke-width="0.5px" stroke="#999" name="W" data-et="life-line" data-id="W"/><g id="root-2" data-et="participant" data-type="participant" data-id="W"><rect x="400" y="0" fill="#eaeaea" stroke="#666" width="150" height="65" name="W" rx="3" ry="3" class="actor actor-top"/><text x="475" y="32.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="475" dy="0">Woodpecker</tspan></text></g></g><g><line id="actor1" x1="275" y1="65" x2="275" y2="335" class="actor-line 200" stroke-width="0.5px" stroke="#999" name="F" data-et="life-line" data-id="F"/><g id="root-1" data-et="participant" data-type="participant" data-id="F"><rect x="200" y="0" fill="#eaeaea" stroke="#666" width="150" height="65" name="F" rx="3" ry="3" class="actor actor-top"/><text x="275" y="32.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="275" dy="0">Forgejo</tspan></text></g></g><g><line id="actor0" x1="75" y1="65" x2="75" y2="335" class="actor-line 200" stroke-width="0.5px" stroke="#999" name="D" data-et="life-line" data-id="D"/><g id="root-0" data-et="participant" data-type="participant" data-id="D"><rect x="0" y="0" fill="#eaeaea" stroke="#666" width="150" height="65" name="D" rx="3" ry="3" class="actor actor-top"/><text x="75" y="32.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="75" dy="0">The desk</tspan></text></g></g><style>#d-702390838d2d{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;font-size:14px;fill:#32d74b;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#d-702390838d2d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#d-702390838d2d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#d-702390838d2d .error-icon{fill:#202225;}#d-702390838d2d .error-text{fill:#e3e3de;stroke:#e3e3de;}#d-702390838d2d .edge-thickness-normal{stroke-width:1px;}#d-702390838d2d .edge-thickness-thick{stroke-width:3.5px;}#d-702390838d2d .edge-pattern-solid{stroke-dasharray:0;}#d-702390838d2d .edge-thickness-invisible{stroke-width:0;fill:none;}#d-702390838d2d .edge-pattern-dashed{stroke-dasharray:3;}#d-702390838d2d .edge-pattern-dotted{stroke-dasharray:2;}#d-702390838d2d .marker{fill:#8b8b84;stroke:#8b8b84;}#d-702390838d2d .marker.cross{stroke:#8b8b84;}#d-702390838d2d svg{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;font-size:14px;}#d-702390838d2d p{margin:0;}#d-702390838d2d .actor{stroke:#3c3f43;fill:#1e1f22;stroke-width:1;}#d-702390838d2d rect.actor.outer-path[data-look="neo"]{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-702390838d2d rect.note[data-look="neo"]{stroke:hsl(52.6829268293, 60%, 93.9215686275%);fill:#fff5ad;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-702390838d2d text.actor&gt;tspan{fill:#32d74b;stroke:none;}#d-702390838d2d .actor-line{stroke:#8b8b84;}#d-702390838d2d .innerArc{stroke-width:1.5;stroke-dasharray:none;}#d-702390838d2d .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#8b8b84;}#d-702390838d2d .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#8b8b84;}#d-702390838d2d [id$="-arrowhead"] path{fill:#8b8b84;stroke:#8b8b84;}#d-702390838d2d .sequenceNumber{fill:#74747b;}#d-702390838d2d [id$="-sequencenumber"]{fill:#8b8b84;}#d-702390838d2d [id$="-crosshead"] path{fill:#8b8b84;stroke:#8b8b84;}#d-702390838d2d .messageText{fill:#32d74b;stroke:none;}#d-702390838d2d .labelBox{stroke:#3c3f43;fill:#1e1f22;filter:none;}#d-702390838d2d .labelText,#d-702390838d2d .labelText&gt;tspan{fill:#32d74b;stroke:none;}#d-702390838d2d .loopText,#d-702390838d2d .loopText&gt;tspan{fill:#32d74b;stroke:none;}#d-702390838d2d .sectionTitle,#d-702390838d2d .sectionTitle&gt;tspan{fill:#32d74b;stroke:none;}#d-702390838d2d .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:#3c3f43;fill:#3c3f43;}#d-702390838d2d .note{stroke:hsl(52.6829268293, 60%, 93.9215686275%);fill:#fff5ad;}#d-702390838d2d .noteText,#d-702390838d2d .noteText&gt;tspan{fill:#333;stroke:none;font-weight:normal;}#d-702390838d2d .activation0{fill:#202225;stroke:hsl(216, 7.2463768116%, 3.5294117647%);}#d-702390838d2d .activation1{fill:#202225;stroke:hsl(216, 7.2463768116%, 3.5294117647%);}#d-702390838d2d .activation2{fill:#202225;stroke:hsl(216, 7.2463768116%, 3.5294117647%);}#d-702390838d2d .actorPopupMenu{position:absolute;}#d-702390838d2d .actorPopupMenuPanel{position:absolute;fill:#1e1f22;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#d-702390838d2d .actor-man circle,#d-702390838d2d line{fill:#1e1f22;stroke-width:2px;}#d-702390838d2d g rect.rect{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));stroke:#3c3f43;}#d-702390838d2d .node .neo-node{stroke:#3c3f43;}#d-702390838d2d [data-look="neo"].node rect,#d-702390838d2d [data-look="neo"].cluster rect,#d-702390838d2d [data-look="neo"].node polygon{stroke:url(#d-702390838d2d-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-702390838d2d [data-look="neo"].swimlane.cluster rect{filter:none;}#d-702390838d2d [data-look="neo"].node path{stroke:url(#d-702390838d2d-gradient);stroke-width:1px;}#d-702390838d2d [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-702390838d2d [data-look="neo"].node .neo-line path{stroke:#3c3f43;filter:none;}#d-702390838d2d [data-look="neo"].node circle{stroke:url(#d-702390838d2d-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-702390838d2d [data-look="neo"].node circle .state-start{fill:#000000;}#d-702390838d2d [data-look="neo"].icon-shape .icon{fill:url(#d-702390838d2d-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-702390838d2d [data-look="neo"].icon-shape .icon-neo path{stroke:url(#d-702390838d2d-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-702390838d2d :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g/><defs><symbol id="d-702390838d2d-computer" width="24" height="24"><path transform="scale(.5)" d="M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z"/></symbol></defs><defs><symbol id="d-702390838d2d-database" fill-rule="evenodd" clip-rule="evenodd"><path transform="scale(.5)" d="M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z"/></symbol></defs><defs><symbol id="d-702390838d2d-clock" width="24" height="24"><path transform="scale(.5)" d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z"/></symbol></defs><defs><marker id="d-702390838d2d-arrowhead" refX="7.9" refY="5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M -1 0 L 10 5 L 0 10 z"/></marker></defs><defs><marker id="d-702390838d2d-crosshead" markerWidth="15" markerHeight="8" orient="auto" refX="4" refY="4.5"><path fill="none" stroke="#000000" stroke-width="1pt" d="M 1,2 L 6,7 M 6,2 L 1,7" style="stroke-dasharray: 0, 0;"/></marker></defs><defs><marker id="d-702390838d2d-filled-head" refX="15.5" refY="7" markerWidth="20" markerHeight="28" orient="auto"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker id="d-702390838d2d-sequencenumber" refX="15" refY="15" markerWidth="60" markerHeight="40" orient="auto"><circle cx="15" cy="15" r="6"/></marker></defs><defs><marker id="d-702390838d2d-solidTopArrowHead" refX="7.9" refY="7.25" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M 0 0 L 10 8 L 0 8 z"/></marker></defs><defs><marker id="d-702390838d2d-solidBottomArrowHead" refX="7.9" refY="0.75" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M 0 0 L 10 0 L 0 8 z"/></marker></defs><defs><marker id="d-702390838d2d-stickTopArrowHead" refX="7.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M 0 0 L 7 7" stroke="black" stroke-width="1.5" fill="none"/></marker></defs><defs><marker id="d-702390838d2d-stickBottomArrowHead" refX="7.5" refY="0" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M 0 7 L 7 0" stroke="black" stroke-width="1.5" fill="none"/></marker></defs><text x="174" y="80" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">git push ( main )</text><line x1="76" y1="109" x2="271" y2="109" class="messageLine0" data-et="message" data-id="i0" data-from="D" data-to="F" stroke-width="2" stroke="none" marker-end="url(#d-702390838d2d-arrowhead)" style="fill: none;"/><text x="374" y="124" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">webhook</text><line x1="276" y1="153" x2="471" y2="153" class="messageLine0" data-et="message" data-id="i1" data-from="F" data-to="W" stroke-width="2" stroke="none" marker-end="url(#d-702390838d2d-arrowhead)" style="fill: none;"/><text x="476" y="168" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">diagrams, changelog, hugo --gc</text><path d="M 476,197 C 536,187 536,227 476,217" class="messageLine0" data-et="message" data-id="i2" data-from="W" data-to="W" stroke-width="2" stroke="none" marker-end="url(#d-702390838d2d-arrowhead)" style="fill: none;"/><text x="581" y="242" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">wrangler pages deploy</text><line x1="476" y1="271" x2="686" y2="271" class="messageLine0" data-et="message" data-id="i3" data-from="W" data-to="C" stroke-width="2" stroke="none" marker-end="url(#d-702390838d2d-arrowhead)" style="fill: none;"/><text x="384" y="286" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">the same site, but newer</text><line x1="689" y1="315" x2="79" y2="315" class="messageLine1" data-et="message" data-id="i4" data-from="C" data-to="D" stroke-width="2" stroke="none" marker-end="url(#d-702390838d2d-arrowhead)" style="stroke-dasharray: 3, 3; fill: none;"/></svg></figure><p>The part I want you to notice is the direction of travel. Under the old setup, Cloudflare reached <em>into</em> GitHub to pull my source. Under the new one, nothing reaches in at all. The forge and the CI server accept no traffic from the internet. The only packet that crosses the network boundary is an <em>outbound</em> upload of compiled static assets.</p>
<p>That reversal is the entire security model which was pretty much the point. What looks like &ldquo;the old way but more steps&rdquo; allows me to control every aspect of the pipeline up to <em>just</em> before we ship the build artefacts. A push-based deploy means self-hosting your forge requires exposing exactly nothing. No reverse proxy gymnastics, no VPN for the webhook and no port forwarding required.</p>
<p>If my homelab bursts into flames mid-afternoon ( a real possibility as I <em>do</em> live in Queensland, Australia ) Cloudflare keeps serving the last deploy and nobody visiting the site would ever know. That being said, nobody actually <em>does</em> visit this site, so&hellip;</p>
<h2 id="a-small-intermission">
  <a class="heading-link" href="#a-small-intermission">A small intermission.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A short aside, because it&rsquo;s a question <em>I&rsquo;d</em> ask. My internal services hang off a domain I genuinely own, but resolving it depends on where you&rsquo;re standing. Inside the network a locally hosted 3-node <a href="https://technitium.com/dns/">Technitium</a> cluster runs the hosted zone and the router sends all DNS requests through it. Initially, I used this for network-wide adblock, which works an absolute treat. It also means the forge and the CI cluster resolve to their internal addresses. Outside the network, the public zone points at a landing page hosted elsewhere.</p>
<p>Same hostname and two completely different worlds depending on which side of the wall you&rsquo;re on. <a href="https://en.wikipedia.org/wiki/Split-horizon_DNS">Split-horizon DNS</a> is nothing new, but it&rsquo;s what lets me use real TLS-friendly hostnames for internal services without those services ever being reachable from the internet.</p>
<p>If I <em>did</em> decide to open some services to the outside world, like my Navidrome server, I could use an overlay network like <a href="https://netbird.io">Netbird</a> to provide friends and family access via a familiar naming convention without ever opening a port on the router.</p>
<h2 id="the-part-cloudflare-doesnt-clearly-advertise">
  <a class="heading-link" href="#the-part-cloudflare-doesnt-clearly-advertise">The part Cloudflare doesn&rsquo;t clearly advertise.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A Pages project is either connected to a git provider or it accepts direct uploads. Never both. As long as your project is wired to GitHub, <code>wrangler pages deploy</code> will be rejected and you will get screamed at.</p>
<p>Disconnect the git integration in the project settings and the project quietly converts to &ldquo;Direct Upload&rdquo; mode. It keeps its name, its custom domain and its whole deployment history. It just stops watching a repository and starts waiting for you to hand it those delicious files. While you&rsquo;re in there, make sure the production branch is properly specified: <code>main</code> in my case. This matters because Wrangler tags each upload with a branch name. An upload whose branch matches the production branch goes live on the site. An upload under any other name becomes a preview deployment with its own URL instead.</p>
<p>After that, the deploy is one command requiring two secrets:</p>
<ul>
<li>An API token scoped to <code>Cloudflare Pages: Edit</code></li>
<li>Your Cloudflare account ID.</li>
</ul>
<p>Both go into the Woodpecker Server as repository secrets.</p>
<h2 id="finally-our-favourite-yaml">
  <a class="heading-link" href="#finally-our-favourite-yaml">Finally, our favourite! YAML.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The whole thing is just one file named <code>.woodpecker.yml</code> that squats in the repository root.</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">YAML</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">when</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">event</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">push, manual]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branch</span><span class="p">:</span><span class="w"> </span><span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">clone</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">git</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">woodpeckerci/plugin-git</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">settings</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">partial</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">diagrams</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">minlag/mermaid-cli:11.16.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">commands</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./bin/render-diagrams</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">hugomods/hugo:debian-git-0.163.3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HUGO_ENV</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HUGO_ENVIRONMENT</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">commands</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">git config --global --add safe.directory &#39;*&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./bin/changelog</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">hugo --gc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">node:22-slim</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CLOUDFLARE_API_TOKEN</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">from_secret</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare_api_token</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CLOUDFLARE_ACCOUNT_ID</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">from_secret</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare_account_id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">commands</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="p">&gt;-</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        npx --yes wrangler@4 pages deploy public
</span></span></span><span class="line"><span class="cl"><span class="sd">        --project-name=wilhelm-codes
</span></span></span><span class="line"><span class="cl"><span class="sd">        --branch=main
</span></span></span><span class="line"><span class="cl"><span class="sd">        --commit-hash=&#34;${CI_COMMIT_SHA}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">when</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">event</span><span class="p">:</span><span class="w"> </span><span class="l">push</span></span></span></code></pre></div>
</div>
<h3 id="only-build-off-main">
  <a class="heading-link" href="#only-build-off-main">Only build off <code>main</code>.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>I test design changes and new entries locally, so a push to any other branch doesn&rsquo;t start a pipeline.</p>
<h3 id="we-want-a-full-clone-not-a-shallow-one">
  <a class="heading-link" href="#we-want-a-full-clone-not-a-shallow-one">We want a full clone, not a shallow one.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>This site&rsquo;s <a href="https://wilhelm.codes/changelog/">changelog page</a> renders the git log and Hugo&rsquo;s <code>enableGitInfo</code> reads commit dates for every page. This is something I wrote about in <a href="https://wilhelm.codes/blog/a-changelog-that-builds-itself/">A Changelog That Builds Itself</a>. CI systems love shallow clones and a shallow clone here means I can&rsquo;t convey the full historical picture on the page. Worse, <code>enableGitInfo</code> derives every article&rsquo;s dates from that same truncated log, so they all come out wrong besides. Setting <code>partial: false</code> makes Woodpecker fetch the entire history.</p>
<h3 id="diagrams-render-before-anything-else">
  <a class="heading-link" href="#diagrams-render-before-anything-else">Diagrams render before anything else.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Articles on this site can embed <a href="https://mermaid.js.org">Mermaid</a> diagrams, like the sequence diagram further up the page. They render to static SVG at build time, not in your browser, so the site ships zero diagram JavaScript. A small script hashes each diagram&rsquo;s source, renders anything that has no matching SVG yet and prunes any SVG nothing references any more. I run it locally and commit the SVGs alongside the content, which means on a clean commit this step does precisely nothing. It exists as a safety net for the day I inevitably forget.</p>
<p>So when I said the diagram was rendered by the pipeline it describes, I meant it literally. If I ever edit that diagram and forget to render it, this step is what quietly saves the deploy.</p>
<h3 id="the-build-step-is-a-stock-hugo-image">
  <a class="heading-link" href="#the-build-step-is-a-stock-hugo-image">The build step is a stock Hugo image.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Nothing is installed on the agent itself unless you count Docker image pulls. The image pins the exact Hugo version, ships the extended edition and includes <code>git</code> and <code>bash</code>. Both of which the changelog script that generates the data requires. The <code>safe.directory</code> line exists because the cloned workspace is owned by a different user than the one inside the container and <code>git</code> these days refuses to read a repository it thinks you stole.</p>
<h3 id="the-deploy-step-is-a-stock-node-image">
  <a class="heading-link" href="#the-deploy-step-is-a-stock-node-image">The deploy step is a stock Node image.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p><code>npx</code> pulls Wrangler at run time. Wrangler then reads the two secrets from the environment and pushes <code>public/</code> up to Pages and that&rsquo;s pretty much it. Dependencies are snagged and cached at build time, which means nothing to maintain on the agents. Either step&rsquo;s version gets bumped by editing one line.</p>
<div class="callout callout-info">
  I already have <a href="https://distribution.github.io">distribution</a> hosted in the homelab for my local Docker images. I could easily configure it to support <a href="https://distribution.github.io/distribution/recipes/mirror/">pull-through caching</a>, so I don&rsquo;t keep having to pull directly from Docker Hub every time a build agent&rsquo;s local cache expires.
</div>

<p>Complete parity with what Cloudflare&rsquo;s own build pipeline was doing, except now I can read every line of it and make it do whatever I need.</p>
<h2 id="i-did-run-into-some-speed-bumps-along-the-way">
  <a class="heading-link" href="#i-did-run-into-some-speed-bumps-along-the-way">I did run into some speed bumps along the way.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>For the record, it took only three tries to get the pipeline to a &ldquo;green&rdquo; state.</p>
<figure class="img-single">
    <button type="button" class="zoom" aria-label="Zoom image: A Woodpecker Server build history screenshot showing the first 3 attempts.">
      <img
  src="https://wilhelm.codes/blog/my-blog-now-ships-from-my-homelab/three-tries_hu_195442048d80ae9b.webp"
  srcset="/blog/my-blog-now-ships-from-my-homelab/three-tries_hu_195442048d80ae9b.webp 736w, /blog/my-blog-now-ships-from-my-homelab/three-tries_hu_ad889cca69fa6327.webp 1104w, /blog/my-blog-now-ships-from-my-homelab/three-tries_hu_132d2f011cbace60.webp 1472w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="232"
  alt="A Woodpecker Server build history screenshot showing the first 3 attempts."
  loading="lazy"
  decoding="async"
  data-full="/blog/my-blog-now-ships-from-my-homelab/three-tries_hu_8fa8b30bf4601afd.webp"
  data-credit="Third time&#39;s a charm."
  
/>

    </button>
    
      <figcaption>
        
          Third time&#39;s a charm.
        
      </figcaption>
    
  </figure>
<p>In order of discovery:</p>
<ul>
<li>The plain version tags of the Hugo image <em>are</em> the extended edition. The <code>reg-</code> prefix means regular and none of them include <code>git</code> unless the tag says <code>git</code>. The tag you guess first ( <code>exts-&lt;version&gt;</code> ) doesn&rsquo;t exist. It&rsquo;s an odd convention, which is why it threw me off.</li>
<li>Woodpecker substitutes <code>${VAR}</code> expressions in the YAML <em>before</em> the shell ever runs. Anything it doesn&rsquo;t know becomes an empty string. My <code>safe.directory &quot;${CI_WORKSPACE}&quot;</code> quietly became <code>safe.directory &quot;&quot;</code>. Hence the asterisk / glob in the YAML.</li>
<li>The changelog script opens with <code>#!/usr/bin/env bash</code> and the default Hugo images are Alpine. <code>env: can't execute 'bash'</code> is the whole error message you get, so I just went with the Debian variants instead which include <code>bash</code> out of the box.</li>
</ul>
<p>None of these are hard problems, but worth pointing out.</p>
<h2 id="im-not-deleting-the-old-repository-completely">
  <a class="heading-link" href="#im-not-deleting-the-old-repository-completely">I&rsquo;m not deleting the old repository&hellip; completely.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The GitHub repository still exists if only to provide a place for the giscus integration to store comments. It offloads most of this functionality to GitHub&rsquo;s Discussions feature. Delete the repo and every comment ever left on this site goes with it; all 2 of them.</p>
<p>It no longer needed the full source history to do that job either. It was ported on over to my local forge with Forgejo&rsquo;s native GitHub repository migration feature. So, I force-pushed the whole history into a black hole, leaving a single commit containing only a README. The remaining husk also has a second job coming; it&rsquo;ll eventually host the companion code for tutorial content, so readers can clone examples without ever touching my forge.</p>
<p>Each service kept exactly the job it&rsquo;s uniquely good at. Cloudflare serves files from the edge better than my house ever could and no fucking way am I publicly opening ports on a router. I&rsquo;d rather lick the rim of a pub toilet. GitHub Discussions gives commenters an identity they already have. Everything else now lives at home.</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>Was any of this necessary? For a static blog that deploys in under a minute, probably not. The old setup worked fine and if yours does too, keep it. I&rsquo;m not here to convince anyone.</p>
<p>There&rsquo;s a special kind of satisfaction in watching a pipeline run end to end on hardware you can physically point at, knowing the only cloud dependency left is the one actually earning its keep. The next GitHub outage can happen without me. At least for <em>this</em> project anyway.</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><item><title>Having a play with a more dynamic menu drawer. Just working out the …</title><link>https://wilhelm.codes/bits/update-new-menu/</link><pubDate>Sat, 04 Jul 2026 13:17:45 +1000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/bits/update-new-menu/</guid><category>wilhelm.codes</category><category>hugo</category><wc:kind>bit</wc:kind><wc:bit>update</wc:bit><wc:untitled>true</wc:untitled><description>Having a play with a more dynamic menu drawer. Just working out the kinks on smaller screens.</description><content:encoded>&lt;p>Having a play with a more dynamic menu drawer. Just working out the kinks on smaller screens.&lt;/p>
</content:encoded></item><item><title>Decided to introduce a new projects page to showcase some of the …</title><link>https://wilhelm.codes/bits/update-new-projects-page/</link><pubDate>Thu, 02 Jul 2026 20:39:18 +1000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/bits/update-new-projects-page/</guid><category>wilhelm.codes</category><category>hugo</category><wc:kind>bit</wc:kind><wc:bit>update</wc:bit><wc:untitled>true</wc:untitled><description>Decided to introduce a new projects page to showcase some of the bigger things I&amp;rsquo;m working on. I&amp;rsquo;m still working out the kinks and I&amp;rsquo;m not 100% sold on the current design. Each card is powered by a .toml based datasource in Hugo, contains a kind of linear heatmap of activity related to the associated project&amp;rsquo;s tag as well as the 5 latest related posts. Good enough to publish, I guess.</description><content:encoded><![CDATA[<p>Decided to introduce a new <a href="https://wilhelm.codes/projects">projects</a> page to showcase some of the bigger things I&rsquo;m working on. I&rsquo;m still working out the kinks and I&rsquo;m not 100% sold on the current design. Each card is powered by a <code>.toml</code> based datasource in Hugo, contains a kind of linear heatmap of activity related to the associated project&rsquo;s tag as well as the 5 latest related posts. Good enough to publish, I guess.</p>
<p>I also can&rsquo;t decide on which activity graph animation I like the most. 😅</p>
]]></content:encoded></item><item><title>Let Me Show You My Bits</title><link>https://wilhelm.codes/blog/let-me-show-you-my-bits/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/let-me-show-you-my-bits/</guid><category>hugo</category><category>css</category><category>tailwindcss</category><category>wilhelm.codes</category><wc:kind>post</wc:kind><description>A while back, in Reading Between the Posts, I wired up the home page so the quiet gaps between articles would narrate themselves - &amp;ldquo;1 bit and 41 changelog events in between&amp;rdquo;. There was just one tiny problem with that sentence. At the time I had written exactly zero bits.</description><content:encoded><![CDATA[<p>A while back, in <a href="https://wilhelm.codes/blog/reading-between-the-posts/">Reading Between the Posts</a>, I wired up the home page so the quiet gaps between articles would narrate themselves - &ldquo;<em>1 bit and 41 changelog events in between</em>&rdquo;. There was just one tiny problem with that sentence. At the time I had written exactly <em>zero</em> bits.</p>
<p>So let me finally show you my bits.</p>
<h2 id="what-even-is-a-bit">
  <a class="heading-link" href="#what-even-is-a-bit">What even is a bit?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A bit is the smallest unit of &ldquo;I made a thing&rdquo; I&rsquo;m willing to commit to. It could be a quote that stuck with me, a link worth keeping, the odd YouTube video, or a one-line thought too small to earn its own long-form entry. Each one is a little markdown file in <code>/bits</code> and  the only thing it really has to declare is what <em>kind</em> of bit it is:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">YAML</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">quote</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-06-17</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Edsger Dijkstra&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;Simplicity is a great virtue but it requires hard work to achieve it.&#34;</span></span></span></code></pre></div>
</div>
<p><code>type</code> is the whole trick that everything else hangs off.</p>
<h2 id="one-shape-per-thought">
  <a class="heading-link" href="#one-shape-per-thought">One shape per thought.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The thing I didn&rsquo;t want was for a pithy quote to render like a bare link or to render like a video. They&rsquo;re different shapes of thought, so they ought to look different.</p>
<p>I&rsquo;d already learned this lesson building the <a href="https://wilhelm.codes/blog/a-changelog-that-builds-itself/">changelog</a>, which leans on one tiny partial per event type. So I stole from myself. There&rsquo;s a single lookup table mapping each <code>type</code> to an icon, an accent colour and a label:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">meta</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">dict</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;quote&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="s">&#34;quote-right&#34;</span><span class="w">    </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;rose&#34;</span><span class="w">    </span><span class="s">&#34;label&#34;</span><span class="w"> </span><span class="s">&#34;quote&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;link&#34;</span><span class="w">  </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="s">&#34;arrow-up-right&#34;</span><span class="w"> </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;blue&#34;</span><span class="w">    </span><span class="s">&#34;label&#34;</span><span class="w"> </span><span class="s">&#34;link&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;video&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="s">&#34;youtube&#34;</span><span class="w">        </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;red&#34;</span><span class="w">     </span><span class="s">&#34;label&#34;</span><span class="w"> </span><span class="s">&#34;video&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;blurb&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="s">&#34;pen-nib&#34;</span><span class="w">        </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;emerald&#34;</span><span class="w"> </span><span class="s">&#34;label&#34;</span><span class="w"> </span><span class="s">&#34;blurb&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>It then matches a micro-partial per type that decides how the content actually renders. Adding a brand new kind of bit is now &ldquo;add one row, add one tiny template&rdquo;, which is about as many steps as I&rsquo;m willing to tolerate.</p>
<p><img
  src="https://wilhelm.codes/blog/let-me-show-you-my-bits/bits_hu_88740d4c34822745.webp"
  srcset="/blog/let-me-show-you-my-bits/bits_hu_88740d4c34822745.webp 736w, /blog/let-me-show-you-my-bits/bits_hu_7c64b843d8098172.webp 1104w, /blog/let-me-show-you-my-bits/bits_hu_321d663198ba2c25.webp 1472w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="596"
  alt=""
  
  loading="lazy"
  decoding="async"
/>
</p>
<h2 id="hugo-why-are-you-like-this">
  <a class="heading-link" href="#hugo-why-are-you-like-this">Hugo, why are you like this?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>My bits started life with two perfectly reasonable front-matter fields: <code>type</code> and <code>url</code>. Hugo took one look and quietly robbed me of both.</p>
<p>It turns out <code>url</code> is a <em>reserved</em> key which Hugo uses to treat as the page&rsquo;s permalink. So, the moment I put a <code>https://...</code> in there, the build fell over with a stern little message about unsupported protocols. <code>type</code> doesn&rsquo;t even have the decency to error. It silently moves out of <code>.Params.type</code> and into <code>.Type</code>, which <em>also</em> hijacks the layout lookup. So my <code>.Params.type</code> was resolving to nothing and every single bit was quietly rendering as the fallback.</p>
<p>Turns out hte fix for this was simply to rename <code>url</code> to <code>link</code> and  read <code>.Type</code> instead of <code>.Params.type</code>. Half an hour of my life dedicated to two words I didn&rsquo;t know were already spoken for.</p>
<p>Coding is my passion.</p>
<h2 id="bits-flapping-in-the-wind">
  <a class="heading-link" href="#bits-flapping-in-the-wind">Bits flapping in the wind.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Rather than dumping bits into the same fold-out list as the GitHub noise, I pulled them out as their own first-class timeline rows. The lower-signal changelog chatter stays tucked behind the &ldquo;<em>N changelog events in between</em>&rdquo; disclosure. So, the home page now reads in three tiers:</p>
<ul>
<li>the loud headline articles</li>
<li>the medium-volume bits</li>
<li>the quiet changelog hum</li>
</ul>
<p>The same interstitial machinery I built last time, but bits just get to stand a little taller than a commit.</p>
<p><img
  src="https://wilhelm.codes/blog/let-me-show-you-my-bits/home_hu_e7f8fe2d00bce999.webp"
  srcset="/blog/let-me-show-you-my-bits/home_hu_e7f8fe2d00bce999.webp 736w, /blog/let-me-show-you-my-bits/home_hu_bf5b90ae24a2b65.webp 1104w, /blog/let-me-show-you-my-bits/home_hu_62fda724f7b72c05.webp 1472w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="310"
  alt=""
  
  loading="lazy"
  decoding="async"
/>
</p>
<h2 id="the-tag-that-went-nowhere">
  <a class="heading-link" href="#the-tag-that-went-nowhere">The tag that went nowhere.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Bits are deliberately <code>render: never</code> in Hugo. They live inline on the bits page and never get their own URL. Except &ldquo;no page&rdquo; also quietly means &ldquo;no place in the taxonomy&rdquo;, which means a tag only ever used by a bit has no <code>/tags/...</code> page to point at. If I link it anyway I&rsquo;ve shipped a <code>404</code>.</p>
<p>So, the tags only become links if there&rsquo;s actually somewhere to go:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">site</span><span class="p">.</span><span class="nf">GetPage</span><span class="w"> </span><span class="p">(</span><span class="nx">printf</span><span class="w"> </span><span class="s">&#34;/tags/%s&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">urlize</span><span class="w"> </span><span class="p">.))</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">&lt;</span><span class="nx">a</span><span class="w"> </span><span class="nx">href</span><span class="p">=</span><span class="s">&#34;{{ .RelPermalink }}&#34;</span><span class="p">&gt;</span><span class="err">#</span><span class="p">{{</span><span class="w"> </span><span class="p">.</span><span class="nx">Data</span><span class="p">.</span><span class="nx">Term</span><span class="w"> </span><span class="p">}}&lt;</span><span class="o">/</span><span class="nx">a</span><span class="p">&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">&lt;</span><span class="nx">span</span><span class="p">&gt;</span><span class="err">#</span><span class="p">{{</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}}&lt;</span><span class="o">/</span><span class="nx">span</span><span class="p">&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>A tag with a real page becomes a link and a bit-only tag stays as plain text. No broken links and the nice part is it heals itself the moment a proper article picks up the same tag.</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 front page has quietly turned into a proper little stream. Long-form when I&rsquo;ve actually got something to say, bits when I don&rsquo;t and  a <a href="https://wilhelm.codes/blog/a-year-in-circles/">year of squircles</a> up top keeping a tally of it all.</p>
<p>Which is the entire point. The less effort it takes to post a half-formed thought, the more likely I am to actually post it. And the more I post, the more those gaps fill in.</p>
<p>And, as always, you can check everything out for yourself in the <a href="https://github.com/wilhelm-murdoch/wilhelm.codes">repo</a>.</p>]]></content:encoded></item><item><title>Reading Between the Posts</title><link>https://wilhelm.codes/blog/reading-between-the-posts/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/reading-between-the-posts/</guid><category>hugo</category><category>css</category><wc:kind>post</wc:kind><description>The front page of this site has always been a plain list of articles. Which would be fine if I wrote longer-form articles a bit more often. I do, however, make code commits to this site far more frequently. I&amp;rsquo;m also working on implementing a short-form section ( a.k.a. &amp;ldquo;bits&amp;rdquo; ) for links, videos and other small low-stakes thoughts. So the front page doesn&amp;rsquo;t really show the full story. In reality, there&amp;rsquo;s a lot more going on behind the scenes.</description><content:encoded><![CDATA[<p>The front page of this site has always been a plain list of articles. Which would be fine if I wrote longer-form articles a bit more often. I do, however, make code commits to this site far more frequently. I&rsquo;m also working on implementing a short-form section ( a.k.a. &ldquo;bits&rdquo; ) for links, videos and other small low-stakes thoughts. So the front page doesn&rsquo;t really show the full story. In reality, there&rsquo;s a lot more going on behind the scenes.</p>
<p>I want to fill those gaps in and not with more full-fat rows, but with a small line between each pair of articles. Something like &ldquo;<em>1 bit and 41 changelog events in between</em>&rdquo;; a kind of interstitial event timeline for all the work that never quite became a full blog post.</p>
<h2 id="why-cant-i-hold-all-these-events">
  <a class="heading-link" href="#why-cant-i-hold-all-these-events">Why can&rsquo;t I hold all these events?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The trick to merging things that don&rsquo;t look alike&hellip; You&rsquo;re never going to believe this&hellip; is to make them look alike. I already pull my GitHub activity for the <a href="https://wilhelm.codes/blog/a-changelog-that-builds-itself/">changelog</a>, or, at least, the last 100 or so events. So, I reused that, threw the bits in alongside it, and normalised the whole lot into one boring, uniform shape. This data structure doesn&rsquo;t hold all the data for each event, but just enough to cover the highlights.</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">events</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="err">$</span><span class="nx">events</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;kind&#34;</span><span class="w">  </span><span class="s">&#34;commit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;date&#34;</span><span class="w">  </span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">AsTime</span><span class="w"> </span><span class="p">.</span><span class="nx">commit</span><span class="p">.</span><span class="nx">author</span><span class="p">.</span><span class="nx">date</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;title&#34;</span><span class="w"> </span><span class="p">(</span><span class="nf">index</span><span class="w"> </span><span class="p">(</span><span class="nx">split</span><span class="w"> </span><span class="p">.</span><span class="nx">commit</span><span class="p">.</span><span class="nx">message</span><span class="w"> </span><span class="s">&#34;\n&#34;</span><span class="p">)</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;url&#34;</span><span class="w">   </span><span class="p">.</span><span class="nx">html_url</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;icon&#34;</span><span class="w">  </span><span class="s">&#34;code-commit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;green&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;data&#34;</span><span class="w">  </span><span class="p">.)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>Every piece of content on this site has <em>at the very last</em> these attributes. So created a unifying model is pretty straight-forward. A <code>kind</code>, a <code>date</code>, a <code>title</code>, a link, and enough leftover metadata to do something creative with later if I feel like it. I made a point of keeping the original object tucked away in <code>data</code> too; a gift to future Wilhelm.</p>
<h2 id="minding-the-gaps">
  <a class="heading-link" href="#minding-the-gaps">Minding the gaps.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>With one big sorted pile of events, the rest is just bookkeeping. Walk the articles newest to oldest, and for each one work out the window between it and the article above it. Then, scoop up every event that falls within the relevant dates:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">upper</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="err">$</span><span class="nx">now</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">gt</span><span class="w"> </span><span class="err">$</span><span class="nx">i</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}}{{</span><span class="w"> </span><span class="err">$</span><span class="nx">upper</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">(</span><span class="nx">index</span><span class="w"> </span><span class="err">$</span><span class="nf">articles</span><span class="w"> </span><span class="p">(</span><span class="nx">sub</span><span class="w"> </span><span class="err">$</span><span class="nx">i</span><span class="w"> </span><span class="mi">1</span><span class="p">)).</span><span class="nx">Date</span><span class="w"> </span><span class="p">}}{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="err">$</span><span class="nx">e</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="err">$</span><span class="nx">events</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">and</span><span class="w"> </span><span class="p">(</span><span class="err">$</span><span class="nx">e</span><span class="p">.</span><span class="nx">date</span><span class="p">.</span><span class="nx">After</span><span class="w"> </span><span class="err">$</span><span class="nx">article</span><span class="p">.</span><span class="nx">Date</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nf">not</span><span class="w"> </span><span class="p">(</span><span class="err">$</span><span class="nx">e</span><span class="p">.</span><span class="nx">date</span><span class="p">.</span><span class="nx">After</span><span class="w"> </span><span class="err">$</span><span class="nx">upper</span><span class="p">))</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">group</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="err">$</span><span class="nx">group</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">append</span><span class="w"> </span><span class="err">$</span><span class="nx">e</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>The newest article&rsquo;s &ldquo;window&rdquo; runs all the way up to <em>now</em>, so whatever I&rsquo;ve been doing since the last post shows up at the very top. If there&rsquo;s an empty gap the event list doesn&rsquo;t render.</p>
<p><img
  src="https://wilhelm.codes/blog/reading-between-the-posts/interstitial-events-top_hu_213053be54f9122c.webp"
  srcset="/blog/reading-between-the-posts/interstitial-events-top_hu_213053be54f9122c.webp 736w, /blog/reading-between-the-posts/interstitial-events-top_hu_f4bb42f82050343a.webp 1104w, /blog/reading-between-the-posts/interstitial-events-top_hu_f4fd9da2a1500826.webp 1472w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="155"
  alt=""
  
  loading="lazy"
  decoding="async"
/>
</p>
<h2 id="listing-the-interstitial-events">
  <a class="heading-link" href="#listing-the-interstitial-events">Listing the interstitial events.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A summary is nice, but I wanted to be able to actually <em>see</em> the events without leaving the page. Normally that&rsquo;s where a pile of JS shows up. But, just as with the activity graph, we&rsquo;re going to rely on pure HTML and CSS. The <code>&lt;details&gt;</code> element does the entire job for free:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HTML</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">details</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;group&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">summary</span><span class="p">&gt;</span>... 41 changelog events in between ...<span class="p">&lt;/</span><span class="nt">summary</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- one little row per event, complete with icon and link --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">details</span><span class="p">&gt;</span></span></span></code></pre></div>
</div>
<p>Click the summary, the gap unfolds into a tidy little list of every commit, PR and bit. Each linked off to wherever it lives. The default disclosure triangle gets binned and replaced with a chevron that flips on open, courtesy of Tailwind&rsquo;s <code>group-open:</code> variant. Still no JS and I&rsquo;ll remain quietly smug about that.</p>
<p><img
  src="https://wilhelm.codes/blog/reading-between-the-posts/interstitial-events-timeline_hu_90a890e91eb37f0a.webp"
  srcset="/blog/reading-between-the-posts/interstitial-events-timeline_hu_90a890e91eb37f0a.webp 736w, /blog/reading-between-the-posts/interstitial-events-timeline_hu_dc4ebb376786fef.webp 1104w, /blog/reading-between-the-posts/interstitial-events-timeline_hu_7197e16fbb375629.webp 1472w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="262"
  alt=""
  
  loading="lazy"
  decoding="async"
/>
</p>
<h2 id="saving-myself-a-small-headache">
  <a class="heading-link" href="#saving-myself-a-small-headache">Saving myself a small headache.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Finally, there&rsquo;s the the responsive question. This site&rsquo;s overall design isn&rsquo;t exactly &ldquo;mobile first&rdquo; and viewing it on a small screen strips away a lot of detail already. All these thin dividers and fold-out lists look great on a wide screen and turn into a cramped mess on a phone.</p>
<p>I thought about it for a while and weighed up some clever reflowing. In the end I did the only sensible thing and hid the whole timeline below the <code>sm</code> breakpoint. On a phone you get the clean list of articles, but on a desktop you get the full story between them.</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>Sometimes the right amount of effort is zero and the discipline is in admitting it.</p>
  </blockquote>
  <figcaption>
    <cite>Me. Wilhelm. I said that.</cite>
  </figcaption>
</figure>

<p>That being said, I do need to return and focus on making the mobile experience a bit more palatable.</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>What I like about this is that it turns my worst blogging habit - vanishing for months and then resurfacing in a manic flurry - into something the site can actually narrate. The big rows are the headlines while the quiet lines between them are everything else.</p>
<p>It&rsquo;s also wired up so I can grow it later without unpicking anything. Right now it&rsquo;s counts and lists, but tomorrow it could filters, more details, or do something I haven&rsquo;t thought of yet. All off the same pile of events.</p>
<p>For now, though, the gaps have something interesting in them. Which is a much nicer thing to scroll past than a year of silence.</p>]]></content:encoded></item><item><title>A Year in Circles... I mean Squares. Or, is it Squircles?</title><link>https://wilhelm.codes/blog/a-year-in-circles/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/a-year-in-circles/</guid><category>hugo</category><category>css</category><category>tailwindcss</category><wc:kind>post</wc:kind><description>If you scroll up to the top of the home page, you&amp;rsquo;ll find a pair of little rows of green squircles. It&amp;rsquo;s my own dumbed-down take on GitHub&amp;rsquo;s contribution graph. Effectively, it&amp;rsquo;s the same idea, except instead of a year of days it&amp;rsquo;s a year of weeks. Two rows of twenty-six with one squircle per week, with each squircle coloured a deeper shade of green the busier that week was.</description><content:encoded><![CDATA[<p>If you scroll up to the top of the home page, you&rsquo;ll find a pair of little rows of green squircles. It&rsquo;s my own dumbed-down take on GitHub&rsquo;s contribution graph. Effectively, it&rsquo;s the same idea, except instead of a year of days it&rsquo;s a year of <em>weeks</em>. Two rows of twenty-six with one squircle per week, with each squircle coloured a deeper shade of green the busier that week was.</p>
<p>I wanted something that summed up &ldquo;Has Wilhelm actually been doing anything lately?&rdquo; at a glance, without the density of 365 tiny day cells. Fifty-two squircles felt about right. Coarse enough to read across the room, fine enough to still give a picture.</p>
<h2 id="what-counts-as-a-weeks-worth-of-activity">
  <a class="heading-link" href="#what-counts-as-a-weeks-worth-of-activity">What counts as a &ldquo;week&rsquo;s worth of activity&rdquo;?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The short answer is <em>everything</em>. The <a href="https://wilhelm.codes/blog/a-changelog-that-builds-itself/">changelog</a> already pulls my GitHub activity at build time, so I reuse that exact same data. Then, I throw in the things GitHub doesn&rsquo;t know about; every type of blog post on this site.</p>
<p>Because the changelog fetching already lives in a tidy little partial, sourcing the everything is just a matter of asking for each one and collecting all the publish dates:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">slice</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">partial</span><span class="w"> </span><span class="s">&#34;changelog/fetch.html&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;url&#34;</span><span class="w"> </span><span class="err">$</span><span class="nx">commitsUrl</span><span class="w"> </span><span class="s">&#34;fixture&#34;</span><span class="w"> </span><span class="err">$</span><span class="nx">fixture</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="w"> </span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">AsTime</span><span class="w"> </span><span class="p">.</span><span class="nx">commit</span><span class="p">.</span><span class="nx">author</span><span class="p">.</span><span class="nx">date</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">where</span><span class="w"> </span><span class="nx">site</span><span class="p">.</span><span class="nx">RegularPages</span><span class="w"> </span><span class="s">&#34;Section&#34;</span><span class="w"> </span><span class="s">&#34;blog&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">append</span><span class="w"> </span><span class="p">.</span><span class="nx">Date</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>Nothing crazy going on here. I&rsquo;m just creating a big list of timestamps from wherever I happen to leave a trail.</p>
<h2 id="sorting-a-pile-of-dates-into-weekly-buckets">
  <a class="heading-link" href="#sorting-a-pile-of-dates-into-weekly-buckets">Sorting a pile of dates into weekly buckets.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Each of these dates now need to land in one of fifty-two buckets. Working out which week a given date belongs to is just some boring epoch arithmetic; how many whole weeks ago was it?</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">ago</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="p">(</span><span class="nf">div</span><span class="w"> </span><span class="p">(</span><span class="nx">sub</span><span class="w"> </span><span class="nx">now</span><span class="p">.</span><span class="nx">Unix</span><span class="w"> </span><span class="err">$</span><span class="nx">d</span><span class="p">.</span><span class="nx">Unix</span><span class="p">)</span><span class="w"> </span><span class="mi">604800</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">and</span><span class="w"> </span><span class="p">(</span><span class="nx">ge</span><span class="w"> </span><span class="err">$</span><span class="nx">ago</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">lt</span><span class="w"> </span><span class="err">$</span><span class="nx">ago</span><span class="w"> </span><span class="mi">52</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">counts</span><span class="p">.</span><span class="nf">Add</span><span class="w"> </span><span class="p">(</span><span class="nx">printf</span><span class="w"> </span><span class="s">&#34;%d&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">sub</span><span class="w"> </span><span class="mi">51</span><span class="w"> </span><span class="err">$</span><span class="nx">ago</span><span class="p">))</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>That <code>604800</code> is the number of seconds in a week. And yes, I did have to look that up. Anything older than fifty-two weeks just falls off the back and is quietly ignored.</p>
<h2 id="painting-the-squircles">
  <a class="heading-link" href="#painting-the-squircles">Painting the squircles.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>My favourite part of this is there&rsquo;s no JS involved. The whole thing is a CSS grid of twenty-six columns, and because each squircle is <code>aspect-square</code>, the rows just work and the squircles stretch to fill whatever container I drop the partial into:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HTML</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;grid grid-cols-26 gap-1.5&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- 52 of these green little dudes --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;aspect-square rounded-full bg-emerald-400&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div>
</div>
<p>Working out the shade was the fiddly bit. My first attempt simply scaled each week against the busiest one, which sounds sensible right up until you remember my data is basically one enormous week and a whole lot of flat nothing. That single monster week hogged the dark end and squashed everything else into the same pale green. So I did what GitHub does and reached for <a href="https://en.wikipedia.org/wiki/Quartile">quartiles</a> instead. You rank the weeks that actually saw some activity, chop them into four groups, and let a week&rsquo;s colour come from where it lands in the pack rather than from some absolute number. Empty weeks stay a faint <code>emerald-100</code>; the rest climb through four steps of green up to <code>emerald-600</code>. The part I like is that it&rsquo;s relative to <em>me</em>; a busy week is only busy compared to my <em>other</em> weeks.</p>
<p>The little tooltip that pops up when you hover over a squircle is also pure CSS. A <code>group</code> on the wrapper, a <code>group-hover:opacity-100</code> on the popup, a <code>transition</code> is all I needed. Each squircle now gives a little <code>scale</code> on hover too, just because it&rsquo;s nice.</p>
<h2 id="the-graphs-painful-honesty">
  <a class="heading-link" href="#the-graphs-painful-honesty">The graph&rsquo;s painful honesty.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Now on to the more embarrassing part. When I first rendered it with real data, I got <em>one</em> lonely green squircle and fifty-one empty ones. Surely, you&rsquo;ve noticed it on the front page.</p>
<p>For the curious, this is what it <em>would</em> look like if I didn&rsquo;t have commitment issues.
<img
  src="https://wilhelm.codes/blog/a-year-in-circles/activity-graph-demo_hu_ea2ab57358bd9cb8.webp"
  srcset="/blog/a-year-in-circles/activity-graph-demo_hu_ea2ab57358bd9cb8.webp 736w, /blog/a-year-in-circles/activity-graph-demo_hu_ec221f404c400ffe.webp 1104w, /blog/a-year-in-circles/activity-graph-demo_hu_1cd3ec9be12d0e93.webp 1472w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="91"
  alt=""
  
  loading="lazy"
  decoding="async"
/>
</p>
<p>At the time I assumed I&rsquo;d done something wrong. But, to my great shame, I hadn&rsquo;t. Turns out that when you vanish from your own website for close to 18 months and then cram an <a href="https://wilhelm.codes/blog/some-long-overdue-housekeeping/">entire renovation</a> into a single week, the graph renders exactly that. A long, flat, pale stretch of road ending with one pathetic little green emerald.</p>
<p>There&rsquo;s an extra little indignity baked into the quartiles, too. They need a <em>spread</em> to rank against, and with exactly one active week there&rsquo;s nothing to compare it to. So my massive renovation-cramming week doesn&rsquo;t even get to be properly dark green. It turns up as a polite, middling shade.</p>
<p>I effectively built a little a tool whose entire job is to hold up a mirror to my own lack of consistency and commitment. Very cool! 😬👌</p>
<h2 id="drop-it-in-anywhere">
  <a class="heading-link" href="#drop-it-in-anywhere">Drop it in anywhere.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>This whole thing is a single self-contained partial. It was waaaay easier to build out than I had originally thought. And, thanks to Tailwind I didn&rsquo;t even have to fall back on any JS! I count that as a bonus.</p>
<p>For the moment, it&rsquo;ll live on the home page, but I can now just place this anywhere in my Hugo site and it&rsquo;ll work:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">partial</span><span class="w"> </span><span class="s">&#34;activity-graph.html&#34;</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>I might even build it out a bit more to support different colour schemes or specificy types of targeted site content. So, the more I write, the more those squircles fill in.</p>
<p>Consider yourself warned, <em>me</em>.</p>]]></content:encoded></item><item><title>Some Long Overdue Housekeeping</title><link>https://wilhelm.codes/blog/some-long-overdue-housekeeping/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/some-long-overdue-housekeeping/</guid><category>hugo</category><category>tailwindcss</category><category>css</category><category>refactoring</category><category>wilhelm.codes</category><wc:kind>post</wc:kind><description>This blog has been quietly chugging along for a while now without me paying it much attention. Which is, I suppose, the whole point of a set-and-forget setup. But &amp;ldquo;set-and-forget&amp;rdquo; has a sneaky way of becoming &amp;ldquo;forgotten&amp;rdquo;, and the longer you leave something untouched, the more it quietly rots behind your back. So I rolled up my sleeves and gave the whole thing a proper tune-up.</description><content:encoded><![CDATA[<p>This blog has been quietly chugging along for a while now without me paying it much attention. Which is, I suppose, the whole point of a <a href="https://wilhelm.codes/blog/my-blog-publishing-setup/">set-and-forget</a> setup. But &ldquo;set-and-forget&rdquo; has a sneaky way of becoming &ldquo;forgotten&rdquo;, and the longer you leave something untouched, the more it quietly rots behind your back. So I rolled up my sleeves and gave the whole thing a proper tune-up.</p>
<h2 id="confession-time">
  <a class="heading-link" href="#confession-time">Confession Time<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>While poking around the build, I discovered something a little embarrassing. My Tailwind setup - the source, the config, and the <em>entire</em> <code>node_modules</code> directory - was living inside Hugo&rsquo;s <code>static/</code> folder.</p>
<p>If you know Hugo, you already know where this is going. Everything in <code>static/</code> gets copied, verbatim, into the final site. Which means I had been cheerfully publishing my whole build toolchain - megabytes of it - to the live site on every single deploy. So, yeah. Production was shipping <code>node_modules</code>. Coding is my passion.</p>
<p>Nobody noticed, nothing broke, and the world kept turning. But it&rsquo;s the kind of thing that, once you see it, you can&rsquo;t <em>un</em>-see. To be fair to myself, when I originally put this Hugo site together, I only learned enough to get something shipped.</p>
<h2 id="letting-hugo-do-the-heavy-lifting">
  <a class="heading-link" href="#letting-hugo-do-the-heavy-lifting">Letting Hugo do the heavy lifting.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The reason that mess existed in the first place was that I&rsquo;d wired up Tailwind as a separate, manual build step that spat out a compiled stylesheet for Hugo to pick up. It worked, but it was a second moving part I had to remember existed.</p>
<p>The good news is that recent versions of Hugo can drive <a href="https://gohugo.io/functions/css/tailwindcss/">Tailwind</a> itself, natively, as part of the normal asset pipeline. Combined with Tailwind v4 - which finally ditches the JavaScript config file in favour of configuring everything in CSS - I got to delete a <em>lot</em> of stuff:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Bash</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ git diff --shortstat main...modernize
</span></span><span class="line"><span class="cl"><span class="m">64</span> files changed, <span class="m">1872</span> insertions<span class="o">(</span>+<span class="o">)</span>, <span class="m">40197</span> deletions<span class="o">(</span>-<span class="o">)</span></span></span></code></pre></div>
</div>
<p>The whole stylesheet now starts its life as a single entry point:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">CSS</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="s2">&#34;tailwindcss&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">@</span><span class="k">plugin</span> <span class="s2">&#34;@tailwindcss/typography&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">@</span><span class="k">source</span> <span class="s2">&#34;hugo_stats.json&#34;</span><span class="p">;</span></span></span></code></pre></div>
</div>
<p>That <code>hugo_stats.json</code> bit is the clever part. Hugo writes out a list of every utility class it actually emits, and Tailwind reads <em>that</em> to decide what to generate. No more pointing Tailwind at my templates and hoping it guesses right.</p>
<p>Then a small partial hands it all off to Hugo to compile, minify and fingerprint:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nf">with</span><span class="w"> </span><span class="p">(</span><span class="nx">templates</span><span class="p">.</span><span class="nf">Defer</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;key&#34;</span><span class="w"> </span><span class="s">&#34;css&#34;</span><span class="p">))</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">resources</span><span class="p">.</span><span class="nx">Get</span><span class="w"> </span><span class="s">&#34;css/main.css&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;minify&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">not</span><span class="w"> </span><span class="nx">hugo</span><span class="p">.</span><span class="nx">IsDevelopment</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">css</span><span class="p">.</span><span class="nx">TailwindCSS</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="cm">/* ...do very cool things... */</span><span class="w"> </span><span class="o">-</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>The <code>templates.Defer</code> wrapper is there because the CSS can&rsquo;t compiled ( transpiled? ) until Hugo has finished rendering every page and knows the full list of classes. So, in a very real way, Hugo solves an annoying 🐔 and 🥚 problem.</p>
<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>
<h2 id="so-many-deprecations">
  <a class="heading-link" href="#so-many-deprecations">So. Many. Deprecations.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Of course, nothing that&rsquo;s been left alone for a year comes back to life cleanly. Bumping Hugo to the latest release lit up the console like a Christmas tree.</p>
<p>A few of my templates were leaning on things that have since been politely shown the door:</p>
<ul>
<li><code>resources.GetRemote ... .Err</code> for the <a href="https://wilhelm.codes/changelog/">changelog page</a> - that pattern was removed in favour of a shiny new <code>try</code> keyword.</li>
<li><code>.Language.LanguageCode</code> and <code>.Language.LanguageDirection</code>, both deprecated in favour of <code>.Locale</code> and <code>.Direction</code>.</li>
<li>The <code>_build</code> and <code>cascade._target</code> front matter keys, now just <code>build</code> and <code>cascade.target</code>.</li>
</ul>
<p>None of it was hard to fix, but it&rsquo;s a good reminder that &ldquo;it still builds&rdquo; and &ldquo;it builds <em>without complaints</em>&rdquo; are two very different bars.</p>
<h2 id="a-footgun">
  <a class="heading-link" href="#a-footgun">A Footgun!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Here&rsquo;s a fun one. The changelog page pulls in a Github event fixture file during local development by fetching it over <code>http://localhost:1313</code>. Effectively, from the very dev server that&rsquo;s <em>trying to build the page</em>.</p>
<p>Those of you who are familiar with such things can probably see the problem. Hugo builds the site <em>before</em> it starts listening on that port, so the build sits there waiting for a server that doesn&rsquo;t exist yet. A deadlock of my own making. The obvious fix was to just read the file off disk instead of asking the network nicely.</p>
<h2 id="some-honourable-mentions">
  <a class="heading-link" href="#some-honourable-mentions">Some honourable mentions.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A grab-bag of smaller wins while I had the hood up:</p>
<ul>
<li>Fonts are now served as <code>woff2</code> instead of raw <code>ttf</code>, with <code>font-display: swap</code> so text shows up immediately instead of hanging around invisible. This alone shaved the font payload down by about 60%.</li>
<li>I also deleted a few MB of fluff that wasn&rsquo;t being loaded by anything.</li>
<li>Prettier got a nice version bump and I taught it to sort my Tailwind classes, so I can stop pretending I do that consistently by hand.</li>
</ul>
<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>Aside from a few cosmetic updates here and there, the site should look pretty much the same as it did before. Which is, weirdly, the whole point; all of this work was about the parts you can&rsquo;t see. I got a leaner build, faster page loads, and a project I can come back to in another year without wincing; famous last words, etc&hellip;</p>
<p>For now, the house is clean. Very nice.</p>]]></content:encoded></item><item><title>A Changelog That Builds Itself</title><link>https://wilhelm.codes/blog/a-changelog-that-builds-itself/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/a-changelog-that-builds-itself/</guid><category>hugo</category><category>github</category><category>api</category><wc:kind>post</wc:kind><description>This site has a changelog page. It&amp;rsquo;s not one I write by hand as it builds itself from my activity on GitHub every time the site deploys. I think it&amp;rsquo;s a neat little trick. It also spent a solid afternoon teaching me that &amp;ldquo;works on my machine&amp;rdquo; and &amp;ldquo;works in production&amp;rdquo; are, once again, two very different things.</description><content:encoded><![CDATA[<p>This site has a <a href="https://wilhelm.codes/changelog/">changelog</a> page. It&rsquo;s not one I write by hand as it builds itself from my activity on GitHub every time the site deploys. I think it&rsquo;s a neat little trick. It also spent a solid afternoon teaching me that &ldquo;works on my machine&rdquo; and &ldquo;works in production&rdquo; are, once again, two very different things.</p>
<p>If you use Github, you know it quietly records nearly everything you do as a stream of <a href="https://docs.github.com/en/rest/activity/events">events</a>. Better still, for a public repository, that stream is available over a public, unauthenticated API endpoint through a simple <code>GET</code> request.</p>
<p>So the plan more or less writes itself. We fetch the stream at build time, group the events by day, and let Hugo render them. Hugo even has a tidy <a href="https://gohugo.io/functions/resources/getremote/">little function</a> for pulling in remote resources that works like so:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">&#34;https://api.github.com/repos/wilhelm-murdoch/wilhelm.codes/events&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nf">try</span><span class="w"> </span><span class="p">(</span><span class="nx">resources</span><span class="p">.</span><span class="nx">GetRemote</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">events</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">.</span><span class="nx">Value</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">transform</span><span class="p">.</span><span class="nx">Unmarshal</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="cm">/* ...range over them and render... */</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>Because this all happens at <em>build</em> time, the visitor never waits on GitHub. By the time the page reaches a browser it&rsquo;s just static HTML like everything else. The changelog is always current as of the last deploy, and I never have to think about it.</p>
<p>That&rsquo;s the part that worked. Now for the parts that didn&rsquo;t.</p>
<h2 id="-then-production-happened">
  <a class="heading-link" href="#-then-production-happened">&hellip; Then, production happened.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I&rsquo;ve been more or less absent from this site for close to a year, but this past week I&rsquo;ve decided to breathe a bit more life into it. I made a few changes to <a href="https://wilhelm.codes/blog/some-long-overdue-housekeeping/">modernise</a> the stack, pushed my changes up and waited on Cloudflare to do its thing and&hellip;</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">ERROR error building site: ... error calling GetRemote:
</span></span><span class="line"><span class="cl">failed to fetch remote resource from &#39;https://api.github.com/...&#39;: Forbidden</span></span></code></pre></div>
</div>
<p>I was greeted with a <code>403</code>. Except&hellip; I could paste that exact URL into my browser and get a perfectly happy wall of JSON back. So what gives?</p>
<p>Rate limiting, that&rsquo;s what. GitHub&rsquo;s unauthenticated API is capped at <strong>60 requests per hour, per IP address</strong>. My build doesn&rsquo;t run on <em>my</em> IP. It runs on a shared build machine alongside who-knows-how-many other people&rsquo;s deploys, all of them hammering GitHub from the same handful of addresses. By the time my build rolled around, that bucket was bone dry.</p>
<p>Honestly, a <code>429</code> instead here would have saved me a bit of investigating, but I digress.</p>
<p>The solution was to created a properly scoped Github PAT. Using an authenticated request gets <strong>5000 requests per hour</strong>, so I went about implementing support and handed it to the build as an environment variable, and taught the template to send it along:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">dict</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="w"> </span><span class="s">&#34;HUGO_GITHUB_TOKEN&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;headers&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;Authorization&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">printf</span><span class="w"> </span><span class="s">&#34;Bearer %s&#34;</span><span class="w"> </span><span class="p">.))</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">try</span><span class="w"> </span><span class="p">(</span><span class="nx">resources</span><span class="p">.</span><span class="nx">GetRemote</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>It&rsquo;s worth noting that Hugo won&rsquo;t read just <em>any</em> environment variable. Its security policy only lets <code>os.Getenv</code> see variables prefixed with <code>HUGO_</code>. Name your token <code>GITHUB_TOKEN</code> and you&rsquo;ll get a confusing fistful of nothing; name it <code>HUGO_GITHUB_TOKEN</code> and it works. Ask me how I know.</p>
<h2 id="hark-another-footgun">
  <a class="heading-link" href="#hark-another-footgun">Hark! Another Footgun!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>With the token in place, the fetch succeeded and the build promptly fell over somewhere new:</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">error calling len: reflect: call of reflect.Value.Type on zero Value</span></span></code></pre></div>
</div>
<p>This never showed up locally, which tells me I should do myself a favour and do a quick local <em>production</em> deployment as a preflight <em>before</em> I push to Github and trigger an automated build. Originally, in development I rendered the changelog from a single large fixture file stored locally on disk. This was before I implemented the PAT so I wouldn&rsquo;t rate limit myself locally when testing. The live stream, however, contains event shapes my fixture simply didn&rsquo;t. One of them was a <code>push</code> event carrying no <code>commits</code> at all, and my template did this without a second thought:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">eq</span><span class="w"> </span><span class="p">(</span><span class="nx">len</span><span class="w"> </span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">commits</span><span class="p">)</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>Call <code>len</code> on something that exists and isn&rsquo;t there and Go has a small panic about it. The fix is boring; stop assuming the field is there. Hugo&rsquo;s <code>with</code> only enters the block when its argument is actually&hellip; something:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">commits</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">eq</span><span class="w"> </span><span class="p">(</span><span class="nx">len</span><span class="w"> </span><span class="p">.)</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="cm">/* the one-commit case */</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="cm">/* the many-commits case */</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>Since I&rsquo;ve implemented PAT support, I&rsquo;m not so concerned about being rate-limited during local development, though I now use the fixtures as a fallback if any request fails. This allows me to keep working locally without disruption.</p>
<h2 id="dont-let-someone-elses-server-break-your-build">
  <a class="heading-link" href="#dont-let-someone-elses-server-break-your-build">Don&rsquo;t let someone else&rsquo;s server break your build.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The real personal lesson is the moment your build depends on a third party at build time, you&rsquo;ve effectively ceded control of your deployment to them. GitHub rate-limits you, or has a wobble, or changes a payload shape, and suddenly your perfectly good site won&rsquo;t deploy and you&rsquo;ll have a hard time.</p>
<p>So the changelog no longer treats GitHub as load-bearing. If the fetch fails for <em>any</em> reason it shrugs, logs a warning, and falls back to that saved fixture instead of taking the whole deploy down with it:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">try</span><span class="w"> </span><span class="p">(</span><span class="nx">resources</span><span class="p">.</span><span class="nx">GetRemote</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="p">.</span><span class="nx">Err</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="nx">warnf</span><span class="w"> </span><span class="s">&#34;changelog: GitHub fetch failed (%s); using the fixture&#34;</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">response</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">ReadFile</span><span class="w"> </span><span class="s">&#34;static/github.json&#34;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">transform</span><span class="p">.</span><span class="nx">Unmarshal</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="p">.</span><span class="nx">Value</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">response</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">transform</span><span class="p">.</span><span class="nx">Unmarshal</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>The <code>try</code> keyword is the hero here as it catches the error that <code>GetRemote</code> would otherwise throw and hands it back to me as a value I can actually <em>do</em> something with, rather than a smoking crater where my build used to be. Worst case, the page shows slightly stale history. That&rsquo;s a trade I&rsquo;ll take every single time over a red deploy or disruption to my local flow.</p>
<h2 id="in-conclusion">
  <a class="heading-link" href="#in-conclusion">In conclusion&hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Ironically, this is something I&rsquo;ve dealt with quite frequently at work. Github &amp; AWS API rate limits ( yes, we frequently got rate limited by the VPC API of all things ), <a href="https://yarnpkg.com/">Yarn</a> will throw a <code>5xx</code> and even Sentry will chimp out during source map uploads. All of these things can disrupt deployments.</p>
<p>The point is just assume these things will happen and then build defensively around them. I, ehhhh, just need to apply this wisdom to my personal stuff. 😅</p>
<p>Anyway, the changelog still builds itself. It just doesn&rsquo;t get to take the rest of the site down with it anymore.</p>]]></content:encoded></item><item><title>Printing Ordinal Numbers in Hugo</title><link>https://wilhelm.codes/blog/printing-ordinal-numbers-in-hugo/</link><pubDate>Fri, 17 Jan 2025 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/printing-ordinal-numbers-in-hugo/</guid><category>hugo</category><category>snippets</category><wc:kind>post</wc:kind><description>I&amp;rsquo;ve been having such a good time building up this website and Hugo has been incredibly fun – and relatively simple – to work with. Though, from time to time, I find myself scratching my head at the absence of a few bits and bobs.</description><content:encoded><![CDATA[<p>I&rsquo;ve been having such a good time building up this website and <a href="https://gohugo.io">Hugo</a> has been incredibly fun – and relatively simple – to work with. Though, from time to time, I find myself scratching my head at the absence of a few bits and bobs.</p>
<p>In this case, what I really needed was a simple way to assign an ordinal to an arbitrary number. For instance, if I have a value of <code>2</code>, I might want to tack on a <code>nd</code> as a suffix, eg; <code>1st</code>, <code>540th</code> or  <code>9001st</code> and so on.</p>
<p>Specifically, I&rsquo;d like to use this with dates, but from what I can tell, Hugo doesn&rsquo;t support this out-of-the-box. Luckily, the framework gives you a few options to extend its functionality.</p>
<h2 id="shortcodes">
  <a class="heading-link" href="#shortcodes">Shortcodes<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>With <a href="https://gohugo.io/content-management/shortcodes/">shortcodes</a> you can create snippets that act as functions which can be used to dynamically inject HTML – among other things – directly into your rendered markdown. You can even pass both named and positional arguments through the shortcode to modify their behaviour as needed.</p>
<p>For instance, I have <a href="https://github.com/wilhelm-murdoch/wilhelm.codes/blob/main/layouts/shortcodes/blockquote.html">this custom</a> shortcode used for displaying styled block quotes like:</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>Out of all the things I have lost, I miss my mind the most.</p>
  </blockquote>
  <figcaption>
    <cite>Mark Twain</cite>
  </figcaption>
</figure>

<p>I also have <a href="https://github.com/wilhelm-murdoch/wilhelm.codes/blob/main/layouts/shortcodes/callouts.html">this one</a> that let&rsquo;s me print out some fancy callouts:</p>
<div class="callout callout-warning">
  You should probably pay attention.
</div>

<div class="callout callout-info">
  This may be of some small interest.
</div>

<div class="callout callout-error">
  Yeah, so, we&rsquo;re all gonna die.
</div>

<p>Hugo ships with a <a href="https://gohugo.io/content-management/shortcodes/#embedded-shortcodes">default set</a> of shortcodes covering a variety of additional kinds of embeddings.</p>
<p>Unfortunately, for my purposes, I&rsquo;m not planning on using dynamic numeric ordinals in my markdown files. I need this to work with templates and for that, we use&hellip;</p>
<h2 id="partials">
  <a class="heading-link" href="#partials">Partials<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<div class="callout callout-notice">
  &hellip; that while you cannot use partials directly within your markdown files, you <em>can</em> reference them <em>indirectly</em> by embedding them within a shortcode .
</div>

<p>Unlike shortcodes, <a href="">partials</a> are small re-usable HTML components that are typically used to keep code duplication down. They are effectively context-aware templates that can accept arbitrary data which can be used in generating desired output.</p>
<p><a href="https://github.com/wilhelm-murdoch/wilhelm.codes/blob/main/layouts/partials/views/small.html">Here</a> is a small example of how I use partials for this blog. It&rsquo;s a small data card component you might find scattered throughout the site. Partials let you quickly change a UI component in one place while having it propagate everywhere else.</p>
<p>For the purpose of this article, they can also be used to create custom template &ldquo;functions&rdquo; like this which solves my very specific problem:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HTML</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{- if and ( eq ( mod . 10 ) 1 ) ( ne ( mod . 100 ) 11 ) -}}
</span></span><span class="line"><span class="cl">    st
</span></span><span class="line"><span class="cl">{{ else if and ( eq ( mod . 10 ) 2 ) ( ne ( mod . 100 ) 12 ) -}}
</span></span><span class="line"><span class="cl">    nd
</span></span><span class="line"><span class="cl">{{ else if and ( eq ( mod . 10 ) 3 ) ( ne ( mod . 100 ) 13 ) -}}
</span></span><span class="line"><span class="cl">    rd
</span></span><span class="line"><span class="cl">{{ else -}}
</span></span><span class="line"><span class="cl">    th
</span></span><span class="line"><span class="cl">{{ end -}}</span></span></code></pre></div>
</div>
<p>This may look a bit unreadable to people who aren&rsquo;t super-familiar with Hugo&rsquo;s template syntax, but it effectively resolves to:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">JavaScript</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">number</span> <span class="o">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">ordinal</span> <span class="o">=</span> <span class="s2">&#34;th&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">number</span> <span class="o">%</span> <span class="mi">10</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="nx">number</span> <span class="o">%</span> <span class="mi">100</span> <span class="o">!=</span> <span class="mi">11</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">ordinal</span> <span class="o">=</span> <span class="s2">&#34;st&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">number</span> <span class="o">%</span> <span class="mi">10</span> <span class="o">==</span> <span class="mi">2</span> <span class="o">&amp;&amp;</span> <span class="nx">number</span> <span class="o">%</span> <span class="mi">100</span> <span class="o">!=</span> <span class="mi">12</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">ordinal</span> <span class="o">=</span> <span class="s2">&#34;nd&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">number</span> <span class="o">%</span> <span class="mi">10</span> <span class="o">==</span> <span class="mi">3</span> <span class="o">&amp;&amp;</span> <span class="nx">number</span> <span class="o">%</span> <span class="mi">100</span> <span class="o">!=</span> <span class="mi">13</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">ordinal</span> <span class="o">=</span> <span class="s2">&#34;rd&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">number</span> <span class="o">+</span> <span class="nx">ordinal</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// prints 10th
</span></span></span></code></pre></div>
</div>
<p>Anyways, I have this saved as <code>partials/functions/ordinal.html</code> and can reference this in any of my templates like so:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HTML</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{ partial &#34;functions/ordinal.html&#34; $number }}</span></span></code></pre></div>
</div>
<p>Where <code>$number</code> is any arbitrary number I&rsquo;d like an ordinal suffix for. Take the following example template which generates a random range of numbers and prints them out using my ordinal partial:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">HTML</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{ range seq 10 }}
</span></span><span class="line"><span class="cl">  {{.}}{{ partial &#34;functions/ordinal.html&#34; . }} 
</span></span><span class="line"><span class="cl">{{ end }}</span></span></code></pre></div>
</div>
<p>Which generates:</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">1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th</span></span></code></pre></div>
</div>
<p>Ground-breaking work! I can finally use ordinals in dates. Not exactly something I expected to write an article about, but here we are. 🤷</p>]]></content:encoded></item><item><title>My Blog Publishing Setup &amp; Workflow</title><link>https://wilhelm.codes/blog/my-blog-publishing-setup/</link><pubDate>Sat, 11 Jan 2025 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/my-blog-publishing-setup/</guid><category>obsidian</category><category>hugo</category><category>bash</category><category>cloudflare</category><wc:kind>post</wc:kind><description>Late last year I parted ways with Hashnode as the platform of choice for my blog. They pivoted a bit too far into the generative AI space which — to me for a blogging platform — made little to no sense. What? You write your content with AI, which then trains their model only to spit out more AI-generated slop? A slop-based Ouroboros, if you will. Good luck with that, but I&amp;rsquo;ll pass thanks.</description><content:encoded><![CDATA[<p>Late last year I parted ways with <a href="https://hashnode.com/">Hashnode</a> as the platform of choice for my blog. They pivoted a bit too far into the generative AI space which —  to me for a blogging platform —  made little to no sense. What? You write your content with AI, which then trains their model only to spit out more AI-generated slop?  A slop-based <a href="https://en.wikipedia.org/wiki/Ouroboros">Ouroboros</a>, if you will. Good luck with that, but I&rsquo;ll pass thanks.</p>
<h2 id="first-a-small-rant">
  <a class="heading-link" href="#first-a-small-rant">First, a Small Rant<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Before you call me a <a href="https://en.wikipedia.org/wiki/Luddite">Luddite</a>, let&rsquo;s be clear here, I don&rsquo;t have an issue with the technology itself. I&rsquo;m an engineer. I think it all has its uses, but force-feeding it into almost every aspect of our lives with no regard to any social and environmental impacts with frequently no chance of being able to easily opt-out does not sit right with me one bit.</p>
<p>We&rsquo;ve seen this happen over the last decade with touchscreens, IoT, blockchain, NFTs, Web3 the list goes on. All promoted by the endless parade of interchangeable, nameless faceless talentless hacks and disingenuous grifters who only care about making &ldquo;BIG LINE GO UP&rdquo;.</p>
<p>It&rsquo;s all so tiring, so perhaps you can understand why I&rsquo;d immediately recoil in disgust and go my own way with an alternative I can control.</p>
<p>Or, to put it in even simpler terms:</p>
<p><img
  src="https://wilhelm.codes/blog/my-blog-publishing-setup/image-1_hu_dbfacd982ca699f4.webp"
  srcset="/blog/my-blog-publishing-setup/image-1_hu_dbfacd982ca699f4.webp 598w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="598"
  height="369"
  alt=""
  
  loading="lazy"
  decoding="async"
/>
</p>
<h2 id="the-stack">
  <a class="heading-link" href="#the-stack">The &ldquo;Stack&rdquo;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I suppose it&rsquo;s technically a &ldquo;stack&rdquo;, but it seems a bit funny to use the term when referencing a blog setup. Just as with my <a href="https://plantsm.art"><span class="wm-plant">Plant</span>🌱<span class="wm-smart">Smart</span></a> project, the goal is to automate as much as possible while keeping maintenance requirements and hard- and soft-dollar costs as low as possible. For this, I need only use a handful of tools to keep this space operational.</p>
<p>As far as the question over &ldquo;cost&rdquo;, outside of time spent, it&rsquo;s about ~$100 AUD per year for the domain name. This blog is a very small and inconsequential part of this domain, so the overall cost is absorbed by &ldquo;other stuff&rdquo;.</p>
<p>So, the greatest cost is&hellip; time? I guess 🤷</p>
<h3 id="obsidian">
  <a class="heading-link" href="#obsidian">Obsidian<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>While I won&rsquo;t be going into what <a href="">Obsidian</a> is and how it works, I will say it&rsquo;s been a personal boon in how I keep notes and track of ideas. I have a lot of fleeting / ephemeral thoughts I that would otherwise lose instantly if I didn&rsquo;t immediately jot them down using this app.</p>
<p>So, it makes perfect sense for me to use my personal <a href="https://help.obsidian.md/Getting+started/Create+a+vault">vault</a> to store all my blog content. The fact Obsidian content nothing more than Markdown files makes working with them using other bits of tech that much easier.</p>
<p>Oh, it&rsquo;s free btw.</p>
<h3 id="hugo">
  <a class="heading-link" href="#hugo">Hugo<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p><a href="https://gohugo.io/">This</a> is probably one of the more popular options when it comes to static site generators. It&rsquo;s easy to install, is incredibly extensible and has loads of documentation to help you along. The template syntax sits upon Golang&rsquo;s templating engine, so if you have a strong background as a Go developer like myself, you&rsquo;ll be right at home.</p>
<p>However, knowing Go isn&rsquo;t a hard requirement though it will make getting on top of things quite a bit easier.</p>
<p>What I enjoyed while learning about this project was I got to control every aspect of how I wanted my blog to look and function. Building the current design from the ground up with Hugo was incredibly fun.</p>
<p>Also, free. Very cool.</p>
<h3 id="cloudflare">
  <a class="heading-link" href="#cloudflare">Cloudflare<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>I already used Cloudflare to manage this domain&rsquo;s DNS settings, so it only made sense to publish my static site to Cloudflare Pages over, say, Vercel or Github Pages. There&rsquo;s nothing spectacular going on here outside of me just wanting to keep these two things in the same place.</p>
<p>That being said, I have no issues with migrating else where if the need should ever arise. Though, I doubt this blog will hit the 20k file limit of Cloudflare Pages any time soon.</p>
<p>Again, free for the purposes of this article. If you don&rsquo;t have a dedicated personal domain, you will be provided a subdomain attached to the <code>pages.dev</code> apex.</p>
<h2 id="the-good-stuff">
  <a class="heading-link" href="#the-good-stuff">The Good Stuff<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>How is this all put together for day-to-day usage? Luckily, the Cloudflare stuff runs on auto-pilot, so it&rsquo;s pretty much set-and-forget. I suppose you could say the same about the other components, but they&rsquo;re the parts I touch the most.</p>
<p>I have a single vault in Obsidian. It&rsquo;s where all my thoughts go. Stored within is a top-level <code>Blog/</code> directory. Care to hazard a guess as to what it may contain?</p>
<p><img
  src="https://wilhelm.codes/blog/my-blog-publishing-setup/image-2_hu_84ed8786abb466c3.webp"
  srcset="/blog/my-blog-publishing-setup/image-2_hu_84ed8786abb466c3.webp 736w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="928"
  alt=""
  
  loading="lazy"
  decoding="async"
/>
</p>
<p>Hugo has the concept of <a href="https://gohugo.io/content-management/page-bundles/">page bundles</a> where you can group all resources associated with your blog articles within a single directory. You&rsquo;ll notice above all directories mirror the blog posts hosted on this site. In my case, we can consider the directory names to be the human-friendly slugs you see in your address bar that point to the associated content. Within these directories, you&rsquo;d see any other resource I may link to; source code, images, etc&hellip; The best thing about this is I only have to perform a relative reference to these resources. If I want to link to <code>image-1.png</code>, I only have to reference it as <code>![](/blog/my-blog-publishing-setup/image-1.png)</code> without worrying about specifying an absolute path; very nice.</p>
<p>When I want to write a new article, I first create a new directory here. I may already have the title of the article in mind, but it can be in flux until I decide to publish it. In the new directory, I create a file called <code>index.md</code>. This file contains all the content of the associated article written using Markdown.</p>
<p>I then use an Obsidian <a href="https://help.obsidian.md/Plugins/Templates">template</a> dedicated to new blog posts and apply it to the new file. This ensures I have all <a href="https://jekyllrb.com/docs/front-matter/">front matter</a> properties ready to go. This makes configuring my posts in the editor that much easier.</p>
<p><img
  src="https://wilhelm.codes/blog/my-blog-publishing-setup/image-3_hu_7267be29c468e715.webp"
  srcset="/blog/my-blog-publishing-setup/image-3_hu_7267be29c468e715.webp 736w, /blog/my-blog-publishing-setup/image-3_hu_1ad568a578b3cf1f.webp 1104w, /blog/my-blog-publishing-setup/image-3_hu_3acce599e1729f38.webp 1472w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="478"
  alt=""
  
  loading="lazy"
  decoding="async"
/>
</p>
<p>All that&rsquo;s left is writing my articles! Well, not really. I need a way to synchronise these files with my local Hugo instance. As you can see <a href="https://github.com/wilhelm-murdoch/wilhelm.codes/tree/main/content/blog">here</a>, the content mirrors what I have stored in my Obsidian vault.</p>
<p>This can be easily addressed with 2 small apps:</p>
<ol>
<li><code>fswatch</code> which monitors a specified directory for changes and allows you to trigger subsequent commands for specific file system events. Basically, I want to watch for any changes in my Obsidian vaults <code>Blog/</code> directory.</li>
<li><code>rsync</code> to dynamically keep the target Hugo directory in sync with the Obsidian source directory.</li>
</ol>
<p>I, of course, have this placed in a script that I have running in the background while I write. This is a great help during the drafting phase as I have Hugo dynamically processing and displaying these changes locally. This allows me to easily proof my work visually as I carry on writing.</p>
<p>Here&rsquo;s the full script:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Bash</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SOURCE</span><span class="o">=</span><span class="si">${</span><span class="nv">SOURCE</span><span class="p">:=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span><span class="si">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> SOURCE
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">DESTINATION</span><span class="o">=</span><span class="si">${</span><span class="nv">DESTINATION</span><span class="p">:=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">2</span><span class="si">}</span><span class="s2">&#34;</span><span class="si">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> DESTINATION
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">fswatch -o <span class="s2">&#34;</span><span class="si">${</span><span class="nv">SOURCE</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r event<span class="p">;</span> <span class="k">do</span> 
</span></span><span class="line"><span class="cl">    rsync -av --delete <span class="s2">&#34;</span><span class="si">${</span><span class="nv">SOURCE</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DESTINATION</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span></span></span></code></pre></div>
</div>
<p>I keep this running whenever I&rsquo;m writing a new article. Obsidian saves in almost realtime, so as I type the changes almost immediately sync across to the local Hugo server so I can review my changes.</p>
<p>You effectively run it as:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Bash</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ ./sync.sh /path/to/obsidian/blog/ /path/to/hugo/content/</span></span></code></pre></div>
</div>
<p>For <code>rsync</code> to work as intended and keep the destination directory completely in sync with the source directory, remember to add a trailing slash to both directory arguments.</p>
<h3 id="no-auto-commit">
  <a class="heading-link" href="#no-auto-commit">No Auto-Commit?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Nope. Not here. There are a few other peeps with blogs out there that &ldquo;commit on change&rdquo;, but I prefer to have a bit more control over what actually gets published. For that, I simply use plain-old git commands to publish my new article:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Bash</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ git add -A
</span></span><span class="line"><span class="cl">$ git commit -m <span class="s1">&#39;some banal new bullshit screed&#39;</span>
</span></span><span class="line"><span class="cl">$ git push origin main</span></span></code></pre></div>
</div>
<p>Cloudflare monitors the associated repository, picks up the change and the auto-magic-ally builds and pushes everything to the public eye.</p>
<p>With this flow, I can see my changes locally in near realtime and push to &ldquo;production&rdquo; only when I&rsquo;m happy with the results.</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>I&rsquo;m stoked to have my very own place again that I have complete control over. I&rsquo;ve thoroughly enjoyed creating this new blog design and I may even open-source it one day.</p>
<p>That said, I&rsquo;ve made a very loose commitment to post a new article every week this year of our lord 2025 and I&rsquo;m hoping this will make things heaps simpler. I only want to worry about what I&rsquo;m going to write and just get the content out there.</p>
<p>I sincerely hope I can keep this self-imposed weekly commitment. 😬👍</p>]]></content:encoded></item><item><title>Testing Markdown Elements</title><link>https://wilhelm.codes/blog/markdown-elements/</link><pubDate>Wed, 18 Dec 2024 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/markdown-elements/</guid><category>markdown</category><category>hugo</category><wc:kind>post</wc:kind><description>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</description><content:encoded><![CDATA[<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<h2 id="headings">
  <a class="heading-link" href="#headings">Headings<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor. Aliquip et adipisicing sit sit fugiat commodo id sunt. Nostrud enim ad commodo incididunt cupidatat in ullamco ullamco Lorem cupidatat velit enim et Lorem. Ut laborum cillum laboris fugiat culpa sint irure do reprehenderit culpa occaecat. Exercitation esse mollit tempor magna aliqua in occaecat aliquip veniam reprehenderit nisi dolor in laboris dolore velit.</p>
<h2 id="heading-two">
  <a class="heading-link" href="#heading-two">Heading two<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Aute officia nulla deserunt do deserunt cillum velit magna. Officia veniam culpa anim minim dolore labore pariatur voluptate id ad est duis quis velit dolor pariatur enim. Incididunt enim excepteur do veniam consequat culpa do voluptate dolor fugiat ad adipisicing sit. Labore officia est adipisicing dolore proident eiusmod exercitation deserunt ullamco anim do occaecat velit. Elit dolor consectetur proident sunt aliquip est do tempor quis aliqua culpa aute. Duis in tempor exercitation pariatur et adipisicing mollit irure tempor ut enim esse commodo laboris proident. Do excepteur laborum anim esse aliquip eu sit id Lorem incididunt elit irure ea nulla dolor et. Nulla amet fugiat qui minim deserunt enim eu cupidatat aute officia do velit ea reprehenderit.</p>
<h3 id="heading-three">
  <a class="heading-link" href="#heading-three">Heading three<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Voluptate cupidatat cillum elit quis ipsum eu voluptate fugiat consectetur enim. Quis ut voluptate culpa ex anim aute consectetur dolore proident voluptate exercitation eiusmod. Esse in do anim magna minim culpa sint. Adipisicing ipsum consectetur proident ullamco magna sit amet aliqua aute fugiat laborum exercitation duis et.</p>
<h4 id="heading-four">
  <a class="heading-link" href="#heading-four">Heading four<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h4>
<p>Commodo fugiat aliqua minim quis pariatur mollit id tempor. Non occaecat minim esse enim aliqua adipisicing nostrud duis consequat eu adipisicing qui. Minim aliquip sit excepteur ipsum consequat laborum pariatur excepteur. Veniam fugiat et amet ad elit anim laborum duis mollit occaecat et et ipsum et reprehenderit. Occaecat aliquip dolore adipisicing sint labore occaecat officia fugiat. Quis adipisicing exercitation exercitation eu amet est laboris sunt nostrud ipsum reprehenderit ullamco. Enim sint ut consectetur id anim aute voluptate exercitation mollit dolore magna magna est Lorem. Ut adipisicing adipisicing aliqua ullamco voluptate labore nisi tempor esse magna incididunt.</p>
<h5 id="heading-five">
  <a class="heading-link" href="#heading-five">Heading five<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h5>
<p>Veniam enim esse amet veniam deserunt laboris amet enim consequat. Minim nostrud deserunt cillum consectetur commodo eu enim nostrud ullamco occaecat excepteur. Aliquip et ut est commodo enim dolor amet sint excepteur. Amet ad laboris laborum deserunt sint sunt aliqua commodo ex duis deserunt enim est ex labore ut. Duis incididunt velit adipisicing non incididunt adipisicing adipisicing. Ad irure duis nisi tempor eu dolor fugiat magna et consequat tempor eu ex dolore. Mollit esse nisi qui culpa ut nisi ex proident culpa cupidatat cillum culpa occaecat anim. Ut officia sit ea nisi ea excepteur nostrud ipsum et nulla.</p>
<h6 id="heading-six">
  <a class="heading-link" href="#heading-six">Heading six<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h6>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<h2 id="paragraphs">
  <a class="heading-link" href="#paragraphs">Paragraphs<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Incididunt ex adipisicing ea ullamco consectetur in voluptate proident fugiat tempor deserunt reprehenderit ullamco id dolore laborum. Do laboris laboris minim incididunt qui consectetur exercitation adipisicing dolore et magna consequat magna anim sunt. Officia fugiat Lorem sunt pariatur incididunt Lorem reprehenderit proident irure. Dolore ipsum aliqua mollit ad officia fugiat sit eu aliquip cupidatat ipsum duis laborum laborum fugiat esse. Voluptate anim ex dolore deserunt ea ex eiusmod irure. Occaecat excepteur aliqua exercitation aliquip dolor esse eu eu.</p>
<p>Officia dolore laborum aute incididunt commodo nisi velit est est elit et dolore elit exercitation. Enim aliquip magna id ipsum aliquip consectetur ad nulla quis. Incididunt pariatur dolor consectetur cillum enim velit cupidatat laborum quis ex.</p>
<p>Officia irure in non voluptate adipisicing sit amet tempor duis dolore deserunt enim ut. Reprehenderit incididunt in ad anim et deserunt deserunt Lorem laborum quis. Enim aute anim labore proident laboris voluptate elit excepteur in. Ex labore nulla velit officia ullamco Lorem Lorem id do. Dolore ullamco ipsum magna dolor pariatur voluptate ipsum id occaecat ipsum. Dolore tempor quis duis commodo quis quis enim.</p>
<h2 id="blockquotes">
  <a class="heading-link" href="#blockquotes">Blockquotes<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Ad nisi laborum aute cupidatat magna deserunt eu id laboris id. Aliquip nulla cupidatat sint ex Lorem mollit laborum dolor amet est ut esse aute. Nostrud ex consequat id incididunt proident ipsum minim duis aliqua ut ex et ad quis. Laborum sint esse cillum anim nulla cillum consectetur aliqua sit. Nisi excepteur cillum labore amet excepteur commodo enim occaecat consequat ipsum proident exercitation duis id in.</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>Ipsum et cupidatat mollit exercitation enim duis sunt irure aliqua reprehenderit mollit. Pariatur Lorem pariatur laboris do culpa do elit irure. Eiusmod amet nulla voluptate velit culpa et aliqua ad reprehenderit sit ut.</p>
  </blockquote>
  <figcaption>
    <cite>Ancient Klingon Proverb ( probably )</cite>
  </figcaption>
</figure>

<p>Labore ea magna Lorem consequat aliquip consectetur cillum duis dolore. Et veniam dolor qui incididunt minim amet laboris sit. Dolore ad esse commodo et dolore amet est velit ut nisi ea. Excepteur ea nulla commodo dolore anim dolore adipisicing eiusmod labore id enim esse quis mollit deserunt est. Minim ea culpa voluptate nostrud commodo proident in duis aliquip minim.</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>Ipsum et cupidatat mollit exercitation enim duis sunt irure aliqua reprehenderit mollit. Pariatur Lorem pariatur laboris do culpa do elit irure. Eiusmod amet nulla voluptate velit culpa et aliqua ad reprehenderit sit ut.</p>
  </blockquote>
  <figcaption>
    <cite>Ancient Klingon Proverb ( probably )</cite>
  </figcaption>
</figure>

<p>Ipsum excepteur cupidatat sunt minim ad eiusmod tempor sit.</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>Ipsum et cupidatat mollit exercitation enim duis sunt irure aliqua reprehenderit mollit. Pariatur Lorem pariatur laboris do culpa do elit irure. Eiusmod amet nulla voluptate velit culpa et aliqua ad reprehenderit sit ut.</p>
  </blockquote>
  <figcaption>
    <cite>Ancient Klingon Proverb ( probably )</cite>
  </figcaption>
</figure>

<p>Adipisicing voluptate ipsum culpa voluptate id aute laboris labore esse fugiat veniam ullamco occaecat do ut. Tempor et esse reprehenderit veniam proident ipsum irure sit ullamco et labore ea excepteur nulla labore ut. Ex aute minim quis tempor in eu id id irure ea nostrud dolor esse.</p>
<h2 id="lists">
  <a class="heading-link" href="#lists">Lists<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<h3 id="ordered-list">
  <a class="heading-link" href="#ordered-list">Ordered List<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<ol>
<li>Longan</li>
<li>Lychee</li>
<li>Excepteur ad cupidatat do elit laborum amet cillum reprehenderit consequat quis.
Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip aliquip velit pariatur dolore.</li>
<li>Marionberry</li>
<li>Melon
<ol>
<li>Cantaloupe</li>
<li>Honeydew</li>
<li>Watermelon</li>
</ol>
</li>
<li>Miracle fruit</li>
<li>Mulberry</li>
</ol>
<h3 id="unordered-list">
  <a class="heading-link" href="#unordered-list">Unordered List<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<ul>
<li>Olive</li>
<li>Orange
<ul>
<li>Blood orange</li>
<li>Clementine</li>
</ul>
</li>
<li>Papaya</li>
<li>Ut aute ipsum occaecat nisi culpa Lorem id occaecat cupidatat id id magna laboris ad duis. Fugiat cillum dolore veniam nostrud proident sint consectetur eiusmod irure adipisicing.</li>
<li>Passionfruit</li>
</ul>
<h2 id="horizontal-rule">
  <a class="heading-link" href="#horizontal-rule">Horizontal rule<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>In dolore velit aliquip labore mollit minim tempor veniam eu veniam ad in sint aliquip mollit mollit. Ex occaecat non deserunt elit laborum sunt tempor sint consequat culpa culpa qui sit. Irure ad commodo eu voluptate mollit cillum cupidatat veniam proident amet minim reprehenderit.</p>
<hr>
<p>In laboris eiusmod reprehenderit aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi. Dolor amet cillum excepteur consequat sint non sint.</p>
<h2 id="code">
  <a class="heading-link" href="#code">Code<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<h3 id="inline-code">
  <a class="heading-link" href="#inline-code">Inline code<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Ad amet irure est magna id mollit Lorem in do duis enim. Excepteur velit nisi magna ea pariatur pariatur ullamco fugiat deserunt sint non sint. Duis duis est <code>code in text</code> velit velit aute culpa ex quis pariatur pariatur laborum aute pariatur duis tempor sunt ad. Irure magna voluptate dolore consectetur consectetur irure esse. Anim magna <code>&lt;strong&gt;in culpa qui officia&lt;/strong&gt;</code> dolor eiusmod esse amet aute cupidatat aliqua do id voluptate cupidatat reprehenderit amet labore deserunt.</p>
<h3 id="tables">
  <a class="heading-link" href="#tables">Tables<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<table>
	<thead>
			<tr>
					<th style="text-align: left">Complexity</th>
					<th style="text-align: center">Normalized Unit</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left">Small</td>
					<td style="text-align: center">1</td>
			</tr>
			<tr>
					<td style="text-align: left">Medium</td>
					<td style="text-align: center">2</td>
			</tr>
			<tr>
					<td style="text-align: left">Large</td>
					<td style="text-align: center">3</td>
			</tr>
	</tbody>
</table>
<h3 id="highlighted">
  <a class="heading-link" href="#highlighted">Highlighted<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Et fugiat ad nisi amet magna labore do cillum fugiat occaecat cillum Lorem proident. In sint dolor ullamco ad do adipisicing amet id excepteur Lorem aliquip sit irure veniam laborum duis cillum. Aliqua occaecat minim cillum deserunt magna sunt laboris do do irure ea nostrud consequat ut voluptate ex.</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s">&#34;fmt&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s">&#34;net/http&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">handler</span><span class="p">(</span><span class="nx">w</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;Hi there, I love %s!&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">r</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Path</span><span class="p">[</span><span class="mi">1</span><span class="p">:])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">handler</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">http</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">(</span><span class="s">&#34;:8080&#34;</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Bash</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tmux <span class="nb">set</span> -g status-right <span class="s2">&#34;#[align=absolute-centre]#[fg=#f8f9f2]#W#[fg=#51cd97]#{?client_prefix,#[fg=#d0689d],#[fg=#51cd97]}.#[align=right]#{?window_bigger,[#{window_offset_x}#,#{window_offset_y}] ,}#[fg=#404040,underscore]#S#[none] &#34;</span>
</span></span><span class="line"><span class="cl">tmux <span class="nb">set</span> -g status-style <span class="s2">&#34;bg=#1f1f1f,fg=#f8f8f2&#34;</span>
</span></span><span class="line"><span class="cl">tmux <span class="nb">set</span> -g window-status-current-format <span class="s2">&#34;#{?client_prefix,#[fg=#d0689d],#[fg=#51cd97]}&#34;</span>
</span></span><span class="line"><span class="cl">tmux <span class="nb">set</span> -g window-status-format <span class="s2">&#34;#[fg=#404040]&#34;</span>
</span></span><span class="line"><span class="cl">tmux <span class="nb">set</span> -g status-left <span class="s1">&#39; &#39;</span>
</span></span><span class="line"><span class="cl">tmux set-window-option -g pane-border-style <span class="s2">&#34;fg=#404040&#34;</span>
</span></span><span class="line"><span class="cl">tmux set-window-option -g pane-active-border-style <span class="s2">&#34;fg=#51cd97&#34;</span></span></span></code></pre></div>
</div>
<p>Ex amet id ex aliquip id do laborum excepteur exercitation elit sint commodo occaecat nostrud est. Nostrud pariatur esse veniam laborum non sint magna sit laboris minim in id. Aliqua pariatur pariatur excepteur adipisicing irure culpa consequat commodo et ex id ad.</p>
<h2 id="diagrams">
  <a class="heading-link" href="#diagrams">Diagrams<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p><a href="https://mermaid.js.org/">Mermaid</a> turns text into diagrams. A fenced code block with the <code>mermaid</code> language renders one, and so does the <code>mermaid</code> shortcode. Each diagram is rendered to an SVG at build time and inlined into the page, so no JavaScript ever ships and the site keeps its no-external-requests rule.</p>
<h3 id="flowchart">
  <a class="heading-link" href="#flowchart">Flowchart<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>The example below is this site&rsquo;s own deployment pipeline, end to end: a <code>git push</code> at the desk, through the homelab, out to the Cloudflare edge and finally into a reader&rsquo;s browser. The full story lives in <a href="https://wilhelm.codes/blog/my-blog-now-ships-from-my-homelab/">My Blog Now Ships From My Homelab!</a>.</p>
<figure class="diagram"><svg id="d-c99b82f2b5cf" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 630.789px; background-color: transparent;" viewBox="0 0 630.7890625 1462" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#d-c99b82f2b5cf{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;font-size:14px;fill:#32d74b;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#d-c99b82f2b5cf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#d-c99b82f2b5cf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#d-c99b82f2b5cf .error-icon{fill:#202225;}#d-c99b82f2b5cf .error-text{fill:#e3e3de;stroke:#e3e3de;}#d-c99b82f2b5cf .edge-thickness-normal{stroke-width:1px;}#d-c99b82f2b5cf .edge-thickness-thick{stroke-width:3.5px;}#d-c99b82f2b5cf .edge-pattern-solid{stroke-dasharray:0;}#d-c99b82f2b5cf .edge-thickness-invisible{stroke-width:0;fill:none;}#d-c99b82f2b5cf .edge-pattern-dashed{stroke-dasharray:3;}#d-c99b82f2b5cf .edge-pattern-dotted{stroke-dasharray:2;}#d-c99b82f2b5cf .marker{fill:#8b8b84;stroke:#8b8b84;}#d-c99b82f2b5cf .marker.cross{stroke:#8b8b84;}#d-c99b82f2b5cf svg{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;font-size:14px;}#d-c99b82f2b5cf p{margin:0;}#d-c99b82f2b5cf .label{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;color:#32d74b;}#d-c99b82f2b5cf .cluster-label text{fill:#e3e3de;}#d-c99b82f2b5cf .cluster-label span{color:#e3e3de;}#d-c99b82f2b5cf .cluster-label span p{background-color:transparent;}#d-c99b82f2b5cf .label text,#d-c99b82f2b5cf span{fill:#32d74b;color:#32d74b;}#d-c99b82f2b5cf .node rect,#d-c99b82f2b5cf .node circle,#d-c99b82f2b5cf .node ellipse,#d-c99b82f2b5cf .node polygon,#d-c99b82f2b5cf .node path{fill:#1e1f22;stroke:#3c3f43;stroke-width:1px;}#d-c99b82f2b5cf .rough-node .label text,#d-c99b82f2b5cf .node .label text,#d-c99b82f2b5cf .image-shape .label,#d-c99b82f2b5cf .icon-shape .label{text-anchor:middle;}#d-c99b82f2b5cf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#d-c99b82f2b5cf .rough-node .label,#d-c99b82f2b5cf .node .label,#d-c99b82f2b5cf .image-shape .label,#d-c99b82f2b5cf .icon-shape .label{text-align:center;}#d-c99b82f2b5cf .node.clickable{cursor:pointer;}#d-c99b82f2b5cf .root .anchor path{fill:#8b8b84!important;stroke-width:0;stroke:#8b8b84;}#d-c99b82f2b5cf .arrowheadPath{fill:#dfddda;}#d-c99b82f2b5cf .edgePaths .path{stroke:#8b8b84;stroke-width:1px;}#d-c99b82f2b5cf .flowchart-link{stroke:#8b8b84;fill:none;}#d-c99b82f2b5cf .edgeLabel{background-color:#1e1f22;text-align:center;}#d-c99b82f2b5cf .edgeLabel p{background-color:#1e1f22;}#d-c99b82f2b5cf .edgeLabel rect{opacity:0.5;background-color:#1e1f22;fill:#1e1f22;}#d-c99b82f2b5cf .labelBkg{background-color:rgba(30, 31, 34, 0.5);}#d-c99b82f2b5cf .cluster rect{fill:#202225;stroke:#2c2e31;stroke-width:1px;}#d-c99b82f2b5cf .cluster text{fill:#e3e3de;}#d-c99b82f2b5cf .cluster span{color:#e3e3de;}#d-c99b82f2b5cf .node .collapsed-indicator{fill:#2c2e31;stroke:none;opacity:0.6;}#d-c99b82f2b5cf .node .collapsed-separator{stroke:#2c2e31;stroke-width:0.75px;}#d-c99b82f2b5cf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;font-size:12px;background:#202225;border:1px solid hsl(216, 0%, 23.5294117647%);border-radius:2px;pointer-events:none;z-index:100;}#d-c99b82f2b5cf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#32d74b;}#d-c99b82f2b5cf rect.text{fill:none;stroke-width:0;}#d-c99b82f2b5cf .icon-shape,#d-c99b82f2b5cf .image-shape{background-color:#1e1f22;text-align:center;}#d-c99b82f2b5cf .icon-shape p,#d-c99b82f2b5cf .image-shape p{background-color:#1e1f22;padding:2px;}#d-c99b82f2b5cf .icon-shape .label rect,#d-c99b82f2b5cf .image-shape .label rect{opacity:0.5;background-color:#1e1f22;fill:#1e1f22;}#d-c99b82f2b5cf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#d-c99b82f2b5cf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#d-c99b82f2b5cf .node .neo-node{stroke:#3c3f43;}#d-c99b82f2b5cf [data-look="neo"].node rect,#d-c99b82f2b5cf [data-look="neo"].cluster rect,#d-c99b82f2b5cf [data-look="neo"].node polygon{stroke:url(#d-c99b82f2b5cf-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-c99b82f2b5cf [data-look="neo"].swimlane.cluster rect{filter:none;}#d-c99b82f2b5cf [data-look="neo"].node path{stroke:url(#d-c99b82f2b5cf-gradient);stroke-width:1px;}#d-c99b82f2b5cf [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-c99b82f2b5cf [data-look="neo"].node .neo-line path{stroke:#3c3f43;filter:none;}#d-c99b82f2b5cf [data-look="neo"].node circle{stroke:url(#d-c99b82f2b5cf-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-c99b82f2b5cf [data-look="neo"].node circle .state-start{fill:#000000;}#d-c99b82f2b5cf [data-look="neo"].icon-shape .icon{fill:url(#d-c99b82f2b5cf-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-c99b82f2b5cf [data-look="neo"].icon-shape .icon-neo path{stroke:url(#d-c99b82f2b5cf-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-c99b82f2b5cf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="d-c99b82f2b5cf_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="d-c99b82f2b5cf_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"><g class="cluster" id="d-c99b82f2b5cf-cloud" data-look="classic"><rect style="" x="272.7890625" y="1067" width="330" height="265"/><g class="cluster-label" transform="translate(395.640625, 1067)"><foreignObject width="84.296875" height="21"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>Cloudflare</p></span></div></foreignObject></g></g><g class="cluster" id="d-c99b82f2b5cf-lab" data-look="classic"><rect style="" x="8" y="172" width="614.7890625" height="803"/><g class="cluster-label" transform="translate(151.02734375, 172)"><foreignObject width="328.734375" height="21"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>The homelab ( accepts nothing inbound )</p></span></div></foreignObject></g></g><g class="cluster" id="d-c99b82f2b5cf-pipeline" data-look="classic"><rect style="" x="272.7890625" y="462" width="330" height="488"/><g class="cluster-label" transform="translate(311.3515625, 462)"><foreignObject width="252.875" height="21"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>Woodpecker agent, one pipeline</p></span></div></foreignObject></g></g></g><g class="edgePaths"><path d="M247.27,80L234.892,87.667C222.513,95.333,197.757,110.667,185.378,126C173,141.333,173,156.667,173,167.833C173,179,173,186,173,189.5L173,193" id="d-c99b82f2b5cf-L_dev_dns_0" class="edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_dev_dns_0" data-points="W3sieCI6MjQ3LjI3MDEwMjg5NjM0MTQ2LCJ5Ijo4MH0seyJ4IjoxNzMsInkiOjEyNn0seyJ4IjoxNzMsInkiOjE3Mn0seyJ4IjoxNzMsInkiOjE5N31d" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/><path d="M363.519,80L375.897,87.667C388.276,95.333,413.032,110.667,425.411,126C437.789,141.333,437.789,156.667,437.789,169.583C437.789,182.5,437.789,193,437.789,198.25L437.789,203.5" id="d-c99b82f2b5cf-L_dev_forge_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_dev_forge_0" data-points="W3sieCI6MzYzLjUxODk1OTYwMzY1ODUsInkiOjgwfSx7IngiOjQzNy43ODkwNjI1LCJ5IjoxMjZ9LHsieCI6NDM3Ljc4OTA2MjUsInkiOjE3Mn0seyJ4Ijo0MzcuNzg5MDYyNSwieSI6MjA3LjV9XQ==" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/><path d="M437.789,258.5L437.789,266.167C437.789,273.833,437.789,289.167,437.789,302.083C437.789,315,437.789,325.5,437.789,330.75L437.789,336" id="d-c99b82f2b5cf-L_forge_server_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_forge_server_0" data-points="W3sieCI6NDM3Ljc4OTA2MjUsInkiOjI1OC41fSx7IngiOjQzNy43ODkwNjI1LCJ5IjozMDQuNX0seyJ4Ijo0MzcuNzg5MDYyNSwieSI6MzQwfV0=" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/><path d="M437.789,391L437.789,396.917C437.789,402.833,437.789,414.667,437.789,426.5C437.789,438.333,437.789,450.167,437.789,459.583C437.789,469,437.789,476,437.789,479.5L437.789,483" id="d-c99b82f2b5cf-L_server_clone_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_server_clone_0" data-points="W3sieCI6NDM3Ljc4OTA2MjUsInkiOjM5MX0seyJ4Ijo0MzcuNzg5MDYyNSwieSI6NDI2LjV9LHsieCI6NDM3Ljc4OTA2MjUsInkiOjQ2Mn0seyJ4Ijo0MzcuNzg5MDYyNSwieSI6NDg3fV0=" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/><path d="M437.789,559L437.789,563.167C437.789,567.333,437.789,575.667,437.789,583.333C437.789,591,437.789,598,437.789,601.5L437.789,605" id="d-c99b82f2b5cf-L_clone_changelog_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_clone_changelog_0" data-points="W3sieCI6NDM3Ljc4OTA2MjUsInkiOjU1OX0seyJ4Ijo0MzcuNzg5MDYyNSwieSI6NTg0fSx7IngiOjQzNy43ODkwNjI1LCJ5Ijo2MDl9XQ==" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/><path d="M437.789,681L437.789,685.167C437.789,689.333,437.789,697.667,437.789,705.333C437.789,713,437.789,720,437.789,723.5L437.789,727" id="d-c99b82f2b5cf-L_changelog_build_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_changelog_build_0" data-points="W3sieCI6NDM3Ljc4OTA2MjUsInkiOjY4MX0seyJ4Ijo0MzcuNzg5MDYyNSwieSI6NzA2fSx7IngiOjQzNy43ODkwNjI1LCJ5Ijo3MzF9XQ==" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/><path d="M437.789,803L437.789,807.167C437.789,811.333,437.789,819.667,437.789,827.333C437.789,835,437.789,842,437.789,845.5L437.789,849" id="d-c99b82f2b5cf-L_build_deploy_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_build_deploy_0" data-points="W3sieCI6NDM3Ljc4OTA2MjUsInkiOjgwM30seyJ4Ijo0MzcuNzg5MDYyNSwieSI6ODI4fSx7IngiOjQzNy43ODkwNjI1LCJ5Ijo4NTN9XQ==" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/><path d="M437.789,925L437.789,929.167C437.789,933.333,437.789,941.667,437.789,950C437.789,958.333,437.789,966.667,437.789,978.5C437.789,990.333,437.789,1005.667,437.789,1021C437.789,1036.333,437.789,1051.667,437.789,1062.833C437.789,1074,437.789,1081,437.789,1084.5L437.789,1088" id="d-c99b82f2b5cf-L_deploy_pages_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_deploy_pages_0" data-points="W3sieCI6NDM3Ljc4OTA2MjUsInkiOjkyNX0seyJ4Ijo0MzcuNzg5MDYyNSwieSI6OTUwfSx7IngiOjQzNy43ODkwNjI1LCJ5Ijo5NzV9LHsieCI6NDM3Ljc4OTA2MjUsInkiOjEwMjF9LHsieCI6NDM3Ljc4OTA2MjUsInkiOjEwNjd9LHsieCI6NDM3Ljc4OTA2MjUsInkiOjEwOTJ9XQ==" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/><path d="M437.789,1185L437.789,1189.167C437.789,1193.333,437.789,1201.667,437.789,1209.333C437.789,1217,437.789,1224,437.789,1227.5L437.789,1231" id="d-c99b82f2b5cf-L_pages_edge_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_pages_edge_0" data-points="W3sieCI6NDM3Ljc4OTA2MjUsInkiOjExODV9LHsieCI6NDM3Ljc4OTA2MjUsInkiOjEyMTB9LHsieCI6NDM3Ljc4OTA2MjUsInkiOjEyMzV9XQ==" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/><path d="M437.789,1307L437.789,1311.167C437.789,1315.333,437.789,1323.667,437.789,1333.75C437.789,1343.833,437.789,1355.667,437.789,1366.833C437.789,1378,437.789,1388.5,437.789,1393.75L437.789,1399" id="d-c99b82f2b5cf-L_edge_reader_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_edge_reader_0" data-points="W3sieCI6NDM3Ljc4OTA2MjUsInkiOjEzMDd9LHsieCI6NDM3Ljc4OTA2MjUsInkiOjEzMzJ9LHsieCI6NDM3Ljc4OTA2MjUsInkiOjEzNjcuNX0seyJ4Ijo0MzcuNzg5MDYyNSwieSI6MTQwM31d" data-look="classic" marker-end="url(#d-c99b82f2b5cf_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(173, 126)"><g class="label" data-id="L_dev_dns_0" transform="translate(-100, -21)"><foreignObject width="200" height="42"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="edgeLabel"><p>name lookup, internal answer</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(437.7890625, 126)"><g class="label" data-id="L_dev_forge_0" transform="translate(-12.6484375, -10.5)"><foreignObject width="25.296875" height="21"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>ssh</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(437.7890625, 304.5)"><g class="label" data-id="L_forge_server_0" transform="translate(-29.5078125, -10.5)"><foreignObject width="59.015625" height="21"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>webhook</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(437.7890625, 426.5)"><g class="label" data-id="L_server_clone_0" transform="translate(-59.0078125, -10.5)"><foreignObject width="118.015625" height="21"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>queues the job</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_clone_changelog_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_changelog_build_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_build_deploy_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(437.7890625, 1021)"><g class="label" data-id="L_deploy_pages_0" transform="translate(-100, -21)"><foreignObject width="200" height="42"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="edgeLabel"><p>outbound only, token scoped to Pages: Edit</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_pages_edge_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(437.7890625, 1367.5)"><g class="label" data-id="L_edge_reader_0" transform="translate(-88.5078125, -10.5)"><foreignObject width="177.015625" height="21"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>https://wilhelm.codes</p></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="d-c99b82f2b5cf-flowchart-dev-0" data-look="classic" transform="translate(305.39453125, 44)"><rect class="basic label-container" style="" x="-130" y="-36" width="260" height="72"/><g class="label" style="" transform="translate(-100, -21)"><rect/><foreignObject width="200" height="42"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>The desk: git push to main</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-dns-1" data-look="classic" transform="translate(173, 233)"><rect class="basic label-container" style="" x="-130" y="-36" width="260" height="72"/><g class="label" style="" transform="translate(-100, -21)"><rect/><foreignObject width="200" height="42"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Technitium DNS x3 ( split-horizon )</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-forge-2" data-look="classic" transform="translate(437.7890625, 233)"><rect class="basic label-container" style="" x="-84.7890625" y="-25.5" width="169.578125" height="51"/><g class="label" style="" transform="translate(-54.7890625, -10.5)"><rect/><foreignObject width="109.578125" height="21"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Forgejo forge</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-server-3" data-look="classic" transform="translate(437.7890625, 365.5)"><rect class="basic label-container" style="" x="-101.6484375" y="-25.5" width="203.296875" height="51"/><g class="label" style="" transform="translate(-71.6484375, -10.5)"><rect/><foreignObject width="143.296875" height="21"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Woodpecker server</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-clone-4" data-look="classic" transform="translate(437.7890625, 523)"><rect class="basic label-container" style="" x="-130" y="-36" width="260" height="72"/><g class="label" style="" transform="translate(-100, -21)"><rect/><foreignObject width="200" height="42"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Full clone ( partial: false, tags: true )</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-changelog-5" data-look="classic" transform="translate(437.7890625, 645)"><rect class="basic label-container" style="" x="-130" y="-36" width="260" height="72"/><g class="label" style="" transform="translate(-100, -21)"><rect/><foreignObject width="200" height="42"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>bin/changelog writes data/changelog.json</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-build-6" data-look="classic" transform="translate(437.7890625, 767)"><rect class="basic label-container" style="" x="-130" y="-36" width="260" height="72"/><g class="label" style="" transform="translate(-100, -21)"><rect/><foreignObject width="200" height="42"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>hugo --gc builds public/</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-deploy-7" data-look="classic" transform="translate(437.7890625, 889)"><rect class="basic label-container" style="" x="-130" y="-36" width="260" height="72"/><g class="label" style="" transform="translate(-100, -21)"><rect/><foreignObject width="200" height="42"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>npx wrangler pages deploy public</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-pages-8" data-look="classic" transform="translate(437.7890625, 1138.5)"><rect class="basic label-container" style="" x="-130" y="-46.5" width="260" height="93"/><g class="label" style="" transform="translate(-100, -31.5)"><rect/><foreignObject width="200" height="63"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Pages project ( direct upload, production branch: main )</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-edge-9" data-look="classic" transform="translate(437.7890625, 1271)"><rect class="basic label-container" style="" x="-130" y="-36" width="260" height="72"/><g class="label" style="" transform="translate(-100, -21)"><rect/><foreignObject width="200" height="42"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Edge cache, serves the last good deploy</p></span></div></foreignObject></g></g><g class="node default" id="d-c99b82f2b5cf-flowchart-reader-10" data-look="classic" transform="translate(437.7890625, 1428.5)"><rect class="basic label-container" style="" x="-110.078125" y="-25.5" width="220.15625" height="51"/><g class="label" style="" transform="translate(-80.078125, -10.5)"><rect/><foreignObject width="160.15625" height="21"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>A reader, allegedly</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="d-c99b82f2b5cf-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="d-c99b82f2b5cf-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="d-c99b82f2b5cf-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#3c3f43" stop-opacity="1"/><stop offset="100%" stop-color="hsl(216, 0%, 23.5294117647%)" stop-opacity="1"/></linearGradient></svg></figure>
<h3 id="sequence">
  <a class="heading-link" href="#sequence">Sequence<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>The same pipeline again, compressed into a handshake. This one is a plain <code>mermaid</code> fence instead of the shortcode. Both forms render the same way.</p>
<figure class="diagram"><svg id="d-fbe8de6d2e78" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="max-width: 865px; background-color: transparent;" viewBox="-50 -10 865 421" role="graphics-document document" aria-roledescription="sequence"><g><rect x="615" y="335" fill="#eaeaea" stroke="#666" width="150" height="65" name="C" rx="3" ry="3" class="actor actor-bottom"/><text x="690" y="367.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="690" dy="0">Cloudflare Pages</tspan></text></g><g><rect x="400" y="335" fill="#eaeaea" stroke="#666" width="150" height="65" name="W" rx="3" ry="3" class="actor actor-bottom"/><text x="475" y="367.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="475" dy="0">Woodpecker</tspan></text></g><g><rect x="200" y="335" fill="#eaeaea" stroke="#666" width="150" height="65" name="F" rx="3" ry="3" class="actor actor-bottom"/><text x="275" y="367.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="275" dy="0">Forgejo</tspan></text></g><g><rect x="0" y="335" fill="#eaeaea" stroke="#666" width="150" height="65" name="D" rx="3" ry="3" class="actor actor-bottom"/><text x="75" y="367.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="75" dy="0">The desk</tspan></text></g><g><line id="actor3" x1="690" y1="65" x2="690" y2="335" class="actor-line 200" stroke-width="0.5px" stroke="#999" name="C" data-et="life-line" data-id="C"/><g id="root-3" data-et="participant" data-type="participant" data-id="C"><rect x="615" y="0" fill="#eaeaea" stroke="#666" width="150" height="65" name="C" rx="3" ry="3" class="actor actor-top"/><text x="690" y="32.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="690" dy="0">Cloudflare Pages</tspan></text></g></g><g><line id="actor2" x1="475" y1="65" x2="475" y2="335" class="actor-line 200" stroke-width="0.5px" stroke="#999" name="W" data-et="life-line" data-id="W"/><g id="root-2" data-et="participant" data-type="participant" data-id="W"><rect x="400" y="0" fill="#eaeaea" stroke="#666" width="150" height="65" name="W" rx="3" ry="3" class="actor actor-top"/><text x="475" y="32.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="475" dy="0">Woodpecker</tspan></text></g></g><g><line id="actor1" x1="275" y1="65" x2="275" y2="335" class="actor-line 200" stroke-width="0.5px" stroke="#999" name="F" data-et="life-line" data-id="F"/><g id="root-1" data-et="participant" data-type="participant" data-id="F"><rect x="200" y="0" fill="#eaeaea" stroke="#666" width="150" height="65" name="F" rx="3" ry="3" class="actor actor-top"/><text x="275" y="32.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="275" dy="0">Forgejo</tspan></text></g></g><g><line id="actor0" x1="75" y1="65" x2="75" y2="335" class="actor-line 200" stroke-width="0.5px" stroke="#999" name="D" data-et="life-line" data-id="D"/><g id="root-0" data-et="participant" data-type="participant" data-id="D"><rect x="0" y="0" fill="#eaeaea" stroke="#666" width="150" height="65" name="D" rx="3" ry="3" class="actor actor-top"/><text x="75" y="32.5" dominant-baseline="central" alignment-baseline="central" class="actor actor-box" style="text-anchor: middle; font-size: 16px; font-weight: 400;"><tspan x="75" dy="0">The desk</tspan></text></g></g><style>#d-fbe8de6d2e78{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;font-size:14px;fill:#32d74b;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#d-fbe8de6d2e78 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#d-fbe8de6d2e78 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#d-fbe8de6d2e78 .error-icon{fill:#202225;}#d-fbe8de6d2e78 .error-text{fill:#e3e3de;stroke:#e3e3de;}#d-fbe8de6d2e78 .edge-thickness-normal{stroke-width:1px;}#d-fbe8de6d2e78 .edge-thickness-thick{stroke-width:3.5px;}#d-fbe8de6d2e78 .edge-pattern-solid{stroke-dasharray:0;}#d-fbe8de6d2e78 .edge-thickness-invisible{stroke-width:0;fill:none;}#d-fbe8de6d2e78 .edge-pattern-dashed{stroke-dasharray:3;}#d-fbe8de6d2e78 .edge-pattern-dotted{stroke-dasharray:2;}#d-fbe8de6d2e78 .marker{fill:#8b8b84;stroke:#8b8b84;}#d-fbe8de6d2e78 .marker.cross{stroke:#8b8b84;}#d-fbe8de6d2e78 svg{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;font-size:14px;}#d-fbe8de6d2e78 p{margin:0;}#d-fbe8de6d2e78 .actor{stroke:#3c3f43;fill:#1e1f22;stroke-width:1;}#d-fbe8de6d2e78 rect.actor.outer-path[data-look="neo"]{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-fbe8de6d2e78 rect.note[data-look="neo"]{stroke:hsl(52.6829268293, 60%, 93.9215686275%);fill:#fff5ad;filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-fbe8de6d2e78 text.actor&gt;tspan{fill:#32d74b;stroke:none;}#d-fbe8de6d2e78 .actor-line{stroke:#8b8b84;}#d-fbe8de6d2e78 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#d-fbe8de6d2e78 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#8b8b84;}#d-fbe8de6d2e78 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#8b8b84;}#d-fbe8de6d2e78 [id$="-arrowhead"] path{fill:#8b8b84;stroke:#8b8b84;}#d-fbe8de6d2e78 .sequenceNumber{fill:#74747b;}#d-fbe8de6d2e78 [id$="-sequencenumber"]{fill:#8b8b84;}#d-fbe8de6d2e78 [id$="-crosshead"] path{fill:#8b8b84;stroke:#8b8b84;}#d-fbe8de6d2e78 .messageText{fill:#32d74b;stroke:none;}#d-fbe8de6d2e78 .labelBox{stroke:#3c3f43;fill:#1e1f22;filter:none;}#d-fbe8de6d2e78 .labelText,#d-fbe8de6d2e78 .labelText&gt;tspan{fill:#32d74b;stroke:none;}#d-fbe8de6d2e78 .loopText,#d-fbe8de6d2e78 .loopText&gt;tspan{fill:#32d74b;stroke:none;}#d-fbe8de6d2e78 .sectionTitle,#d-fbe8de6d2e78 .sectionTitle&gt;tspan{fill:#32d74b;stroke:none;}#d-fbe8de6d2e78 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:#3c3f43;fill:#3c3f43;}#d-fbe8de6d2e78 .note{stroke:hsl(52.6829268293, 60%, 93.9215686275%);fill:#fff5ad;}#d-fbe8de6d2e78 .noteText,#d-fbe8de6d2e78 .noteText&gt;tspan{fill:#333;stroke:none;font-weight:normal;}#d-fbe8de6d2e78 .activation0{fill:#202225;stroke:hsl(216, 7.2463768116%, 3.5294117647%);}#d-fbe8de6d2e78 .activation1{fill:#202225;stroke:hsl(216, 7.2463768116%, 3.5294117647%);}#d-fbe8de6d2e78 .activation2{fill:#202225;stroke:hsl(216, 7.2463768116%, 3.5294117647%);}#d-fbe8de6d2e78 .actorPopupMenu{position:absolute;}#d-fbe8de6d2e78 .actorPopupMenuPanel{position:absolute;fill:#1e1f22;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#d-fbe8de6d2e78 .actor-man circle,#d-fbe8de6d2e78 line{fill:#1e1f22;stroke-width:2px;}#d-fbe8de6d2e78 g rect.rect{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));stroke:#3c3f43;}#d-fbe8de6d2e78 .node .neo-node{stroke:#3c3f43;}#d-fbe8de6d2e78 [data-look="neo"].node rect,#d-fbe8de6d2e78 [data-look="neo"].cluster rect,#d-fbe8de6d2e78 [data-look="neo"].node polygon{stroke:url(#d-fbe8de6d2e78-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-fbe8de6d2e78 [data-look="neo"].swimlane.cluster rect{filter:none;}#d-fbe8de6d2e78 [data-look="neo"].node path{stroke:url(#d-fbe8de6d2e78-gradient);stroke-width:1px;}#d-fbe8de6d2e78 [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-fbe8de6d2e78 [data-look="neo"].node .neo-line path{stroke:#3c3f43;filter:none;}#d-fbe8de6d2e78 [data-look="neo"].node circle{stroke:url(#d-fbe8de6d2e78-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-fbe8de6d2e78 [data-look="neo"].node circle .state-start{fill:#000000;}#d-fbe8de6d2e78 [data-look="neo"].icon-shape .icon{fill:url(#d-fbe8de6d2e78-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-fbe8de6d2e78 [data-look="neo"].icon-shape .icon-neo path{stroke:url(#d-fbe8de6d2e78-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#d-fbe8de6d2e78 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g/><defs><symbol id="d-fbe8de6d2e78-computer" width="24" height="24"><path transform="scale(.5)" d="M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z"/></symbol></defs><defs><symbol id="d-fbe8de6d2e78-database" fill-rule="evenodd" clip-rule="evenodd"><path transform="scale(.5)" d="M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z"/></symbol></defs><defs><symbol id="d-fbe8de6d2e78-clock" width="24" height="24"><path transform="scale(.5)" d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z"/></symbol></defs><defs><marker id="d-fbe8de6d2e78-arrowhead" refX="7.9" refY="5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M -1 0 L 10 5 L 0 10 z"/></marker></defs><defs><marker id="d-fbe8de6d2e78-crosshead" markerWidth="15" markerHeight="8" orient="auto" refX="4" refY="4.5"><path fill="none" stroke="#000000" stroke-width="1pt" d="M 1,2 L 6,7 M 6,2 L 1,7" style="stroke-dasharray: 0, 0;"/></marker></defs><defs><marker id="d-fbe8de6d2e78-filled-head" refX="15.5" refY="7" markerWidth="20" markerHeight="28" orient="auto"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker id="d-fbe8de6d2e78-sequencenumber" refX="15" refY="15" markerWidth="60" markerHeight="40" orient="auto"><circle cx="15" cy="15" r="6"/></marker></defs><defs><marker id="d-fbe8de6d2e78-solidTopArrowHead" refX="7.9" refY="7.25" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M 0 0 L 10 8 L 0 8 z"/></marker></defs><defs><marker id="d-fbe8de6d2e78-solidBottomArrowHead" refX="7.9" refY="0.75" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M 0 0 L 10 0 L 0 8 z"/></marker></defs><defs><marker id="d-fbe8de6d2e78-stickTopArrowHead" refX="7.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M 0 0 L 7 7" stroke="black" stroke-width="1.5" fill="none"/></marker></defs><defs><marker id="d-fbe8de6d2e78-stickBottomArrowHead" refX="7.5" refY="0" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto-start-reverse"><path d="M 0 7 L 7 0" stroke="black" stroke-width="1.5" fill="none"/></marker></defs><text x="174" y="80" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">git push ( main )</text><line x1="76" y1="109" x2="271" y2="109" class="messageLine0" data-et="message" data-id="i0" data-from="D" data-to="F" stroke-width="2" stroke="none" marker-end="url(#d-fbe8de6d2e78-arrowhead)" style="fill: none;"/><text x="374" y="124" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">webhook</text><line x1="276" y1="153" x2="471" y2="153" class="messageLine0" data-et="message" data-id="i1" data-from="F" data-to="W" stroke-width="2" stroke="none" marker-end="url(#d-fbe8de6d2e78-arrowhead)" style="fill: none;"/><text x="476" y="168" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">clone, changelog, hugo --gc</text><path d="M 476,197 C 536,187 536,227 476,217" class="messageLine0" data-et="message" data-id="i2" data-from="W" data-to="W" stroke-width="2" stroke="none" marker-end="url(#d-fbe8de6d2e78-arrowhead)" style="fill: none;"/><text x="581" y="242" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">wrangler pages deploy</text><line x1="476" y1="271" x2="686" y2="271" class="messageLine0" data-et="message" data-id="i3" data-from="W" data-to="C" stroke-width="2" stroke="none" marker-end="url(#d-fbe8de6d2e78-arrowhead)" style="fill: none;"/><text x="384" y="286" text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle" class="messageText" dy="1em" style="font-size: 16px; font-weight: 400;">the same site, but newer</text><line x1="689" y1="315" x2="79" y2="315" class="messageLine1" data-et="message" data-id="i4" data-from="C" data-to="D" stroke-width="2" stroke="none" marker-end="url(#d-fbe8de6d2e78-arrowhead)" style="stroke-dasharray: 3, 3; fill: none;"/></svg></figure><h2 id="inline-elements">
  <a class="heading-link" href="#inline-elements">Inline elements<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Sint ea anim ipsum ad commodo cupidatat do <strong>exercitation</strong> incididunt et minim ad labore sunt. Minim deserunt labore laboris velit nulla incididunt ipsum nulla. Ullamco ad laborum ea qui et anim in laboris exercitation tempor sit officia laborum reprehenderit culpa velit quis. <strong>Consequat commodo</strong> reprehenderit duis <a href="#!">irure</a> esse esse exercitation minim enim Lorem dolore duis irure. Nisi Lorem reprehenderit ea amet excepteur dolor excepteur magna labore proident voluptate ipsum. Reprehenderit ex esse deserunt aliqua ea officia mollit Lorem nulla magna enim. Et ad ipsum labore enim ipsum <strong>cupidatat consequat</strong>. Commodo non ea cupidatat magna deserunt dolore ipsum velit nulla elit veniam nulla eiusmod proident officia.</p>
<p><img
  src="https://wilhelm.codes/blog/markdown/city-above_hu_5649e0e90a2a3de4.webp"
  srcset="/blog/markdown/city-above_hu_5649e0e90a2a3de4.webp 736w, /blog/markdown/city-above_hu_155d0a5ddcc6b38d.webp 1104w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="414"
  alt="A city from above at night"
  
  loading="lazy"
  decoding="async"
/>
</p>
<p><em>Proident sit veniam in est proident officia adipisicing</em> ea tempor cillum non cillum velit deserunt. Voluptate laborum incididunt sit consectetur Lorem irure incididunt voluptate nostrud. Commodo ut eiusmod tempor cupidatat esse enim minim ex anim consequat. Mollit sint culpa qui laboris quis consectetur ad sint esse. Amet anim anim minim ullamco et duis non irure. Sit tempor adipisicing ea laboris <code>culpa ex duis sint</code> anim aute reprehenderit id eu ea. Aute <a href="#!">excepteur proident</a> Lorem minim adipisicing nostrud mollit ad ut voluptate do nulla esse occaecat aliqua sint anim.</p>
<p>Incididunt in culpa cupidatat mollit cillum qui proident sit. In cillum aliquip incididunt voluptate magna amet cupidatat cillum pariatur sint aliqua est <em>enim <strong>anim</strong> voluptate</em>. Magna aliquip proident incididunt id duis pariatur eiusmod incididunt commodo culpa dolore sit. Culpa do nostrud elit ad exercitation anim pariatur non minim nisi <strong>adipisicing sunt <em>officia</em></strong>. Do deserunt magna mollit Lorem commodo ipsum do cupidatat mollit enim ut elit veniam ea voluptate.</p>
<p>Reprehenderit non eu quis in ad elit esse qui aute id <a href="#!">incididunt</a> dolore cillum. Esse laboris consequat dolor anim exercitation tempor aliqua deserunt velit magna laboris. Culpa culpa minim duis amet mollit do quis amet commodo nulla irure.</p>
<h2 id="callouts">
  <a class="heading-link" href="#callouts">Callouts<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Nostrud ex consequat id incididunt proident ipsum minim duis aliqua ut ex et ad quis.</p>
<div class="callout callout-notice">
  A notice. Duis duis est velit velit aute culpa ex quis pariatur laborum aute pariatur.
</div>

<div class="callout callout-info">
  Some information. Excepteur velit nisi magna ea pariatur ullamco fugiat deserunt sint.
</div>

<div class="callout callout-warning">
  A warning. Anim magna dolor eiusmod esse amet aute cupidatat aliqua do id voluptate.
</div>

<div class="callout callout-error">
  An error. Irure magna voluptate dolore consectetur consectetur irure esse.
</div>

<h2 id="images">
  <a class="heading-link" href="#images">Images<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Every image on this page is <a href="https://unsplash.com/">Unsplash</a> stock, hosted here so the site keeps its no-external-requests rule. Click any of them to zoom in. Click again, or press Escape, to put it back.</p>
<h3 id="single">
  <a class="heading-link" href="#single">Single<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>A lone image takes the <code>img</code> shortcode: a figure, an optional attribution underneath and the zoom.</p>
<figure class="img-single">
    <button type="button" class="zoom" aria-label="Zoom image: A wet street at night in Tokyo, neon signs reflecting on the pavement">
      <img
  src="https://wilhelm.codes/blog/markdown/tokyo-street_hu_e5c50287801d328c.webp"
  srcset="/blog/markdown/tokyo-street_hu_e5c50287801d328c.webp 736w, /blog/markdown/tokyo-street_hu_ba568f598d59a3f9.webp 1104w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="1103"
  alt="A wet street at night in Tokyo, neon signs reflecting on the pavement"
  loading="lazy"
  decoding="async"
  data-full="/blog/markdown/tokyo-street_hu_784bc02980fe6a3e.webp"
  data-credit="Photo by Valentin BEAUVAIS on Unsplash"
  data-credit-href="https://unsplash.com/photos/wet-street-at-night-in-tokyo-yVUQlyRlJSw"
/>

    </button>
    
      <figcaption>
        
          <a href="https://unsplash.com/photos/wet-street-at-night-in-tokyo-yVUQlyRlJSw" target="_blank" rel="noopener">Photo by Valentin BEAUVAIS on Unsplash</a>
        
      </figcaption>
    
  </figure>
<p>Aute officia nulla deserunt do deserunt cillum velit magna. Officia veniam culpa anim minim dolore labore pariatur voluptate id ad est duis quis velit dolor pariatur enim.</p>
<h3 id="strip">
  <a class="heading-link" href="#strip">Strip<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>A run of images scrolls sideways in the <code>strip</code> shortcode. Each image keeps its own zoom, and its credit follows it into the lightbox.</p>
<div class="strip">
  
  <button type="button" class="zoom" aria-label="Zoom image: Red neon signs reflected in a street puddle">
      <img
    src="https://wilhelm.codes/blog/markdown/neon-puddle_hu_7b6d6f59e5ddfc10.webp"
    srcset="/blog/markdown/neon-puddle_hu_7b6d6f59e5ddfc10.webp 1x, /blog/markdown/neon-puddle_hu_3647ba2eb6e24731.webp 2x"
    
    width="336"
    height="224"
    alt="Red neon signs reflected in a street puddle"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/neon-puddle_hu_ff42f54c862d30e7.webp"
    data-credit="Photo by cheng feng on Unsplash"
    data-credit-href="https://unsplash.com/photos/neon-street-reflection-in-night-puddle-4pTY4ty7jXM"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: A city from above at night">
      <img
    src="https://wilhelm.codes/blog/markdown/city-above_hu_3d2045a0494e4370.webp"
    srcset="/blog/markdown/city-above_hu_3d2045a0494e4370.webp 1x, /blog/markdown/city-above_hu_df60f704bb22bbba.webp 2x"
    
    width="398"
    height="224"
    alt="A city from above at night"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/city-above_hu_56bb6d6a477e88e7.webp"
    data-credit="Photo by Francois Hoang on Unsplash"
    data-credit-href="https://unsplash.com/photos/high-angle-photography-of-city-during-nighttime-MFYlCoSm-0o"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: People walking through a neon walkway at night">
      <img
    src="https://wilhelm.codes/blog/markdown/walkway_hu_b24822e8741b3d78.webp"
    srcset="/blog/markdown/walkway_hu_b24822e8741b3d78.webp 1x, /blog/markdown/walkway_hu_824953d065f58427.webp 2x"
    
    width="336"
    height="224"
    alt="People walking through a neon walkway at night"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/walkway_hu_7016b54a2c5bbc52.webp"
    data-credit="Photo by kevin laminto on Unsplash"
    data-credit-href="https://unsplash.com/photos/people-walking-on-walkway-during-nighttime-7PqRZK6rbaE"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: A crowded street at night">
      <img
    src="https://wilhelm.codes/blog/markdown/night-crowd_hu_e5982ecec0b9b59e.webp"
    srcset="/blog/markdown/night-crowd_hu_e5982ecec0b9b59e.webp 1x, /blog/markdown/night-crowd_hu_2a16cc909794e2ae.webp 2x"
    
    width="149"
    height="224"
    alt="A crowded street at night"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/night-crowd_hu_35abd7f5b2b88018.webp"
    data-credit="Photo by Harrison Qi on Unsplash"
    data-credit-href="https://unsplash.com/photos/people-walking-on-street-during-nighttime-ce-K_rj1P7U"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: Night traffic between high-rise buildings">
      <img
    src="https://wilhelm.codes/blog/markdown/night-traffic_hu_c8b96f20fb90ea41.webp"
    srcset="/blog/markdown/night-traffic_hu_c8b96f20fb90ea41.webp 1x, /blog/markdown/night-traffic_hu_abe8f08cc3fcb6c0.webp 2x"
    
    width="149"
    height="224"
    alt="Night traffic between high-rise buildings"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/night-traffic_hu_c356e8d14ec539d5.webp"
    data-credit="Photo by Stéphan Valentin on Unsplash"
    data-credit-href="https://unsplash.com/photos/vehicles-on-road-near-high-rise-buildings-oqYLdbuJDQU"
  />
  
    </button>

</div>

<p>Labore ea magna Lorem consequat aliquip consectetur cillum duis dolore. Et veniam dolor qui incididunt minim amet laboris sit.</p>
<h3 id="masonry">
  <a class="heading-link" href="#masonry">Masonry<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>The <code>masonry</code> shortcode packs a wall of mixed heights into columns. Same zoom, same credits.</p>
<div class="masonry">
  
  <button type="button" class="zoom" aria-label="Zoom image: A vintage grey game console and joystick on a magenta background">
      <img
    src="https://wilhelm.codes/blog/markdown/console_hu_9f8704775736b8c4.webp"
    srcset="/blog/markdown/console_hu_9f8704775736b8c4.webp 232w, /blog/markdown/console_hu_85d24f50a46dd7b6.webp 464w, /blog/markdown/console_hu_fa24477c2b0f7c11.webp 696w"
    sizes="(max-width: 40rem) 50vw, 232px"
    width="232"
    height="155"
    alt="A vintage grey game console and joystick on a magenta background"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/console_hu_12e3b3a05e270cdb.webp"
    data-credit="Photo by Lorenzo Herrera on Unsplash"
    data-credit-href="https://unsplash.com/photos/vintage-gray-game-console-and-joystick-p0j-mE6mGo4"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: Cabled network switches in a rack">
      <img
    src="https://wilhelm.codes/blog/markdown/server-cables_hu_5a0026c5dfacf58d.webp"
    srcset="/blog/markdown/server-cables_hu_5a0026c5dfacf58d.webp 232w, /blog/markdown/server-cables_hu_96bc67206c0724c3.webp 464w, /blog/markdown/server-cables_hu_93866602d9487e49.webp 696w"
    sizes="(max-width: 40rem) 50vw, 232px"
    width="232"
    height="130"
    alt="Cabled network switches in a rack"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/server-cables_hu_d7489352bd0202f6.webp"
    data-credit="Photo by Taylor Vick on Unsplash"
    data-credit-href="https://unsplash.com/photos/cable-network-M5tzZtFCOfs"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: A dark blue circuit board up close">
      <img
    src="https://wilhelm.codes/blog/markdown/circuit-blue_hu_fb606dd5b10cb137.webp"
    srcset="/blog/markdown/circuit-blue_hu_fb606dd5b10cb137.webp 232w, /blog/markdown/circuit-blue_hu_24e1ca785ff29b94.webp 464w, /blog/markdown/circuit-blue_hu_ce5aa786d6004352.webp 696w"
    sizes="(max-width: 40rem) 50vw, 232px"
    width="232"
    height="174"
    alt="A dark blue circuit board up close"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/circuit-blue_hu_e79783b2bbe48544.webp"
    data-credit="Photo by Vishnu Mohanan on Unsplash"
    data-credit-href="https://unsplash.com/photos/close-up-of-dark-blue-circuit-board-pfR18JNEMv8"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: A stack of assorted CRT monitors">
      <img
    src="https://wilhelm.codes/blog/markdown/crt-stack_hu_e2450c79214f8261.webp"
    srcset="/blog/markdown/crt-stack_hu_e2450c79214f8261.webp 232w, /blog/markdown/crt-stack_hu_d484ff96005f12cb.webp 464w, /blog/markdown/crt-stack_hu_14a7def7bc889fb5.webp 696w"
    sizes="(max-width: 40rem) 50vw, 232px"
    width="232"
    height="154"
    alt="A stack of assorted CRT monitors"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/crt-stack_hu_ec7865487d822f93.webp"
    data-credit="Photo by Rubenz Arizta on Unsplash"
    data-credit-href="https://unsplash.com/photos/assorted-crt-monitor-3uQWtvUPjyg"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: A red and black circuit board">
      <img
    src="https://wilhelm.codes/blog/markdown/circuit-red_hu_8d921bca68ecd16.webp"
    srcset="/blog/markdown/circuit-red_hu_8d921bca68ecd16.webp 232w, /blog/markdown/circuit-red_hu_2d90e29275615c6e.webp 464w, /blog/markdown/circuit-red_hu_7f68810c0c65d632.webp 696w"
    sizes="(max-width: 40rem) 50vw, 232px"
    width="232"
    height="182"
    alt="A red and black circuit board"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/circuit-red_hu_7df031890e235073.webp"
    data-credit="Photo by Michael Dziedzic on Unsplash"
    data-credit-href="https://unsplash.com/photos/red-and-black-circuit-board-aQYgUYwnCsM"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: A green motherboard in tilt-shift focus">
      <img
    src="https://wilhelm.codes/blog/markdown/motherboard_hu_98700506ebffc8ba.webp"
    srcset="/blog/markdown/motherboard_hu_98700506ebffc8ba.webp 232w, /blog/markdown/motherboard_hu_def8aa27e2e417a6.webp 464w, /blog/markdown/motherboard_hu_84f3893b6a0f3cc2.webp 696w"
    sizes="(max-width: 40rem) 50vw, 232px"
    width="232"
    height="155"
    alt="A green motherboard in tilt-shift focus"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/motherboard_hu_3d740513539771e0.webp"
    data-credit="Photo by Chris Ried on Unsplash"
    data-credit-href="https://unsplash.com/photos/tilt-shift-photography-of-green-computer-motherboard-bN5XdU-bap4"
  />
  
    </button>
  <button type="button" class="zoom" aria-label="Zoom image: A black CRT beside a white keyboard">
      <img
    src="https://wilhelm.codes/blog/markdown/crt-keyboard_hu_b5498c4eeeca1fc4.webp"
    srcset="/blog/markdown/crt-keyboard_hu_b5498c4eeeca1fc4.webp 232w, /blog/markdown/crt-keyboard_hu_8188fb2b95493889.webp 464w, /blog/markdown/crt-keyboard_hu_5b37d15cf9925867.webp 696w"
    sizes="(max-width: 40rem) 50vw, 232px"
    width="232"
    height="155"
    alt="A black CRT beside a white keyboard"
    loading="lazy"
    decoding="async"
    data-full="/blog/markdown/crt-keyboard_hu_a3a7b0cd64792094.webp"
    data-credit="Photo by Senad Palic on Unsplash"
    data-credit-href="https://unsplash.com/photos/black-crt-tv-beside-white-computer-keyboard-js39s4h6dt8"
  />
  
    </button>

</div>

<p>Ex amet id ex aliquip id do laborum excepteur exercitation elit sint commodo occaecat nostrud est.</p>]]></content:encoded></item></channel></rss>