<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Blog feed</title>
    <link>https://kaugesaar.se/blog</link>
    <description>Latest blog updates from https://kaugesaar.se</description>
    <language>en-us</language>
    <lastBuildDate>Sun, 22 Mar 2026 00:00:00 +0000</lastBuildDate>
    <item>
      <title>Speed is not velocity</title>
      <link>https://kaugesaar.se/blog/speed-is-not-velocity</link>
      <guid>https://kaugesaar.se/blog/speed-is-not-velocity</guid>
      <pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>I&rsquo;ve been thinking a lot about the gap between how powerful LLMs feel when you&rsquo;re using them, and how little that seems to matter for actually getting meaningful work over the line.</p>
<p>Not because I don&rsquo;t use them. I do. A lot.</p>
<p>At this point, they probably write more than 90% of the code I produce. If you measure output in lines of code per second, I&rsquo;m obviously much faster than I was before. But if you measure the thing that actually matters - shipping something useful to production - the improvement is much smaller. Certainly nowhere near the 10-100x some people keep claiming.</p>
<p>If something really made a team 10x faster, that would not be subtle. Something that used to take a month would now take three days. Something that used to take a year would be done in five weeks. We would all notice.</p>
<p>I think part of the confusion is that we&rsquo;re mistaking speed for velocity.</p>
<p>Speed is how quickly code appears on the screen. Velocity is whether the system is actually moving in the right direction. LLMs are extremely good at speed. But the direction problem is still mostly there, and in some ways more visible than before.</p>
<h2 id="the-easy-part-was-never-the-hard-part">The easy part was never the hard part</h2>
<p>Getting something that vaguely works on your laptop was never the whole job. The job is making it hold up inside a real codebase, with actual constraints, with other people touching it, using it, deploying it, debugging it and living with it afterwards.</p>
<p>People point at how quickly they can generate code now, and they are not lying. They probably <em>are</em> generating code much faster. But writing the code was never the only bottleneck, and in many cases not even the main one.</p>
<p>The hard parts are still the hard parts: deciding what should exist, agreeing on the shape of it, and dealing with the consequences afterwards.</p>
<h2 id="more-output-same-throughput">More output, same throughput</h2>
<p>LLMs absolutely increase output. They make it easier to start. Easier to explore. Easier to get to a rough first version. Easier to open ten threads of work instead of one.</p>
<p>But more output is not the same thing as more progress. Quite often it just means more unfinished work.</p>
<p>LLMs move quickly. But they do not know where to go in the deeper sense. They do not know which tradeoff matters, which future constraint will hurt, which messy edge case is actually the product, or which feature should simply not exist.</p>
<h2 id="why-we-keep-pressing-the-button">Why we keep pressing the button</h2>
<p>Now there is always something that could be running. Another prompt. Another refactor. Another tiny tool. Another MVP. Another experiment that is suddenly cheap enough to start, but not cheap enough to finish.</p>
<p>And of course we keep pressing the button.</p>
<p>Why wouldn&rsquo;t we?</p>
<p>You describe something in plain English, wait a few seconds, and there it is: a first iteration, a quick hack, something working. Even when the result is mediocre, it still gives you that little reward of momentum. Something moved. Something appeared. The machine answered.</p>
<p>The loop is so short, and the payoff so immediate, that it becomes difficult not to reach for it again. Not because the result is always good, but because the experience itself is rewarding.</p>
<h2 id="output-does-not-equal-value">Output does not equal value</h2>
<p>If you spend enough time close to the actual work, it becomes very hard to get excited by demos of prototypes, huge line counts, or vague claims about replacing entire functions of a company.</p>
<p>Because there currently is an enormous amount of noise.</p>
<p>And one of the easiest ways to generate noise right now is to confuse &ldquo;I can produce more artifacts&rdquo; with &ldquo;I can create more value&rdquo;.</p>
<p>Those are not the same thing.</p>
<p>This is also why some of the current hype feels shallow. Stories about how many lines got written, endless demos of things that vaguely work on first pass. None of that really addresses whether anything of value got created, other than the act of making more software-shaped output.</p>
<h2 id="friction-was-doing-us-a-favour">Friction was doing us a favour</h2>
<p>LLMs remove friction from building. But friction was sometimes doing us a favour.</p>
<p>When building something took longer, you had to choose more carefully. You had to kill more ideas earlier. You had to be more selective about which direction deserved a week of your time.</p>
<p>Now the cost of indulging every passing idea is much lower, so you can follow all of them a little bit. The result is not necessarily more clarity. Sometimes it is the opposite.</p>
<p>You end up with more code, more branches, more prototypes, more motion.</p>
<p>But some things just take time.</p>
<p>Understanding a domain takes time. Building trust in a product takes time. Seeing how real users respond takes time. Learning whether a project deserves years of maintenance takes time. No amount of generated code removes that.</p>
<p>Maybe that is why so much of this can feel vaguely hopeless even while the tooling keeps improving. LLMs get better and better at helping us do work that was never the true bottleneck.</p>
<p>And yes, I use LLMs heavily. I am not writing this from the outside. They are useful. Sometimes extremely useful. But I also think they make it easier to confuse acceleration with direction.</p>
<p>And if you accelerate without improving direction, you do not necessarily get where you want to go faster.</p>
<p>You just get lost at higher speed.</p>
]]></description>
    </item>
    <item>
      <title>Building a static site generator in Go</title>
      <link>https://kaugesaar.se/blog/building-a-static-site-generator-in-go</link>
      <guid>https://kaugesaar.se/blog/building-a-static-site-generator-in-go</guid>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>In a <a href="/blog/the-best-part">previous post</a> I mentioned that this site runs on a custom Go-based static site generator. So I thought I&rsquo;d walk through it to show what it actually looks like.</p>
<p>The whole thing is about 800 lines across five packages. It takes markdown files, runs them through an asset pipeline and template renderer, and outputs static HTML that gets deployed to Cloudflare Pages. Nothing revolutionary - but every piece is mine, and I understand exactly what happens when I run <code>go run cmd/web/main.go -build-site</code>.</p>
<h2 id="the-build-pipeline">The build pipeline</h2>
<p>The entry point wires everything together in sequence:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 1. Bundle and minify assets, get back a manifest
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">assetBundler</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">assets</span><span class="p">.</span><span class="nf">NewBundler</span><span class="p">(</span><span class="nx">cfg</span><span class="p">.</span><span class="nx">Assets</span><span class="p">,</span> <span class="s">&#34;public&#34;</span><span class="p">,</span> <span class="nx">staticDir</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">manifest</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">assetBundler</span><span class="p">.</span><span class="nf">Build</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 2. Pass manifest to builder so templates can resolve cache-busted URLs
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="nx">b</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">builder</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">builder</span><span class="p">.</span><span class="nx">Options</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">TemplateDir</span><span class="p">:</span>   <span class="s">&#34;templates&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">OutputDir</span><span class="p">:</span>     <span class="nx">staticDir</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">AssetManifest</span><span class="p">:</span> <span class="nx">manifest</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">Target</span><span class="p">:</span>        <span class="nx">target</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">// 3. Parse markdown, render HTML, generate sitemap and RSS
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="nx">b</span><span class="p">.</span><span class="nf">Build</span><span class="p">()</span>
</span></span></code></pre><p>Assets must be bundled first because the builder needs the manifest to render templates correctly. Templates reference assets like <code>{{ asset &quot;app.css&quot; }}</code>, and that function needs to know that <code>app.css</code> maps to <code>/css/app.a1b2c3d4.css</code>. Without the manifest, the links would be wrong.</p>
<p>That&rsquo;s the whole build. Three steps.</p>
<h2 id="parsing-markdown">Parsing markdown</h2>
<p>The parser is the simplest piece. Markdown files have YAML frontmatter between <code>---</code> delimiters, followed by content:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Some post&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;About something&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-02-21</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="nt">published</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="ln">6</span><span class="cl"><span class="w"></span><span class="nt">view</span><span class="p">:</span><span class="w"> </span><span class="l">blog</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w"></span><span class="l">The actual content here...</span><span class="w">
</span></span></span></code></pre><p>Parsing this is a <code>bytes.SplitN</code> call:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">Parser</span><span class="p">)</span> <span class="nf">Parse</span><span class="p">(</span><span class="nx">path</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">data</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Content</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">parts</span> <span class="o">:=</span> <span class="nx">bytes</span><span class="p">.</span><span class="nf">SplitN</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;---\n&#34;</span><span class="p">),</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">parts</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">3</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;invalid markdown format: expected frontmatter between --- delimiters&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="kd">var</span> <span class="nx">meta</span> <span class="nx">Metadata</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">yaml</span><span class="p">.</span><span class="nf">Unmarshal</span><span class="p">(</span><span class="nx">parts</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="o">&amp;</span><span class="nx">meta</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;failed to parse frontmatter: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="nx">meta</span><span class="p">.</span><span class="nx">Slug</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="nx">slug</span> <span class="o">:=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">TrimSuffix</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">filepath</span><span class="p">.</span><span class="nf">Ext</span><span class="p">(</span><span class="nx">path</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="nx">slug</span> <span class="p">=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">TrimPrefix</span><span class="p">(</span><span class="nx">slug</span><span class="p">,</span> <span class="s">&#34;./&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nx">meta</span><span class="p">.</span><span class="nx">Slug</span> <span class="p">=</span> <span class="nx">filepath</span><span class="p">.</span><span class="nf">ToSlash</span><span class="p">(</span><span class="nx">strings</span><span class="p">.</span><span class="nf">Trim</span><span class="p">(</span><span class="nx">slug</span><span class="p">,</span> <span class="s">&#34;/&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Content</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="nx">Metadata</span><span class="p">:</span> <span class="nx">meta</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="nx">Body</span><span class="p">:</span>     <span class="nx">bytes</span><span class="p">.</span><span class="nf">TrimSpace</span><span class="p">(</span><span class="nx">parts</span><span class="p">[</span><span class="mi">2</span><span class="p">]),</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">},</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>Split on <code>---\n</code>, unmarshal the middle part as YAML, derive the slug from the filename. That&rsquo;s it. No external frontmatter library, no special parser - just <code>SplitN</code> into three parts and take the pieces.</p>
<p>The <code>ParseDir</code> method walks a directory and collects all <code>.md</code> files, sorted newest first. One thing I like about it - it takes an <code>fs.FS</code> instead of a raw directory path. In the build pipeline it gets <code>os.DirFS(&quot;content&quot;)</code>, but it means the parser is easy to test with <code>fstest.MapFS</code> without touching the filesystem.</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">Parser</span><span class="p">)</span> <span class="nf">ParseDir</span><span class="p">(</span><span class="nx">fsys</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">FS</span><span class="p">,</span> <span class="nx">includeUnpublished</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">([]</span><span class="nx">Content</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="kd">var</span> <span class="nx">contents</span> <span class="p">[]</span><span class="nx">Content</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">err</span> <span class="o">:=</span> <span class="nx">fs</span><span class="p">.</span><span class="nf">WalkDir</span><span class="p">(</span><span class="nx">fsys</span><span class="p">,</span> <span class="s">&#34;.&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">path</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">d</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">DirEntry</span><span class="p">,</span> <span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">if</span> <span class="nx">d</span><span class="p">.</span><span class="nf">IsDir</span><span class="p">()</span> <span class="o">||</span> <span class="nx">filepath</span><span class="p">.</span><span class="nf">Ext</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="o">!=</span> <span class="s">&#34;.md&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">data</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">fs</span><span class="p">.</span><span class="nf">ReadFile</span><span class="p">(</span><span class="nx">fsys</span><span class="p">,</span> <span class="nx">path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="c1">// ...parse and filter unpublished...
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span>        <span class="nx">contents</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">contents</span><span class="p">,</span> <span class="o">*</span><span class="nx">content</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nx">sort</span><span class="p">.</span><span class="nf">Slice</span><span class="p">(</span><span class="nx">contents</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">i</span><span class="p">,</span> <span class="nx">j</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">return</span> <span class="nx">contents</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Date</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="nx">contents</span><span class="p">[</span><span class="nx">j</span><span class="p">].</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Date</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">return</span> <span class="nx">contents</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><h2 id="asset-bundling-and-cache-busting">Asset bundling and cache busting</h2>
<p>The asset pipeline takes source CSS and JS from <code>/public</code>, bundles them, minifies them, and generates cache-busted filenames. It&rsquo;s configured in <code>config.yaml</code>:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">assets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">css</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">bundles</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">        </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">          </span>- <span class="l">reset.css</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">          </span>- <span class="l">style.css</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">js</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">bundles</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">          </span>- <span class="l">main.js</span><span class="w">
</span></span></span></code></pre><p>The bundler concatenates files in order, minifies the result, then hashes it:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">b</span> <span class="o">*</span><span class="nx">Bundler</span><span class="p">)</span> <span class="nf">bundleCSS</span><span class="p">(</span><span class="nx">bundle</span> <span class="nx">Bundle</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="kd">var</span> <span class="nx">combined</span> <span class="nx">strings</span><span class="p">.</span><span class="nx">Builder</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">file</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">bundle</span><span class="p">.</span><span class="nx">Files</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nx">data</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">ReadFile</span><span class="p">(</span><span class="nx">filepath</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">publicDir</span><span class="p">,</span> <span class="s">&#34;css&#34;</span><span class="p">,</span> <span class="nx">file</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;failed to read %s: %w&#34;</span><span class="p">,</span> <span class="nx">file</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">combined</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">combined</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="s">&#34;\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">minified</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">b</span><span class="p">.</span><span class="nx">minifier</span><span class="p">.</span><span class="nf">Bytes</span><span class="p">(</span><span class="s">&#34;text/css&#34;</span><span class="p">,</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">combined</span><span class="p">.</span><span class="nf">String</span><span class="p">()))</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nx">slog</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span><span class="s">&#34;failed to minify CSS, using original&#34;</span><span class="p">,</span> <span class="s">&#34;bundle&#34;</span><span class="p">,</span> <span class="nx">bundle</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="s">&#34;error&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="nx">minified</span> <span class="p">=</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">combined</span><span class="p">.</span><span class="nf">String</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="nx">hash</span> <span class="o">:=</span> <span class="nx">b</span><span class="p">.</span><span class="nf">hash</span><span class="p">(</span><span class="nx">minified</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nx">filename</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%s.%s.css&#34;</span><span class="p">,</span> <span class="nx">bundle</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="nx">hash</span><span class="p">[:</span><span class="mi">8</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="nx">os</span><span class="p">.</span><span class="nf">WriteFile</span><span class="p">(</span><span class="nx">filepath</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">outputDir</span><span class="p">,</span> <span class="s">&#34;css&#34;</span><span class="p">,</span> <span class="nx">filename</span><span class="p">),</span> <span class="nx">minified</span><span class="p">,</span> <span class="mi">0</span><span class="nx">o644</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="nx">b</span><span class="p">.</span><span class="nx">manifest</span><span class="p">[</span><span class="nx">bundle</span><span class="p">.</span><span class="nx">Name</span><span class="o">+</span><span class="s">&#34;.css&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="s">&#34;/css/&#34;</span> <span class="o">+</span> <span class="nx">filename</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>So <code>app.css</code> becomes <code>/css/app.a1b2c3d4.css</code>. The manifest maps <code>&quot;app.css&quot;</code> to <code>&quot;/css/app.a1b2c3d4.css&quot;</code>, and that manifest gets passed to the template renderer.</p>
<p>The nice thing about hashing the <em>output</em> rather than the input is that the filename only changes when the actual delivered bytes change. Reformat your source CSS, add comments, reorganize - if the minified output is identical, the hash stays the same and browsers keep their cached version.</p>
<h2 id="templates-and-the-asset-function">Templates and the asset function</h2>
<p>Templates are split into three directories: <code>common/</code> (shared layout, header, footer), <code>views/</code> (content type templates like blog, page), and <code>pages/</code> (static pages like index, 404).</p>
<p>The renderer concatenates common templates with each view template, so every view inherits the shared layout:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">Renderer</span><span class="p">)</span> <span class="nf">registerTemplates</span><span class="p">(</span><span class="nx">dir</span><span class="p">,</span> <span class="nx">prefix</span><span class="p">,</span> <span class="nx">common</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">return</span> <span class="nx">filepath</span><span class="p">.</span><span class="nf">WalkDir</span><span class="p">(</span><span class="nx">dir</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">path</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">d</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">DirEntry</span><span class="p">,</span> <span class="nx">walkErr</span> <span class="kt">error</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">data</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">ReadFile</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="kd">var</span> <span class="nx">templateContent</span> <span class="nx">strings</span><span class="p">.</span><span class="nx">Builder</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">templateContent</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">common</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">templateContent</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">r</span><span class="p">.</span><span class="nx">templates</span><span class="p">[</span><span class="nx">renderName</span><span class="p">]</span> <span class="p">=</span> <span class="nx">template</span><span class="p">.</span><span class="nf">Must</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nx">template</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">renderName</span><span class="p">).</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">                <span class="nf">Funcs</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">templateFuncs</span><span class="p">()).</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">                <span class="nf">Parse</span><span class="p">(</span><span class="nx">templateContent</span><span class="p">.</span><span class="nf">String</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>The <code>asset</code> template function is where the manifest pays off:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="s">&#34;asset&#34;</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">name</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="nx">url</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">manifest</span><span class="p">[</span><span class="nx">name</span><span class="p">];</span> <span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">return</span> <span class="nx">url</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="s">&#34;/&#34;</span> <span class="o">+</span> <span class="nx">name</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">},</span>
</span></span></code></pre><p>In a template, <code>{{ asset &quot;app.css&quot; }}</code> resolves to <code>/css/app.a1b2c3d4.css</code>. Simple lookup, no magic.</p>
<p>This pairs with the cache config:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">cache</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">html</span><span class="p">:</span><span class="w"> </span><span class="l">max-age=0, must-revalidate</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">assets</span><span class="p">:</span><span class="w"> </span><span class="l">public, max-age=31536000, immutable</span><span class="w">
</span></span></span></code></pre><p>HTML is never cached. Assets are cached forever and marked immutable. When the CSS changes, the hash changes, the URL changes, and browsers fetch the new version. Old cached versions just expire naturally.</p>
<h2 id="markdown-rendering">Markdown rendering</h2>
<p>For converting markdown to HTML, I use <a href="https://github.com/yuin/goldmark">goldmark</a>.</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">md</span> <span class="o">:=</span> <span class="nx">goldmark</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">goldmark</span><span class="p">.</span><span class="nf">WithParserOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">parser</span><span class="p">.</span><span class="nf">WithAutoHeadingID</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">parser</span><span class="p">.</span><span class="nf">WithASTTransformers</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="nx">util</span><span class="p">.</span><span class="nf">Prioritized</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">imagePathTransformer</span><span class="p">{},</span> <span class="mi">100</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">goldmark</span><span class="p">.</span><span class="nf">WithExtensions</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">extension</span><span class="p">.</span><span class="nx">GFM</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">extension</span><span class="p">.</span><span class="nx">Footnote</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">extension</span><span class="p">.</span><span class="nx">Typographer</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">highlighting</span><span class="p">.</span><span class="nf">NewHighlighting</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="nx">highlighting</span><span class="p">.</span><span class="nf">WithStyle</span><span class="p">(</span><span class="s">&#34;catppuccin-mocha&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="nx">highlighting</span><span class="p">.</span><span class="nf">WithFormatOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">                <span class="nx">chromahtml</span><span class="p">.</span><span class="nf">WithLineNumbers</span><span class="p">(</span><span class="kc">true</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">                <span class="nx">chromahtml</span><span class="p">.</span><span class="nf">WithClasses</span><span class="p">(</span><span class="kc">true</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">)</span>
</span></span></code></pre><p>A few choices here:</p>
<p><strong>Syntax highlighting</strong> uses Chroma with the Catppuccin Mocha theme, rendered as CSS classes rather than inline styles. This means the color scheme lives in a stylesheet instead of being baked into every code block&rsquo;s HTML. Change the theme in one place, every code block updates.</p>
<p><strong>The image path transformer</strong> is a custom AST transformer that rewrites relative image paths to absolute ones:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">imagePathTransformer</span><span class="p">)</span> <span class="nf">Transform</span><span class="p">(</span><span class="nx">node</span> <span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Document</span><span class="p">,</span> <span class="nx">reader</span> <span class="nx">text</span><span class="p">.</span><span class="nx">Reader</span><span class="p">,</span> <span class="nx">pc</span> <span class="nx">parser</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">_</span> <span class="p">=</span> <span class="nx">ast</span><span class="p">.</span><span class="nf">Walk</span><span class="p">(</span><span class="nx">node</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">n</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">(</span><span class="nx">ast</span><span class="p">.</span><span class="nx">WalkStatus</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">entering</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">WalkContinue</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">img</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">n</span><span class="p">.(</span><span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Image</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">WalkContinue</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">dest</span> <span class="o">:=</span> <span class="nb">string</span><span class="p">(</span><span class="nx">img</span><span class="p">.</span><span class="nx">Destination</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="c1">// Skip already-absolute URLs
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span>        <span class="k">if</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">dest</span><span class="p">,</span> <span class="s">&#34;http&#34;</span><span class="p">)</span> <span class="o">||</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">dest</span><span class="p">,</span> <span class="s">&#34;/&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">WalkContinue</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="nx">dest</span> <span class="p">=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">TrimPrefix</span><span class="p">(</span><span class="nx">dest</span><span class="p">,</span> <span class="s">&#34;./&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="nx">img</span><span class="p">.</span><span class="nx">Destination</span> <span class="p">=</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;/&#34;</span> <span class="o">+</span> <span class="nx">dest</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">WalkContinue</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>This means I can write <code>![alt](images/photo.png)</code> in markdown and it becomes <code>/images/photo.png</code> in the HTML. Content images live next to the markdown in <code>/content/images/</code> and get copied to <code>/static/images/</code> during the build.</p>
<p>A nice side effect is that this plays well with Obsidian as a markdown editor. I can drag and drop images directly into a post and they just work - Obsidian writes the relative path, the transformer rewrites it at build time. No manual path fiddling.</p>
<p>Goldmark&rsquo;s AST transformer API makes this surprisingly clean - you walk the tree, find image nodes, rewrite their destinations.</p>
<h2 id="building-a-page">Building a page</h2>
<p>With all the pieces in place, building a single page is straightforward:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">b</span> <span class="o">*</span><span class="nx">Builder</span><span class="p">)</span> <span class="nf">buildPage</span><span class="p">(</span><span class="nx">content</span> <span class="nx">internalparser</span><span class="p">.</span><span class="nx">Content</span><span class="p">,</span> <span class="nx">contents</span> <span class="p">[]</span><span class="nx">internalparser</span><span class="p">.</span><span class="nx">Content</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="c1">// Convert markdown to HTML
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>    <span class="kd">var</span> <span class="nx">buf</span> <span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">b</span><span class="p">.</span><span class="nx">markdown</span><span class="p">.</span><span class="nf">Convert</span><span class="p">(</span><span class="nx">content</span><span class="p">.</span><span class="nx">Body</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">buf</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="c1">// Render through the template
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>    <span class="nx">html</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">b</span><span class="p">.</span><span class="nx">renderer</span><span class="p">.</span><span class="nf">Render</span><span class="p">(</span><span class="nx">content</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">View</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">any</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="s">&#34;title&#34;</span><span class="p">:</span>       <span class="nx">content</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Title</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="s">&#34;description&#34;</span><span class="p">:</span> <span class="nx">content</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Description</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="s">&#34;body&#34;</span><span class="p">:</span>        <span class="nx">buf</span><span class="p">.</span><span class="nf">String</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="s">&#34;date&#34;</span><span class="p">:</span>        <span class="nx">content</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Date</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="s">&#34;site&#34;</span><span class="p">:</span>        <span class="nx">b</span><span class="p">.</span><span class="nf">fullSiteContext</span><span class="p">(</span><span class="nx">contents</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1">// Minify the final HTML
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span>    <span class="nx">minified</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">b</span><span class="p">.</span><span class="nx">minifier</span><span class="p">.</span><span class="nf">Bytes</span><span class="p">(</span><span class="s">&#34;text/html&#34;</span><span class="p">,</span> <span class="nx">html</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="c1">// Write to disk
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span>    <span class="nx">outFile</span> <span class="o">:=</span> <span class="nx">filepath</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">outputDir</span><span class="p">,</span> <span class="nx">content</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Slug</span><span class="o">+</span><span class="s">&#34;.html&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">return</span> <span class="nx">os</span><span class="p">.</span><span class="nf">WriteFile</span><span class="p">(</span><span class="nx">outFile</span><span class="p">,</span> <span class="nx">minified</span><span class="p">,</span> <span class="mi">0</span><span class="nx">o644</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>Markdown in, HTML out, minified, written to disk. The <code>view</code> field in the frontmatter determines which template renders it - <code>blog</code> uses <code>templates/views/blog.html</code>, <code>page</code> uses <code>templates/views/page.html</code>. Each template inherits the common layout, so there&rsquo;s no duplication.</p>
<h2 id="deployment-targets">Deployment targets</h2>
<p>The builder has a concept of deployment targets. Right now there are two: <code>local</code> and <code>cloudflare</code>. The local target just generates HTML files. The Cloudflare target also generates <code>_headers</code> and <code>_redirects</code> files that Cloudflare Pages reads at deploy time:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">b</span> <span class="o">*</span><span class="nx">Builder</span><span class="p">)</span> <span class="nf">generateTargetArtifacts</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">switch</span> <span class="nx">b</span><span class="p">.</span><span class="nx">target</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">case</span> <span class="nx">TargetCloudflare</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">b</span><span class="p">.</span><span class="nf">writeHeaders</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;failed to write headers file: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">b</span><span class="p">.</span><span class="nf">writeRedirects</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;failed to write redirects file: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>The headers file maps path patterns to cache-control values from the config. The redirects file handles two things: custom redirects from <code>config.yaml</code> (like old blog URLs that moved) and canonical redirects so <code>/blog/some-post/index.html</code> redirects to <code>/blog/some-post</code>.</p>
<p>Adding a new deployment target - say, Netlify or S3 - would just be another case in the switch.</p>
<h2 id="og-images">OG images</h2>
<p>Every post needs a social sharing image for Twitter/LinkedIn previews. I didn&rsquo;t want to browse Unsplash for a vaguely related stock photo every time I published something, so I just didn&rsquo;t.</p>
<p>Instead, the build generates them. If a post doesn&rsquo;t have a custom <code>image</code> in its frontmatter, the builder creates one using the title and description:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">b</span> <span class="o">*</span><span class="nx">Builder</span><span class="p">)</span> <span class="nf">generateOGImages</span><span class="p">(</span><span class="nx">contents</span> <span class="p">[]</span><span class="nx">internalparser</span><span class="p">.</span><span class="nx">Content</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">contents</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">if</span> <span class="nx">contents</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Image</span> <span class="o">!=</span> <span class="s">&#34;&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">slug</span> <span class="o">:=</span> <span class="nx">contents</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Slug</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">safeSlug</span> <span class="o">:=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">ReplaceAll</span><span class="p">(</span><span class="nx">slug</span><span class="p">,</span> <span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="s">&#34;-&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">webPath</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">ogimage</span><span class="p">.</span><span class="nf">Generate</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="nx">b</span><span class="p">.</span><span class="nx">outputDir</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nx">safeSlug</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="nx">contents</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Title</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="nx">contents</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Description</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="nx">slog</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span><span class="s">&#34;failed to generate og image&#34;</span><span class="p">,</span> <span class="s">&#34;slug&#34;</span><span class="p">,</span> <span class="nx">slug</span><span class="p">,</span> <span class="s">&#34;error&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nx">contents</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">Metadata</span><span class="p">.</span><span class="nx">Image</span> <span class="p">=</span> <span class="nx">webPath</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>The generator itself uses <a href="https://github.com/fogleman/gg">gg</a> (a 2D graphics library) with embedded JetBrains Mono fonts. It renders at 2x resolution and downscales for sharp text:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">//go:embed fonts/JetBrainsMono-Bold.ttf
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">boldFont</span> <span class="p">[]</span><span class="kt">byte</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">const</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">width</span>   <span class="p">=</span> <span class="mi">1200</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">height</span>  <span class="p">=</span> <span class="mi">630</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">scale</span>   <span class="p">=</span> <span class="mi">2</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">func</span> <span class="nf">Generate</span><span class="p">(</span><span class="nx">outputDir</span><span class="p">,</span> <span class="nx">slug</span><span class="p">,</span> <span class="nx">title</span><span class="p">,</span> <span class="nx">description</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">dc</span> <span class="o">:=</span> <span class="nx">gg</span><span class="p">.</span><span class="nf">NewContext</span><span class="p">(</span><span class="nx">width</span><span class="o">*</span><span class="nx">scale</span><span class="p">,</span> <span class="nx">height</span><span class="o">*</span><span class="nx">scale</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="c1">// Dark background
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>    <span class="nx">dc</span><span class="p">.</span><span class="nf">SetColor</span><span class="p">(</span><span class="nx">color</span><span class="p">.</span><span class="nx">NRGBA</span><span class="p">{</span><span class="nx">R</span><span class="p">:</span> <span class="mh">0x0A</span><span class="p">,</span> <span class="nx">G</span><span class="p">:</span> <span class="mh">0x0A</span><span class="p">,</span> <span class="nx">B</span><span class="p">:</span> <span class="mh">0x0A</span><span class="p">,</span> <span class="nx">A</span><span class="p">:</span> <span class="mh">0xFF</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nx">dc</span><span class="p">.</span><span class="nf">Clear</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="c1">// Theme-colored square in the corner
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>    <span class="nx">dc</span><span class="p">.</span><span class="nf">SetColor</span><span class="p">(</span><span class="nx">color</span><span class="p">.</span><span class="nx">NRGBA</span><span class="p">{</span><span class="nx">R</span><span class="p">:</span> <span class="mh">0x24</span><span class="p">,</span> <span class="nx">G</span><span class="p">:</span> <span class="mh">0x33</span><span class="p">,</span> <span class="nx">B</span><span class="p">:</span> <span class="mh">0xf3</span><span class="p">,</span> <span class="nx">A</span><span class="p">:</span> <span class="mh">0xFF</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="nx">dc</span><span class="p">.</span><span class="nf">DrawRectangle</span><span class="p">(</span><span class="nx">padding</span><span class="o">*</span><span class="nx">s</span><span class="p">,</span> <span class="nx">padding</span><span class="o">*</span><span class="nx">s</span><span class="p">,</span> <span class="nx">boxSize</span><span class="p">,</span> <span class="nx">boxSize</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nx">dc</span><span class="p">.</span><span class="nf">Fill</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="c1">// Draw title, site name, description...
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="c1">// Downscale 2x → 1x for crisp output
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"></span>    <span class="nx">dst</span> <span class="o">:=</span> <span class="nx">image</span><span class="p">.</span><span class="nf">NewRGBA</span><span class="p">(</span><span class="nx">image</span><span class="p">.</span><span class="nf">Rect</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="nx">draw</span><span class="p">.</span><span class="nx">CatmullRom</span><span class="p">.</span><span class="nf">Scale</span><span class="p">(</span><span class="nx">dst</span><span class="p">,</span> <span class="nx">dst</span><span class="p">.</span><span class="nf">Bounds</span><span class="p">(),</span> <span class="nx">dc</span><span class="p">.</span><span class="nf">Image</span><span class="p">(),</span> <span class="nx">dc</span><span class="p">.</span><span class="nf">Image</span><span class="p">().</span><span class="nf">Bounds</span><span class="p">(),</span> <span class="nx">draw</span><span class="p">.</span><span class="nx">Over</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="c1">// Write PNG
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="s">&#34;/images/og/&#34;</span> <span class="o">+</span> <span class="nx">slug</span> <span class="o">+</span> <span class="s">&#34;.png&#34;</span><span class="p">,</span> <span class="nx">png</span><span class="p">.</span><span class="nf">Encode</span><span class="p">(</span><span class="nx">f</span><span class="p">,</span> <span class="nx">dst</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>The 2x render trick is the same idea as Retina displays - draw everything at double resolution, then downscale with a good interpolation algorithm (<code>CatmullRom</code>). The result is noticeably sharper than rendering at 1x, especially for small text like the description.</p>
<p>Here&rsquo;s what it generated for this post:</p>
<p><img src="/images/og/blog-building-a-static-site-generator-in-go.png" alt="OG image for this post"></p>
<p>No Figma, no Canva, no manual step. Write a post, run the build, get an image.</p>
<h2 id="was-it-worth-it">Was it worth it?</h2>
<p>Objectively? No. Hugo would do everything I need and more. But building this taught me things I wouldn&rsquo;t have learned otherwise:</p>
<ul>
<li>How cache busting actually works end-to-end, from hashing bytes to setting headers</li>
<li>How goldmark&rsquo;s AST transformer API lets you rewrite content at the tree level</li>
<li>How little code a static site generator actually needs</li>
</ul>
<p>The whole thing compiles in under a second and builds the site in about 50ms. There are no node_modules, no config files for the config files, no plugins. Just Go, some markdown, and a <code>config.yaml</code>.</p>
<p>Sometimes the best tool is the one you understand completely.</p>
]]></description>
    </item>
    <item>
      <title>AEO was never about answers</title>
      <link>https://kaugesaar.se/blog/aeo-was-never-about-answers</link>
      <guid>https://kaugesaar.se/blog/aeo-was-never-about-answers</guid>
      <pubDate>Tue, 17 Feb 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>For the past couple(?) of years, &ldquo;AEO&rdquo; has been floating around the SEO industry. Answer Engine Optimization - the idea that you should optimize for AI-generated answers, not just search rankings.</p>
<p>It sounds like a paradigm shift until you look at what people actually do. They write FAQ sections. They add structured data. They tweak content to show up in AI Overviews.</p>
<p>In other words, they do SEO and call it AEO. But WebMCP might actually be the first thing that changes that.</p>
<h2 id="webmcp">WebMCP</h2>
<p><a href="https://github.com/webmachinelearning/webmcp">WebMCP</a> is a proposal from Google and Microsoft and that recently <a href="https://developer.chrome.com/blog/webmcp-epp">opened up for early preview</a> in Chrome. It&rsquo;s a JavaScript API that lets websites expose structured tools to AI agents running in the browser. Not content. Not markup hints. Actual callable functions.</p>
<p>Think of it like this: instead of hoping an AI agent can figure out how to navigate your e-commerce site by reading your DOM, you hand it a function called <code>addToCart(productId, quantity)</code> with a schema and a description. The agent calls it, your JavaScript runs, and the UI updates. No guessing, no brittle screen-scraping.</p>
<p>The proposal defines two types of APIs:</p>
<ul>
<li><strong>Declarative</strong>: Standard actions defined directly in HTML forms</li>
<li><strong>Imperative</strong>: More dynamic interactions that require JavaScript</li>
</ul>
<p>Here&rsquo;s a simplified example from the spec:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="s2">&#34;modelContext&#34;</span> <span class="k">in</span> <span class="nb">window</span><span class="p">.</span><span class="nx">navigator</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nb">window</span><span class="p">.</span><span class="nx">navigator</span><span class="p">.</span><span class="nx">modelContext</span><span class="p">.</span><span class="nx">provideContext</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">tools</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;search-products&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                <span class="nx">description</span><span class="o">:</span> <span class="s2">&#34;Search for products matching a query&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                <span class="nx">inputSchema</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                    <span class="nx">type</span><span class="o">:</span> <span class="s2">&#34;object&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                    <span class="nx">properties</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">                        <span class="nx">query</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s2">&#34;string&#34;</span><span class="p">,</span> <span class="nx">description</span><span class="o">:</span> <span class="s2">&#34;Search terms&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">                        <span class="nx">maxPrice</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s2">&#34;number&#34;</span><span class="p">,</span> <span class="nx">description</span><span class="o">:</span> <span class="s2">&#34;Maximum price&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">                    <span class="p">},</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">                    <span class="nx">required</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;query&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">                <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">                <span class="nx">execute</span><span class="p">({</span> <span class="nx">query</span><span class="p">,</span> <span class="nx">maxPrice</span> <span class="p">})</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">                    <span class="c1">// Your existing search logic
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"></span>                    <span class="k">return</span> <span class="p">{</span> <span class="nx">content</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">type</span><span class="o">:</span> <span class="s2">&#34;text&#34;</span><span class="p">,</span> <span class="nx">text</span><span class="o">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">results</span><span class="p">)</span> <span class="p">}]</span> <span class="p">};</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>If you&rsquo;ve worked with MCP before, this will look familiar. That&rsquo;s intentional - WebMCP is designed to align with MCP, but instead of running tools on a backend server, the tools run as client-side JavaScript in the browser. The website <em>is</em> the MCP server.</p>
<h2 id="why-this-feels-different">Why this feels different</h2>
<p>The &ldquo;A&rdquo; in AEO has always stood for &ldquo;Answer&rdquo;. Optimize your content so AI models cite you. But that&rsquo;s still just visibility - you&rsquo;re optimizing to be <em>quoted</em>.</p>
<p>WebMCP isn&rsquo;t about being quoted. It&rsquo;s about being <em>operated</em>. You&rsquo;re not hoping an agent reads your page. You&rsquo;re giving it a <code>searchProducts</code> function and letting it call it.</p>
<p>That&rsquo;s not Answer Engine Optimization. That&rsquo;s Agent Engine Optimization - or whatever you want to call it. The work is fundamentally different. You&rsquo;re thinking about your site as an API, not a document. What tools should you expose? What inputs does an agent need? What should it get back?</p>
<p>For e-commerce, it&rsquo;s product search, cart management, checkout. For SaaS, it might be account actions or configuration. Content sites may not expose a checkout, but they might offer scoped search, filtered retrieval, or citation bundles. Different tools, but still tools.</p>
<p>Either way, writing WebMCP tools is the first time &ldquo;optimizing for AI&rdquo; means doing something other than SEO.</p>
<h2 id="early-days">Early days</h2>
<p>WebMCP is still in early preview. But the direction feels very interesting. We&rsquo;ve been adding machine-readable layers to the web for years - meta tags, structured data, OpenGraph, web manifests. Each one helped machines understand what a page <em>is</em>. WebMCP is different because you&rsquo;re telling agents what your page can <em>do</em>.</p>
<p>And when you&rsquo;re writing those tools - deciding what to expose, how to describe it, what schema to use - that&rsquo;s AEO. It just happened to be so that that the A was never about answers.</p>
]]></description>
    </item>
    <item>
      <title>Running Screaming Frog on GCP with Cloud Run Jobs</title>
      <link>https://kaugesaar.se/blog/screaming-frog-google-cloud-run</link>
      <guid>https://kaugesaar.se/blog/screaming-frog-google-cloud-run</guid>
      <pubDate>Thu, 29 Jan 2026 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>Every guide I&rsquo;ve seen about running Screaming Frog in the cloud has you spin up a VM that sits there running 24/7 - even when you&rsquo;re not crawling. That means paying for idle time around the clock, and I don&rsquo;t want that. <a href="https://docs.cloud.google.com/run/docs/create-jobs">Cloud Run Jobs</a> on GCP flips this: spin up a container, run the crawl, container shuts down - all automatically. You only pay for the minutes the crawl actually runs.</p>
<p>At <a href="http://www.precis.com">Precis</a>, I built an internal service that runs <a href="https://www.screamingfrog.co.uk/seo-spider/">Screaming Frog</a> crawls at scale using Cloud Run Jobs. This post walks through the core setup - a simplified version you can deploy in about 30 minutes.</p>
<p>For this proof-of-concept, we&rsquo;re going to use Cloud Run Jobs, Cloud Storage, and a simple Dockerfile combined with a bash file used as its entrypoint.</p>
<h2 id="why-cloud-run-jobs">Why Cloud Run Jobs</h2>
<p>Assuming 4 CPU / 16GB - a size I&rsquo;ve found works well for a wide variety of crawls (more on that later) - here&rsquo;s how Cloud Run Jobs compares to an always-on VM of the same size:</p>
<table>
<thead>
<tr>
<th>Usage</th>
<th>Cloud Run Jobs</th>
<th>Always-on VM</th>
</tr>
</thead>
<tbody>
<tr>
<td>30 min/week</td>
<td>~$1/mo</td>
<td>~$97/mo</td>
</tr>
<tr>
<td>30 min/day</td>
<td>~$7/mo</td>
<td>~$97/mo</td>
</tr>
<tr>
<td>2 hrs/day</td>
<td>~$29/mo</td>
<td>~$97/mo</td>
</tr>
</tbody>
</table>
<p>You&rsquo;d need over 6 hours of daily crawl time before Cloud Run Jobs even matches the cost of a single VM.</p>
<p>It also scales horizontally. Need to crawl 10 sites at the same time? Just fire off 10 jobs - each gets its own container with dedicated resources. With a VM, you&rsquo;d have to run crawls sequentially - and if all crawls need to be ready by the time you get to work in the morning, that single VM becomes a bottleneck fast. Now you&rsquo;re provisioning multiple VMs, and the cost comparison shifts even further.</p>
<h2 id="what-were-building">What we&rsquo;re building</h2>
<p>The setup is straightforward:</p>
<ol>
<li>Dockerfile that installs Screaming Frog</li>
<li>Entrypoint script that runs the crawl</li>
<li>Cloud Run Job that executes the container</li>
<li>GCS bucket to store exports</li>
</ol>
<h2 id="prerequisites">Prerequisites</h2>
<p>You&rsquo;ll need:</p>
<ul>
<li>Google Cloud Project with billing enabled</li>
<li><code>gcloud</code> CLI installed and configured</li>
<li>Screaming Frog license</li>
<li>Basic Docker knowledge</li>
</ul>
<h2 id="setting-up-gcp-resources">Setting up GCP resources</h2>
<p>These next steps assume you have <a href="https://docs.cloud.google.com/sdk/docs/install-sdk">gcloud sdk</a> installed and that you are somewhat familiar with GCP.</p>
<p>Note that many of the steps below require you to have these two ENVs set.</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">PROJECT_ID</span><span class="o">=</span><span class="s2">&#34;your-gcp-project&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">REGION</span><span class="o">=</span><span class="s2">&#34;your-preferred-region&#34;</span>
</span></span></code></pre><p>Enable the required APIs:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">gcloud services <span class="nb">enable</span> artifactregistry.googleapis.com run.googleapis.com
</span></span></code></pre><p>Create a storage bucket for the CSV exports that Screaming Frog will generate. We&rsquo;ll later mount this bucket to the Cloud Run Job instance.</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">gsutil mb -l <span class="si">${</span><span class="nv">REGION</span><span class="si">}</span> gs://<span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span>-crawl-output
</span></span></code></pre><p>Create a service account for the job and give it permission to read, write and delete files:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">gcloud iam service-accounts create screaming-frog-runner <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>    --display-name<span class="o">=</span><span class="s2">&#34;ScreamingFrog Runner&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">gcloud projects add-iam-policy-binding <span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>    --member<span class="o">=</span><span class="s2">&#34;serviceAccount:screaming-frog-runner@</span><span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span><span class="s2">.iam.gserviceaccount.com&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>    --role<span class="o">=</span><span class="s2">&#34;roles/storage.objectAdmin&#34;</span>
</span></span></code></pre><p>Store your Screaming Frog license and a persistent machine ID in Secret Manager:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Enable Secret Manager API</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud services <span class="nb">enable</span> secretmanager.googleapis.com
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># Create license secret</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;YOUR-LICENSE-KEY&#34;</span> <span class="p">|</span> gcloud secrets create screaming-frog-license <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>    --data-file<span class="o">=</span>-
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># Create a persistent machine ID</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">uuidgen <span class="p">|</span> gcloud secrets create screaming-frog-machine-id <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>    --data-file<span class="o">=</span>-
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># Grant access to service account</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">gcloud secrets add-iam-policy-binding screaming-frog-license <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>    --member<span class="o">=</span><span class="s2">&#34;serviceAccount:screaming-frog-runner@</span><span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span><span class="s2">.iam.gserviceaccount.com&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>    --role<span class="o">=</span><span class="s2">&#34;roles/secretmanager.secretAccessor&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">gcloud secrets add-iam-policy-binding screaming-frog-machine-id <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>    --member<span class="o">=</span><span class="s2">&#34;serviceAccount:screaming-frog-runner@</span><span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span><span class="s2">.iam.gserviceaccount.com&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="se"></span>    --role<span class="o">=</span><span class="s2">&#34;roles/secretmanager.secretAccessor&#34;</span>
</span></span></code></pre><h2 id="building-the-docker-image">Building the Docker image</h2>
<p>Create a <code>Dockerfile</code>:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">FROM</span><span class="s"> ubuntu:22.04</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="err"></span><span class="c"># Install dependencies</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="err"></span><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> apt-get install -y <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>    openjdk-21-jre <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>    xvfb <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>    wget <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>    ca-certificates <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>    <span class="o">&amp;&amp;</span> rm -rf /var/lib/apt/lists/*<span class="err">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="err"></span><span class="c"># Download and install Screaming Frog</span><span class="err">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="err"></span><span class="k">RUN</span> wget -O /tmp/screamingfrog.deb <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>    https://download.screamingfrog.co.uk/products/seo-spider/screamingfrogseospider_23.2_all.deb <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>    apt-get update <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>    apt-get install -y /tmp/screamingfrog.deb <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>    rm /tmp/screamingfrog.deb<span class="err">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="err"></span><span class="c"># Copy entrypoint script</span><span class="err">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="err"></span><span class="k">COPY</span> entrypoint.sh /entrypoint.sh<span class="err">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="err"></span><span class="k">RUN</span> chmod +x /entrypoint.sh<span class="err">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;/entrypoint.sh&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre><p>A few things to note:</p>
<ul>
<li><strong>Ubuntu 22.04</strong>: Screaming Frog distributes as a .deb package, so any Debian-based image should work fine</li>
<li><strong>xvfb</strong>: Screaming Frog requires a display even in CLI mode, xvfb provides a virtual one so it can run fully headless</li>
</ul>
<p>Now create <code>entrypoint.sh</code>:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cp"></span><span class="nb">set</span> -e
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">URL</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">CRAWL_URL</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nv">OUTPUT_BASE</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">OUTPUT_DIR</span><span class="k">:-</span><span class="p">/mnt/crawl-output</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># Create a per-run subfolder: domain/YYYY-MM-DD_HHMMSS</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nv">DOMAIN</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$URL</span><span class="s2">&#34;</span> <span class="p">|</span> sed -E <span class="s1">&#39;s|https?://([^/]+).*|\1|&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nv">TIMESTAMP</span><span class="o">=</span><span class="k">$(</span>date -u +<span class="s2">&#34;%Y-%m-%d_%H%M%S&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">OUTPUT_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">OUTPUT_BASE</span><span class="si">}</span><span class="s2">/</span><span class="si">${</span><span class="nv">DOMAIN</span><span class="si">}</span><span class="s2">/</span><span class="si">${</span><span class="nv">TIMESTAMP</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$OUTPUT_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># Setup license, machine ID, and EULA</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">mkdir -p /root/.ScreamingFrogSEOSpider
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">SF_LICENSE</span><span class="si">}</span><span class="s2">&#34;</span> &gt; /root/.ScreamingFrogSEOSpider/licence.txt
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">SF_MACHINE_ID</span><span class="si">}</span><span class="s2">&#34;</span> &gt; /root/.ScreamingFrogSEOSpider/machine-id.txt
</span></span><span class="line"><span class="ln">17</span><span class="cl">cat &gt; /root/.ScreamingFrogSEOSpider/spider.config <span class="s">&lt;&lt; &#39;EOF&#39;
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s">eula.accepted=15
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># Run the crawl</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">xvfb-run screamingfrogseospider <span class="se">\
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="se"></span>    --headless <span class="se">\
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="se"></span>    --crawl <span class="s2">&#34;</span><span class="nv">$URL</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="se"></span>    --output-folder <span class="s2">&#34;</span><span class="nv">$OUTPUT_DIR</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="se"></span>    --export-tabs <span class="s2">&#34;Internal:All,External:All,Response Codes:All,Page Titles:All,Meta Description:All,H1:All,Images:All&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="se"></span>    --overwrite <span class="se">\
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="se"></span>    --save-crawl
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Crawl completed successfully&#34;</span>
</span></span></code></pre><p>The script does four things:</p>
<ol>
<li><strong>License setup</strong>: Writes the license key we stored in Secret Manager to the path Screaming Frog expects.</li>
<li><strong>Machine ID</strong>: Writes the persistent UUID so each container run identifies as the same machine.</li>
<li><strong>EULA acceptance</strong>: Required for it to run.</li>
<li><strong>Runs the crawl</strong>: <code>xvfb-run</code> provides the virtual display, and we export a few selected tabs - feel free to edit.</li>
</ol>
<p>So now in your directory you should have:</p>
<pre><code>.
├── Dockerfile
└── entrypoint.sh
</code></pre>
<h2 id="deploying-the-cloud-run-job">Deploying the Cloud Run Job</h2>
<p>Deploy the job directly from source (this builds the image using Cloud Build and deploys in one command):</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl">gcloud run <span class="nb">jobs</span> deploy screaming-frog-crawler <span class="se">\
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="se"></span>    --source . <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>    --region<span class="o">=</span><span class="si">${</span><span class="nv">REGION</span><span class="si">}</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>    --service-account<span class="o">=</span>screaming-frog-runner@<span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span>.iam.gserviceaccount.com <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>    --cpu<span class="o">=</span><span class="m">4</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>    --memory<span class="o">=</span>16Gi <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>    --max-retries<span class="o">=</span><span class="m">0</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>    --task-timeout<span class="o">=</span><span class="m">3600</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>    --set-env-vars<span class="o">=</span><span class="nv">OUTPUT_DIR</span><span class="o">=</span>/mnt/crawl-output <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>    --set-secrets<span class="o">=</span><span class="nv">SF_LICENSE</span><span class="o">=</span>screaming-frog-license:latest,SF_MACHINE_ID<span class="o">=</span>screaming-frog-machine-id:latest <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>    --add-volume <span class="nv">name</span><span class="o">=</span>crawl-storage,type<span class="o">=</span>cloud-storage,bucket<span class="o">=</span><span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span>-crawl-output <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>    --add-volume-mount <span class="nv">volume</span><span class="o">=</span>crawl-storage,mount-path<span class="o">=</span>/mnt/crawl-output
</span></span></code></pre><p>This command will:</p>
<ul>
<li>Build your Docker image using Cloud Build</li>
<li>Push it to Artifact Registry automatically</li>
<li>Create (or update) the Cloud Run Job</li>
<li>Mount the bucket we created as a volume</li>
</ul>
<h3 id="resource-sizing">Resource sizing</h3>
<p>The <strong>4 CPU, 16GB RAM</strong> I found to be a good starting point for most crawls. Scale up to 8 CPU / 32GB for large sites (100K+ URLs).</p>
<p><strong>Important</strong>: Screaming Frog periodically checks available disk space and stops the crawl if it detects 5GB or less remaining. On Cloud Run, available memory serves as disk space - there&rsquo;s no separate disk allocation, even with the GCS mount. So while 2 CPU / 8GB technically works, you&rsquo;ll be cutting it close with Screaming Frog&rsquo;s 5GB threshold.</p>
<h2 id="running-crawls">Running crawls</h2>
<p>Manual execution:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">gcloud run <span class="nb">jobs</span> execute screaming-frog-crawler <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>    --region<span class="o">=</span><span class="si">${</span><span class="nv">REGION</span><span class="si">}</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>    --update-env-vars<span class="o">=</span><span class="nv">CRAWL_URL</span><span class="o">=</span>https://example.com
</span></span></code></pre><p>Check execution status:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">gcloud run <span class="nb">jobs</span> executions list <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>    --job<span class="o">=</span>screaming-frog-crawler <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>    --region<span class="o">=</span><span class="si">${</span><span class="nv">REGION</span><span class="si">}</span>
</span></span></code></pre><p>View logs:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">gcloud logging <span class="nb">read</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>    <span class="s2">&#34;resource.type=cloud_run_job AND resource.labels.job_name=screaming-frog-crawler&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>    --limit<span class="o">=</span><span class="m">50</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>    --format<span class="o">=</span>json
</span></span></code></pre><h2 id="accessing-crawl-results">Accessing crawl results</h2>
<p>You can browse and download files directly from the <a href="https://console.cloud.google.com/storage/browser">GCP Console</a> by navigating to your bucket. Or use the CLI:</p>
<p>First create a folder where you want to store the files.</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">mkdir -p ./crawl-output/
</span></span></code></pre><p>List crawl outputs:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">gsutil ls gs://<span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span>-crawl-output/
</span></span></code></pre><p>Download the latest crawl for a domain:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">LATEST</span><span class="o">=</span><span class="k">$(</span>gsutil ls gs://<span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span>-crawl-output/domain.tld/ <span class="p">|</span> sort <span class="p">|</span> tail -1<span class="k">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gsutil -m cp -r <span class="s2">&#34;</span><span class="si">${</span><span class="nv">LATEST</span><span class="si">}</span><span class="s2">*&#34;</span> ./crawl-output/
</span></span></code></pre><p>Or download all crawls:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl">gsutil -m cp -r gs://<span class="si">${</span><span class="nv">PROJECT_ID</span><span class="si">}</span>-crawl-output/* ./crawl-output/
</span></span></code></pre><p>The output directory includes:</p>
<ul>
<li><code>internal_all.csv</code> - All internal URLs discovered</li>
<li><code>external_all.csv</code> - External links</li>
<li><code>response_codes_all.csv</code> - HTTP status codes</li>
<li><code>page_titles_all.csv</code> - Page titles</li>
<li><code>meta_description_all.csv</code> - Meta descriptions</li>
<li><code>h1_all.csv</code> - H1 tags</li>
<li><code>images_all.csv</code> - Image inventory</li>
<li><code>crawl.seospider</code> - Full crawl file (open in Screaming Frog GUI)</li>
</ul>
<p>If all went well, you should now have something that looks close to this:</p>
<p><img src="/images/screaming-frog-result.png" alt="Screaming Frog output example" title="Example of output"></p>
<h2 id="whats-next">What&rsquo;s next</h2>
<p>This gives you a working setup for running Screaming Frog crawls serverless. From here, you could:</p>
<ul>
<li><strong>Add configuration files</strong>: Use Screaming Frog&rsquo;s config files to standardize crawl settings across executions</li>
<li><strong>Implement progress tracking</strong>: Parse log output to report crawl progress in real-time</li>
<li><strong>Build a web UI</strong>: Create a simple interface for managing crawls and viewing results (hint: this is what we built)</li>
<li><strong>Add notifications</strong>: Send alerts when crawls complete or fail</li>
<li><strong>Track changes</strong>: Compare crawls over time to detect new issues</li>
</ul>
<p>Once deployed, crawls run unattended and you only pay for what you use.</p>
]]></description>
    </item>
    <item>
      <title>Accept interfaces, return structs</title>
      <link>https://kaugesaar.se/blog/accept-interfaces-return-structs</link>
      <guid>https://kaugesaar.se/blog/accept-interfaces-return-structs</guid>
      <pubDate>Sat, 29 Nov 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>There&rsquo;s a Go proverb you might have heard: accept interfaces, return structs. It sounds straightforward, but when I first started with Go, it took me a while before the benefits really clicked.</p>
<h2 id="the-problem-tight-coupling">The problem: tight coupling</h2>
<p>Let&rsquo;s say you&rsquo;re building a user service. You have a database package that handles persistence:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">package</span> <span class="nx">db</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="s">&#34;database/sql&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">type</span> <span class="nx">DB</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">conn</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">func</span> <span class="nf">NewDB</span><span class="p">(</span><span class="nx">conn</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span><span class="p">)</span> <span class="o">*</span><span class="nx">DB</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">DB</span><span class="p">{</span><span class="nx">conn</span><span class="p">:</span> <span class="nx">conn</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">ID</span>       <span class="kt">string</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nx">Username</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">Email</span>    <span class="kt">string</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">db</span> <span class="o">*</span><span class="nx">DB</span><span class="p">)</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="c1">// database implementation
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">db</span> <span class="o">*</span><span class="nx">DB</span><span class="p">)</span> <span class="nf">GetUserByID</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="c1">// database implementation
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre><p>And a user service that uses it:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">package</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="s">&#34;yourapp/db&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">type</span> <span class="nx">Service</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">db</span> <span class="o">*</span><span class="nx">db</span><span class="p">.</span><span class="nx">DB</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">func</span> <span class="nf">NewService</span><span class="p">(</span><span class="nx">database</span> <span class="o">*</span><span class="nx">db</span><span class="p">.</span><span class="nx">DB</span><span class="p">)</span> <span class="o">*</span><span class="nx">Service</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Service</span><span class="p">{</span><span class="nx">db</span><span class="p">:</span> <span class="nx">database</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Service</span><span class="p">)</span> <span class="nf">RegisterUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">username</span><span class="p">,</span> <span class="nx">email</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">db</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">user</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">db</span><span class="p">.</span><span class="nx">User</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nx">ID</span><span class="p">:</span>       <span class="nf">generateID</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="nx">Username</span><span class="p">:</span> <span class="nx">username</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">Email</span><span class="p">:</span>    <span class="nx">email</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">CreateUser</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="k">return</span> <span class="nx">user</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>This looks reasonable at first, but there are problems:</p>
<ol>
<li><strong>Misplaced domain model</strong>: <code>User</code> is in the <code>db</code> package, but it&rsquo;s a domain concept, not a database concern. It feels wrong there.</li>
<li><strong>Can&rsquo;t test without a database</strong>: To test <code>user.Service</code>, you need a real <code>*db.DB</code>. No mocks, no in-memory stores.</li>
<li><strong>Tight coupling</strong>: The service is tied to the exact database implementation.</li>
</ol>
<p>The typical workaround is moving <code>User</code> to a shared <code>models</code> package. But now you have a grab-bag package that everyone imports, and you still can&rsquo;t test without the database.</p>
<h2 id="what-does-accept-interfaces-return-structs-actually-mean">What does &ldquo;accept interfaces, return structs&rdquo; actually mean?</h2>
<p>The idea is straightforward:</p>
<ul>
<li>When your function or method needs a dependency, accept an <strong>interface</strong> describing only the behavior you need.</li>
<li>When your function creates something, return a <strong>concrete type</strong>, not an interface.</li>
</ul>
<p>Here&rsquo;s the same example, redesigned. The user service defines what it needs:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">package</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">ID</span>       <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">Username</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">Email</span>    <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">type</span> <span class="nx">userStore</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nf">GetUserByID</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nf">UpdateUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nf">DeleteUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="kd">type</span> <span class="nx">Service</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nx">userStore</span> <span class="nx">userStore</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="kd">func</span> <span class="nf">NewService</span><span class="p">(</span><span class="nx">userStore</span> <span class="nx">userStore</span><span class="p">)</span> <span class="o">*</span><span class="nx">Service</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Service</span><span class="p">{</span><span class="nx">userStore</span><span class="p">:</span> <span class="nx">userStore</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Service</span><span class="p">)</span> <span class="nf">RegisterUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">username</span><span class="p">,</span> <span class="nx">email</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="nx">user</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">User</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="nx">ID</span><span class="p">:</span>       <span class="nf">generateID</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="nx">Username</span><span class="p">:</span> <span class="nx">username</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="nx">Email</span><span class="p">:</span>    <span class="nx">email</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nx">userStore</span><span class="p">.</span><span class="nf">CreateUser</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="k">return</span> <span class="nx">user</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>Notice what changed:</p>
<ul>
<li>The <code>User</code> type now lives in the <code>user</code> package where it belongs</li>
<li>The <code>Service</code> accepts a <code>userStore</code> interface, not <code>*db.DB</code></li>
<li>The interface defines only what the service needs - four methods</li>
<li><code>NewService</code> still returns a concrete <code>*Service</code>, not an interface</li>
</ul>
<p>No circular dependencies, no shared models package.</p>
<h2 id="why-accept-interfaces">Why accept interfaces?</h2>
<p>When you define an interface at the point of use, you&rsquo;re being explicit about your actual requirements. The <code>user.Service</code> above doesn&rsquo;t need a full database connection. It needs four methods. That&rsquo;s it.</p>
<p>Since your code depends on an interface, you can swap in a mock for tests:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">package</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kd">type</span> <span class="nx">mockUserStore</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">user</span> <span class="o">*</span><span class="nx">User</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">mockUserStore</span><span class="p">)</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">mockUserStore</span><span class="p">)</span> <span class="nf">GetUserByID</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="nx">m</span><span class="p">.</span><span class="nx">user</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">mockUserStore</span><span class="p">)</span> <span class="nf">UpdateUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">mockUserStore</span><span class="p">)</span> <span class="nf">DeleteUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>Now you can test your service logic without touching a database:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">func</span> <span class="nf">TestService_GetUser</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">expectedUser</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">User</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;user-123&#34;</span><span class="p">,</span> <span class="nx">Username</span><span class="p">:</span> <span class="s">&#34;testuser&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">store</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">mockUserStore</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nx">user</span><span class="p">:</span> <span class="nx">expectedUser</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">service</span> <span class="o">:=</span> <span class="nf">NewService</span><span class="p">(</span><span class="nx">store</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">service</span><span class="p">.</span><span class="nf">GetUser</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;user-123&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">assert</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">assert</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">expectedUser</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>No database setup, no cleanup. Just fast, deterministic unit tests.</p>
<p>The same interface can be satisfied by completely different implementations:</p>
<ul>
<li>A <code>PostgresUserStore</code> for production</li>
<li>A <code>MockUserStore</code> for unit tests</li>
<li>An <code>InMemoryUserStore</code> for integration tests</li>
<li>A <code>CachedUserStore</code> that wraps another store</li>
</ul>
<p>Your service doesn&rsquo;t know or care which one it gets.</p>
<h2 id="why-return-structs">Why return structs?</h2>
<p>When you return a concrete type, the caller knows exactly what they&rsquo;re getting. They can see all the methods, all the fields (if exported), and the compiler can help them use it correctly.</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">package</span> <span class="nx">db</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">func</span> <span class="nf">NewUserStore</span><span class="p">(</span><span class="nx">pool</span> <span class="o">*</span><span class="nx">pgxpool</span><span class="p">.</span><span class="nx">Pool</span><span class="p">)</span> <span class="o">*</span><span class="nx">UserStore</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">UserStore</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nx">pool</span><span class="p">:</span> <span class="nx">pool</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>Anyone calling <code>db.NewUserStore</code> knows they&rsquo;re getting a <code>*db.UserStore</code>. No guessing, no type assertions needed.</p>
<p>In Go, interfaces are satisfied implicitly - there&rsquo;s no <code>implements</code> keyword. This means the <em>consumer</em> of a package should define what interface they need, not the producer.</p>
<p>Your <code>db.UserStore</code> doesn&rsquo;t need to know it implements <code>user.userStore</code>. It just has methods. The <code>user.Service</code> defines the interface it requires, and Go&rsquo;s type system figures out the rest.</p>
<p>If you return an interface from your constructor, you&rsquo;re forcing all callers to use that interface. But different callers might need different subsets of functionality. By returning a struct, each consumer can define their own interface with just the methods they actually use.</p>
<h2 id="putting-it-together">Putting it together</h2>
<p>Here&rsquo;s how this looks in practice. You have a database package with a concrete store:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">package</span> <span class="nx">db</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="s">&#34;yourapp/user&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">type</span> <span class="nx">UserStore</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">pool</span> <span class="o">*</span><span class="nx">pgxpool</span><span class="p">.</span><span class="nx">Pool</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">func</span> <span class="nf">NewUserStore</span><span class="p">(</span><span class="nx">pool</span> <span class="o">*</span><span class="nx">pgxpool</span><span class="p">.</span><span class="nx">Pool</span><span class="p">)</span> <span class="o">*</span><span class="nx">UserStore</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">UserStore</span><span class="p">{</span><span class="nx">pool</span><span class="p">:</span> <span class="nx">pool</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">UserStore</span><span class="p">)</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">u</span> <span class="o">*</span><span class="nx">user</span><span class="p">.</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="c1">// actual database implementation
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">UserStore</span><span class="p">)</span> <span class="nf">GetUserByID</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">user</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="c1">// actual database implementation
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">// ... more methods
</span></span></span></code></pre><p>And a service package that defines what it needs:</p>
<pre class="chroma"><code><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">package</span> <span class="nx">user</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kd">type</span> <span class="nx">userStore</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nf">GetUserByID</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nf">UpdateUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nf">DeleteUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">type</span> <span class="nx">Service</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">userStore</span> <span class="nx">userStore</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kd">func</span> <span class="nf">NewService</span><span class="p">(</span><span class="nx">userStore</span> <span class="nx">userStore</span><span class="p">)</span> <span class="o">*</span><span class="nx">Service</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Service</span><span class="p">{</span><span class="nx">userStore</span><span class="p">:</span> <span class="nx">userStore</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>In your main package, you wire them together:</p>
<pre class="chroma"><code><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">pool</span> <span class="o">:=</span> <span class="nf">connectToDatabase</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">userStore</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">NewUserStore</span><span class="p">(</span><span class="nx">pool</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">userService</span> <span class="o">:=</span> <span class="nx">user</span><span class="p">.</span><span class="nf">NewService</span><span class="p">(</span><span class="nx">userStore</span><span class="p">)</span>
</span></span></code></pre><p>The <code>db.UserStore</code> satisfies the <code>user.userStore</code>. The service is testable, the store is reusable, and the dependency flows in one direction.</p>
<h2 id="to-summarize">To summarize</h2>
<p>This pattern gives you three things that are hard to get any other way:</p>
<ol>
<li><strong>Your domain logic lives where it belongs</strong> - <code>User</code> is in the <code>user</code> package, not scattered across <code>db</code> or <code>models</code></li>
<li><strong>Testing becomes trivial</strong> - swap implementations without mocking frameworks or test databases</li>
<li><strong>Dependencies flow one direction</strong> - high-level packages define interfaces, low-level packages implement them</li>
</ol>
<p>It&rsquo;s not about being dogmatic. Sometimes you need to return an interface - particularly when you&rsquo;re building a library and need to hide implementation details. But as a default? Accept interfaces, return structs. Your future self will thank you.</p>
]]></description>
    </item>
    <item>
      <title>The best part</title>
      <link>https://kaugesaar.se/blog/the-best-part</link>
      <guid>https://kaugesaar.se/blog/the-best-part</guid>
      <pubDate>Wed, 05 Nov 2025 00:00:00 +0000</pubDate>
      <description><![CDATA[<p>People sometimes ask me how I learned to program - or how they can learn too. I never have a great answer. I think they’re hoping I’ll say it was a two-week bootcamp, and not a two-decade hobby.</p>
<p>Because, the same way some people pick up a hammer or a pencil, I open a text editor. It’s how I build things, fix what’s broken, or follow a half-formed idea just to see if it works.</p>
<p>However, if we rewind the tape a bit, where did it all start?</p>
<h2 id="the-microsoft-frontpage-era">The Microsoft FrontPage era</h2>
<p>We&rsquo;d have to travel back to somewhere between 2000 and 2002. One could argue that HTML and CSS aren&rsquo;t <em>real</em> programming - and I&rsquo;d probably agree. But to ten-year-old-me, sitting in front of a big beige CRT monitor in the early 2000s, HTML <strong>was</strong> programming.</p>
<p><img src="/images/ms-frontpage.png" alt="Microsoft FrontPage in early 2000s" title="Feeling old now?"></p>
<p>Back then, the internet - at least the part I was building - was mostly held together by <strong>tables</strong>. Not the database kind - the layout kind: <code>&lt;table&gt;</code>.</p>
<p>Technically <code>&lt;div&gt;</code> and <code>&lt;span&gt;</code> did exist, but they didn&rsquo;t do anything. I&rsquo;d drop one into FrontPage - nothing. No borders, no padding - just emptiness. Tables, on the other hand, actually made something appear on the screen.</p>
<p>What I ended up with was endless rows and columns, nested so deeply you&rsquo;d think a recursive loop had written them. Somehow, though, it worked. I&rsquo;d drag things around in FrontPage until everything looked sort of right.</p>
<p>Then came deployment:</p>
<ol>
<li>Save my files.</li>
<li>Fire up an FTP client<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</li>
<li>Upload my changes.</li>
<li>Send the link to my friends on MSN Messenger.</li>
</ol>
<p>No build step, no deploy pipeline, no twelve layers of abstraction. Just HTML, an FTP client, and the thrill of seeing it work.</p>
<h2 id="php-my-first-real-programming-language">PHP: my first &ldquo;real&rdquo; programming language</h2>
<p>A little after figuring out basic HTML, I stumbled onto <strong>PHP</strong>. Suddenly, I could create one file for my header and footer and include them on every page instead of copying and pasting. <em>Mind blown.</em> It also meant I could start building things like <strong>guestbooks</strong> and <strong>visitor counters</strong>.</p>
<p>My first database? Ever heard of <code>.txt</code>?</p>
<ul>
<li>Atomic? No.</li>
<li>Did it work? Yes.</li>
</ul>
<p>I stuck with PHP for many years whenever I built something for the web. I was there in the early days of WordPress, drifted away for a while, then came back around the time Laravel appeared - and used it as my main framework for a couple of years.</p>
<h2 id="sockets-servers-and-java">Sockets, Servers and Java</h2>
<p>Around the same time, I started writing IRC bots - the kind that hung out in a channel, asked quiz questions, or replied with jokes. I remember using a framework called <a href="https://www.jibble.org/pircbot.php">PircBot</a>.</p>
<p>Somewhere during my IRC years, I also started running servers. I hosted Ventrilo and Counter-Strike servers for friends and eventually had a bot that could spin one up on command.</p>
<p>Later, it even accepted text messages - so friends (and friends of friends) could pay a few kronor via SMS to rent a CS server for a couple of hours.</p>
<p>Looking back, I guess that was my first taste of automation, even some entrepreneurship, though I didn’t think of it as such back then. To me, it was just another problem to solve.</p>
<h2 id="still-the-same-feeling">Still the same feeling</h2>
<p>Because I wasn’t trying to build a career or make a living out of it. I was just trying to make things work, to automate some small problem, or just to see if I actually could build it.</p>
<p>That same curiosity started bleeding into everything else.</p>
<ul>
<li>I built small websites that needed visitors, so I learned about SEO.</li>
<li>I wanted to measure how people were using my websites, so I set up <a href="https://en.wikipedia.org/wiki/Urchin_(software)">urchin.js</a><sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</li>
</ul>
<p>So while programming was my way into tech, it also quietly opened the door to the world I’ve now worked in for the past fifteen years.</p>
<p>Over time, the projects changed, but the feeling has always stayed the same.</p>
<h2 id="so-of-course-i-built-my-own-blog-engine">So of course I built my own blog engine</h2>
<p>I could’ve used Astro, Hugo, or heck, even WordPress. But I like building things for the sake of building things.</p>
<p>So this site runs on a Go-based setup I wrote for myself. It takes Markdown content, turns it into HTML, bundles and minifies CSS and JS, fingerprints assets for caching, generates RSS feeds and sitemaps, and even spits out Cloudflare Pages-compatible <code>_headers</code> and <code>_redirects</code> files.</p>
<p>It’s not meant to be the simplest solution - it’s meant to be <em>my</em> solution. I want to understand every moving part, from how Markdown turns into a page to how cache headers get set. The end result is something that works exactly the way I want it to.</p>
<p>If it’s overkill for a personal site? Yes. And that’s exactly the point. No frameworks, no plugins - just the joy of wiring the pieces together.</p>
<p>Programming started as a way to tinker, and it ended up shaping most of what I’ve done since. Two decades later, not much has changed. The code looks different, the tools are fancier, but the feeling is the same - that moment when something I built appears on the screen.</p>
<p>That’s still the best part.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>WS_FTP the only correct answer&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Fun fact: UTM - those tracking links marketers still use today - literally stands for Urchin Tracking Module&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
    </item>
  </channel>
</rss>