<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>ttntm.me - Everything</title>
<subtitle>Tom&#39;s homepage. A personal website, journal and playground.</subtitle>
<link href="https://ttntm.me/everything.xml" rel="self" type="application/atom+xml" />
<link href="https://ttntm.me/everything/" rel="alternate" type="text/html"/>
<author>
  <name>ttntm.me</name>
</author>
  
    <updated>2026-08-06T11:30:00Z</updated>
  
  <id>https://ttntm.me/everything</id>
      
      <entry>
        <title>Week 32, 2026</title>
        
        <link href="https://ttntm.me/likes/2026-w32/"/>
        
        <updated>2026-08-06T11:30:00Z</updated>
        <id>https://ttntm.me/likes/2026-w32/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://piechowski.io/post/git-commands-before-reading-code/" class="ext u-like-of" rel="noopener">The Git Commands I Run Before Reading Any Code</a><br>
Good stuff: how to diagnose a new codebase before you open a single file</li>
<li><a href="https://jembendell.com/2024/08/25/the-nine-lies-of-the-fake-green-fairytale/" class="ext u-like-of" rel="noopener">The Nine Lies of the Fake Green Fairytale</a><br>
No joy in this one: about the damage being done to both people and planet</li>
<li><a href="https://smallsheds.garden/blog/2026/on-the-acceptance-of-genai/" class="ext u-like-of" rel="noopener">On the acceptance of GenAI</a><br>
I do not accept</li>
<li><a href="https://andrewmurphy.io/blog/if-you-thought-the-speed-of-writing-code-was-your-problem-you-have-bigger-problems" class="ext u-like-of" rel="noopener">If you thought the speed of writing code was your problem - you have bigger problems</a><br>
The speed of writing code was never the problem</li>
<li><a href="https://www.lkhrs.com/blog/a-theory-on-why-contact-forms-get-more-spam-than-email-addresses/" class="ext u-like-of" rel="noopener">A theory on why contact forms get more spam than email addresses</a><br>
Good one, I always enjoy Luke’s posts</li>
<li><a href="https://mohkohn.co.uk/writing/html-first/" class="ext u-like-of" rel="noopener">How building an HTML-first site doubled our users overnight</a><br>
I’d be lying if I said I was surprised about that…</li>
<li><a href="https://ky.fyi/posts/ai-burnout" class="ext u-like-of" rel="noopener">Do I belong in tech anymore?</a><br>
Not quite there yet myself, but I can relate</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://lastmuseum.com" class="ext u-like-of" rel="noopener">The Last Museum</a><br>
The largest search engine for museum art: provides search for artworks by theme, style, subject, etc.</li>
<li><a href="https://michaelsoftbinbows.co.uk" class="ext u-like-of" rel="noopener">Michaelsoft Binbows</a><br>
It never gets old :D</li>
<li><a href="https://rip.so" class="ext u-like-of" rel="noopener">rip.so</a><br>
A digital graveyard</li>
<li><a href="https://sinceyouarrived.world/taken" class="ext u-like-of" rel="noopener">taken.</a><br>
About the cost of “free”</li>
<li><a href="https://www.openculture.com/2025/11/inside-the-making-of-the-alien-suit.html" class="ext u-like-of" rel="noopener">Inside the Making of the Alien Suit</a><br>
About how HR Giger’s dark vision came to life in Ridley Scott’s film</li>
<li><a href="https://www.youtube.com/watch?v=Fd4xg0h8vdA" class="ext u-like-of" rel="noopener">The Best Modern Way to Visualise the Fretboard</a><br>
An absolute eye-opener (for me), that instantly changed how I now learn and practice guitar (no more scale diagrams aka “boxes”, only intervallic functions)</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>A New Guitar</title>
        
          
            <category term="guitar" />
          
            <category term="offline" />
          
        
        <link href="https://ttntm.me/blog/a-new-guitar/"/>
        
        <updated>2026-08-04T14:50:00Z</updated>
        <id>https://ttntm.me/blog/a-new-guitar/</id>
        <content type="html"><![CDATA[
          
            
            <p>I never thought I’d own a Strat, but now I do.</p>
<p>Guess it’s true that the amount of guitars one needs will always be <code>n + 1</code>, haha. Anyway, a lucky find on a second hand platform that I was monitoring for some weeks.</p>
<p>It’s a 2015 model, made by Cor-Tek (aka Cort), offering decent craftsmanship that takes it well into the solid mid-range when paired with a proper setup. I paid 80 Euro for it, and spent about two hours on the setup.</p>
<p>What I did:</p>
<ul>
<li>Cleaned thoroughly</li>
<li>Polished the frets</li>
<li>Oiled the fretboard</li>
<li>New strings</li>
<li>Custom tremolo block (sanded down my own wood block)</li>
<li>Complete setup (neck relief, action, intonation, pickup height)</li>
</ul>
<p>…all according to <a class="ext" href="https://support.fender.com/hc/en-us/articles/42584764005019-How-do-I-set-up-my-Stratocaster-guitar-properly" rel="noopener">the manufacturer’s official setup guide</a>.</p>
<p>I’m very pleased with the outcome. The guitars plays <em>very</em> nicely, sounds great when played over my THR10, and provides the iconic single coil sound (aka <em>Strat twang</em>) that I was looking for recently.</p>
<p>And finally, here’s a photo:</p>
<img src="https://ttntm.me/static/img/blog/guitars/strat.jpg" class="img-fluid img-center" alt="A photo of a Squier Stratocaster electric guitar">

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>What&#39;s next?</title>
        
          
            <category term="news" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/whats-next/"/>
        
        <updated>2026-07-27T14:30:00Z</updated>
        <id>https://ttntm.me/blog/whats-next/</id>
        <content type="html"><![CDATA[
          
            
            <p>Plans for the future of this website.</p>
<p>To keep this brief, it’s probably best served as a list:</p>
<ul>
<li>Probably won’t write any more long and detailed web dev guides and tutorials</li>
<li>Killed the <code>/notes</code> section, and the respective RSS feed</li>
<li>Old <code>/notes</code> content is archived <a href="https://ttntm.me/blog/archived-notes/" class="int">here</a></li>
<li>Tinkering with the site is nice, but unless something big pops up, I’m done with it at the moment</li>
<li><code>/likes</code> continue, and should become more frequent again</li>
<li>Future writing will be less frequent, and focus on my <a href="https://ttntm.me/interests/" class="int">interests</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Archived Notes</title>
        
          
            <category term="learning" />
          
        
        <link href="https://ttntm.me/blog/archived-notes/"/>
        
        <updated>2026-07-26T10:30:00Z</updated>
        <id>https://ttntm.me/blog/archived-notes/</id>
        <content type="html"><![CDATA[
          
            
            <p>An archive of what used to be my /notes page.</p>
<h2 id="node-js-buffer-in-cloudflare-workers" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#node-js-buffer-in-cloudflare-workers"><span>Node.js: Buffer in Cloudflare Workers</span></a></h2>
<p>Using things like <code>Buffer.from()</code> in a Cloudflare Worker will result in an <code>undefined</code> error.</p>
<p>To make it work, the <code>nodejs_compat</code> flag has to be enabled in the worker configuration file (in the <code>compatibility_flags</code> array). When that’s done, <code>Buffer</code> can be used:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> Buffer <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'node:buffer'</span><br><br>globalThis<span class="token punctuation">.</span>Buffer <span class="token operator">=</span> Buffer<br><br><span class="token comment">// rest of the code</span></code></pre>
<h2 id="multiple-ssh-keys-for-the-same-git-provider" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#multiple-ssh-keys-for-the-same-git-provider"><span>Multiple SSH-keys for the same Git provider</span></a></h2>
<p>Use an SSH config file.</p>
<p>In <code>~/.ssh/config</code>:</p>
<pre class="language-bash"><code class="language-bash">Host foo-azure<br>  HostName vs-ssh.visualstudio.com<br>  User <span class="token function">git</span><br>  IdentityFile ~/.ssh/ado_foo<br>  IdentitiesOnly <span class="token function">yes</span></code></pre>
<p>Then, change the default remote URLs from <code>git@ssh.dev.azure.com:v3/customer/project/repo</code> to <code>git@foo-azure:v3/customer/project/repo</code>.</p>
<h2 id="azure-dev-ops-deployment-issues-node-js" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#azure-dev-ops-deployment-issues-node-js"><span>Azure DevOps: deployment issues (Node.js)</span></a></h2>
<p>When your pipeline succeeds, but your function/s simply won’t appear, check the following:</p>
<ul>
<li>Deployment package must be <em>the whole project directory</em>, not just the <code>/dist</code> folder</li>
<li>Manually syncing triggers might be necessary; include a quick bash script using <code>curl</code> (docs: <a class="ext" href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-deployment-technologies?tabs=linux#trigger-syncing" rel="noopener">Trigger syncing</a>)</li>
</ul>
<h2 id="vs-code-reg-ex-replace-and-append" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vs-code-reg-ex-replace-and-append"><span>VS Code: RegEx replace and append</span></a></h2>
<p>Use case: append time to date values</p>
<p>Search: <code>^updated: [0-9]{4}-[0-9]{2}-[0-9]{2}$</code></p>
<p>Replace: <code>$0T10:30:00Z</code></p>
<p><code>$0</code> uses the string matched by the expression in the “Search” field and appends whatever value follows.</p>
<p>NB: the option “Use Regular Expression” must be enabled for the “Search” field.</p>
<h2 id="css-targeting-external-links" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#css-targeting-external-links"><span>CSS: targeting external links</span></a></h2>
<p>Use case: displaying an icon with external links.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">a[href^="https://"]</span> <span class="token punctuation">{</span><br>  <span class="token selector">&amp;::after</span> <span class="token punctuation">{</span><br>    <span class="token comment">/* rule */</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<h2 id="java-script-element-visibility" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#java-script-element-visibility"><span>JavaScript: element visibility</span></a></h2>
<p>Use case: run specific code for visible elements only, do not waste resources on event handlers attached to invisible elements.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">isVisible</span><span class="token punctuation">(</span><span class="token parameter">el</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> window<span class="token punctuation">.</span><span class="token function">getComputedStyle</span><span class="token punctuation">(</span>el<span class="token punctuation">)</span><span class="token operator">?.</span><span class="token function">getPropertyValue</span><span class="token punctuation">(</span><span class="token string">'display'</span><span class="token punctuation">)</span> <span class="token operator">!==</span> <span class="token string">'none'</span><br><span class="token punctuation">}</span><br><br>window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'scroll'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> targetEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'xmpl'</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">isVisible</span><span class="token punctuation">(</span>targetEl<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// element not visible - abort</span><br>    <span class="token keyword">return</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token comment">// actual event handler logic</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>NB: using a function (<code>isVisible()</code>) and calling it from inside the event listener makes sure that visibility changes get noticed immediately. Determining element visibility outside the event listener will make a page refresh necessary to determine changes in visibility (i.e. due to changes in the viewport size when rotating a device).</p>
<p>More info (MDN): <a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle" rel="noopener">Window: getComputedStyle() method</a></p>
<h2 id="11-ty-passing-collections-to-shortcodes" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#11-ty-passing-collections-to-shortcodes"><span>11ty: passing collections to shortcodes</span></a></h2>
<p>Use case: building a custom filter based on arbitrary property of the items in the collection.</p>
<p>The syntax is a bit clunky, but this works:</p>
<pre class="language-html"><code class="language-html">{% customFilter collections.whisky, "region", "status", "wanted" %}</code></pre>
<p>…and can be used in a shortcode to render something:</p>
<pre class="language-js"><code class="language-js"><span class="token function-variable function">customFilter</span><span class="token operator">:</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">collection<span class="token punctuation">,</span> filterKey<span class="token punctuation">,</span> excludeKey <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">,</span> excludeValue <span class="token operator">=</span> <span class="token keyword">null</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> terms <span class="token operator">=</span> collection<span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">map<span class="token punctuation">,</span> currentItem</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token comment">// some reducing of a collection into a map-like object</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">return</span> Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>terms<span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">sort</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">t</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;li></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>t<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/li></span><span class="token template-punctuation string">`</span></span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span></code></pre>
<p>The first code snippet above shows the input collection, the property to use for the filter (<code>region</code>) and another property (<code>status</code>) that’s used to exclude collection items where <code>status == wanted</code>.</p>
<p>This works rather well so far - it was inspired by the comments in <a class="ext" href="https://github.com/11ty/eleventy/issues/813" rel="noopener">eleventy/issues/813</a> and <a class="ext" href="https://github.com/11ty/eleventy/issues/1818" rel="noopener">eleventy/issues/1818</a>.</p>
<h2 id="sql-applying-distinct-to-a-single-column" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#sql-applying-distinct-to-a-single-column"><span>SQL: applying DISTINCT to a single column</span></a></h2>
<p>Use case: customers that bought items from a range of products in one or more orders in the last 90 days.</p>
<p>Example data:</p>
<pre class="language-bash"><code class="language-bash">OrderId <span class="token operator">|</span> Email <span class="token operator">|</span> ProductId<br><span class="token number">123</span> <span class="token operator">|</span> bob@xmpl.com <span class="token operator">|</span> A1<br><span class="token number">123</span> <span class="token operator">|</span> bob@xmpl.com <span class="token operator">|</span> B4<br><span class="token number">456</span> <span class="token operator">|</span> bob@xmpl.com <span class="token operator">|</span> E1</code></pre>
<p>We want the email address <code>bob@xmpl.com</code> to appear only once in our query results. All other data is irrelevant, because we already know that every row in the source table qualifies for the above criteria based on a previous query.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> <span class="token operator">*</span><br><span class="token keyword">FROM</span> <span class="token punctuation">(</span><br>  <span class="token keyword">SELECT</span><br>    Email<br>    <span class="token punctuation">,</span> ROW_NUMBER<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">OVER</span><span class="token punctuation">(</span><span class="token keyword">PARTITION</span> <span class="token keyword">BY</span> Email <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> OrderId <span class="token keyword">DESC</span><span class="token punctuation">)</span> rn<br>  <span class="token keyword">FROM</span> xmpl<br><span class="token punctuation">)</span> <span class="token keyword">AS</span> tmp<br><span class="token keyword">WHERE</span> tmp<span class="token punctuation">.</span>rn <span class="token operator">=</span> <span class="token number">1</span></code></pre>
<h2 id="sql-join-2-tables-that-have-nothing-in-common" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#sql-join-2-tables-that-have-nothing-in-common"><span>SQL: join 2 tables that have nothing in common</span></a></h2>
<p>Use case example: table A contains customers, table B contains coupon codes that haven’t been assigned to customers yet.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span><br>  CU<span class="token punctuation">.</span>Id<br>  <span class="token punctuation">,</span> CU<span class="token punctuation">.</span>EmailAddress<br>  <span class="token punctuation">,</span> CC<span class="token punctuation">.</span>Code <span class="token keyword">AS</span> CouponCode<br><span class="token keyword">FROM</span> <span class="token punctuation">(</span><br>  <span class="token keyword">SELECT</span><br>    Id<br>    <span class="token punctuation">,</span> EmailAddress<br>    <span class="token punctuation">,</span> ROW_NUMBER<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">OVER</span> <span class="token punctuation">(</span><span class="token keyword">ORDER</span> <span class="token keyword">BY</span> Id<span class="token punctuation">)</span> <span class="token keyword">AS</span> Rn<br>  <span class="token keyword">FROM</span> Customers<br><span class="token punctuation">)</span> <span class="token keyword">AS</span> CU<br><span class="token keyword">INNER</span> <span class="token keyword">JOIN</span><br><span class="token punctuation">(</span><br>  <span class="token keyword">SELECT</span><br>    Code<br>    <span class="token punctuation">,</span> ROW_NUMBER<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">OVER</span> <span class="token punctuation">(</span><span class="token keyword">ORDER</span> <span class="token keyword">BY</span> Code<span class="token punctuation">)</span> <span class="token keyword">AS</span> Rn<br>  <span class="token keyword">FROM</span> CouponCode<br>  <span class="token keyword">WHERE</span> CustomerId <span class="token operator">IS</span> <span class="token boolean">NULL</span><br>    <span class="token operator">OR</span> CustomerId <span class="token operator">=</span> <span class="token string">''</span><br><span class="token punctuation">)</span> <span class="token keyword">AS</span> CC<br><span class="token keyword">ON</span> CU<span class="token punctuation">.</span>Rn <span class="token operator">=</span> CC<span class="token punctuation">.</span>Rn</code></pre>
<h2 id="java-script-unique-i-ds" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#java-script-unique-i-ds"><span>JavaScript: unique IDs</span></a></h2>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> uniqueId <span class="token operator">=</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token number">36</span><span class="token punctuation">)</span> <span class="token operator">+</span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token number">36</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">substring</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>For RFC compatible version 4 GUIDs:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> u <span class="token operator">=</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span> <span class="token operator">+</span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">'0'</span><span class="token punctuation">.</span><span class="token function">repeat</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">let</span> guid <span class="token operator">=</span> <span class="token punctuation">[</span>u<span class="token punctuation">.</span><span class="token function">substr</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span><span class="token number">8</span><span class="token punctuation">)</span><span class="token punctuation">,</span> u<span class="token punctuation">.</span><span class="token function">substr</span><span class="token punctuation">(</span><span class="token number">8</span><span class="token punctuation">,</span><span class="token number">4</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">'4000-8'</span> <span class="token operator">+</span> u<span class="token punctuation">.</span><span class="token function">substr</span><span class="token punctuation">(</span><span class="token number">13</span><span class="token punctuation">,</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">,</span> u<span class="token punctuation">.</span><span class="token function">substr</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">,</span><span class="token number">12</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">'-'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>If IDs are generated more than 1 millisecond apart, they are 100% unique.</p>
<p><em>NB: alread mentioned in <a href="https://ttntm.me/likes/2021-w21" class="int">/likes/2021-w21</a>, but I keep coming back to it.</em></p>
<h2 id="recursively-download-a-directory-with-wget" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#recursively-download-a-directory-with-wget"><span>Recursively download a directory with wget</span></a></h2>
<pre class="language-bash"><code class="language-bash"><span class="token function">wget</span> --no-verbose --no-parent <span class="token parameter variable">--recursive</span> <span class="token punctuation">\</span><br>--no-host-directories --no-clobber <span class="token parameter variable">--continue</span> <span class="token punctuation">\</span><br><span class="token parameter variable">--reject</span> <span class="token string">"index.html*"</span> <span class="token string">"https://example.com/cool_stuff/"</span></code></pre>
<p>Works well, see <a class="ext" href="https://explainshell.com/explain?cmd=wget+--no-verbose+--no-parent+--recursive+%5C+--no-host-directories+--no-clobber+--continue+%5C+--reject+%22index.html*%22+%22https%3A%2F%2Fexample.com%2Fcool-stuff%2F%22" rel="noopener">explainshell</a> for details about the flags.</p>
<h2 id="batch-replace-a-string-in-many-folders-and-update-git" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#batch-replace-a-string-in-many-folders-and-update-git"><span>Batch-replace a string in many folders and update GIT</span></a></h2>
<p>Needed to replace Skypack with another CDN across many projects.</p>
<p>This script automates the taks and should serve as a starting point for many similar use cases:</p>
<pre class="language-bash"><code class="language-bash"><span class="token shebang important">#!/bin/bash</span><br><br><span class="token keyword">for</span> <span class="token for-or-select variable">D</span> <span class="token keyword">in</span> *<span class="token punctuation">;</span> <span class="token keyword">do</span><br>  <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-d</span> <span class="token string">"<span class="token variable">${D}</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span><br>    <span class="token builtin class-name">cd</span> <span class="token variable">${D}</span><br><br>    <span class="token builtin class-name">echo</span> <span class="token string">"processing <span class="token variable">${D}</span>..."</span><br><br>    <span class="token punctuation">(</span> <span class="token builtin class-name">shopt</span> <span class="token parameter variable">-s</span> globstar dotglob<br>      <span class="token function">sed</span> <span class="token parameter variable">-i</span> -- <span class="token string">'s/cdn[.]skypack[.]dev/esm.sh/g'</span> **/*.js<br>      <span class="token function">sed</span> <span class="token parameter variable">-i</span> -- <span class="token string">'s/cdn[.]skypack[.]dev/esm.sh/g'</span> **/*.html<br>    <span class="token punctuation">)</span><br><br>    <span class="token builtin class-name">echo</span> <span class="token string">"updating git..."</span><br><br>    <span class="token function">git</span> <span class="token function">add</span> <span class="token parameter variable">--all</span><br>    <span class="token function">git</span> commit <span class="token parameter variable">-a</span> <span class="token parameter variable">-m</span> <span class="token string">"replace skypack"</span><br>    <span class="token function">git</span> push<br><br>    <span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable">${D}</span> processed!"</span><br><br>    <span class="token builtin class-name">cd</span> <span class="token punctuation">..</span><br>  <span class="token keyword">fi</span><br><span class="token keyword">done</span></code></pre>
<h2 id="vite-https-on-localhost-basic-ssl" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vite-https-on-localhost-basic-ssl"><span>Vite: HTTPS on localhost (basic SSL)</span></a></h2>
<p>Had to look this up today, here’s how to do it.</p>
<p>First install a Vite plugin:</p>
<p><code>npm install -D @vitejs/plugin-basic-ssl</code></p>
<p>Then use it in <code>vite.config.ts</code>:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">import</span> basicSsl <span class="token keyword">from</span> <span class="token string">'@vitejs/plugin-basic-ssl'</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span><br>  plugins<span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token function">basicSsl</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  <span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
<p><em>NB: works with Astro too, simply create the Vite config file in case it’s not there yet.</em></p>
<h2 id="ssh-no-matching-host-key-type-found-their-offer-ssh-rsa" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#ssh-no-matching-host-key-type-found-their-offer-ssh-rsa"><span>SSH: no matching host key type found. Their offer: ssh-rsa</span></a></h2>
<p>Got this error when trying to use <code>sftp</code> in a Linux terminal.</p>
<p>Resolution:</p>
<p>Edit <code>/etc/ssh/ssh_config</code>, add those lines:</p>
<pre class="language-bash"><code class="language-bash">HostKeyAlgorithms ssh-rsa,ssh-dss<br>PubkeyAcceptedKeyTypes ssh-rsa,ssh-dss</code></pre>
<p><em>NB: might break some other stuff (GitLab in my case), so be careful and remember you added those lines.</em></p>
<h2 id="vue-array-push-used-in-vuex-mutation-not-triggering-watch" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vue-array-push-used-in-vuex-mutation-not-triggering-watch"><span>Vue: Array.push() used in Vuex mutation not triggering watch()</span></a></h2>
<p>I was doing this in Vuex:</p>
<pre class="language-js"><code class="language-js"><span class="token constant">ADD_RECIPE_USER</span><span class="token punctuation">(</span><span class="token parameter">state<span class="token punctuation">,</span> value</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  state<span class="token punctuation">.</span>userRecipes<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>value<span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>It was working fine, but a <code>watch()</code> effect used in a Vue component didn’t catch the update while going back and forth between routes made the updated data appear as if it was always there.</p>
<p>After adding a bunch of cheap <code>console.log()</code> ‘breakpoints’, I eventually got to the bottom of it and changed my code to this:</p>
<pre class="language-js"><code class="language-js"><span class="token constant">ADD_RECIPE_USER</span><span class="token punctuation">(</span><span class="token parameter">state<span class="token punctuation">,</span> value</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  state<span class="token punctuation">.</span>userRecipes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token operator">...</span>state<span class="token punctuation">.</span>userRecipes<span class="token punctuation">,</span> value<span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
<p>Conclusion: <code>watch()</code> paired with Vuex getters seems to require the <code>state.key = newValue</code> assignment.</p>
<p>Seems a bit weird, but might have been mentioned in the docs somewhere.</p>
<h2 id="netlify-secure-http-headers" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#netlify-secure-http-headers"><span>Netlify: secure HTTP headers</span></a></h2>
<p>Sensible default config that returns an A+ in Mozilla Observatory:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token punctuation">[</span><span class="token punctuation">[</span>headers<span class="token punctuation">]</span><span class="token punctuation">]</span><br>  for = "/<span class="token important">*"</span><br>  <span class="token punctuation">[</span>headers.values<span class="token punctuation">]</span><br>    Content<span class="token punctuation">-</span>Security<span class="token punctuation">-</span>Policy = "default<span class="token punctuation">-</span>src 'self'; img<span class="token punctuation">-</span>src <span class="token important">*;</span> frame<span class="token punctuation">-</span>ancestors 'none'"<br>    Referrer<span class="token punctuation">-</span>Policy = "same<span class="token punctuation">-</span>origin"<br>    Strict<span class="token punctuation">-</span>Transport<span class="token punctuation">-</span>Security = "max<span class="token punctuation">-</span>age=63072000; includeSubdomains; preload"<br>    X<span class="token punctuation">-</span>Content<span class="token punctuation">-</span>Type<span class="token punctuation">-</span>Options = "nosniff"<br>    X<span class="token punctuation">-</span>Frame<span class="token punctuation">-</span>Options = "DENY"<br>    X<span class="token punctuation">-</span>XSS<span class="token punctuation">-</span>Protection = "1; mode=block"</code></pre>
<p><em>NB: adjust CSP as necessary when working on sites that actually load scripts.</em></p>
<h2 id="vue-limiting-the-number-of-array-items-inside-v-for" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vue-limiting-the-number-of-array-items-inside-v-for"><span>Vue: limiting the number of array items inside ‘v-for’</span></a></h2>
<p>Didn’t know that this could work, but it does:</p>
<pre class="language-js"><code class="language-js"><span class="token operator">&lt;</span>Component v<span class="token operator">-</span><span class="token keyword">for</span><span class="token operator">=</span><span class="token string">"(item, index) in list.slice(0, 20)"</span> <span class="token operator">...</span> <span class="token operator">/</span><span class="token operator">></span></code></pre>
<p>Use case: getting the first 20 items of an array.</p>
<p>If the array is shorter, all items will be displayed.</p>
<h2 id="java-script-native-date-formatting" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#java-script-native-date-formatting"><span>JavaScript: native Date formatting</span></a></h2>
<p>I needed to format a date from <code>new Date()</code> recently and using any libs (like <em>date-fns</em> or <em>moment.js</em>) and was not an option; doing that would have been overkill for the small project I was working on.</p>
<p>After a bit of research, I came across the <code>Date.prototype.toLocaleDateString</code> method and implemented it like this:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">getDate</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> date <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">let</span> dateOptions <span class="token operator">=</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">weekday</span><span class="token operator">:</span> <span class="token string">'long'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">year</span><span class="token operator">:</span> <span class="token string">'numeric'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">month</span><span class="token operator">:</span> <span class="token string">'long'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">day</span><span class="token operator">:</span> <span class="token string">'numeric'</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> date<span class="token punctuation">.</span><span class="token function">toLocaleDateString</span><span class="token punctuation">(</span><span class="token string">'en-US'</span><span class="token punctuation">,</span> dateOptions<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p><strong>Result</strong>: <em>Tuesday, April 13, 2021</em></p>
<h2 id="git-ssh-with-multiple-identities-for-the-same-host" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#git-ssh-with-multiple-identities-for-the-same-host"><span>GIT: SSH with multiple identities for the same host</span></a></h2>
<p>Had some issues working with 2 different accounts at GitHub recently.</p>
<p>Initial <code>git clone</code> with the desired identity (specified explicitly) sets up the repository correctly:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">git</span> clone <span class="token parameter variable">-c</span> <span class="token assign-left variable">core.sshCommand</span><span class="token operator">=</span><span class="token string">"/usr/bin/ssh -i /home/me/.ssh/id_rsa_foo"</span> git@git-provider.com:me/repo.git</code></pre>
<p>GIT <code>config</code> should then contain this automatically:</p>
<pre class="language-bash"><code class="language-bash"><span class="token punctuation">[</span>core<span class="token punctuation">]</span><br>  <span class="token punctuation">..</span>.<br>  sshcommand <span class="token operator">=</span> /usr/bin/ssh <span class="token parameter variable">-i</span> /home/me/.ssh/id_rsa_foo</code></pre>
<h2 id="vue-3-handle-page-refresh-f-5-in-your-application" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vue-3-handle-page-refresh-f-5-in-your-application"><span>Vue3: handle page refresh (F5) in your application</span></a></h2>
<p>I recently noticed that there was an issue with cross device data in WATCH3R. Adding/removing list items on one device would not update the app’s (cached) state on another device. Logging out and back in would resolve this issue, but refreshing the page wouldn’t.</p>
<p>After a bit of research, I found out that there wasn’t really a “best practice” on how to handle page refresh in Vue, so I simply tried some things that seemed promising.</p>
<p>What I ended up with is the following:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">updateList</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>mode<span class="token punctuation">.</span>value<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// double check 'mode' here, just in case</span><br>    store<span class="token punctuation">.</span><span class="token function">dispatch</span><span class="token punctuation">(</span><span class="token string">'list/readList'</span><span class="token punctuation">,</span> mode<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token function">onMounted</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token function">setTimeout</span><span class="token punctuation">(</span>updateList<span class="token punctuation">,</span> <span class="token number">250</span><span class="token punctuation">)</span> <span class="token comment">// timeout is required here; otherwise 'route.meta.mode' is undefined</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>I placed this code directly in <code>App.vue</code>, which is the entrypoint of my application and gets re-loaded on page refresh. <code>route.meta.mode</code> controls which list’s data to load - I decided against re-loading all list data, as that wouldn’t be in line with the app’s caching strategy.</p>
<p><strong>12/2021</strong> - Refer to the following commits on how to <em>block</em> page refresh when handling unsaved changes:</p>
<ul>
<li><a class="ext" href="https://codeberg.org/ttntm/itrack/commit/9b85052bd19ab97b45dc755047ab67a612b1b167" rel="noopener">ttntm/itrack@9b85052</a></li>
<li><a class="ext" href="https://codeberg.org/ttntm/recept0r/commit/810360eb7d2960f239b55037bfd4ba53fab52094" rel="noopener">ttntm/recept0r@810360e</a></li>
</ul>
<h2 id="vue-3-select-default-value" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vue-3-select-default-value"><span>Vue3: select default value</span></a></h2>
<p>Cost me more time than I anticipated, esp. due to the fact that <em>it only happened for production builds</em>.</p>
<p>So, in order for a <code>&lt;select&gt;</code> (with an Object-based <code>v-model</code>) to properly display its default value in Vue 3 (3.0.2), this had to be done:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>select</span> <span class="token attr-name">...</span> <span class="token attr-name">v-model.lazy</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>selected<span class="token punctuation">"</span></span> <span class="token attr-name">@change</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>update(selected)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span> <span class="token attr-name">disabled</span> <span class="token attr-name">:value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{}<span class="token punctuation">"</span></span> <span class="token attr-name">:selected</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>selected === {}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Select Something...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>option</span> <span class="token attr-name">v-for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(item, index) in list<span class="token punctuation">"</span></span> <span class="token attr-name">:key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>index<span class="token punctuation">"</span></span> <span class="token attr-name">:value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>item<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>option</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>select</span><span class="token punctuation">></span></span></code></pre>
<p>The <code>:selected=&quot;selected === {}&quot;</code> was necessary, as the <code>&lt;select&gt;</code> would otherwise display as a blank box once the <code>v-for</code> rendered <code>&lt;option&gt;</code> elements came in.</p>
<p>Again, this only happened in <em>production</em>, local dev builds <em>did not</em> behave that way which made it extremely frustrating to debug.</p>
<p>Probably worthy of further investigation and a report to the Vue team, but I don’t have time to try and get a reproducible example done at the moment.</p>
<h2 id="vue-3-click-outside-directive" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vue-3-click-outside-directive"><span>Vue3: click outside directive</span></a></h2>
<p>Directives are quite different in Vue 3 - see: <a class="ext" href="https://v3.vuejs.org/guide/migration/custom-directives.html#custom-directives" rel="noopener">Vue 3 docs</a></p>
<p>Here’s what I came up with for a simple “close an element (modal etc.) when a click outside of this element registers” directive. Clicks on elements within the target element and elements with the class <code>click-outside-ignore</code> will be ignored.</p>
<p>The function/method to close the element is registered as <code>binding.value</code>, so when using it on a component, it should look like this: <code>&lt;Component v-click-outside=&quot;closeComponent&quot; /&gt;</code></p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> handleOutsideClick <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span><br><br>app<span class="token punctuation">.</span><span class="token function">directive</span><span class="token punctuation">(</span><span class="token string">'click-outside'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>  <span class="token function">beforeMount</span><span class="token punctuation">(</span><span class="token parameter">el<span class="token punctuation">,</span> binding<span class="token punctuation">,</span> vnode</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token function-variable function">handleOutsideClick</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      e<span class="token punctuation">.</span><span class="token function">stopPropagation</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>el<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>target<span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span> <span class="token operator">!</span>e<span class="token punctuation">.</span>target<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span><span class="token string">'click-outside-ignore'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        binding<span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>    document<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> handleOutsideClick<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    document<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'touchstart'</span><span class="token punctuation">,</span> handleOutsideClick<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token function">beforeUnmount</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    document<span class="token punctuation">.</span><span class="token function">removeEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> handleOutsideClick<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    document<span class="token punctuation">.</span><span class="token function">removeEventListener</span><span class="token punctuation">(</span><span class="token string">'touchstart'</span><span class="token punctuation">,</span> handleOutsideClick<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h2 id="android-use-adb-to-uninstall-bloatware" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#android-use-adb-to-uninstall-bloatware"><span>Android: use ADB to uninstall bloatware</span></a></h2>
<p>I recently got the Android 10 update and had to factory reset my phone thanks to the phone’s language settings not sticking anymore. Factory reset unearthed all the bloatware again that I had gotten rid of years ago, so I also had to do that again.</p>
<p>Here’s a quick reminder of how to do that:</p>
<ol>
<li>Get ADB platform tools for your OS</li>
<li>On your phone: enable developer mode and USB debudding</li>
<li>Connect phone</li>
<li>Test if connected successfully and allow debugging (will be prompted): <code>adb devices</code></li>
<li><code>adb shell</code> to get into the phone</li>
<li>List packages with <code>pm list packages | grep 'pkg.name.etc'</code> or search for them like this: <code>pm list packages -f TESTPKG</code></li>
<li>Uninstall bloatware like this: <code>pm uninstall -k --user 0 com.android.google.youtube</code></li>
<li>See bloatware disappear 😁</li>
</ol>
<p>Based on this guide at xda: <a class="ext" href="https://www.xda-developers.com/uninstall-carrier-oem-bloatware-without-root-access/" rel="noopener">How to Uninstall Carrier/OEM Bloatware Without Root Access</a></p>
<h2 id="shell-script-launch-vs-code-for-a-specific-folder-repository" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#shell-script-launch-vs-code-for-a-specific-folder-repository"><span>Shell Script: launch VS Code for a specific folder/repository</span></a></h2>
<p>I wanted to have an easy way to launch VS Code for specific repositories and came across the <code>code PATH-TO-REPO</code> command.</p>
<p>This little shell script will open a specific folder/repository based on an argument given to it or based on a directory listing of the <code>/home/user/Repos</code> folder:</p>
<pre class="language-bash"><code class="language-bash"><span class="token shebang important">#!/bin/bash</span><br><br><span class="token comment"># get param</span><br><span class="token assign-left variable">repo</span><span class="token operator">=</span><span class="token variable">$1</span><br><br><span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-z</span> <span class="token string">"<span class="token variable">$repo</span>"</span> <span class="token punctuation">]</span><br>  <span class="token keyword">then</span><br>    <span class="token comment"># check first</span><br>    <span class="token function">ls</span> /home/<span class="token environment constant">USER</span>/Repos/<br><br>    <span class="token comment"># ask user for input</span><br>    <span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"which repo: "</span><br>    <span class="token builtin class-name">read</span> repo<br>  <span class="token keyword">else</span><br>    <span class="token builtin class-name">echo</span> <span class="token string">"opening "</span><span class="token variable">$repo</span><br><span class="token keyword">fi</span><br><br>code /home/<span class="token environment constant">USER</span>/Repos/<span class="token variable">$repo</span></code></pre>
<p>I’ve also configured an alias for that script; simply typing <code>lcode REPO-NAME</code> now opens VS Code for that folder/repository.</p>
<h2 id="vue-2-handle-components-click-event-in-its-parent" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vue-2-handle-components-click-event-in-its-parent"><span>Vue2: handle component’s click event in its parent</span></a></h2>
<p>Use case: a component creates nothing but a <code>&lt;button&gt;</code> with a <code>slot</code> that handles its display state internally but hasn’t got any actual functionality; the handler method is defined in the parent.</p>
<p>In this case, <code>@click.native</code> has to be used to call the method defined in the parent:</p>
<pre class="language-html"><code class="language-html">// in parent component<br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>cButton</span> <span class="token attr-name">@click.native</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>theMethod(praram)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Button Name<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>cButton</span><span class="token punctuation">></span></span></code></pre>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Hiding</title>
        
          
            <category term="personal" />
          
            <category term="thoughts" />
          
        
        <link href="https://ttntm.me/blog/hiding/"/>
        
        <updated>2026-04-08T13:25:00Z</updated>
        <id>https://ttntm.me/blog/hiding/</id>
        <content type="html"><![CDATA[
          
            
            <p>The title - <em>Hiding</em> - is a pretty good description of the state of mind I find myself in right now.</p>
<p>I could leave it at that, keep things mysterious, and publish the shortest post ever on this site. But that’s not why I started typing today. So, here’s what <em>hiding</em> refers to, grouped into some categories.</p>
<h2 id="life" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#life"><span>Life</span></a></h2>
<p>Purchasing real estate isn’t cheap, and we’ve gone to the limits of what we can afford at the moment. As such, our family life’s turned towards homebody-style. We’re focusing on what we have, keeping non-food purchases to an absolute minimum, while tending to our garden, and our cat. We’ve also got a ton of unfinished projects (mostly carpentry) to work on that we’ve amassed over the years.</p>
<p>Inflation, significantly more expensive fuel, and a growing disdain for the “bread and circuses” of late capitalism, make staying at home feel even more comfortable and calming. It’s amazing what a difference a small garden, and your own fence around it, can make. I’m also trying to be the “gravity well” in my marriage at the moment. My wife’s clearly struggling with the reality of a war in Iran, the country where she’s from and spent most of her life in. Supporting her, even just as little as an apolitical foreigner can, is much easier in person, on a couch, than via text from the office.</p>
<p>The few things that keep me sane these days - aka <em>hiding places</em> - are regular trips to the gym, my guitar, and escaping into the virtual world of building up ancient Rome, with the help of “Anno 117: Pax Romana”.</p>
<h2 id="work" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#work"><span>Work</span></a></h2>
<p>I’m still working at a small company, sub 50 employees, and we’re hearing more and more bullshit along the lines of “We’ve got AI now, so we’ve got to get cheaper…”. However, the infamous benefit of “writing code faster…” has never been our problem - it’s always been lacking requirements, inconsequential project management, bizarre customer requirements, meeting madness, non-billable overhead, etc.</p>
<p>Considering any kind of critical analysis of the technical intricacies and the impact of guesswork code on long term maintenance of a codebase, the comments I keep hearing from some of my coworkers (i.e. “It works, but I don’t know how or why…”), and seeing some of the questions Claude was asked not too long ago (i.e. “How to open a pull request against a branch in git”, not joke, sadly), only makes me want to do one thing: hide and avoid even more.</p>
<p>The AI craze is hitting hard. I’m a skeptic, at best, and I managed to avoid most of it successfully so far. Lucky for me, that’s not too hard: I work with ancient, badly documented systems that don’t have a delicious ecosystem of free-for-all content that the insatiable machinery of bots and scrapers can devour.</p>
<p>The Gram editor, a no bullshit fork of Zed, has become my favourite hiding place at work recently. I’ve still got to use VSCodium for some projects, mostly due to specific extensions, but overall, Gram’s becoming my preferred text editor.</p>
<h2 id="elsewhere" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#elsewhere"><span>Elsewhere</span></a></h2>
<p>My various side projects are mostly dormant, I just can’t find any motivation for any of them right now. The same is true for most of what I used to publish on this website. Technical articles don’t really make sense to me anymore, it’s all just fodder for the scrapers anyway, and I refuse to feed them. Bookmarks/links are good, I still like collecting them, but the intervals of publishing a meaningful <code>/likes</code> entry are definitely increasing.</p>
<p>It also doesn’t help that this website was stuck in a war with bots and scrapers for the past 6 months. It seems like I won, for now, but I spent many hours tinkering with my defense systems to get there. Despite the success, the whole process also killed some of the joy of running a website and publishing things. I also ended up retiring my - formerly public - personal wiki (see: <a href="https://ttntm.me/blog/personal-knowledge-base/" class="int">My Personal Knowledge Base</a>). It’s an Obsidian vault now, that lives in a private repository.</p>
<p>More hiding, I suppose.</p>
<p>And all I can really be bothered to publish on this site, will probably end up being similar to what you just read. If you read it this far at all, that is, and if you’re not a bot, of course.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 12, 2026</title>
        
        <link href="https://ttntm.me/likes/2026-w12/"/>
        
        <updated>2026-03-19T13:30:00Z</updated>
        <id>https://ttntm.me/likes/2026-w12/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.scirp.org/journal/paperinformation?paperid=149987" class="ext u-like-of" rel="noopener">Comparative Study of React, Astro and Eleventy Technologies in Developing a Lottery Statistical Analysis Application </a><br>
Astro offers an optimal balance between performance and flexibility, React stands out for its interactivity, and Eleventy for its simplicity and speed</li>
<li><a href="https://www.kytta.dev/blog/agents-md/" class="ext u-like-of" rel="noopener">Teach humans to contribute, not machines</a><br>
We should, yes</li>
<li><a href="https://paperclover.net/blog/webdev/one-year-next-app-router" class="ext u-like-of" rel="noopener">One Year with Next.js App Router - Why We’re Moving On</a><br>
A critique of React Server Components and Next.js</li>
<li><a href="https://www.lorenstew.art/blog/10-kanban-boards/" class="ext u-like-of" rel="noopener">I Built the Same App 10 Times: Evaluating Frameworks for Mobile Performance</a><br>
Remembered this one while researching for tech stack decisions; was favoring Nuxt, apparently not a bad idea: “…competitive mobile web performance while maintaining a mature ecosystem. React and Angular show no path to similar results.”</li>
<li><a href="https://aroceu.com/manifesto" class="ext u-like-of" rel="noopener">A manifesto of sorts; or, my love letter to the personal website</a><br>
The realm of personal websites is the wild west of authenticity</li>
<li><a href="https://taggart-tech.com/control/" class="ext u-like-of" rel="noopener">Control</a><br>
How long do we have until the cyberpunk dystopia is complete?</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://justthebrowser.com" class="ext u-like-of" rel="noopener">Just the Browser</a><br>
Remove AI features, telemetry data reporting, sponsored content, product integrations, and other annoyances from desktop web browsers</li>
<li><a href="https://doesitmutate.xyz" class="ext u-like-of" rel="noopener">Does it mutate</a><br>
Now you know</li>
<li><a href="https://github.com/alvi-se/ai-ublock-blacklist" class="ext u-like-of" rel="noopener">AI uBlock Origin Blacklist</a><br>
Block AI content farms</li>
<li><a href="https://localsend.org" class="ext u-like-of" rel="noopener">LocalSend</a><br>
No more cables</li>
<li><a href="https://colorpeek.com" class="ext u-like-of" rel="noopener">Colorpeek</a><br>
A great way to share color palettes</li>
<li><a href="https://thelinuxbook.com" class="ext u-like-of" rel="noopener">Foreward - The Linux Desktop Guide</a><br>
A very neat guide</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Done Moving</title>
        
          
            <category term="offline" />
          
            <category term="personal" />
          
        
        <link href="https://ttntm.me/blog/done-moving/"/>
        
        <updated>2026-02-24T11:25:00Z</updated>
        <id>https://ttntm.me/blog/done-moving/</id>
        <content type="html"><![CDATA[
          
            
            <p>If you had told me a year ago, that I’d be living in a new apartment now, I wouldn’t have taken you seriously, laughing very hard…</p>
<p>But somehow, we ended up finding a great place, without really looking for one, bought it, and moved within 3 months, give or take. I wrote <a href="https://ttntm.me/blog/out-of-time/" class="int">about how it all started</a> some months ago, and I won’t get back into that, so this post is about what happened since then.</p>
<p>Our apartment’s in a building that was finished in 2023, and nobody ever lived there. As such, one of the first things to take care of was the getting a kitchen. We looked at out options, compared some offers, and ended up with Ikea eventually - they offered the best value for our limited budget, which made it possible to get some proper kitchen appliances. We kept our trusty old dishwasher, only to learn that it’s a bit too short for Ikea’s 80cm countertop heights. A solution was found, however: the dishwasher now stands on top of a spare part of the kitchen countertop, and it works well.</p>
<p>Other than taking care of getting a kitchen, we also painted most walls, added some wood paneling to a corridor, and put Ethernet cables into our walls before the actual moving process even started.</p>
<p>Regarding furniture, we didn’t take much of what we had, because our old apartment was sold with about 80% of all furniture that was in it. So, we only had to move 2 tables, some chairs, and some living room furniture (some shelves and vitrines). We had to build a ton of new furniture however, and there was an abundance of stuff to pack into boxes, and to move.</p>
<p>Most of the actual move was done using a big Volvo that we could borrow from my family, and the proximity of the 2 apartments helped immensely. This process of packing, moving, and unpacking took us about 3 weeks - we made good use of the Christmas holiday season. About halfway through these 3 weeks, on Dec. 28th, we moved the cat, and slept in the new place for the first time. He did exceptionally well, much better than expected, and is happy there since then.</p>
<p>January 12th was the day we gave the keys to our old place to its new owners, so that’d be the official “done moving” date. There was a ton of stuff left to do though, and we’ve been working on that ever since. By now, everything’s mostly gone to normal, and the things we did recently we’re basically just the finishing touches - the last missing ceiling lights, some tiles here and there, and some purposefully picked pieces of furniture to fill the inevitable gaps (both in terms of looks, and usability).</p>
<p>Ultimately, we’re happy that we moved the way we did (i.e. gradually). Yes, it was stressful and exhausting, but overall, it worked out as planned, and we learned that we can take care of a lot of things ourselves. Working on our own place, using our tools, and some skills we didn’t know we had, felt rewarding and has become something that we’ll keep doing.</p>
<p>It’ll be spring soon, and that means that an entirely new chapter will be starting soon: taking care of our garden.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 08, 2026</title>
        
        <link href="https://ttntm.me/likes/2026-w08/"/>
        
        <updated>2026-02-18T15:30:00Z</updated>
        <id>https://ttntm.me/likes/2026-w08/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://aredridel.dinhe.net/2026/02/12/the-ai-haters-guide-to-code-with-llms/" class="ext u-like-of" rel="noopener">The AI hater’s guide to code with LLMs (The Overview)</a><br>
“As activists mitigating the harms of AI, we need to be well informed, and we need to understand what the specific harms are”</li>
<li><a href="https://aredridel.dinhe.net/2026/02/15/an-ai-haters-guide-to-code-with-llms-the-how-to/" class="ext u-like-of" rel="noopener">An AI hater’s guide to code with LLMs (The How-to)</a><br>
Part 2, read after the article above</li>
<li><a href="https://quinnmaclay.com/posts/early-blogs" class="ext u-like-of" rel="noopener">Blogging Before Blogs</a><br>
Something entirely different, but great</li>
<li><a href="https://www.betweenthebolterandme.com/2017/01/building-true-scale-space-marine.html" class="ext u-like-of" rel="noopener">Building a True-scale Space Marine</a><br>
Space Marine anatomy is quite bizarre, best ignored, and surprisingly different when it gets fixed</li>
<li><a href="https://keith.is/post/html-sanitizer-api" class="ext u-like-of" rel="noopener">Browser-Native HTML Sanitization</a><br>
Pretty cool, hoping for better browser support soon</li>
<li><a href="https://pythonbynight.com/blog/a-place-for-my-things" class="ext u-like-of" rel="noopener">Hetzner and Coolify for My Side Projects</a><br>
High up on the list of things I’d like to do soon</li>
<li><a href="https://idiallo.com/blog/zipbomb-protection" class="ext u-like-of" rel="noopener">I use Zip Bombs to Protect my Server</a><br>
Intriguing; of course, I’d never think of doing that to those poor bots…</li>
<li><a href="https://alexschroeder.ch/view/2025-03-21-defence-summary" class="ext u-like-of" rel="noopener">A summary of my bot defence systems</a><br>
Helpful article, but painful that it’s necessary to take care of such things nowadays</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://pandoc.org/app/" class="ext u-like-of" rel="noopener">pandoc for the people</a><br>
pandoc in the browser, thanks to WebAssembly</li>
<li><a href="https://poster.54gradsoftware.de" class="ext u-like-of" rel="noopener">MapToPoster</a><br>
Turns any place on earth into a beautiful poster</li>
<li><a href="https://aidirtylist.info" class="ext u-like-of" rel="noopener">The AI Dirty List</a><br>
“those who choose to bathe in AI slop will never be washed clean”</li>
<li><a href="https://modern-css.com" class="ext u-like-of" rel="noopener">modern.css</a><br>
Modern CSS code snippets, side by side with the old hacks they replace - very helpful!</li>
<li><a href="https://record.club" class="ext u-like-of" rel="noopener">Record Club</a><br>
A social music network - interesting, will give it a try</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 06, 2026</title>
        
        <link href="https://ttntm.me/likes/2026-w06/"/>
        
        <updated>2026-02-06T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2026-w06/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://martijnhols.nl/blog/accessibility-essentials-every-front-end-developer-should-know" class="ext u-like-of" rel="noopener">Accessibility essentials every front-end developer should know</a><br>
“Accessibility isn’t a separate task to tackle later, it’s something that should be a part of your development process from the start” - yes!</li>
<li><a href="https://mattweidner.com/2025/05/21/text-without-crdts.html" class="ext u-like-of" rel="noopener">Collaborative Text Editing without CRDTs or OT</a><br>
An interesting topic that I keep coming back to somehow</li>
<li><a href="https://evilmartians.com/chronicles/exploring-the-oklch-ecosystem-and-its-tools" class="ext u-like-of" rel="noopener">Exploring the OKLCH ecosystem and its tools</a><br>
I’m still using HEX colors for most things, but OKLCH looks like it’s worth diving into</li>
<li><a href="https://www.jonoalderson.com/conjecture/javascript-broke-the-web-and-called-it-progress/" class="ext u-like-of" rel="noopener">JavaScript broke the web (and called it progress)</a><br>
“…question whether it’s working for your users, or just for your developers” - so true</li>
<li><a href="https://www.learnwithgurpreet.com/posts/the-hard-truth-about-running-nextjs-at-scale/" class="ext u-like-of" rel="noopener">The Hard Truth About Running Next.js at Scale</a><br>
Interesting read, that makes me even more happy that I’m successfully avoiding Next at work</li>
<li><a href="https://www.bram.us/2025/10/22/solved-by-css-scroll-state-queries-hide-a-header-when-scrolling-down-show-it-again-when-scrolling-up/" class="ext u-like-of" rel="noopener">Solved by CSS Scroll State Queries: hide a header when scrolling down, show it again when scrolling up</a><br>
Lots of great stuff is happening to CSS recently</li>
<li><a href="https://www.mostlylucid.net/blog/writingfeaturespecs" class="ext u-like-of" rel="noopener">Agile Speccing: Writing Feature Specs That Actually Work</a><br>
Great input for the next retro at work…</li>
<li><a href="https://thenewstack.io/css-in-js-the-great-betrayal-of-frontend-sanity/" class="ext u-like-of" rel="noopener">CSS-in-JS: The Great Betrayal of Frontend Sanity</a><br>
Me smiling, because I avoided having to write CSS-in-JS in the first place</li>
<li><a href="https://henry.codes/writing/a-website-to-destroy-all-websites/" class="ext u-like-of" rel="noopener">A website to destroy all websites</a><br>
About “building the Web We Want” - absolutely delightful, and visually stunning</li>
<li><a href="https://crescentro.se/posts/linux-music-players-2026/" class="ext u-like-of" rel="noopener">The state of Linux music players in 2026</a><br>
I’m happy with <code>cmus</code>, but there are some cool options out there, it seems</li>
<li><a href="https://www.esquire.com/entertainment/tv/a69918939/did-mtv-shut-down/" class="ext u-like-of" rel="noopener">MTV Is Still Alive. So Why Does Everyone Think It Shut Down Last Week?</a><br>
Don’t trust anything you read online</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://github.com/davidjerleke/embla-carousel" class="ext u-like-of" rel="noopener">davidjerleke/embla-carousel</a><br>
For the next time you fail to convince your client not to use a carousel…</li>
<li><a href="https://breezepdf.com" class="ext u-like-of" rel="noopener">Breeze PDF</a><br>
I used this one quite often recently, mostly for forms related to the apartment purchase, and it’s been doing a splendid job</li>
<li><a href="https://anubis.techaro.lol" class="ext u-like-of" rel="noopener">Anubis</a><br>
High up on my list of things to do if/when I move my projects to my own server</li>
<li><a href="https://openwebsearch.eu" class="ext u-like-of" rel="noopener">OpenWebSearch.eu</a><br>
An interesting project; curious to see what comes out of it in the long term</li>
<li><a href="https://chartscss.org" class="ext u-like-of" rel="noopener">Charts.css</a><br>
Charts, using CSS, without any JavaScript</li>
<li><a href="https://www.iec.ch/world-plugs" class="ext u-like-of" rel="noopener">World plugs</a><br>
The odd link here, but who knows, might be helpful at some point</li>
<li><a href="https://wantmymtv.vercel.app" class="ext u-like-of" rel="noopener">I Want My MTV!</a><br>
Some more MTV</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>40 Questions – 2025 Edition</title>
        
          
            <category term="40questions" />
          
            <category term="personal" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/40-questions-2025/"/>
        
        <updated>2026-01-14T10:30:00Z</updated>
        <id>https://ttntm.me/blog/40-questions-2025/</id>
        <content type="html"><![CDATA[
          
            
            <p>A list of 40 Questions to ask yourself every year.</p>
<p>I saw it <a class="ext" href="https://stephango.com/40-questions" rel="noopener">elsewhere</a>, and decided to give it a go.</p>
<p>–</p>
<p><strong><em>What did you do this year that you’d never done before?</em></strong></p>
<p>Bought a new apartment.</p>
<p><strong><em>Did you keep your new year’s resolutions?</em></strong></p>
<p>Didn’t have any, and generally don’t do that.</p>
<p><strong><em>Did anyone close to you give birth?</em></strong></p>
<p>My new next door neighbors, they’re close by proximity.</p>
<p><strong><em>Did anyone close to you die?</em></strong></p>
<p>No, unless Ozzy counts.</p>
<p><strong><em>What cities/states/countries did you visit?</em></strong></p>
<p>Munich, for a work trip in May.</p>
<p><strong><em>What would you like to have next year that you lacked this year?</em></strong></p>
<p>An actual vacation - spent most of my time off on moving to the new place.</p>
<p><strong><em>What date(s) from this year will remain etched upon your memory, and why?</em></strong></p>
<p>“Back to the Beginning”, on July 5th, the final concert of Black Sabbath in its original lineup. And signing the contract for the new apartment, on Oct. 17th, probably a once in a lifetime thing.</p>
<p><strong><em>What was your biggest achievement of the year?</em></strong></p>
<p>Getting the new apartment. And a 130 kg deadlift. And profit on our ETF investment, despite having bought into them at the peak before that fuckery in early March.</p>
<p><strong><em>What was your biggest failure?</em></strong></p>
<p>A 140 kg deadlift. And taking a guitar to a store for a sale on commission - would like to get it back, but the damn store’s never open, it seems.</p>
<p><strong><em>What other hardships did you face?</em></strong></p>
<p>DIY-ing our move to the new place was a lot more exhausting and time-consuming than we anticipated.</p>
<p>Also, our cat had to go to the hospital in summer, and stay there for about a week. That was tough.</p>
<p><strong><em>Did you suffer illness or injury?</em></strong></p>
<p>A mild flu at the beginning of the year, despite having been vaccinated.</p>
<p><strong><em>What was the best thing you bought?</em></strong></p>
<p>A PRS SE Hollowbody Standard electric guitar. And the “Back to the Beginning” live stream ticket, for the final concert of Black Sabbath in its original lineup.</p>
<p><strong><em>Whose behavior merited celebration?</em></strong></p>
<p>Some guy at one of the banks we asked for loan offers at. Called back on a Sunday, worked until the late evening, and had that offer ready the same day. That’s basically unheard of in Austria.</p>
<p><strong><em>Whose behavior made you appalled?</em></strong></p>
<p>Realtors, and their whole business model basically. And the lawyers, notaries and courts, with their never ending fees.</p>
<p><strong><em>Where did most of your money go?</em></strong></p>
<p>The new apartment.</p>
<p><strong><em>What did you get really, really, really excited about?</em></strong></p>
<p>The “Back to the Beginning” concert, the new apartment, and having a garden now.</p>
<p><strong><em>What song will always remind you of this year?</em></strong></p>
<p>The whole Black Sabbath set from “Back to the Beginning”, and “Perihelion” by King Gizzard and the Lizard Wizard (I’m still shocked to have ignored that band for so many years, they’re great).</p>
<p><strong><em>Compared to this time last year, are you: happier or sadder? Richer or poorer? Healthier or unhealthier?</em></strong></p>
<p>Happier, poorer, and healthier.</p>
<p><strong><em>What do you wish you’d done more of?</em></strong></p>
<p>Working on side projects, bicycling, swimming in summer.</p>
<p><strong><em>What do you wish you’d done less of?</em></strong></p>
<p>Packing boxes, going to furniture stores, carrying boxes.</p>
<p><strong><em>How are you spending the holidays?</em></strong></p>
<p>We moved to the new apartment.</p>
<p><strong><em>Did you fall in love this year?</em></strong></p>
<p>No. Unless the new guitar counts.</p>
<p><strong><em>Do you hate anyone now that you didn’t hate this time last year?</em></strong></p>
<p>No.</p>
<p><strong><em>What was your favorite show?</em></strong></p>
<p>Alien: Earth</p>
<p><strong><em>What was the best book you read?</em></strong></p>
<p>I had a lot of fun reading “Iron Man”, the Tony Iommi biography, but “If Cats Disappeared from the World” was great too.</p>
<p><strong><em>What was your greatest musical discovery of the year?</em></strong></p>
<p>King Gizzard And The Lizard Wizard</p>
<p><strong><em>What was your favorite film?</em></strong></p>
<p>“Ballerina”, with “Bugonia”, and “Dracula: A Love Tale” as close contenders.</p>
<p><strong><em>What was your favorite meal?</em></strong></p>
<p>We went to a small Asian restaurant called “Mochi” for our wedding anniversary, and the food there was really great.</p>
<p><strong><em>What did you want and get?</em></strong></p>
<p>A hollowbody electric guitar.</p>
<p><strong><em>What did you want and not get?</em></strong></p>
<p>Too many things, probably, but nothing that really mattered.</p>
<p><strong><em>What did you do on your birthday?</em></strong></p>
<p>Went to one of the hot springs with my wife.</p>
<p><strong><em>What one thing would have made your year immeasurably more satisfying?</em></strong></p>
<p>An actual week or two of holidays away from home. Also maybe better conditions on the loan for the apartment, but that’s probably illusory anyway.</p>
<p><strong><em>How would you describe your personal fashion this year?</em></strong></p>
<p>Same as always, I guess?</p>
<p><strong><em>What kept you sane?</em></strong></p>
<p>Getting back into playing guitar, some really great video games (i.e. Kingdom Come II, Stellar Blade, Expedition 33, CarX Street, Rogue Trader).</p>
<p><strong><em>Which celebrity/public figure did you admire the most?</em></strong></p>
<p>Don’t know, don’t care.</p>
<p><strong><em>What political issue stirred you the most?</em></strong></p>
<p>No comment.</p>
<p><strong><em>Who did you miss?</em></strong></p>
<p>My father, who died in 2022.</p>
<p><strong><em>Who was the best new person you met?</em></strong></p>
<p>A new colleague at work.</p>
<p><strong><em>What valuable life lesson did you learn this year?</em></strong></p>
<p>Both me, and my wife, are stubborn as hell, which hasn’t changed in almost 10 years of being together.</p>
<p>Also, my craftsmanship skills are better than I thought, and pricier tools are actually worth it, and have a huge impact on the result (i.e. a cut made with a proper circular saw and a guide rail, instead of a cheapo jigsaw).</p>
<p><strong><em>What is a quote that sums up your year?</em></strong></p>
<p>“Constant dropping wears away a stone” - with regard to moving apartments, which seemed overwhelming at first, progressed slowly, but eventually concluded successfully.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>About the Past Year - 2025</title>
        
          
            <category term="personal" />
          
            <category term="recap" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/about-the-past-year-2025/"/>
        
        <updated>2026-01-01T21:30:00Z</updated>
        <id>https://ttntm.me/blog/about-the-past-year-2025/</id>
        <content type="html"><![CDATA[
          
            
            <p>The first 2/3 of 2025 were rather uneventful, business - and life - as usual. The final 1/3 was absolute madness, because we bought a new apartment and severely underestimated what it means to move after having amassed things and stuff for more than 10 years. We finally moved into the new place last Sunday, Dec. 28th, but we’re still moving our belongings around in boxes. I’ve got 11 more days left until I have to go back to work, and I can only hope we’ll manage to get it all done until then.</p>
<p>Anyway, these are some highlights from last year:</p>
<p><strong>Work</strong></p>
<ul>
<li>Accidentally ended up doing too much requirements engineering during the first half of the year, realized in time, and got myself out of that project; it all started harmlessly, “helping out some colleagues”, but it soon turned into meeting- and prose-hell, which is not for me - I prefer reading/writing/discussing code</li>
<li>Only good projects after that</li>
<li>Also “big new project” since October, which is really nice (almost no context switching)</li>
</ul>
<p><strong>FOSS &amp; Side Projects</strong></p>
<ul>
<li>Finally implemented webmentions on this website, then rewrote most of it a couple of months later</li>
<li>Finished working on a CMS migration incl. website relaunch for a nonprofit organization (Astro/Contentful)</li>
<li>Migrated 2 projects from Fauna to Supabase, after Fauna got EoL’d</li>
<li>Finally relaunched my wife’s website (Astro/Contentful)</li>
<li>Celebrated the <a href="https://ttntm.me/blog/seven-years/" class="int">7 years anniversary</a> of this website</li>
<li><a href="https://ttntm.me/blog/#2025" class="int">Published 14 blog posts</a>, and <a href="https://ttntm.me/tags/evolution/" class="int">added a few things</a> to this website</li>
</ul>
<p>My commit activity for 2025 looks like this:</p>
<p>Codeberg:</p>
<img src="https://ttntm.me/static/img/blog/codeberg2025.jpg" class="img-fluid img-center auto-invert" alt="ttntm's Codeberg contributions 2025">
<p>GitHub:</p>
<img src="https://ttntm.me/static/img/blog/gh2025.jpg" class="img-fluid img-center auto-invert" alt="ttntm's GitHub contributions 2025">
<p>Website: <a href="https://ttntm.me/changelog/#timeline" class="int">Activity Timeline</a></p>
<p><strong>Personal Life</strong></p>
<ul>
<li>Got back into playing guitar, and found myself a lovely PRS SE Hollowbody Standard in early summer</li>
<li>Consistently went to gym 3x/week, until the moving started; love living the strong life, will get back to the weights asap, and cry about lost strength and bodyweight soon</li>
<li>Bought apartment, sold old apartment, madness ensued, yada yada yada</li>
<li>Noticed I know more than I thought about tools and using them, happily DIY-ing electricity, plumbing, woodworking etc. - nice!</li>
</ul>
<p><strong>Best _ of 2025</strong></p>
<ul>
<li><strong>Game</strong>: Kingdom Come: Deliverance II</li>
<li><strong>Movie</strong>: Ballerina (“Bugonia”, and “Dracula: A Love Tale” as close contenders)</li>
<li><strong>Series</strong>: Alien: Earth</li>
</ul>
<p><strong>Games of 2025</strong></p>
<div class="grid grid2 gap1">
<div>
<p>In somewhat chronological order:</p>
<ul>
<li>Hogwarts Legacy</li>
<li>Brotato</li>
<li>Sense: A Cyberpunk Ghost Story</li>
<li>Citizen Sleeper *</li>
<li>The Outer Worlds *</li>
<li>Transistor</li>
<li>Stellar Blade *</li>
<li>The Hong Kong Massacre</li>
<li>Clair Obscur: Expedition 33 *</li>
<li>Dorfromantik</li>
<li>Cocoon</li>
<li>CarX Street</li>
<li>Star Wars Outlaws</li>
<li>Warhammer 40,000: Rogue Trader</li>
</ul>
<p><code>*</code> <em>marks finished games (main story).</em></p>
</div>
<div>
<p>Games I tried but dropped (&lt;4h):</p>
<ul>
<li>Aliens: Dark Descent</li>
</ul>
</div>
</div>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>App Defaults - 2025</title>
        
          
            <category term="defaults" />
          
            <category term="personal" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/app-defaults-2025/"/>
        
        <updated>2025-12-05T13:30:00Z</updated>
        <id>https://ttntm.me/blog/app-defaults-2025/</id>
        <content type="html"><![CDATA[
          
            
            <p>It’s that time of the year again: here’s my extended “frozen <a href="https://ttntm.me/uses" class="int">/uses</a> page” for late 2025.</p>
<p>Whenever multiple applications are listed, <code>p</code> marks private use, <code>w</code> marks software that I (have to) use at work. Changes compared to the previous year are highlighted using <em>colored text</em>.</p>
<ul>
<li>
<p><strong>Mail Client</strong>: Proton Mail (<code>p</code>), Outlook PWA (<code>w</code>)</p>
</li>
<li>
<p><strong>Calendar</strong>: Proton Calendar (<code>p</code>), Outlook PWA (<code>w</code>)</p>
</li>
<li>
<p><strong>Notes</strong>: <em>Proton Drive documents</em>, and my personal wiki (TKB) (<code>p</code>), <em>Obsidian</em> (<code>w</code>)</p>
</li>
<li>
<p><strong>Todos</strong>: <em>paper, Proton Drive documents</em> (<code>p</code>), <em>Obsidian</em> (<code>w</code>)</p>
</li>
<li>
<p><strong>Terminal</strong>: WezTerm (<code>p</code>), <em>Ghostty</em> (<code>w</code>)</p>
</li>
<li>
<p><strong>Shell</strong>: zsh with Oh My Zsh and p10k</p>
</li>
<li>
<p><strong>Browser (Desktop)</strong>: Zen Browser, Ungoogled Chromium (<code>w</code>)</p>
</li>
<li>
<p><strong>Browser (Mobile)</strong>: Cromite (Android), Safari (iOS)</p>
</li>
<li>
<p><strong>Search Engine</strong>: DuckDuckGo</p>
</li>
<li>
<p><strong>Text Editor</strong>: <em>VS Codium</em>, micro or nvim (terminal editing), gedit (quick notes, previews; <code>w</code>)</p>
</li>
<li>
<p><strong>Git Client</strong>: terminal</p>
</li>
<li>
<p><strong>Passwords</strong>: Proton Pass (<code>p</code>), KeePassXC (<code>w</code>)</p>
</li>
<li>
<p><strong>File Sharing</strong>: Proton Drive (<code>p</code>), OneDrive (<code>w</code>)</p>
</li>
<li>
<p><strong>Docs/Wiki</strong>: TKB (<code>p</code>), Confluence (<code>w</code>)</p>
</li>
<li>
<p><strong>Spreadsheets</strong>: Google Sheets (<code>p</code>), LibreOffice Calc (<code>w</code>)</p>
</li>
<li>
<p><strong>Photos</strong>: Google Photos</p>
</li>
<li>
<p><strong>RSS</strong>: <em>QiReader</em></p>
</li>
<li>
<p><strong>Chat</strong>: Signal (<code>p</code>), Teams (<code>w</code>)</p>
</li>
<li>
<p><strong>Bookmarks/Links</strong>: <em><span>raindrop.io</span></em>, TKB, /bookmarks on this website</p>
</li>
<li>
<p><strong>Shopping List</strong>: Google Keep</p>
</li>
<li>
<p><strong>Music</strong>: cmus (Desktop), Poweramp (Android)</p>
</li>
<li>
<p><strong>Weight Lifting</strong>: <em>Liftosaur</em></p>
</li>
<li>
<p><strong>Social Media</strong>: Mastodon via Elk (Desktop), IceCubes (iOS) and Moshidon (Android), Pixelfed (PWA)</p>
</li>
<li>
<p><strong>Ad Blocking</strong>: RethinkDNS (Android), uBlock Origin</p>
</li>
<li>
<p><strong>VPN</strong>: Proton VPN</p>
</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Out of Time</title>
        
          
            <category term="offline" />
          
            <category term="personal" />
          
        
        <link href="https://ttntm.me/blog/out-of-time/"/>
        
        <updated>2025-10-10T13:05:00Z</updated>
        <id>https://ttntm.me/blog/out-of-time/</id>
        <content type="html"><![CDATA[
          
            
            <p>During summer, my wife and I were entertaining the thought of moving to a bigger/better/… apartment. We started looking, and quickly realized that we can’t afford what we like, and that we don’t really like most apartments and houses we could actually afford. We made our peace with that, and let it go for the time being.</p>
<p>A couple of weeks ago, I took the bus to work and saw a bunch of “for sale” signs about 2 stops from where we currently live. I looked the place up online, and liked what I saw - a ground floor apartment of 83 m², with a 105 m² garden, situated in a building that’s barely 2 years old. And listed for a price that we could actually manage to pay. I sent my wife there, while I was at work, and she also liked what she saw.</p>
<p>We arranged a showing with the realtor some days later, and weren’t disappointed by the inside of the place either. What followed, were a couple of stressful weeks, figuring things out with banks, preparing an offer, and working on getting it accepted. Right now, we’re in the middle of buying the place, and there’s just <em>so much</em> to do - tons of documents, phone calls, appointments, planning etc., all while trying my best to find 8 hours for work every day.</p>
<p>I’m glad that my situation at work is very convenient at the moment. I’m basically working on my tickets and tasks alone, which allows for weird, irregular hours, and the flexibility we currently need with everything else going on.</p>
<p>So yeah, it’s all a bit much for me these days, and it’ll probably stay like that for weeks, if not months - until we’ve moved and settled into the new place, I suppose. There’s hardly any time left for anything else right now, so one could consider this post some kind of early notice of hibernation until things ease up a little.</p>
<p>Having said that, I’ll get back to work now, my lunch break’s over.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 39, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w39/"/>
        
        <updated>2025-09-27T20:30:00Z</updated>
        <id>https://ttntm.me/likes/2025-w39/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://karl-voit.at/2025/08/17/Markdown-disaster/" class="ext u-like-of" rel="noopener">Markdown Is a Disaster: Why and What to Do Instead</a><br>
An interesting read with some good arguments</li>
<li><a href="https://gomakethings.com/diy-netlify/" class="ext u-like-of" rel="noopener">DIY Netlify</a><br>
“Own the process. Own the setup. Own the code.” - <em>YES!</em></li>
<li><a href="https://www.jonoalderson.com/conjecture/its-time-for-modern-css-to-kill-the-spa/" class="ext u-like-of" rel="noopener">It’s time for modern CSS to kill the SPA</a><br>
“Build like it’s 2025 – not like you’re trapped in a 2018 demo of Gatsby.” - this line should probably be on a t-shirt</li>
<li><a href="https://www.juanfernandes.uk/blog/automating-my-links-list/" class="ext u-like-of" rel="noopener">Automating my links list</a><br>
A cool approach that I might consider adopting for my setup at some point</li>
<li><a href="https://justinfagnani.com/2025/06/26/the-time-is-right-for-a-dom-templating-api/" class="ext u-like-of" rel="noopener">The time is right for a DOM templating API</a><br>
Super curious to see if what - if anything - is going to come out of this</li>
<li><a href="https://iyer.ru/2025/08/20/i-styled-my-rss-feed-with-xslt/" class="ext u-like-of" rel="noopener">I styled my RSS feed with XSLT</a><br>
Might do this, if I find the time, as long as XSLT is still alive</li>
<li><a href="https://jakearchibald.com/2025/making-xml-human-readable-without-xslt/" class="ext u-like-of" rel="noopener">Making XML human-readable without XSLT</a><br>
Another option re: the article linked above this one</li>
<li><a href="https://deplet.ing/the-copilot-delusion/" class="ext u-like-of" rel="noopener">The Copilot Delusion</a><br>
“Defer your thinking to the bot, and we all rot.” - couldn’t agree more, and another one for a t-shirt</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://vintageapple.org/macworld/" class="ext u-like-of" rel="noopener">Vintage Macworld Magazine Library</a><br>
I’m not even an Apple user, but I love browsing old magazines sometimes</li>
<li><a href="https://gradient.style" class="ext u-like-of" rel="noopener">CSS HDR Gradients</a><br>
A CSS4 and CSS5 full features gradient generator</li>
<li><a href="https://www.crowdsupply.com/open-tools/open-printer" class="ext u-like-of" rel="noopener">Open Printer</a><br>
A pretty cool device</li>
<li><a href="https://feedgrab.net" class="ext u-like-of" rel="noopener">Feedgrab</a><br>
Quite happy with my current RSS reader, but this one looks <em>really</em> good - might try and switch</li>
<li><a href="https://github.com/bgreenwell/doxx" class="ext u-like-of" rel="noopener">bgreenwell/doxx</a><br>
<code>.docx</code> files without leaving your terminal - might try next time someone sends me one of those</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Seven Years</title>
        
          
            <category term="news" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/seven-years/"/>
        
        <updated>2025-08-13T17:30:00Z</updated>
        <id>https://ttntm.me/blog/seven-years/</id>
        <content type="html"><![CDATA[
          
            
            <img src="https://ttntm.me/static/img/blog/7years.jpg" class="img-fluid img-center" alt="Illustration showing birthday celebration, including an orange bubble saying '7 years' and a browser window showing the domain 'ttntm.me'">
<p>It’s been seven years since <a href="https://ttntm.me/blog/hello-world/" class="int">Hello World</a>. <em>A lot</em> has happened since then, in the world, in my life, and also on this website.</p>
<p>What started as a minimal blog, grew into a proper online home for myself. Collections and pages were added, additional features and functionality came hand in hand with them. Infrastructure and tools changed over the years too, and will probably keep changing. A personal website working as intended, I guess, and offering a perfect testing ground for new ideas, technologies, and really just about anything that one feels like doing within (or even beyond) the limitations of the web as a medium.</p>
<p>When wondering about the “why” in what I’m doing here, I often remind myself that I’m really just running this website for myself. But I will gladly admit that the occasional feature of an article in a larger newsletter, new online acquaintances, and a steady stream of 10-15k unique visitors/month do actually feel great. I have zero interest in “building a brand”, or monetizing anything, but I believe, and hope, that what I’m doing here might end up being helpful for someone else out there in a way - as entertainment, information, or even inspiration, maybe.</p>
<h2 id="evolution" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#evolution"><span>Evolution</span></a></h2>
<p>These screenshots, taken with the help of the Internet Archive, offer some snapshots of how this website evolved over time.</p>
<p>Details and context can be found in the <a href="https://ttntm.me/changelog/" class="int">Changelog</a>. In addition to that, posts published in the website <a href="https://ttntm.me/tags/evolution/" class="int">#evolution</a> series offer even more details.</p>
<ul>
<li><a href="https://ttntm.me/img/blog/anniversary/2019.png" class="int">2019</a></li>
<li><a href="https://ttntm.me/img/blog/anniversary/2020.png" class="int">2020</a></li>
<li><a href="https://ttntm.me/img/blog/anniversary/2021.png" class="int">2021</a></li>
<li><a href="https://ttntm.me/img/blog/anniversary/2022.png" class="int">2022</a></li>
<li><a href="https://ttntm.me/img/blog/anniversary/2023.png" class="int">2023</a></li>
<li>2024
<ul>
<li><a href="https://ttntm.me/img/blog/anniversary/2024_1.png" class="int">2024_1</a></li>
<li><a href="https://ttntm.me/img/blog/anniversary/2024_2.png" class="int">2024_2</a></li>
</ul>
</li>
<li>2025
<ul>
<li><a href="https://ttntm.me/img/blog/anniversary/2025_1.png" class="int">2025_1</a></li>
<li><a href="https://ttntm.me/img/blog/anniversary/2025_2.png" class="int">2025_2</a></li>
</ul>
</li>
</ul>
<p><em>Note: uncompressed screenshots, i.e. relatively big images.</em></p>
<h2 id="whats-next" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#whats-next"><span>What’s Next?</span></a></h2>
<p>I don’t think I can find better words than what’s already been written for the <a href="https://ttntm.me/appendix/" class="int">Appendix</a>: this website should “feel like me and change with me”, and “offer content about things that matter to me”.</p>
<p>That’s some pretty decent principles that I intend to adhere to. And considering the amount of past changelog entries, future changes are basically a certainty anyway.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>One Year of Lifting Weights</title>
        
          
            <category term="lifting" />
          
            <category term="offline" />
          
        
        <link href="https://ttntm.me/blog/one-year-of-lifting-weights/"/>
        
        <updated>2025-08-01T12:05:00Z</updated>
        <id>https://ttntm.me/blog/one-year-of-lifting-weights/</id>
        <content type="html"><![CDATA[
          
            
            <p>We visited Portugal last year. I still remember how heavy the luggage felt that we had with us: 2 suitcases, weighing around 20 kg each.</p>
<p>One year later, I now joke about that, and laugh, whenever I tell anyone about it. I didn’t think much of it at the time, but these suitcases pissed me off so much that I got back into lifting weights. That’s something I’d only briefly been doing during my early 20s, but stopped at some point. A brand-new gym opening around the corner from where we live, only a couple of weeks prior to the suitcase frustration, certainly helped with that.</p>
<p>Nowadays, I’m handling 500 times more weight than that ridiculous 20 kg suitcase on an average Wednesday. Or Monday. And also on Friday.</p>
<p>In the past, I worked out at home. I owned a bench, a barbell, weight plates, dumbbells and a lot of other stuff that I eventually ran out of space for when my girlfriend, now wife, moved in. What followed, were comfortable years of sedentary life, with the occasional bike ride, swim, or walk. No proper exercise, just <em>lots</em> of sitting.</p>
<p>I also lost weight gradually, probably due to my insane metabolism, and went down to less than ~70 kg measured in June '24. That’s not concerningly low, but also far from ideal for a 188 cm tall man. No wonder I felt weak. I <em>was</em> weak.</p>
<h2 id="taking-action" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#taking-action"><span>Taking Action</span></a></h2>
<p>Joining a gym is one thing, but you’ve also got to go there and do something useful. Fortunately, I still had some old training plan lying around, some pretty basic stuff for absolute beginners. I started with that, but quickly realized just how useless it actually was. Not saying that I’m an expert trainer or anything, but if you’ve got half a brain and only a minimal sense of feeling your own body, a waste of time becomes obvious easily.</p>
<p>I switched to <a class="ext" href="https://swoleateveryheight.blogspot.com/2016/02/gzcl-applications-adaptations.html" rel="noopener">GZCLP</a> (GZCL Linear Progression for new lifters) on July 19th '24 and did two 12-week cycles of the program. Working through it was extremely motivating, and the results came in as expected. Easy novice gains, well deserved.</p>
<p>When I got close to finishing the first cycle, around December '24, I had to deal with increasingly bothersome hip pain from squatting. I managed to find the reason (long femurs), and also figured out how to fix it: proper shoes that elevate the heel, and a couple of weeks of re-learning proper technique. No more pain.</p>
<p>After GZCLP, I did a month of arm specialization. Another program I had lying around from the past. It was ok, results were good, and it made me realize that I should focus on arms in the assistance work of the next long term program.</p>
<p>Research and decision-making took me a couple of weeks, but I eventually chose something simple, with proven results, and ended up with <a class="ext" href="https://www.jimwendler.com/blogs/jimwendler-com/101065094-5-3-1-for-a-beginner" rel="noopener">5/3/1 for Beginners</a>. I started the program on June 9th '25, and I’m very happy with my choice. I just finished the second week of my third 3-week cycle, and progress has been very good so far: tons of volume, and manageable recovery.</p>
<h2 id="results" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#results"><span>Results</span></a></h2>
<p>Lifting 100 kg off the floor for the first time felt <em>amazing</em>, and my deadlift is at a comfortable 130 kg now. I haven’t tested for a 1RM in a while, but I like to believe that 140 kg should be possible.</p>
<p>Squatting 100 kg was scary the first time, but worked out surprisingly well. I did 105 kg in April '25, but haven’t tested for anything higher than that yet.</p>
<p>Bench strength is growing slower, but there is good progress: from sub 40 kg reps a year ago, to a 1RM of 70+ kg (haven’t tested above 70 yet).</p>
<p>And then there’s the overhead press. Weakest for most, even more so for weak-shouldered computer workers - I started with the empty bar (20 kg), and got it up to 50+ kg in a year.</p>
<p>Progress was also made with my body weight. I gained about 15 kg, reaching a total mass of 90 kg, and I’m at a comfortable 88 kg now. I still eat <em>everything</em>, and I’ve definitely got to reduce the body fat percentage soon. But: I look healthy (maybe even fit?) now, and I had to get a couple of new clothes.</p>
<p>—</p>
<p>On to another year of lifting heavy stuff off the ground, I guess - let’s see where this journey takes me.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 31, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w31/"/>
        
        <updated>2025-08-01T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2025-w31/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" class="ext u-like-of" rel="noopener">Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity</a><br>
I’d be lying if I said that I’m surprised by the observed result</li>
<li><a href="https://leanrada.com/notes/css-only-lqip/" class="ext u-like-of" rel="noopener">Minimal CSS-only blurry image placeholders</a><br>
Definitely something I’m going to try the next time I need image placeholders anywhere</li>
<li><a href="https://ctrl-c.club/~loghead/zine/Ctrl-ZINE.Issue.21.pdf" class="ext u-like-of" rel="noopener">Ctrl-ZINE #21</a><br>
Ctrl-ZINE’s Issue 21, full of fresh articles and links</li>
<li><a href="https://dev.to/rgeraldporter/building-expressive-monads-in-javascript-introduction-23b" class="ext u-like-of" rel="noopener">Building Expressive Monads in Javascript</a><br>
About monads in JS</li>
<li><a href="https://blog.basementcommunity.com/everything-i-do-when-hosting-a-website/" class="ext u-like-of" rel="noopener">everything i personally do when hosting a website on a new server</a><br>
A link for future me, and anyone else who might want to self-host things at some point (again)</li>
<li><a href="https://martinrue.com/javascript-chose-violence/" class="ext u-like-of" rel="noopener">JavaScript Chose Violence</a><br>
Reason why we love to hate JS</li>
<li><a href="https://www.kryogenix.org/code/browser/theme-transition.html" class="ext u-like-of" rel="noopener">View transitions for changing themes</a><br>
Keeping this in mind for the future; looking forward to playing with view transition when they’re finally supported by all major browsers</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://psychicnewborn.neocities.org" class="ext u-like-of" rel="noopener">psychic newborn</a><br>
The coolest website I visited in a very long time</li>
<li><a href="https://whoisguilty.com" class="ext u-like-of" rel="noopener">Who is guilty</a><br>
“A story about a designer and a developer making a website”</li>
<li><a href="https://kohlhofer.com/city/" class="ext u-like-of" rel="noopener">City Generator</a><br>
Prcedurally generated, hand-drawn cityscape - love it!</li>
<li><a href="https://rssfilter.com" class="ext u-like-of" rel="noopener">Clean Your Feed</a><br>
Haven’t used it yet, but looks quite interesting: a tool that gives you “a clean, filtered version of an RSS feed, so you only see the content that matters to you”</li>
<li><a href="https://mostly-adequate.gitbook.io/mostly-adequate-guide" class="ext u-like-of" rel="noopener">Professor Frisby’s Mostly Adequate Guide to Functional Programming in JavaScript</a><br>
A <em>great</em> resource for anyone working with JS</li>
<li><a href="https://webstatus.dev" class="ext u-like-of" rel="noopener">Web Platform Status</a><br>
A tool for web developers - “query and track features shipped in major browsers”</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Webmentions V2</title>
        
          
            <category term="database" />
          
            <category term="guide" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/webmentions-v2/"/>
        
        <updated>2025-06-19T12:05:00Z</updated>
        <id>https://ttntm.me/blog/webmentions-v2/</id>
        <content type="html"><![CDATA[
          
            
            <p>Earlier this year, <a href="https://ttntm.me/blog/implementing-webmentions/" class="int">I added webmentions to my website</a>.</p>
<p>Back then, I relied on the API that <span>webmention.io</span> provides, and on fetching data from there as part of this website’s build pipeline. I added caching, of course, but considering the frequency of both, new content, and new webmentions, my site’s API usage felt a little excessive.</p>
<p>So, I started thinking about ways to persist my webmention data somewhere.</p>
<p>After a bit of research, I figured out that I could use <a class="ext" href="https://developers.cloudflare.com/kv/" rel="noopener">Cloudflare Workers KV</a> to persist my webmention data. This website’s already hosted there, so adding Workers KV as a data storage felt like a really convenient option.</p>
<p>A high level overview of the revised implementation:</p>
<ol>
<li><span>webmention.io</span> collects webmentions</li>
<li>For each new mention: <span>webmention.io</span> sends an HTTP request to a Cloudflare Worker (via its webhooks feature)</li>
<li>The CF Worker updates the key-value storage using the incoming data</li>
<li>The CF Worker triggers a rebuild of the website</li>
<li>Webmentions are fetched from the key-value storage as part of the website build</li>
</ol>
<h2 id="worker-logic" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#worker-logic"><span>Worker Logic</span></a></h2>
<p>I set up a Cloudflare Worker to handle incoming webhooks from <span>webmention.io</span>.</p>
<p>The code is pretty simple and can be found here: <a class="ext" href="https://codeberg.org/ttntm/wm-svc-ttntm/src/branch/main/src/index.ts" rel="noopener">ttntm/wm-svc-ttntm</a></p>
<p>I’ve kept my previous approach of aggregating metrics only (i.e. counts of likes, replies and shares per post), and I’m storing that data in KV’s <a class="ext" href="https://developers.cloudflare.com/kv/api/write-key-value-pairs/#metadata" rel="noopener">metadata</a>. There’s a reason for doing that, and I’ll elaborate on that in the next section.</p>
<p>After storing the data, the worker triggers a website build, to ensure that new and updated webmention data gets published as soon as possible.</p>
<h2 id="website-build" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#website-build"><span>Website Build</span></a></h2>
<p>As part of the build, webmention data gets fetched from the KV storage. I’m using the <a class="ext" href="https://github.com/cloudflare/cloudflare-typescript" rel="noopener">TypeScript library for the Cloudflare API</a>, which keeps things rather simple.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> Cloudflare <span class="token keyword">from</span> <span class="token string">'cloudflare'</span><br><span class="token keyword">import</span> dotenv <span class="token keyword">from</span> <span class="token string">'dotenv'</span><br><br>dotenv<span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> wmMap <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Map</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Cloudflare</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      <span class="token literal-property property">apiToken</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CF_TOKEN</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>    <span class="token comment">// Automatically fetches more pages as needed.</span><br>    <span class="token keyword">for</span> <span class="token keyword">await</span> <span class="token punctuation">(</span><span class="token keyword">const</span> key <span class="token keyword">of</span> client<span class="token punctuation">.</span>kv<span class="token punctuation">.</span>namespaces<span class="token punctuation">.</span>keys<span class="token punctuation">.</span><span class="token function">list</span><span class="token punctuation">(</span><span class="token string">'09b53...'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">account_id</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CF_ACC_ID</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      wmMap<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>key<span class="token punctuation">.</span>name<span class="token punctuation">,</span> <span class="token punctuation">{</span><br>        <span class="token operator">...</span>key<span class="token punctuation">.</span>metadata<br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>ex<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>ex<span class="token punctuation">.</span>message <span class="token operator">||</span> ex<span class="token punctuation">)</span><br>  <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> wmMap<br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>The <code>list()</code> method (<a class="ext" href="https://developers.cloudflare.com/kv/api/list-keys/#list-method" rel="noopener">CF docs</a>) is used to list all the keys in my KV namespace, and the keys are returned including their metadata. As mentioned above, that’s the reason I’m storing the webmention data there - no need for an additional loop to obtain each key’s value, leading to faster builds.</p>
<p><em>NB: there are limits to data storage in metadata, 1024 bytes to be specific, but I’m not even close to that with the miniature objects I’m storing there.</em></p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>I’m happy to have come up with this v2 approach of my webmentions integration, and I’m also glad I got to try out something new. Cloudflare Workers KV is a pretty cool convenience feature when an actual database might be overkill, and I’ll definitely keep that in mind for future projects.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 24, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w24/"/>
        
        <updated>2025-06-11T14:55:00Z</updated>
        <id>https://ttntm.me/likes/2025-w24/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://cybercultural.com/p/web-design-1997/" class="ext u-like-of" rel="noopener">The 3 Gurus of 90s Web Design: Zeldman, Siegel, Nielsen</a><br>
A very cool article, that sparked a lot of nostalgia in me</li>
<li><a href="https://ohhfishal.net/posts/dont-solve-problems-you-dont-have/" class="ext u-like-of" rel="noopener">Don’t solve problems you don’t have. You’re literally creating problems.</a><br>
Good one, and also something that should be kept in mind whenever starting a new project</li>
<li><a href="https://fredrocha.net/2025/05/21/small-web-is-beautiful/" class="ext u-like-of" rel="noopener">Small (web) is beautiful</a><br>
Very true, indeed; also lots of good links in that post</li>
<li><a href="https://bemorewithless.com/the-psychology-of-stuff/" class="ext u-like-of" rel="noopener">The Psychology Of Stuff: Why We Want More</a><br>
Super interesting, and also useful when asking yourself “what do I actually need in my life?”</li>
<li><a href="https://chrisburnell.com/note/eleventy-animated-88x31/" class="ext u-like-of" rel="noopener">“Built with Eleventy” Animated 88x31</a><br>
This should not need an explanation - go and add it to your 11ty sites now!</li>
<li><a href="https://christianheilmann.com/2025/04/28/0x32/" class="ext u-like-of" rel="noopener">0x32</a><br>
“The 80s were shit” - a harsh reality check for 80s romanticists (like me, occasionally)</li>
<li><a href="https://82mhz.net/posts/2025/05/old-software-still-has-value/" class="ext u-like-of" rel="noopener">Old software still has value</a><br>
“A piece of software might be outdated, […] but that doesn’t mean it’s bad and can no longer be used”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.lumon-industries.work" class="ext u-like-of" rel="noopener">Work at Lumon MDR</a><br>
I was hoping somebody would make the MDR experience interactive when watching Severance S2 earlier this year; I recently discovered this project, and it’s just great!</li>
<li><a href="https://github.com/BenjaminAster/CSS-Minecraft" class="ext u-like-of" rel="noopener">CSS-Minecraft</a><br>
Quite amazing actually: a Minecraft clone made with pure HTML &amp; CSS – no JavaScript</li>
<li><a href="https://humans.maxcomperatore.com" class="ext u-like-of" rel="noopener">What the Hell are Ppl Doing?</a><br>
A website that’s literally going to inform you about what people are currently doing</li>
<li><a href="https://justfuckingusehtml.com" class="ext u-like-of" rel="noopener">Just fucking use HTML</a><br>
Just do it</li>
<li><a href="https://purrli.com" class="ext u-like-of" rel="noopener">Purrli</a><br>
An online cat purr generator!</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 19, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w19/"/>
        
        <updated>2025-05-06T22:00:00Z</updated>
        <id>https://ttntm.me/likes/2025-w19/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.loopwerk.io/articles/2025/cloudflare-waf-block-php/" class="ext u-like-of" rel="noopener">Blocking PHP requests using CloudFlare’s WAF rules</a><br>
<em>Super helpful</em>, can’t believe I didn’t think of doing that sooner</li>
<li><a href="https://www.ajpmonline.org/article/S0749-3797(25)00072-8/fulltext" class="ext u-like-of" rel="noopener">Premature Mortality Attributable to Ultraprocessed Food Consumption in 8 Countries</a><br>
Very interesting, and a reminder for folks to be mindful of what you eat</li>
<li><a href="https://moderncss.dev/modern-css-for-dynamic-component-based-architecture/" class="ext u-like-of" rel="noopener">Modern CSS For Dynamic Component-Based Architecture</a><br>
A long, detailed, and very helpful collection of CSS features and improvements from the last few years</li>
<li><a href="https://atthis.link/blog/2025/58671.html" class="ext u-like-of" rel="noopener">You Should Use /tmp/ More</a><br>
Something to consider, and it probably helps to stop unintentional digital hoarding</li>
<li><a href="https://www.wheresyoured.at/wheres-the-money/" class="ext u-like-of" rel="noopener">There Is No AI Revolution</a><br>
“Consumers don’t want this shit”, and neither do I…</li>
<li><a href="https://chsmc.org/2025/01/archivists/" class="ext u-like-of" rel="noopener">We could all be archivists</a><br>
IMO we <em>should</em> all be archivists to some extent</li>
<li><a href="https://osteophage.neocities.org/writing/in-praise-of-links" class="ext u-like-of" rel="noopener">In Praise of Links</a><br>
Something quite remarkable, about the value and importance of links - “a link compilation in praise of links”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://framework7.io" class="ext u-like-of" rel="noopener">Framework7</a><br>
“A free and open source framework to develop mobile, desktop or web apps with native look and feel” - looks interesting</li>
<li><a href="https://peoplesgdarchive.org" class="ext u-like-of" rel="noopener">The People’s Graphic Design Archive</a><br>
A stunning “crowd-sourced virtual archive of inclusive graphic design history”</li>
<li><a href="https://exploringjs.com/ts/" class="ext u-like-of" rel="noopener">Exploring TypeScript: TS 5.8 edition</a><br>
“A book for JS programmers” with a free HTML version available online</li>
<li><a href="https://standardebooks.org" class="ext u-like-of" rel="noopener">Standard Ebooks</a><br>
A collection of “free and liberated ebooks”</li>
<li><a href="https://github.com/sindresorhus/ky" class="ext u-like-of" rel="noopener">sindresorhus/ky</a><br>
“A tiny and elegant JavaScript HTTP client based on the Fetch API”; also something that made me realize that there’s always something you missed when you think you’ve seen it all</li>
<li><a href="https://naas.isalman.dev/no" class="ext u-like-of" rel="noopener">“No” as a service</a><br>
Without a doubt the best API I’ve come across in a very long time</li>
<li><a href="https://distrosea.com" class="ext u-like-of" rel="noopener">DistroSea</a><br>
A very cool “online platform that allows you to test drive 50+ Linux operating systems” in your browser</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Six String Memories</title>
        
          
            <category term="guitar" />
          
            <category term="offline" />
          
        
        <link href="https://ttntm.me/blog/six-string-memories/"/>
        
        <updated>2025-05-05T15:30:00Z</updated>
        <id>https://ttntm.me/blog/six-string-memories/</id>
        <content type="html"><![CDATA[
          
            
            <style>
  .guitar {
    max-height: 400px;
    mix-blend-mode: luminosity;
    object-fit: cover;
    transition: all .5s ease-in-out;

    &:hover {
      mix-blend-mode: revert;
    }
  }
</style>
<p>It must’ve been 2005 when I started playing guitar, fiercely motivated by the music I listened to at the time: anything loud and aggressive, from the Sex Pistols, Nirvana and the Misfits, all the way to Metallica, Slayer and some - mostly Swedish - old school Death Metal.</p>
<p>Fast-forwarding to present day, I recently sold the last electric guitar I owned, and used the money to buy myself a hollow body guitar. While doing that, I thought of all the guitars I ever owned, and realized that it’d be a great idea to write down what I remember about them.</p>
<h2 id="a-random-acoustic-guitar" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#a-random-acoustic-guitar"><span>A Random Acoustic Guitar</span></a></h2>
<p>My very first guitar, that I borrowed it from a classmate’s girlfriend. Unfortunately, I couldn’t find a picture of it anywhere.</p>
<p>It was a regular nylon string acoustic guitar that was alright, I guess, and served me well learning the basics. But considering that I was after “loud and aggressive”, the lack of amplified distortion had me looking into electric guitars after a couple of months.</p>
<h2 id="an-epiphone-sg-special" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#an-epiphone-sg-special"><span>An Epiphone SG Special</span></a></h2>
<div class="grid grid2 gap2 mt1">
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/epiphone-sg.jpg" class="img-fluid guitar img-center m0" alt="A photo of an Epiphone SG Special electric guitar">
  </div>
  <div>
    <p>
      The first electric guitar I bought. I loved the look of the SG back then, but it was really cheap, and only "special" was in its name. I was somehow quite unlucky with it too, because soon after I bought it, a piece of the nut broke off and I had to have it repaired, which ended up costing about half of what I paid for the guitar.
    </p>
    <p>
      Customizing it with some stickers and a spider web felt appropriate, considering I spent a couple of months in a hardcore punk band with some friends. We didn't manage to write more than one song, but we definitely had fun playing the hell out of that one until the band broke up because we were kicked out of the rehearsal room we were using at the time.
    </p>
  </div>
</div>
<p>Despite not playing it at all, I kept this guitar around for many years, but eventually sold it in 2016.</p>
<h2 id="a-jackson-kelly-ke-3" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#a-jackson-kelly-ke-3"><span>A Jackson Kelly KE3</span></a></h2>
<div class="grid grid2 gap2 mt1 mb1">
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/j1.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Jackson Kelly KE3 electric guitar">
  </div>
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/j2.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Jackson Kelly KE3 electric guitar">
  </div>
</div>
<p>I bought this guitar in 2006 or 2007, and it was a significant upgrade from the SG. It was also my first true “metal axe”, more evil looking than an Explorer, and close enough to what I actually wanted, but couldn’t get anywhere at the time (an Ironbird, see below).</p>
<p>The guitar played well enough too, and I learned a ton about guitar maintenance and especially Floyd Rose Tremolo systems. I also replaced the Jackson humbuckers with Seymour Duncans, which was a very reasonable upgrade.</p>
<p>I sold the Kelly in 2013 or 2014, after it’d seen a lot of action, both on stage and for a couple of recordings.</p>
<h2 id="a-ran-guitars-ironbird" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#a-ran-guitars-ironbird"><span>A Ran Guitars Ironbird</span></a></h2>
<div class="grid grid2 gap2 mt1 mb1">
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/ran-ironbird1.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Ran Guitars Ironbird electric guitar">
  </div>
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/ran-ironbird2.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Ran Guitars Ironbird electric guitar">
  </div>
</div>
<p>Ever since seeing the old photos of Morbid Angel’s Trey Azagthoth and his red Ironbird, I knew that this was the guitar I wanted. It took me years of scouring ebay and other second hand option until I found one in 2011.</p>
<p>It was a Ran Guitars made-to-order piece that I bought for a <em>very</em> good price from some guy in Spain, and I was ecstatic when it arrived. I loved playing it and there was hardly anything to complain about. I quickly replaced the EMG active humbucker with a custom Bare Knuckle Warpig humbucker that was perfectly suited for the extremist Black/Death Metal I used the guitar for at the time.</p>
<p>After 2016, I hardly played guitar, and when I did, it wasn’t with a band, and it wasn’t any metal. It took me a couple of years to get ready to sell this beast, however. But when I did, at some point in 2021, I ended up getting about double of what I paid for it, which was very helpful at the time.</p>
<h2 id="a-bc-rich-ironbird" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#a-bc-rich-ironbird"><span>A BC Rich Ironbird</span></a></h2>
<div class="grid grid2 gap2 mt1 mb1">
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/bcr-ironbird1.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Ran Guitars Ironbird electric guitar">
  </div>
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/bcr-ironbird2.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Ran Guitars Ironbird electric guitar">
  </div>
</div>
<p>My roommate found this one in a second hand shop in 2013, and I pretty much ran there to get it.</p>
<p>It turned out that it was a model from 1996, possibly even made in the USA, but definitely properly built. It also came with the reversed headstock, which made it look somewhat similar to a scythe, which I loved.</p>
<p>Compared to the Ran Ironbird, it was a little less beastly sounding (no Warpig in this one), but I loved it anyway, because playing a real 90s instrument felt a bit more authentic to my old school Black/Death Metal obsessed self back then.</p>
<p>I sold this one in 2021, at the same time as the other Ironbird, and got a good price for it too. Interestingly, they were both bought by the same guy, a collector from a nearby country.</p>
<h2 id="a-gibson-flying-v" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#a-gibson-flying-v"><span>A Gibson Flying V</span></a></h2>
<div class="grid grid2 gap2 mt1 mb1">
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/gibson-v1.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Gibson Flying V electric guitar">
  </div>
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/gibson-v2.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Gibson Flying V electric guitar">
  </div>
</div>
<p>I’m not exactly sure why I bought this guitar, but I did so at some point in 2013. It was fun to play though, and I enjoyed both how it sounded, and how light it was. Playing it while seated was pretty annoying, however, and I didn’t use it very much compared to the two Ironbirds.</p>
<p>When I eventually sold it in 2017, I lost quite a bit of money - the buyer was an extremely annoying guy that just kept talking, mostly nonsense, and eventually wore me out. I really needed the money back then, but I should’ve walked away instead. Not doing that is something I still regret.</p>
<h2 id="a-mysterious-les-paul" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#a-mysterious-les-paul"><span>A Mysterious Les Paul</span></a></h2>
<div class="grid grid2 gap2 mt1 mb1">
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/lespaul1.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Les Paul electric guitar">
  </div>
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/lespaul2.jpg" class="img-fluid guitar img-center m0" alt="A photo of a Les Paul electric guitar">
  </div>
</div>
<p>I saw this guitar listed on a classified ads site in 2015. It looked nice and wasn’t too expensive, so I decided to go there to check it out. The seller was a boy who inherited the guitar from his grandpa, but he was into playing the piano and wanted to get himself a keyboard. I was told that the grandpa brought the guitar from a trip to Asia, probably China or Japan, at some point in the 70s, and that it may be a copy of an old Ibanez, or an actual one that “fell off a truck…” before getting stamped with the logo.</p>
<p>It sounded alright and was in a good condition, so I walked away with it and enjoyed it for a while. It was also the only guitar I had left after selling the two Ironbirds in 2021, but I didn’t play a lot in recent years.</p>
<p>A couple of weeks ago, I decided to sell this Les Paul to get myself a hollow body guitar instead.</p>
<h2 id="an-ibanez-as-53" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#an-ibanez-as-53"><span>An Ibanez AS53</span></a></h2>
<div class="grid grid2 gap2 mt1 mb1">
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/ibanez1.jpg" class="img-fluid guitar img-center m0" alt="A photo of an Ibanez AS53 electric guitar">
  </div>
  <div>
    <img src="https://ttntm.me/static/img/blog/guitars/ibanez2.jpg" class="img-fluid guitar img-center m0" alt="A photo of an Ibanez AS53 electric guitar">
  </div>
</div>
<p>After selling the above Les Paul, I added a bit of money and bought an Ibanez AS53. It’s my first hollow body guitar, and I chose it because not having to set up an amplifier etc. is definitely going to help me get back into playing more regularly.</p>
<p>The guitar itself looks absolutely stunning and sounds great. It also plays excellently, and it definitely feels like an instrument with a much higher price tag, which is quite remarkable. It’s also a little bigger than the other guitars I owned, but that’s not a problem at all.</p>
<p>Right now, I really love playing it and I often find myself looking at it for a moment due to how good it looks. It’s definitely not a metal guitar, so I’m actually motivated to play a lot more diverse stuff, and to explore the many styles and genres that I have neglected over the years.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 16, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w16/"/>
        
        <updated>2025-04-15T14:10:00Z</updated>
        <id>https://ttntm.me/likes/2025-w16/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://kayserifserif.place/posts/2025/what-should-i-use-to-make-a-website" class="ext u-like-of" rel="noopener">What should I use to make a website?</a><br>
A list of “some resources for website-curious friends”</li>
<li><a href="https://microsoft.design/articles/introducing-kermit-a-typeface-for-kids/" class="ext u-like-of" rel="noopener">Introducing Kermit: A typeface for kids</a><br>
A pretty cool idea - I despise today’s “video first” approach very much, and I think that the idea of putting “kids in a good mood, making them interested in reading” is great</li>
<li><a href="https://www.theverge.com/tech/639126/india-frankenstein-laptops" class="ext u-like-of" rel="noopener">The rise of ‘Frankenstein’ laptops in New Delhi’s repair markets</a><br>
Very interesting - about “India’s repair culture” that “gives new life to dead tech”</li>
<li><a href="https://antfu.me/posts/async-sync-in-between" class="ext u-like-of" rel="noopener">Async, Sync, in Between</a><br>
A very detailed article about the function coloring problem, and what we could do about it</li>
<li><a href="https://www.atlasobscura.com/articles/capn-crunch-whistle" class="ext u-like-of" rel="noopener">Early Hackers Used Whistles From Cap’n Crunch Cereal Boxes</a><br>
About the “connections between hacking, computing, and cereal”</li>
<li><a href="https://arstechnica.com/ai/2025/03/devs-say-ai-crawlers-dominate-traffic-forcing-blocks-on-entire-countries/" class="ext u-like-of" rel="noopener">Open source devs say AI crawlers dominate traffic, forcing blocks on entire countries</a><br>
About the plague that is AI crawlers</li>
<li><a href="https://northflank.com/blog/why-we-ditched-next-js-and-never-looked-back" class="ext u-like-of" rel="noopener">Why we ditched Next.js and never looked back</a><br>
Many more reasons why “Next.js is bloated, slow, and unnecessarily complex”</li>
<li><a href="https://heydonworks.com/article/poisoning-well/" class="ext u-like-of" rel="noopener">Poisoning Well</a><br>
About “bad actor crawlers” and poisoning the LLMs they work for</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://webtui.ironclad.sh" class="ext u-like-of" rel="noopener">WebTUI</a><br>
A modular CSS library that brings the beauty of Terminal UIs to the browser</li>
<li><a href="https://jumbled.netlify.app" class="ext u-like-of" rel="noopener">Jumbled</a><br>
A game: rearrange jumbled letters to find words</li>
<li><a href="https://codepen.io/HejChristian/full/KwKJdNd" class="ext u-like-of" rel="noopener">Blend Mode Cheat Sheet</a><br>
A <em>super helpful</em> comparison tool for CSS’ <code>mix-blend-mode</code></li>
<li><a href="https://betula.mycorrhiza.wiki" class="ext u-like-of" rel="noopener">Betula</a><br>
A “free federated self-hosted single-user bookmarking software”</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Migrating from Fauna to Supabase</title>
        
          
            <category term="database" />
          
            <category term="guide" />
          
            <category term="project" />
          
        
        <link href="https://ttntm.me/blog/migrating-from-fauna-to-supabase/"/>
        
        <updated>2025-04-12T17:45:00Z</updated>
        <id>https://ttntm.me/blog/migrating-from-fauna-to-supabase/</id>
        <content type="html"><![CDATA[
          
            
            <p>The announcement that Fauna made the decision to sunset their service hit me by surprise. The main reason for this decision seems to be money - as they’ve put it: “it is not possible to raise the capital needed” (see <a class="ext" href="https://fauna.com/blog/the-future-of-fauna" rel="noopener">The Future of Fauna</a>).</p>
<p>That’s not exactly great news, but I’m in no position to complain about that anyway, considering that I was only using their free tier for 2 of my side projects. Over time, those 2 projects became rather important to me (see <a href="https://ttntm.me/blog/making-your-own-tools/" class="int">Making Your Own Tools</a>), so letting them die with their database service wasn’t ever an option.</p>
<p>I quickly started looking for alternatives to the service Fauna used to offer and even considered self-hosting a database server. Eventually, I found myself exploring Supabase and dug into their documentation. It looked just like what I needed, and a couple of days later, they published their own guide about migrating from Fauna to Supabase (linked <a href="https://ttntm.me/everything.xml#data-migration" class="int">below</a>) - great timing!</p>
<p>Switching to a relational database is definitely not an insignificant change, but I’m using them a lot at work. Also, the Supabase service offers convenience features, like “automatically generating REST APIs for each table, allowing effortless querying from your application” via their SDKs.</p>
<h2 id="supabase-setup" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#supabase-setup"><span>Supabase Setup</span></a></h2>
<p>Account creation and project setup was straightforward, no surprises there. The only thing to keep in mind, is that “free projects are paused after 1 week of inactivity” and that there’s a “limit of 2 active projects” (see <a class="ext" href="https://supabase.com/pricing" rel="noopener">Pricing</a>) - both things that I’m ok with at the moment.</p>
<p>I proceeded to set up the data model (tables), which was easy too, thanks to good documentation I wrote many years ago. For watch3r, the resulting database schema looks like this:</p>
<img src="https://ttntm.me/static/img/blog/db_schema_watch3r.jpg" class="img-fluid img-center auto-invert" alt="A screenshot of a database schema that consists of 3 tables">
<p>Finally, there’s <a class="ext" href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener">RLS</a> and policies. That stuff cost me a bit more time than expected, and I ended up with a <em>very</em> permissive setup, while keeping RLS on, because I’m only ever accessing the database with a backend service user.</p>
<p>A policy hint if you should struggle with errors when performing <code>delete</code>, <code>insert</code>, or <code>update</code> operations:</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">--this code creates a permissive `insert` policy for the given table</span><br><span class="token keyword">create</span> policy <span class="token string">"Allow insert"</span><br><span class="token keyword">on</span> <span class="token string">"public"</span><span class="token punctuation">.</span><span class="token string">"watchlist"</span><br><span class="token keyword">for</span> <span class="token keyword">insert</span><br><span class="token keyword">to</span> <span class="token keyword">public</span><br><span class="token keyword">with</span> <span class="token keyword">check</span> <span class="token punctuation">(</span> <span class="token boolean">true</span> <span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And for code <a class="ext" href="https://supabase.com/docs/guides/troubleshooting/performing-administration-tasks-on-the-server-side-with-the-servicerole-secret-BYM4Fa" rel="noopener">performing tasks on the server side</a>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> createClient <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@supabase/supabase-js'</span><br><br><span class="token keyword">const</span> supabase <span class="token operator">=</span> <span class="token function">createClient</span><span class="token punctuation">(</span>supabaseUrl<span class="token punctuation">,</span> serviceRoleSecret<span class="token punctuation">,</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">auth</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">persistSession</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">autoRefreshToken</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">detectSessionInUrl</span><span class="token operator">:</span> <span class="token boolean">false</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<h2 id="data-migration" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#data-migration"><span>Data Migration</span></a></h2>
<p>For recept0r, I followed <a class="ext" href="https://supabase.com/blog/migrating-from-fauna-to-supabase" rel="noopener">Supabase’s migration guide</a>. I worked with freshly exported data (see <a href="https://ttntm.me/blog/how-to-export-data-from-fauna/" class="int">How to Export Data from Fauna</a> from last year), and there were no surprises - things just worked.</p>
<p>However, normalizing the data and making sure it ends up in multiple columns (compared to just one column with JSON in it) was a little trickier. Properly extracting arrays and timestamps from the JSON took me a moment, but I eventually figured out how to make my <a class="ext" href="https://codeberg.org/ttntm/spb_migrate/src/branch/main/scripts/recept0r/migrate_recept0r.sql" rel="noopener">SQL query</a> produce the correct result.</p>
<p>For watch3r, I chose a different approach: I skipped the temporary table filled with JSON, and inserted properly mapped data right away instead. I don’t mind writing SQL queries, but skipping the extra step actually saved me some time. Time, that I immediately used to figure out a rather hacky way of preserving the chronological order of the database entries.</p>
<p>I’m not proud of it, and I’m sure there are many better ways to accomplish chronological order (i.e. working with a proper <code>created</code> date from the beginning…), but it worked - you can have a look at <a class="ext" href="https://codeberg.org/ttntm/spb_migrate/src/branch/main/scripts/watch3r/migrate.js#L26" rel="noopener">the script</a> if you’re curious.</p>
<h2 id="updating-the-web-applications" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#updating-the-web-applications"><span>Updating the Web Applications</span></a></h2>
<p>recept0r is built using Vue 3 with TypeScript, thanks to a complete rewrite in 2021. Updating the back end functions led to matching changes in the front end code, mostly affecting Vuex actions. Changes to actual UI components were handled using search and replace, and TypeScript made errors visible immediately.</p>
<p>Supabase’s RLS caused me quite a bit of pain when I started to working on <code>insert</code>/<code>update</code> operations, but - as mentioned above - I eventually figured out how to properly initialize the SDK and how to set up permissive policies for my tables.</p>
<p>watch3r is also built using Vue 3, but I didn’t use TypeScript back in 2020. Updating the back end was rather easy, but the front end was a bit of a challenge. No TypeScript meant that I had to dig deep into the code to make sure I don’t forget any dependencies. Also, past me was lazy and (ab)used IMDb IDs as primary keys, which really wasn’t a good idea.</p>
<p>In addition to that, I noticed that a function, that’s triggered automatically whenever a user signs up, somehow stopped working. I had to repair that, because I’m using user IDs as foreign keys in the data model (see above) now.</p>
<p>Both applications are functional again, with 0 actual downtime, and that leaves me with a positive experience regarding this migration. I’ll definitely remember that it’s a <em>lot</em> easier to update TypeScript applications, however.</p>
<h2 id="backups" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#backups"><span>Backups</span></a></h2>
<p>As mentioned earlier, using Supabase’s free tier means that “projects are paused after 1 week of inactivity”. The free plan also doesn’t include project backups.  That’s not ideal, but I figured out an approach that can take care of both: using scheduled backups to cause the necessary “activity” to prevent your projects getting paused.</p>
<p>I’m using <a class="ext" href="https://supabase.com/docs/guides/storage" rel="noopener">Supabase Storage</a> to store JSON backups of all tables used in the respective project. These JSON backups are created by <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/functions/backup.js" rel="noopener">a simple HTTP-triggered function</a> that runs every 6 days based on a Make scenario (workflow).</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>It’s a bit sad that Fauna’s going to disappear, but it’s no use to complain about it. I handled 2 successful migrations to Supabase in less time than expected, learned a couple of things, and kept both applications alive - I’m glad things turned out that way, but I also hope I won’t have to do it all again any time soon.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Website Evolution: Q1 2025</title>
        
          
            <category term="evolution" />
          
            <category term="series" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/website-evolution-q1-2025/"/>
        
        <updated>2025-04-02T15:15:00Z</updated>
        <id>https://ttntm.me/blog/website-evolution-q1-2025/</id>
        <content type="html"><![CDATA[
          
            
            <p>As the first quarter of 2025 comes to an end, it’s time for another entry in the website <a href="https://ttntm.me/tags/evolution/" class="int">#evolution</a> series.</p>
<p>This post covers versions <a href="https://ttntm.me/changelog/#3-20" class="int">3.20</a> to <a href="https://ttntm.me/changelog/#4-1" class="int">4.1</a>.</p>
<h2 id="content-and-pages" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#content-and-pages"><span>Content and Pages</span></a></h2>
<p>This section provides an overview of content and pages with details on what was added, changed and/or (re)moved.</p>
<h3 id="best-of" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#best-of"><span>Best Of</span></a></h3>
<p>URL: <a href="https://ttntm.me/best-of/" class="int">/best-of</a></p>
<p>A new page, and essentially a list of my most popular content.</p>
<p>I’m using <a href="https://ttntm.me/everything.xml#webmentions" class="int">webmention data</a> and a custom filter to transform the raw data into a list of my top 16 blog posts based on a “popularity score” (i.e. a sum of likes, reposts and replies). That data doesn’t include any page properties (i.e. title, publication date), so I had to figure out a way to access properties of the <code>page</code> object using <code>slug</code> values.</p>
<p>It took a minute, but I eventually remembered that 11ty collections can be passed to shortcodes (see <a href="https://ttntm.me/notes/#33" class="int">/notes/#33</a>). Using that approach, getting specific <code>page</code> properties is simple and reusable by default (code: <a href="https://github.com/ttntm/ttntm/blob/master/utils/shortcodes.js#L106" class="ext u-like-of" rel="noopener">shortcodes.js#L106</a>).</p>
<h3 id="homepage" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#homepage"><span>Homepage</span></a></h3>
<p>The homepage looks different, just in case you haven’t noticed.</p>
<p>“Recent Activity” was turned into a list of the 5 most recent entries grouped by collection (i.e. blog, likes, notes), which should offer a more hub-like experience than the previous version (4 most recent items from all collections). The layout also changed slightly to accommodate the extra content.</p>
<p>At the bottom of the page, there’s a new “Tagged Content” section, providing quick access to posts about specific topics.</p>
<h3 id="then" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#then"><span>Then</span></a></h3>
<p>URL: <a href="https://ttntm.me/then/" class="int">/then</a></p>
<p>An archive of past /now page updates.</p>
<p>Nothing fancy: I started archiving older versions of my <a href="https://ttntm.me/now/" class="int">/now</a> page, which is something I wanted to do for a long time. I’m glad I finally added it to this website, and I’m looking forward to seeing it grow over time.</p>
<h3 id="other-content-changes" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#other-content-changes"><span>Other Content Changes</span></a></h3>
<ul>
<li>Added a <a href="https://ttntm.me/bookmarks/#web-directories" class="int">new section for web directories</a> to my /bookmarks page</li>
<li>Added an architecture diagram to the <a href="https://ttntm.me/appendix/" class="int">/appendix</a></li>
<li>Added status badges for entries on the <a href="https://ttntm.me/projects/" class="int">/projects</a> page</li>
<li>Added the <a href="https://ttntm.me/tags/project/" class="int">#project</a> tag</li>
<li>Added annual grouping to the <a href="https://ttntm.me/likes/" class="int">/likes</a> page</li>
<li>Rewrote the text on the <a href="https://ttntm.me/about/" class="int">/about</a> page</li>
</ul>
<h2 id="features-and-functionality" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#features-and-functionality"><span>Features and Functionality</span></a></h2>
<p>Additional features and changes enhancing the website’s functionality.</p>
<h3 id="copy-buttons-for-code-blocks" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#copy-buttons-for-code-blocks"><span>Copy Buttons for Code Blocks</span></a></h3>
<p>Over the years, I published a ton of code snippets on this website, but adding copy functionality wasn’t a priority.</p>
<p>I finally added copy buttons for code blocks - it only took ~60 lines of JavaScript, and implementing it was much less effort than I anticipated.</p>
<p>All details can be found in this post: <a href="https://ttntm.me/blog/adding-a-copy-button-to-code-blocks" class="int">Adding a Copy Button to Code Blocks</a></p>
<h3 id="webmentions" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#webmentions"><span>Webmentions</span></a></h3>
<p>Another things I wanted to add to this website for a <em>very</em> long time. Also, something that I thought would be a lot more complicated, but that was a wrong assumption.</p>
<p>Nowadays, there are a couple of wonderful services that make things rather easy, and it doesn’t take a lot of time - or effort - until real data comes in, making that whole IndieWeb thing feel very much alive.</p>
<p>I also figured out an alternative approach compared to seemingly inefficient code (i.e. the same sequential functions for <em>every</em> page/post) that’s prevalent in <em>a lot</em> of guides out there, which is always a good thing.</p>
<p>On this website, webmentions are displayed as rather minimal “vanity metrics” in the header at the moment:</p>
<img src="https://ttntm.me/static/img/blog/postHeader_wm.jpg" class="img-fluid img-center auto-invert" alt="screenshot of a post header showing title, published date, tags, and webmentions as icons (i.e. likes, comments) with numbers">
<p>I personally like it that way, and I’ve also got some positive feedback from readers about it.</p>
<p>All details can be found in this post: <a href="https://ttntm.me/blog/implementing-webmentions" class="int">Implementing Webmentions</a></p>
<h3 id="other-functional-changes" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#other-functional-changes"><span>Other Functional Changes</span></a></h3>
<ul>
<li>Added “Discuss on Mastodon” links to the feedback/share section at the end of blog posts/likes</li>
<li>Added the feedback/share section to entries in the <a href="https://ttntm.me/likes/" class="int">/likes</a> collection</li>
</ul>
<h2 id="look-and-feel" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#look-and-feel"><span>Look and Feel</span></a></h2>
<p>The website got a fresh coat of <s>paint</s> font. It’s been 4 years since <a href="https://ttntm.me/changelog/#3-0" class="int">3.0</a>, and after a lot of deliberation, I’ve decided to use a new font - “Space Mono” - for the content on this website. Headings, buttons, and a couple of other non-content elements are still using “Titillium Web”, which is a good match with the new content font.</p>
<p>Other than that:</p>
<ul>
<li>Added a slight gradient to the footer</li>
<li>Improved some blur that could occur during scale transforms
<ul>
<li>Almost perfect in FF now, still a bit wonky in Chromium-based browsers</li>
</ul>
</li>
<li>Tuned the external link icon position in hopes of better handling of edge cases (i.e. line breaks)</li>
<li>Updated the background color of code blocks (light theme)</li>
<li>Updated the placement of “Last update” dates across various pages</li>
<li>Updated the styling of links used in content
<ul>
<li>Boldness is mostly gone, except for headings/titles</li>
<li>Link colors were updated (light theme)</li>
</ul>
</li>
</ul>
<h2 id="setup-and-other-technical-stuff" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#setup-and-other-technical-stuff"><span>Setup and Other Technical Stuff</span></a></h2>
<p>Until <a href="https://ttntm.me/changelog/#3-6" class="int">3.6</a>, this website wasn’t using any JavaScript. I gradually added JS, and recently noticed that it’s all been inlined, which I felt should be changed. There are a couple of dedicated JS files now, containing code that’s been bundled/split with reusability and caching in mind.</p>
<p>A couple of months ago, Omnivore, a read-it-later app that used to provide the data for my <a href="https://ttntm.me/reading/" class="int">reading list</a>, shut down and left me in need of an alternative. I eventually decided to use <span>Raindrop.io</span>, transferred my data there, and recently wrote an article about the update: <a href="https://ttntm.me/blog/reading-list-redux" class="int">Reading List Redux</a></p>
<h3 id="plugins" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#plugins"><span>Plugins</span></a></h3>
<p>My plugin configuration changed, and I’m - finally! - using <a class="ext" href="https://github.com/11ty/eleventy-img" rel="noopener">eleventy-img</a>.</p>
<p>Other than that, I also added <a class="ext" href="https://github.com/rotorz/markdown-it-external-links" rel="noopener">markdown-it-external-links</a> to support outgoing webmentions for <a href="https://ttntm.me/likes/" class="int">/likes</a>.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 13, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w13/"/>
        
        <updated>2025-03-26T11:50:00Z</updated>
        <id>https://ttntm.me/likes/2025-w13/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://danburzo.ro/eleventy-slotted-content/" class="ext u-like-of" rel="noopener">Slotted content in Eleventy</a><br>
<em>Very</em> helpful - I needed this for a client project I’m currently working on, and I’ll probably end up refactoring parts of my website using this approach too</li>
<li><a href="https://syntackle.com/blog/eleventy-image-html-transform-plugin-disk-cache/" class="ext u-like-of" rel="noopener">Eleventy’s Image Plugin Disk Caching Approach For HTML Transform Method</a><br>
A pretty cool idea, and something I want to do for this site</li>
<li><a href="https://almanac.httparchive.org/en/2024/" class="ext u-like-of" rel="noopener">Web Almanac 2024</a><br>
A “comprehensive report on the state of the web, backed by real data and trusted web experts”</li>
<li><a href="https://michaelnotebook.com/wn/website_enhance.html" class="ext u-like-of" rel="noopener">How to use a personal website to enhance your ability to think and create?</a><br>
Many good thoughts in there, inspiring</li>
<li><a href="https://boehs.org/node/2025-call-for-bloggers" class="ext u-like-of" rel="noopener">Not Feeling Big Tech This Year? Start an Indie Blog!</a><br>
About owning your website, and being able to say and do as you please</li>
<li><a href="https://blog.cloudflare.com/open-source-all-the-way-down-upgrading-our-developer-documentation/" class="ext u-like-of" rel="noopener">Open source all the way down: Upgrading our developer documentation</a><br>
About Cloudflare migrating their developer documentation to Astro/Starlight</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://techcats.netlify.app" class="ext u-like-of" rel="noopener">TechCats</a><br>
A directory of the cats of people who make websites - simply delightful!</li>
<li><a href="https://benjojo.co.uk/u/benjojo/h/cR4dJWj3KZltPv3rqX" class="ext u-like-of" rel="noopener">benjojo/h/cR4dJWj3KZltPv3rqX</a><br>
About Cloudflare admitting that they’re analyzing login credentials</li>
<li><a href="https://servo.org" class="ext u-like-of" rel="noopener">Servo</a><br>
A “web browser rendering engine” that’s “adaptable to desktop, mobile, and embedded applications”</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/image" class="ext u-like-of" rel="noopener">input type=“image”</a><br>
I probably knew this existed at some point, but I forgot</li>
<li><a href="https://zero.rocicorp.dev" class="ext u-like-of" rel="noopener">Zero</a><br>
An “an open-source, general-purpose sync engine for the web” - almost sounds too good to be true, gotta give this one a try</li>
<li><a href="https://noyaml.com" class="ext u-like-of" rel="noopener">A lot of YAML</a><br>
A fun site about “why YAML is the right devops technology” for all of us</li>
<li><a href="https://slowroads.io" class="ext u-like-of" rel="noopener">slow roads</a><br>
A nice endless driving game that works in the browser</li>
<li><a href="https://nationalparktypeface.com" class="ext u-like-of" rel="noopener">National Park Typeface</a><br>
A <em>very</em> nice font</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 10, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w10/"/>
        
        <updated>2025-03-07T15:30:00Z</updated>
        <id>https://ttntm.me/likes/2025-w10/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://localghost.dev/blog/this-page-is-under-construction/" class="ext u-like-of" rel="noopener">This page is under construction</a><br>
A wonderful “love letter to the personal website”</li>
<li><a href="https://82mhz.net/posts/2025/03/listening-to-every-album-black-sabbath/" class="ext u-like-of" rel="noopener">Listening to every Album: Black Sabbath</a><br>
All Black Sabbath albums, remembered and reviewed by Andreas</li>
<li><a href="https://dusanmitrovic.rs/blog/post/2024-06-13/Sorry-1x-engineer-we're-not-all-equally-capable" class="ext u-like-of" rel="noopener">Sorry 1x engineer, we’re not all equally capable</a><br>
Some good points in that article</li>
<li><a href="https://www.lkhrs.com/blog/2025/that-was-good/" class="ext u-like-of" rel="noopener">“That was good”</a><br>
About the uncanny ability of smart people to find “the best reasons for being very sad”</li>
<li><a href="https://www.markpitblado.me/blog/busywork/" class="ext u-like-of" rel="noopener">Busywork</a><br>
About mankind’s “greatest gift”, “the ability to process information and produce novel insights”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.fringeconnections.com" class="ext u-like-of" rel="noopener">Fringe Connections</a><br>
Recently finished watching “Fringe”; this website offers a <em>ton</em> of information to dive into, like all the things one might have missed while watching, “Walter’s Food”, Observer appearances etc.</li>
<li><a href="https://coveryourtracks.eff.org" class="ext u-like-of" rel="noopener">Cover Your Tracks</a><br>
A website about “how trackers view your browser”</li>
<li><a href="https://patrickbrosset.com/lab/navigating-the-web-platform/" class="ext u-like-of" rel="noopener">Navigating the web platform</a><br>
Resources for web developers: “ways to keep track of what you can use, of what’s new in web browsers, and ways you can influence the development of the platform”</li>
<li><a href="https://www.c82.net/natural-colors/" class="ext u-like-of" rel="noopener">The Natural System of Colours</a><br>
A stunningly beautiful site about Moses Harris’ color wheels from the eighteenth century</li>
<li><a href="https://node-modules.dev" class="ext u-like-of" rel="noopener">Node ModulesInspector</a><br>
A tool to “visualize your node_modules, inspect dependencies, and more”</li>
<li><a href="https://github.com/baltiqa/paxromana" class="ext u-like-of" rel="noopener">baltiqa/paxromana</a><br>
The source code of a dark web marketplace that got hacked a while ago; an interesting codebase to explore</li>
<li><a href="https://tracker.greenweb.org" class="ext u-like-of" rel="noopener">Green Web Tracker</a><br>
A website about the “progress of the internet towards 100% fossil free hosting”</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 08, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w08/"/>
        
        <updated>2025-02-24T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2025-w08/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://selfh.st/sick-freaks/" class="ext u-like-of" rel="noopener">You are all a bunch of sick freaks</a><br>
About self-hosting</li>
<li><a href="https://flowchainsensei.wordpress.com/2025/02/17/is-software-development-really-a-kind-of-collaborative-knowledge-work/" class="ext u-like-of" rel="noopener">Is Software Development Really a Kind of Collaborative Knowledge Work?</a><br>
An interesting read that suggests that software development is “perhaps one of the purest examples of collaborative knowledge work in the modern economy”</li>
<li><a href="https://schepp.dev/posts/rethinking-find-in-page-accessibility-making-hidden-text-work-for-everyone/" class="ext u-like-of" rel="noopener">Rethinking Find-in-Page Accessibility: Making Hidden Text Work for Everyone</a><br>
About “the importance of accommodating diverse navigation patterns” when trying to “improve accessibility for everyone”</li>
<li><a href="https://obsidian.md/blog/free-for-work/" class="ext u-like-of" rel="noopener">Obsidian is now free for work</a><br>
Amazing news, will try to figure out if/how it fits the way I work with TKB</li>
<li><a href="https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-11/" class="ext u-like-of" rel="noopener">Speeding up the JavaScript ecosystem - Rust and JavaScript Plugins</a><br>
“Rust can work together with JavaScript plugins in a performant way”, which might be “the future of JavaScript tooling”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://minifeed.net" class="ext u-like-of" rel="noopener">minifeed</a><br>
A curated blog reader and search engine that collects humans-written blogs, making them discoverable and searchable</li>
<li><a href="https://wc-toolkit.com" class="ext u-like-of" rel="noopener">Web Components Toolkit</a><br>
Tools to help you build web components</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=starbist.clamp-it" class="ext u-like-of" rel="noopener">Clamp it!</a><br>
A VS Code extension that generates CSS <code>clamp()</code> functions</li>
<li><a href="https://www.netflix-codes.com" class="ext u-like-of" rel="noopener">Netflix-Codes</a><br>
A guide to Netflix’ hidden categories</li>
<li><a href="https://github.com/githubnext/monaspace" class="ext u-like-of" rel="noopener">monaspace</a><br>
An innovative superfamily of fonts for code - interesting, but I’ll stick with my beloved GitLab Mono for the time being</li>
<li><a href="https://fmhy.net" class="ext u-like-of" rel="noopener">freemediaheckyeah</a><br>
Allegedly “the largest collection of free stuff on the internet”</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Reading List Redux</title>
        
          
            <category term="news" />
          
            <category term="serverless" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/reading-list-redux/"/>
        
        <updated>2025-02-20T14:30:00Z</updated>
        <id>https://ttntm.me/blog/reading-list-redux/</id>
        <content type="html"><![CDATA[
          
            
            <p>Discovering Omnivore about a year ago felt like an instant win. A free, open source, read-it-later app that could also handle all my newsletter subscriptions and RSS feeds - absolutely marvelous!</p>
<p>Less than a year later, <a class="ext u-in-reply-to" href="https://www.theverge.com/2024/10/29/24283055/one-of-my-favorite-read-later-apps-is-shutting-down" rel="noopener">Omnivore shut down</a> because they were acquired by a “software company that specializes in developing natural-sounding speech synthesis software using deep learning”, as their Wikipedia page states. Shit happens, as we all know too well, and that also meant that <a href="https://ttntm.me/reading/" class="int">my reading list</a> had no more data left to show.</p>
<p>I found myself doing some research and evaluated some alternatives at the end of 2024, during the holidays. <a class="ext" href="https://www.wallabag.it/en" rel="noopener">Wallabag</a> was the first application I evaluated:</p>
<ul>
<li>Omnivore import worked</li>
<li>SaaS available at reasonable cost (still don’t want to self-host), and with a 14-day free trial</li>
<li>Features sufficient (no newsletters/RSS though)</li>
<li>API available</li>
<li>Mobile app available</li>
</ul>
<p>The mobile and web app both behaved a bit sluggish, and the UI felt a little dated. Don’t get me wrong: functionality-wise, everything was fine, and I really don’t want this to come across negatively - cool project, but it just didn’t feel right <em>for me</em>.</p>
<p>So, I kept looking for other alternatives and discovered <a class="ext" href="https://raindrop.io" rel="noopener">Raindrop.io</a>:</p>
<ul>
<li>Omnivore import worked well</li>
<li>SaaS with a generous free tier</li>
<li>Features sufficient (no newsletters/RSS either)</li>
<li>API available</li>
<li>Mobile app available</li>
</ul>
<p>Importing my data was a breeze (and faster than expected), after which I tinkered with “collections” to categorize and group my bookmarks. After about an hour, I had things at an acceptable working state, and I also felt familiar with the UI. Setting up the mobile app (Android) wasn’t much effort either, good experience.</p>
<p>I’m not interested in additional browser extensions, so I ended up with a bookmarklet to save things:</p>
<pre class="language-js"><code class="language-js"><span class="token literal-property property">javascript</span><span class="token operator">:</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">=></span><span class="token punctuation">{</span><span class="token keyword">var</span> i<span class="token punctuation">,</span>o<span class="token punctuation">;</span>window<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span><span class="token punctuation">(</span>i<span class="token operator">=</span>window<span class="token punctuation">.</span>location<span class="token punctuation">,</span>o<span class="token operator">=</span>document<span class="token punctuation">.</span>title<span class="token punctuation">,</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://app.raindrop.io/add?link=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>i<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;title=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>o<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>After a couple of days of using Raindrop, I decided to stick with it. I’ll miss the newsletter/RSS feed support Omnivore had, but I couldn’t find any (FOSS) option out there that’d offer this whole package anyway.</p>
<h2 id="website-integration" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#website-integration"><span>Website Integration</span></a></h2>
<p>Coming back to the data needed for my reading list, updating the code wasn’t a big deal at all.</p>
<p>Before: <a href="https://ttntm.me/blog/creating-a-reading-list-with-eleventy-and-omnivore/#data-fetching-with-eleventy" class="int">View Code</a></p>
<p>After: <a class="ext" href="https://github.com/ttntm/ttntm/blob/master/src/_data/reading.js" rel="noopener">View Code (GitHub)</a></p>
<p>The current version is actually <em>less</em> code, because there’s no more GraphQL query.</p>
<p>If you’re curious: the <code>-1</code> in the URL the data gets fetched from, references Raindrop’s <code>Unsorted</code> collection, where bookmarked links go by default. That’s a good place for me to store things that I haven’t read yet, so it makes sense to use that collection for my reading list.</p>
<p>Happy to have my reading list back, I realized that there was one more thing I almost forgot to take care of: updates of the reading list data should trigger website builds, so the list stays up to date. That’s another thing that Omnivore made easy - a few clicks to configure a webhook, and that’s it. No webhooks in Raindrop, however, so I had to figure out another solution…</p>
<p>I checked their documentation on <a class="ext" href="https://raindrop.io/integrations" rel="noopener">integrations</a>, and remembered that I signed up for IFTTT a very long time ago. That wasn’t very helpful - as it turned out, IFTTT doesn’t allow using outgoing HTTP requests on their free plan. Too bad, but they won’t get my money.</p>
<p><a class="ext" href="https://www.make.com/en/integrations/raindrop-io" rel="noopener">Make’s Raindrop.io integrations</a> came up as the next best thing, and their free tier seemed sufficient for my rather simple needs. My assumptions proved true, and soon after I’d signed up there, I ended up with a little scenario (workflow) that’d check my bookmarks once per day and trigger builds when necessary (via Cloudflare’s <a class="ext" href="https://developers.cloudflare.com/pages/configuration/deploy-hooks/" rel="noopener">Deploy Hooks</a>):</p>
<img src="https://ttntm.me/static/img/blog/make-scenario.jpg" class="img-fluid img-center" alt="A screenshot showing a scenario (workflow) from make.com">
<p>That’s a neat solution, and it works well enough to forget about it easily. Daily updates of my reading list are sufficient for me, but the interval can be shortened, should that ever become necessary.</p>
<h2 id="newsletters-and-rss" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#newsletters-and-rss"><span>Newsletters and RSS?</span></a></h2>
<p>I was very happy to have my reading list back, but I still hadn’t found a replacement for Omnivore’s newsletter subscriptions and RSS functionality.</p>
<p>Regarding RSS, my needs are very simple - I want to:</p>
<ul>
<li>add feeds</li>
<li>read their entries</li>
<li>do that across devices</li>
</ul>
<p>With that in mind, I eventually found <a class="ext" href="https://www.qireader.com" rel="noopener">Qi Reader</a>, which describes itself as “a modern RSS reader for the web”. It’s a minimal application focused on its core functionality, uBlock doesn’t complain about a single thing (Umami instead of GA), and it comes with the added bonus of not having to install anything anywhere - I really like it so far, and it offers exactly what I was looking for.</p>
<p>Newsletters… well, I actually didn’t miss them at all. So, I decided to let my newsletter subscriptions die with Omnivore, and moved on.</p>
<p>It’s the second half of February now, and I’ve been using this new setup for about 6 weeks now. It all works, my reading list is back, and I can only hope that the new tools I decided to use survive the year. Sarcasm aside, it’s a solid setup, and I’m glad I found good alternatives to the all-in-one convenience that Omnivore used to provide - RIP, old friend.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Implementing Webmentions</title>
        
          
            <category term="eleventy" />
          
            <category term="guide" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/implementing-webmentions/"/>
        
        <updated>2025-02-13T17:05:00Z</updated>
        <id>https://ttntm.me/blog/implementing-webmentions/</id>
        <content type="html"><![CDATA[
          
            
            <p>I wanted to add webmentions to this website for a very long time, and I finally got it done today. The reason for this task rotting on my list for such a long time (years…) was that thought it’d be a lot more complicated. I was wrong.</p>
<p><a class="ext" href="https://webmention.io" rel="noopener">webmention.io</a> makes things very easy - it’s a wonderful service for lazy people like me, and it can be added to a website in a matter of minutes. Just don’t forget to include the HTTP header in addition to the HTML tags - for this site:</p>
<pre class="language-yaml"><code class="language-yaml">/*<br>  <span class="token punctuation">...</span>other stuff<br>  <span class="token key atrule">Link</span><span class="token punctuation">:</span> &lt;https<span class="token punctuation">:</span>//webmention.io/ttntm.me/webmention<span class="token punctuation">></span>; rel="webmention"</code></pre>
<p>Once that’s take care of, collecting mentions for existing content can also be done in easy mode using <a class="ext" href="https://brid.gy" rel="noopener">brid.gy</a>. That’s a service that “connects your website to social media” and sends likes, replies and shares as webmentions over to your website (which is already configured to receive them).</p>
<p>Next step: fetching webmentions and displaying them.</p>
<p>My website’s built using Eleventy, and I’ve already got some other “fetch external data” code running. There’s a site-specific endpoint in <span>webmention.io</span> that can be used to obtain the data:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> dotenv <span class="token keyword">from</span> <span class="token string">'dotenv'</span><br><span class="token keyword">import</span> eleventyFetch <span class="token keyword">from</span> <span class="token string">'@11ty/eleventy-fetch'</span><br><br>dotenv<span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> wmMap <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Map</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  <span class="token keyword">const</span> wmURL <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://webmention.io/api/mentions.jf2?domain=ttntm.me&amp;token=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">WM_TOKEN</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;per-page=1000</span><span class="token template-punctuation string">`</span></span><br><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">eleventyFetch</span><span class="token punctuation">(</span>wmURL<span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">directory</span><span class="token operator">:</span> <span class="token string">'.cache'</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">duration</span><span class="token operator">:</span> <span class="token string">'1d'</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">fetchOptions</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'GET'</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'application/json; charset=utf-8'</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>response <span class="token operator">&amp;&amp;</span> response<span class="token punctuation">.</span>children <span class="token operator">&amp;&amp;</span> response<span class="token punctuation">.</span>children<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      response<span class="token punctuation">.</span>children<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">entry</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token keyword">let</span> <span class="token punctuation">{</span><br>          <span class="token string-property property">'wm-property'</span><span class="token operator">:</span> type<span class="token punctuation">,</span><br>          <span class="token string-property property">'wm-target'</span><span class="token operator">:</span> target<br>        <span class="token punctuation">}</span> <span class="token operator">=</span> entry<br><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>type <span class="token operator">===</span> <span class="token string">'mention-of'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          type <span class="token operator">=</span> <span class="token string">'in-reply-to'</span><br>        <span class="token punctuation">}</span><br><br>        <span class="token keyword">const</span> existing <span class="token operator">=</span> wmMap<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>target<span class="token punctuation">)</span><br><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>existing<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          wmMap<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>target<span class="token punctuation">,</span> <span class="token punctuation">{</span><br>            <span class="token operator">...</span>existing<span class="token punctuation">,</span><br>            <span class="token punctuation">[</span>type<span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span>existing<span class="token punctuation">[</span>type<span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">1</span><br>          <span class="token punctuation">}</span><span class="token punctuation">)</span><br>        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>          wmMap<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>target<span class="token punctuation">,</span> <span class="token punctuation">{</span><br>            <span class="token punctuation">[</span>type<span class="token punctuation">]</span><span class="token operator">:</span> <span class="token number">1</span><br>          <span class="token punctuation">}</span><span class="token punctuation">)</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>ex<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>ex<span class="token punctuation">.</span>message <span class="token operator">||</span> ex<span class="token punctuation">)</span><br>  <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> wmMap<br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>There are many implementation guides out there that use code which runs the same sequential array <code>filter()</code> functions <em>for every page/post</em> - that’s not very efficient, and also not necessary…</p>
<p>As an alternative, I wrote the code that you can see above - with the data from the API, my code is building a <a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map" rel="noopener">Map</a> of all content with mentions. The code in the data file runs <em>once</em> (at build time), using post URLs (<code>wm-target</code>) as unique keys, and the result looks like this (via <code>Object.fromEntries()</code>):</p>
<pre class="language-js"><code class="language-js"><span class="token punctuation">{</span><br>  <span class="token string-property property">'https://ttntm.me/blog/building-a-custom-filter-for-eleventy-collections/'</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token string-property property">'repost-of'</span><span class="token operator">:</span> <span class="token number">3</span><span class="token punctuation">,</span><br>    <span class="token string-property property">'like-of'</span><span class="token operator">:</span> <span class="token number">10</span><span class="token punctuation">,</span><br>    <span class="token string-property property">'in-reply-to'</span><span class="token operator">:</span> <span class="token number">3</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token string-property property">'https://ttntm.me/blog/making-your-own-tools/'</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token string-property property">'repost-of'</span><span class="token operator">:</span> <span class="token number">5</span><span class="token punctuation">,</span><br>    <span class="token string-property property">'like-of'</span><span class="token operator">:</span> <span class="token number">4</span><span class="token punctuation">,</span><br>    <span class="token string-property property">'in-reply-to'</span><span class="token operator">:</span> <span class="token number">6</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token comment">// etc</span><br><span class="token punctuation">}</span></code></pre>
<p>That’s data we can work with easily, and it’s available globally. So, finally, it’s time for some template adjustments.</p>
<p>Posts need to get their mentions from the <code>Map</code>, which makes a tiny Nunjucks filter necessary:</p>
<pre class="language-js"><code class="language-js"><span class="token function-variable function">getPostMentions</span><span class="token operator">:</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">mentions<span class="token punctuation">,</span> url</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> data <span class="token operator">=</span> mentions<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://ttntm.me</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>url<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>  <span class="token keyword">return</span> <span class="token function">Boolean</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token operator">?</span> data <span class="token operator">:</span> <span class="token keyword">undefined</span><br><span class="token punctuation">}</span></code></pre>
<p>In the template, this filter is used with the global data:</p>
<pre class="language-html"><code class="language-html">{%- set postMentions = webmentions | getPostMentions(page.url) -%}<br>{% if postMentions %}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">...</span> <span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span><span class="token punctuation">></span></span>{{ postMentions['like-of'] or '-' }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span><br>    <span class="token comment">&lt;!-- more code to render replies/shares --></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span><br>{% endif %}</code></pre>
<p>That’s pretty simple template code - it gets the job done and displays an icon and a number for each kind of mention. That’s more than enough information for me, but there are many other guides out there for people that want to display more, i.e. actual webmention content, like comments.</p>
<p>A final detail: I don’t want to have forms on my website, so I’ve decided to add a link to my <span>webmention.io</span> endpoint that can be used by website visitors to create a new webmention manually.</p>
<p>In conclusion, adding webmentions wasn’t even remotely as complicated as I thought it’d be - I’m glad I finally did it.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>My Favorite Albums - The 2010s</title>
        
          
            <category term="albums" />
          
            <category term="personal" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/my-favorite-albums-from-the-2010s/"/>
        
        <updated>2025-02-12T14:30:00Z</updated>
        <id>https://ttntm.me/blog/my-favorite-albums-from-the-2010s/</id>
        <content type="html"><![CDATA[
          
            
            <p>A chronological list of my favorite albums from 2010 to 2019 with reasons why I like them.</p>
<p>As of late 2024, this is the final post in the <a href="https://ttntm.me/tags/albums/" class="int">#albums</a> series.</p>
<div class="hr shadow mt2 mb2"></div>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/c-cc2.jpg" width="125" alt="Crystal Castles - Crystal Castles (II)" title="Crystal Castles - Crystal Castles (II)" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="crystal-castles-crystal-castles-ii" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#crystal-castles-crystal-castles-ii"><span>Crystal Castles - Crystal Castles (II)</span></a>
          </h2>
          <p class="m0">Release: 2010</p>
        </hgroup>
    </div>
<p>A weird record, that I discovered while exploring witch house music.</p>
<p>I ended up liking it at lot for its weirdness, and for songs like “Celestica” and “Baptism”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/ng-ng.jpg" width="125" alt="NiT GriT - NiT GriT" title="NiT GriT - NiT GriT" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="ni-t-gri-t-ni-t-gri-t" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#ni-t-gri-t-ni-t-gri-t"><span>NiT GriT - NiT GriT</span></a>
          </h2>
          <p class="m0">Release: 2010</p>
        </hgroup>
    </div>
<p>More electronic music.</p>
<p>I can’t quite remember how I found out about this album, but the song “Let me die” reminded me of the (phenomenal!) Unreal Tournament soundtrack that I used to listen to <em>a lot</em> when I was 12.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/m-whtn.jpg" width="125" alt="Mgła - With Hearts Toward None" title="Mgła - With Hearts Toward None" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="mgla-with-hearts-toward-none" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#mgla-with-hearts-toward-none"><span>Mgła - With Hearts Toward None</span></a>
          </h2>
          <p class="m0">Release: 2012</p>
        </hgroup>
    </div>
<p>An absolute masterpiece, and definitely among the best Black Metal albums I know. It’s a fierce, dark record that’s evil to the bone. It’s also showcasing Mgła’s mastery of musical craftsmanship - both composition and delivery of the tracks on this record are outstanding.</p>
<p>The final track, VII, has a very special place in my heart; it’s left a mark so deep, that the mere thought of it sends a shiver down my spine – “Always downwards - and toward the farthest sides of the north!”</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/p-iatn.jpg" width="125" alt="Perturbator - I am the Night" title="Perturbator - I am the Night" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="perturbator-i-am-the-night" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#perturbator-i-am-the-night"><span>Perturbator - I am the Night</span></a>
          </h2>
          <p class="m0">Release: 2012</p>
        </hgroup>
    </div>
<p>An album that I loved from the very first second I listened to it.</p>
<p>Enjoying it so much was weird at first, considering that I listened to metal almost exclusively at the time. It’s also a considerably significant record for me, because it introduced me to the Synthwave/Retrowave genre, which became one of my favorite non-metal music genres.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/b-c.jpg" width="125" alt="Beastmilk - Climax" title="Beastmilk - Climax" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="beastmilk-climax" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#beastmilk-climax"><span>Beastmilk - Climax</span></a>
          </h2>
          <p class="m0">Release: 2013</p>
        </hgroup>
    </div>
<p>A pretty great album, that includes catchy sing-along tracks like “The Wind Blows Through Their Skulls”, “Nuclear Winter”, and “Genocidal Crush”.</p>
<p>Musically, this record probably fits in somewhere between (Post-)Punk, New-Wave and Gothic Rock. It sounds great, and I often find myself pulling it out of my record shelf.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/md-h.jpg" width="125" alt="Mega Drive - Hardwired" title="Mega Drive - Hardwired" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="mega-drive-hardwired" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#mega-drive-hardwired"><span>Mega Drive - Hardwired</span></a>
          </h2>
          <p class="m0">Release: 2013</p>
        </hgroup>
    </div>
<p>Another Synthwave album.</p>
<p>On this list because of kickass tracks like “Completely Circuitry”, “Delta Protocol” and “I am a Program”. These standout tracks were remixed by other greats of the genre, and these remixes are featured on the extended version of this album, “Hardwired V1.4”, from 2015.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/dc-potf.jpg" width="125" alt="Dead Congregation - Promulgation of the Fall" title="Dead Congregation - Promulgation of the Fall" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="dead-congregation-promulgation-of-the-fall" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#dead-congregation-promulgation-of-the-fall"><span>Dead Congregation - Promulgation of the Fall</span></a>
          </h2>
          <p class="m0">Release: 2014</p>
        </hgroup>
    </div>
<p>An outstanding Death Metal record that shows how Dead Congregation are among the masters of the genre. It’s a worthy successor to their debut that delivers dark and brutally obscure tracks that could be compared to bands like Deicide, Immolation, or Incantation.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/d-sc.jpg" width="125" alt="Domains - Sinister Ceremonies" title="Domains - Sinister Ceremonies" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="domains-sinister-ceremonies" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#domains-sinister-ceremonies"><span>Domains - Sinister Ceremonies</span></a>
          </h2>
          <p class="m0">Release: 2014</p>
        </hgroup>
    </div>
<p>Got to know about this album randomly, saw it mentioned a newsletter, I think, and decided to give it a try. And hell, that was a good decision!</p>
<p>“Sinister Ceremonies” is one of the best Death Metal albums of this decade, without a doubt, and probably among the best ever. It’s a razor sharp record, full of dark, occult energy, backed by a surprisingly refined production that leaves a very professional impression.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/md-198xad.jpg" width="125" alt="Mega Drive - 198XAD" title="Mega Drive - 198XAD" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="mega-drive-198-xad" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#mega-drive-198-xad"><span>Mega Drive - 198XAD</span></a>
          </h2>
          <p class="m0">Release: 2014</p>
        </hgroup>
    </div>
<p>An album full of superb tracks. Also, my favorite Mega Drive album, and one of my favorites of the Synthwave genre.</p>
<p>It’s hard to just pick a few, but some of my favorites from this album are: “Acid Spit”, “Osaka Sewers”, and “Zero Point Non-Response”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/p-dd.jpg" width="125" alt="Perturbator - Dangerous Days" title="Perturbator - Dangerous Days" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="perturbator-dangerous-days" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#perturbator-dangerous-days"><span>Perturbator - Dangerous Days</span></a>
          </h2>
          <p class="m0">Release: 2014</p>
        </hgroup>
    </div>
<p>More Perturbator, more killer Synthwave.</p>
<p>My favorite album of this artist, complete with tracks like “Raw Power”, “Future Club”, and “Humans Are Such Easy Prey” that get the blood pumping. Also, an excellent workout soundtrack.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/vd-ta.jpg" width="125" alt="VHS Dreams - Trans Am" title="VHS Dreams - Trans Am" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="vhs-dreams-trans-am" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#vhs-dreams-trans-am"><span>VHS Dreams - Trans Am</span></a>
          </h2>
          <p class="m0">Release: 2015</p>
        </hgroup>
    </div>
<p>One of my favorite highway driving albums, and something I’d imagine would be a great soundtrack for a remake of the movie “Vanishing Point”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/mbr-ip.jpg" width="125" alt="Master Boot Record - Internet Protocol" title="Master Boot Record - Internet Protocol" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="master-boot-record-internet-protocol" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#master-boot-record-internet-protocol"><span>Master Boot Record - Internet Protocol</span></a>
          </h2>
          <p class="m0">Release: 2019</p>
        </hgroup>
    </div>
<p>An album that became one of my favorites very quickly.</p>
<p>The music sits right at the intersection of metal and electronic music, which resonated with me almost immediately. Also, the songs on this album are all named after actual internet protocols, so one finally knows what FTP sounds like in 2019.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 05, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w05/"/>
        
        <updated>2025-01-31T13:30:00Z</updated>
        <id>https://ttntm.me/likes/2025-w05/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://kranga.notion.site/The-fizzbuzz-that-did-not-get-me-the-job-180e7c22ef3b80c3a386f7f8de720ac7" class="ext u-like-of" rel="noopener">The fizzbuzz that did not get me the job</a><br>
A very interesting story about the absurdity of technical interviews and live coding</li>
<li><a href="https://robinrendle.com/notes/learnable-by-default/" class="ext u-like-of" rel="noopener">Learnable by default</a><br>
About HTML and its learnability that “all languages, systems, and programs should aspire to”</li>
<li><a href="https://gamehistory.org/vghf-library-launch/" class="ext u-like-of" rel="noopener">The VGHF Library opens in early access</a><br>
About the launch of a “digital archive of video game history research materials”</li>
<li><a href="https://www.einenlum.com/articles/the-beauty-of-goofy-diagrams" class="ext u-like-of" rel="noopener">The beauty of goofy diagrams</a><br>
I like drawing goofy diagrams myself, reading this article resonated with me</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://getfullyear.com" class="ext u-like-of" rel="noopener">Never Have Outdated Footer Dates Again</a><br>
Finally - an amazing API that solves the recurring problem of wrong dates in footers</li>
<li><a href="https://flameshot.org" class="ext u-like-of" rel="noopener">Flameshot</a><br>
A great screenshot tool with much needed annotation features</li>
<li><a href="https://easingwizard.com" class="ext u-like-of" rel="noopener">Easing Wizard</a><br>
A pretty cool tool that can be used to edit and preview various CSS easing functions</li>
<li><a href="https://grumpy.website" class="ext u-like-of" rel="noopener">Grumpy Website</a><br>
Hilarious reports on all kinds of questionable UI/UX choices</li>
<li><a href="https://lume.land" class="ext u-like-of" rel="noopener">Lume</a><br>
“The static site generator for Deno” (that I didn’t know about)</li>
<li><a href="https://github.com/Einenlum/tailwind-alchemist" class="ext u-like-of" rel="noopener">tailwind-alchemist</a><br>
“A CLI tool to find and replace Tailwind CSS colors” - might come in handy at some point</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Adding a Copy Button to Code Blocks</title>
        
          
            <category term="eleventy" />
          
            <category term="guide" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/adding-a-copy-button-to-code-blocks/"/>
        
        <updated>2025-01-16T13:15:00Z</updated>
        <id>https://ttntm.me/blog/adding-a-copy-button-to-code-blocks/</id>
        <content type="html"><![CDATA[
          
            
            <p>Copying code from code blocks, without having to manually select it all first, is very handy convenience feature. But despite having published a ton of code snippets on this website, adding copy functionality wasn’t a top priority so far. A bit sad, considering that it can be done in less than an hour, using only 61 lines of vanilla JavaScript (and some CSS, of course).</p>
<p>The approach described here is using client-side JS to manipulate the DOM, adding copy buttons when a page with code blocks in its DOM gets displayed. It works without any 3rd party resources, won’t interfere with existing functionality, and follows the strategy of <a class="ext" href="https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement" rel="noopener">progressive enhancement</a>. The solution described here isn’t specific to my website either - it should only require minor adjustments to make it work on any other website using code blocks.</p>
<h2 id="the-code" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-code"><span>The Code</span></a></h2>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">createCopyBtn</span><span class="token punctuation">(</span><span class="token parameter">blockIndex</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;div class="cc-wrapper d-none d-sm-block"><br>    &lt;button class="cc-btn btn-muted shadow" title="Copy code" data-target="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>blockIndex<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"><br>      &lt;svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon">&lt;path stroke="none"d="M0 0h24v24H0z"fill="none"/>&lt;path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2"/>&lt;path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"/>&lt;/svg><br>    &lt;/button><br>  &lt;/div></span><span class="token template-punctuation string">`</span></span><br><span class="token punctuation">}</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">copyCode</span><span class="token punctuation">(</span><span class="token parameter">block</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> code <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">[data-block-id="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>block<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"]</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>  <span class="token keyword">const</span> <span class="token function-variable function">doCopy</span> <span class="token operator">=</span> <span class="token keyword">async</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">await</span> navigator<span class="token punctuation">.</span>clipboard<span class="token punctuation">.</span><span class="token function">writeText</span><span class="token punctuation">(</span>code<span class="token operator">?.</span>innerText <span class="token operator">??</span> <span class="token string">''</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>navigator<span class="token punctuation">.</span>userAgent<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">'Firefox'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> navigator<span class="token punctuation">.</span>permissions<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'clipboard-write'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>result<span class="token punctuation">.</span>state <span class="token operator">===</span> <span class="token string">'granted'</span> <span class="token operator">||</span> result<span class="token punctuation">.</span>state <span class="token operator">===</span> <span class="token string">'prompt'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token function">doCopy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token function">doCopy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">handleCopyBtnClick</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> btn <span class="token operator">=</span> event<span class="token operator">?.</span>target<br>  <span class="token keyword">const</span> btnTarget <span class="token operator">=</span> btn<span class="token operator">?.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-target'</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>btn <span class="token operator">&amp;&amp;</span> btnTarget<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> originalText <span class="token operator">=</span> btn<span class="token punctuation">.</span>innerHTML<br><br>    <span class="token keyword">await</span> <span class="token function">copyCode</span><span class="token punctuation">(</span>btnTarget<span class="token punctuation">)</span><br><br>    btn<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token string">'&lt;svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon">&lt;path stroke="none" d="M0 0h24v24H0z" fill="none"/>&lt;path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2" />&lt;path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z" />&lt;path d="M9 14l2 2l4 -4" />&lt;/svg>'</span><br><br>    <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      btn<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> originalText<br>    <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1500</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br>document<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'DOMContentLoaded'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> notAPhone <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">'(min-width: 768px)'</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>notAPhone<span class="token punctuation">.</span>matches<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> allCodeBlocks <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'pre[class^="language-"]'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><br>    allCodeBlocks<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">b<span class="token punctuation">,</span> i</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">const</span> code <span class="token operator">=</span> b<span class="token punctuation">.</span>childNodes<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><br>      <span class="token keyword">const</span> codeBlockIndex <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">cb-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>i<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><br><br>      b<span class="token punctuation">.</span><span class="token function">insertAdjacentHTML</span><span class="token punctuation">(</span><span class="token string">'beforebegin'</span><span class="token punctuation">,</span> <span class="token function">createCopyBtn</span><span class="token punctuation">(</span>codeBlockIndex<span class="token punctuation">)</span><span class="token punctuation">)</span><br>      code<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'data-block-id'</span><span class="token punctuation">,</span> codeBlockIndex<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>    <span class="token keyword">const</span> allCopyBtns <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'.cc-btn'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><br>    allCopyBtns<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">btn</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      btn<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> handleCopyBtnClick<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>A basic outline of what the code does:</p>
<ol>
<li>Wait for the DOM to be ready</li>
<li>Find all code blocks rendered on the page (based on CSS classes)</li>
<li>Assign a unique ID to each code block and store it in a data attribute (<code>data-block-id</code>)</li>
<li>Inject a copy button at the beginning of each code block</li>
<li>Register an event handler for each button that copies the matching block’s <code>innerText</code> (via <code>data-target</code> and <code>data-block-id</code>)</li>
</ol>
<p>The actual copy functionality (<code>copyCode()</code>) uses the <a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API" rel="noopener">Clipboard API</a>; it looks a little convoluted due to the fact that the matching <code>clipboard-read</code> and <code>clipboard-write</code> permissions are not supported by Firefox (see <a class="ext" href="https://stackoverflow.com/a/79250919" rel="noopener">navigator.clipboard.writeText in Firefox</a>).</p>
<p>As always, I used SVGs from <a class="ext" href="https://tabler.io/icons" rel="noopener">Tabler Icons</a>, and matching CSS to make them overlap the code blocks in the top right corner. My copy buttons will not render on mobile devices - a deliberate choice, due to design and space constraints, but also arguably because the functionality won’t be of much use on a mobile device. Simply remove the <code>if</code> statement (<code>if (notAPhone.matches) {...}</code>) to change that.</p>
<h2 id="alternatives" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#alternatives"><span>Alternatives</span></a></h2>
<p>I briefly considered using a plugin (i.e. <a class="ext" href="https://github.com/mamezou-tech/eleventy-plugin-code-clipboard" rel="noopener">eleventy-plugin-code-clipboard</a>), but I didn’t like the idea of adding a couple of external dependencies for functionality that’s rather simple and widely supported by modern browsers.</p>
<p>I’m sure there are similar plugins for many other platforms, SSGs, etc. - it might be a good idea to have a proper look at them, and to evaluate what (if anything) they can offer compared to an approach like the one described above.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 03, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w03/"/>
        
        <updated>2025-01-14T16:30:00Z</updated>
        <id>https://ttntm.me/likes/2025-w03/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.wired.com/story/gravy-location-data-app-leak-rtb/" class="ext u-like-of" rel="noopener">Thousands of Apps Hijacked to Spy on Your Location</a><br>
About the Gravy Analytics hack</li>
<li><a href="https://82mhz.net/posts/2025/01/i-will-never-need-to-buy-a-new-computer-again/" class="ext u-like-of" rel="noopener">I will never need to buy a new computer again</a><br>
About keeping the hardware that you’ve got for as long as it works, and replacing broken parts with used ones instead of buying new devices</li>
<li><a href="https://visitmy.website/2020/07/13/this-website-is-killing-the-planet/" class="ext u-like-of" rel="noopener">This website is killing the planet</a><br>
An older article about the carbon footprint of our websites - more relevant than ever</li>
<li><a href="https://www.wired.com/story/html-is-actually-a-programming-language-fight-me/" class="ext u-like-of" rel="noopener">HTML Is Actually a Programming Language</a><br>
About “the most significant computing language ever developed”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://duckduckgo.com/bangs" class="ext u-like-of" rel="noopener">DuckDuckGo !Bangs</a><br>
Didn’t know about that feature - looks pretty cool</li>
<li><a href="https://bobatealee.itch.io/doom-the-gallery-experience" class="ext u-like-of" rel="noopener">DOOM: The Gallery Experience</a><br>
+1 wonderful thing that one can do with good old DOOM</li>
<li><a href="https://dontfuckwithscroll.com" class="ext u-like-of" rel="noopener">Don’t Fuck With Scroll</a><br>
Just don’t</li>
<li><a href="https://noclip.website" class="ext u-like-of" rel="noopener">noclip</a><br>
A digital museum of video game levels - fly through game worlds in your browser</li>
<li>Some VS Code extensions:
<ul>
<li><a href="https://github.com/arciisine/vscode-chronicler" class="ext u-like-of" rel="noopener">Chronicler</a><br>
A cool screen recording extension</li>
<li><a href="https://github.com/vscode-shellcheck/vscode-shellcheck" class="ext u-like-of" rel="noopener">ShellCheck</a><br>
A linter for Shell scripts</li>
</ul>
</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Blog Questions Challenge</title>
        
          
            <category term="personal" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/blog-questions-challenge/"/>
        
        <updated>2025-01-08T13:30:00Z</updated>
        <id>https://ttntm.me/blog/blog-questions-challenge/</id>
        <content type="html"><![CDATA[
          
            
            <p>I saw a couple of posts about a “blog questions challenge” pop up in my feed reader this week (written by <a class="ext u-in-reply-to" href="https://leilukin.com/blog/posts/2025-01-07-blog-questions-challenge/" rel="noopener">Leilukin</a>, <a class="ext u-in-reply-to" href="https://home.hedy.dev/posts/blog-questions-challenge/" rel="noopener">Hedy</a>, or <a class="ext u-in-reply-to" href="https://www.lkhrs.com/blog/2025/blog-questions-challenge/" rel="noopener">Luke</a>, for example) and decided to participate.</p>
<p>It’s a collection of interesting questions, and I enjoyed reading what other people had to say so far.<br>
Below you’ll find my answers.</p>
<h2 id="why-did-you-start-blogging-in-the-first-place" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#why-did-you-start-blogging-in-the-first-place"><span>Why Did You Start Blogging in the First Place?</span></a></h2>
<p>I noticed that I’m very comfortable solving some my own problems with the help of other people’s written instructions - anything from blogs and personal sites to various StackExchange communities - and I started thinking about sharing some of my experience and knowledge too. Over time, the content I published became a bit more personal, including thoughts, opinions and posts that weren’t about anything technical at all.</p>
<h2 id="what-platform-are-you-using-to-manage-your-blog-and-why-did-you-choose-it" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#what-platform-are-you-using-to-manage-your-blog-and-why-did-you-choose-it"><span>What Platform Are You Using to Manage Your Blog, and Why Did You Choose it?</span></a></h2>
<p>I’m currently using using Eleventy, a static site generator, with content written in Markdown and stored in a git repository. Hosting, as well as builds and deployments, are handled by Cloudflare.</p>
<p>The site’s architecture (JAMstack) is still the same that I created it with in 2018, but some components changed since then:</p>
<ul>
<li><strong>Hugo &gt; Eleventy</strong>: I switched to 11ty back in spring '21, because working with (and around…) Hugo wasn’t much fun anymore. 11ty is native to the Node ecosystem, and offers significantly better authoring/developer experience (for my use cases). There’s a dedicated <a href="https://ttntm.me/blog/migrating-from-hugo-to-eleventy/" class="int">post about it</a>, if you want to read more about the migration.</li>
<li><strong>Netlify &gt; Cloudflare</strong>: a recent choice (spring '24), but a good one. CF’s free tier is vastly superior to Netlify’s offering, and a certain degree of <a href="https://ttntm.me/blog/about-diversification/" class="int">diversification</a>, in terms of hosting provider/platform choices is definitely not a bad thing until (if ever…) I decide to self-host everything.</li>
</ul>
<h2 id="have-you-blogged-on-other-platforms-before" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#have-you-blogged-on-other-platforms-before"><span>Have You Blogged on Other Platforms Before?</span></a></h2>
<p>Most of it wasn’t “blogging”, but I’ve published my own written content on websites I’ve operated since the early 2000s.</p>
<p>Somewhat chronologically, that’d have been on the following platforms:</p>
<ul>
<li>hard-coded HTML</li>
<li>Blogger</li>
<li>WordPress</li>
<li>TYPO3</li>
<li>Public Wikis (i.e. Confluence, MediaWiki)</li>
<li>GitHub/GitLab Pages</li>
<li><span>Dev.to</span></li>
<li>Hugo</li>
</ul>
<h2 id="how-do-you-write-your-posts" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#how-do-you-write-your-posts"><span>How Do You Write Your Posts?</span></a></h2>
<p>I almost exclusively write in my code editor (VS Code) these days. Having the whole site context, shortcodes and snippets available while writing is a huge benefit compared to writing in other tools. I often used Notion in the past, and that made it necessary to edit almost every post before publishing - not really something that felt like time well spent.</p>
<h2 id="when-do-you-feel-most-inspired-to-write" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#when-do-you-feel-most-inspired-to-write"><span>When Do You Feel Most Inspired to Write?</span></a></h2>
<p>It varies.</p>
<p>For documentation, guides, or technical articles: usually while doing, or after having done the thing I’d like to write about, i.e. after building a demo/finishing a project, or after/while solving a problem. The further away the “done” milestone moves, the harder it becomes to pick up the respective topic again. I sometimes do, but unfortunately, most of my “write about something you did too long ago…” ideas rot in the <code>drafts</code> folder, and get discarded eventually.</p>
<p>For other kinds of writing, I often struggle with getting started. But once started, inspiration isn’t really a factor anymore, and I usually wrap it up within a week or so, depending on the subject.</p>
<h2 id="do-you-publish-immediately-after-writing-or-do-you-let-it-simmer-a-bit-as-a-draft" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#do-you-publish-immediately-after-writing-or-do-you-let-it-simmer-a-bit-as-a-draft"><span>Do You Publish Immediately After Writing, or Do You Let it Simmer a Bit as a Draft?</span></a></h2>
<p>I have a dedicated folder for drafts and ideas that I’d like to come back to at some point. Most posts are written and published immediately though. I prefer them to be properly edited and readable right away, mostly free of typos (thanks to LTeX) and decently formatted - 11ty’s local preview is a huge help there, of course.</p>
<h2 id="whats-your-favorite-post-on-your-blog" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#whats-your-favorite-post-on-your-blog"><span>What’s Your Favorite Post on Your Blog?</span></a></h2>
<p>That’s a hard question.</p>
<p>For a long time, it’s probably been <a href="https://ttntm.me/blog/serverless-recipes-app-faunadb-vuejs/" class="int">this one</a>. It got a ton of exposure back in the day, was featured in various newsletters, and was one of the more “successful” articles - in terms of backlinks, clicks, engagement, etc. - I published on this website.</p>
<p>Considering recent times, my favorite posts are the ones I wrote for the (unfinished…) <a href="https://ttntm.me/tags/albums/" class="int">#albums</a> series. Digging through my record collection and picking favorites for each decade was a fun experience, full of memories and nostalgia. It also started a couple of email conversations with people who shared their memories and thoughts related to what I wrote, a wonderful example of the internet still working as a medium to connect people.</p>
<h2 id="any-future-plans-for-your-blog" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#any-future-plans-for-your-blog"><span>Any Future Plans for Your Blog?</span></a></h2>
<p>A couple of things, yes.</p>
<p>Something I’d like to take care of rather soon, is adding a copy button to code blocks. That’d be a convenient feature for everyone, myself included.</p>
<p>An archive of past versions of the <a href="https://ttntm.me/now/" class="int">/now</a> page would also be nice.</p>
<p>And finally, adding search functionality to this website has been on my list for a while. I’ve done it a couple of times for other static sites, but I haven’t felt it’s needed here. But: the amount of content published on this website is growing steadily, so it’d definitely be nice to make it searchable at some point.</p>
<p>NB: I usually keep track of these things in the <a href="https://ttntm.me/appendix/#intentions-and-to-dos" class="int">Appendix</a></p>
<h2 id="why-do-you-write-other-than-your-blog-do-you-write-long-form-content-elsewhere" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#why-do-you-write-other-than-your-blog-do-you-write-long-form-content-elsewhere"><span>Why do you write? Other Than Your Blog, Do You Write Long-Form Content Elsewhere?</span></a></h2>
<p>An interesting question. Many reasons, I think.</p>
<p>I often write things down to remember them at later point in time, and writing also helps me with structured thinking, planning, as well as systematically approaching any kind of problem. Communication is also an important aspect, especially today, considering that 99% of the phone calls I used to make 10 years ago have turned into (asynchronous) written communication.</p>
<p>In terms of writing <em>and publishing it on this website</em>, the answer’s pretty much the same thing I wrote <a href="https://ttntm.me/everything.xml#why-did-you-start-blogging-in-the-first-place" class="int">above</a>. When it comes to the more personal content though, it’s hard to tell - there are tons of thoughts, opinions etc. that might be interesting, but will never end up here. I guess it also depends on whether I’d think that someone else might be interested in reading it, even though that should probably matter a lot less.</p>
<p>Writing elsewhere?<br>
Sometimes.</p>
<p>I used to publish some of my articles on <code>Dev.to</code> (in addition to being published here), but left that platform a while ago. I also had an article published in the <a class="ext" href="https://ctrl-c.club/~loghead/ctrl-zine.html" rel="noopener">Ctrl-ZINE</a> recently, and might do that again at some point. Not much else otherwise.</p>
<h2 id="its-your-turn-now" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#its-your-turn-now"><span>It’s Your Turn Now</span></a></h2>
<p>I’ll follow Leilukin’s example and refrain from encouraging anyone in particular to participate in this challenge.</p>
<p>Instead, I’d like to encourage participation by everyone who’s interested in answering (some of) these questions - regardless of whether you consider your website a blog, or simply publish written content somewhere outside social media.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 01, 2025</title>
        
        <link href="https://ttntm.me/likes/2025-w01/"/>
        
        <updated>2025-01-04T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2025-w01/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://berthub.eu/articles/posts/on-long-term-software-development/" class="ext u-like-of" rel="noopener">On Long Term Software Development</a><br>
Fantastic article, also includes recommendations for long-term software development at the end</li>
<li><a href="https://cheezburger.com/38321925/bring-back-epic-movie-posters-these-sword-and-sorcery-masterpieces-put-todays-floating-heads-posters" class="ext u-like-of" rel="noopener">Bring Back Epic Movie Posters</a><br>
Some great posters there indeed</li>
<li><a href="https://css-tip.com/overflow-detection/" class="ext u-like-of" rel="noopener">Overflow/scrollbar detection using modern CSS</a><br>
Good to keep in mind what’s possible</li>
<li><a href="https://imadethissite.com/posts/html-the-programming-language" class="ext u-like-of" rel="noopener">HTML, The Programming Language</a><br>
Yes - “let us focus on the outcomes we produce”</li>
<li><a href="https://notlaura.com/is-css-turing-complete/" class="ext u-like-of" rel="noopener">Is CSS Turing Complete?</a><br>
An interesting exploration</li>
<li><a href="https://www.silvestar.codes/articles/infinite-scrolling-logos-in-flat-html-and-pure-css/" class="ext u-like-of" rel="noopener">Infinite-Scrolling Logos In Flat HTML And Pure CSS</a><br>
Nice one, with greeting from our old friend <code>&lt;marquee&gt;</code></li>
<li><a href="https://infrequently.org/2024/11/if-not-react-then-what/" class="ext u-like-of" rel="noopener">If Not React, Then What?</a><br>
Some thoughts on why “nobody should start a new project in the 2020s based on React”</li>
<li><a href="https://www.lloydatkinson.net/posts/2022/modelling-workflows-with-finite-state-machines-in-dotnet/" class="ext u-like-of" rel="noopener">Modelling Workflows With Finite State Machines</a><br>
Needed this for work, very helpful</li>
<li><a href="https://boehs.org/node/dark-web-security" class="ext u-like-of" rel="noopener">The fascinating security model of dark web marketplaces</a><br>
A fascinating read, make sure you have a look at it</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://github.com/CorentinTh/it-tools" class="ext u-like-of" rel="noopener">it-tools</a><br>
A vast collection of online tools for developers</li>
<li><a href="https://cobalt.tools" class="ext u-like-of" rel="noopener">cobalt</a><br>
In their own words: “save anything from your favorite websites: video, audio, photos or gifs”</li>
<li><a href="https://rudokemper.github.io/google-maps-places-to-organic-maps/" class="ext u-like-of" rel="noopener">Convert Google Maps saved places to Organic Maps</a></li>
<li><a href="https://github.com/microsoft/markitdown" class="ext u-like-of" rel="noopener">markitdown</a><br>
M$ Office to Markdown - wow!</li>
<li><a href="https://cube.fyi" class="ext u-like-of" rel="noopener">CUBE CSS</a><br>
“Simplicity, pragmatism and consistency” - good to keep that one in mind for the future</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>About the Past Year - 2024</title>
        
          
            <category term="personal" />
          
            <category term="recap" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/about-the-past-year-2024/"/>
        
        <updated>2025-01-02T12:30:00Z</updated>
        <id>https://ttntm.me/blog/about-the-past-year-2024/</id>
        <content type="html"><![CDATA[
          
            
            <p>Wow, it’s been a quarter of a century since Y2K.<br>
We all know that time flies, but it still feels surreal sometimes.</p>
<p>Saying that 2024 was a rather uneventful year would probably be too much, but at least at work, it was a lot of “business as usual”. The small company I’m with grew a little larger, we’re about 30 people now. My responsibilities mostly stayed the same, but I got into some cool new projects and made good use of past work experience, especially working with process driven automation/workflow engines. I also got to work with a new (to me) headless CMS, and lead a small team to a successful relaunch of our company website.</p>
<p>Highlights from my personal life include my wife getting her Austrian citizenship, a wonderful trip to Portugal (including my first scuba diving experience), and getting back into weight lifting.</p>
<p>To follow the series’ established format, these are some highlights from last year:</p>
<p><strong>Work</strong></p>
<ul>
<li>Worked as an architect/team lead on the relaunch of our company website (built on Azure using Astro/Kontent.ai and interfaces to CMS/recruiting)</li>
<li>Proved expertise with process driven automation/workflow engines and became de facto architect for two custom software development projects using process-driven orchestration</li>
<li>Got some certifications
<ul>
<li>Back End Development and APIs (FCC)</li>
<li><span>Kontent.ai</span> Developer</li>
</ul>
</li>
</ul>
<p><strong>FOSS &amp; Side Projects</strong></p>
<ul>
<li>Started a web directory called <a class="ext" href="https://bukmark.club" rel="noopener">BUKMARK.CLUB</a> and grew it to 241 listings</li>
<li>Re-launched <a class="ext" href="https://bundestrojaner.at" rel="noopener">bundestrojaner.at</a> using 11ty (with the help of my wife, who did the re-design)</li>
<li>Got most of <a class="ext" href="https://codeberg.org/ttntm/recept0r/milestone/6230" rel="noopener">recept0r v3</a> done</li>
<li>Built and published an <a class="ext" href="https://github.com/ttntm/astro-potfolio-starter/" rel="noopener">astro-potfolio-starter</a></li>
<li>Started working on a CMS migration incl. website relaunch for a nonprofit organization (Astro/Contentful)</li>
<li><a href="https://ttntm.me/blog/#2024" class="int">Published 24 blog posts</a>, and <a href="https://ttntm.me/tags/evolution/" class="int">added a ton of features, pages etc.</a> to this website</li>
</ul>
<p>My commit activity for 2024 looks like this:</p>
<p>Codeberg:</p>
<img src="https://ttntm.me/static/img/blog/cdbrg_2024.png" class="img-fluid img-center auto-invert" alt="ttntm's Codeberg contributions 2024">
<p>GitHub:</p>
<img src="https://ttntm.me/static/img/blog/gh_2024.png" class="img-fluid img-center auto-invert" alt="ttntm's GitHub contributions 2024">
<p>Website: <a href="https://ttntm.me/changelog/#timeline" class="int">Activity Timeline</a></p>
<p><strong>Personal Life</strong></p>
<ul>
<li>Wife finally got her Austrian citizenship</li>
<li>Had a good time in Portugal (Lisbon/Porto), also tried scuba diving</li>
<li>Mostly stopped drinking alcohol in June, with the occasional social event as an exception</li>
<li>Got back into weight lifting thanks to a new gym around the corner</li>
</ul>
<p>Lifting progress after the first 12-week cycle of GZCLP (July 19th to Dec. 9th, ~2,5 workouts/week):</p>
<ul>
<li>Bench: 35 kg &gt;&gt; 60 kg (3RM)</li>
<li>Deadlift: 45 kg &gt;&gt; 100 kg (5RM)</li>
<li>OHP: 25 kg &gt;&gt; 50 kg (1RM)</li>
<li>Squat: 40 kg &gt;&gt; 95 kg (3RM)</li>
</ul>
<p><strong>Best _ of 2024</strong></p>
<ul>
<li><strong>Drink</strong>: Protein shakes</li>
<li><strong>Game</strong>: Diablo II: Resurrected</li>
<li><strong>Movie</strong>: Dune II (Alien: Romulus as a close 2nd)</li>
<li><strong>Series</strong>: Shōgun</li>
</ul>
<div class="hr shadow mt2 mb2"></div>
<p><strong>Games of 2024</strong></p>
<div class="grid grid2 gap1">
<div>
<p>In somewhat chronological order:</p>
<ul>
<li>Tiny Tina’s Wonderlands *</li>
<li>Cloudpunk</li>
<li>Urbek City Builder</li>
<li>Ghost of Tsushima</li>
<li>Kingdom Come: Deliverance *<br>
(accidentally deleted my 80h save)</li>
<li>Tunic</li>
<li>Metro Exodus</li>
<li>The First Descendant *<br>
(stopped playing after the final trophy)</li>
<li>Deus Ex: Mankind Divided</li>
<li>Everspace 2</li>
<li>The Outer Worlds</li>
<li>Diablo II: Resurrected *<br>
(never really finished, is it?)</li>
<li>Brotato</li>
<li>Hogwarts Legacy</li>
</ul>
<p><code>*</code> <em>marks finished games (main story).</em></p>
</div>
<div>
<p>Games I tried but dropped (&lt;4h):</p>
<ul>
<li>Sifu</li>
<li>Firewatch</li>
<li>Evil West</li>
<li>Descenders</li>
<li>Borderlands 3</li>
</ul>
</div>
</div>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 49, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w49/"/>
        
        <updated>2024-12-02T13:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w49/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://infrequently.org/2024/11/if-not-react-then-what/" class="ext u-like-of" rel="noopener">If Not React, Then What?</a><br>
“Frameworkism isn’t delivering. The answer isn’t a different tool, it’s the courage to do engineering.”</li>
<li><a href="https://dbushell.com/2024/11/25/links-initiative/" class="ext u-like-of" rel="noopener">Links Initiative</a><br>
Inspiring; might write a little post to create awareness for my lists of resources and tools I collected over the years</li>
<li><a href="https://www.keithcirkel.co.uk/i-dont-have-time-to-learn-react/" class="ext u-like-of" rel="noopener">I don’t have time to learn React</a><br>
Some good points in there</li>
<li><a href="https://www.maaikebrinkhof.nl/i-lost-something-precious-without-realizing/" class="ext u-like-of" rel="noopener">I lost something precious (without realizing)</a><br>
Sometimes less is more</li>
<li><a href="https://dbushell.com/2024/11/21/static-search-page-find/" class="ext u-like-of" rel="noopener">Static Search with Pagefind</a><br>
Adding search is something I considered doing for a while, reading about it is a good reminder</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://calculatingempires.net" class="ext u-like-of" rel="noopener">Calculating Empires</a><br>
“A Genealogy of Technology and Power Since 1500” - amazing!</li>
<li><a href="https://neal.fun" class="ext u-like-of" rel="noopener">neal.fun</a><br>
A wonderful collection of interactive things that feels like the old days, when the internet was creative and fun (rest assured that there are no videos, memes or other crap there)</li>
<li><a href="https://codeberg.org/flohmarkt/flohmarkt" class="ext u-like-of" rel="noopener">flohmarkt</a><br>
“Federated decentral classified ad software using activitypub” - looks great, considering setting up an instance for the city I live in</li>
<li><a href="https://monsterbrains.blogspot.com/2021/02/fritz-schwimbeck-1889-1972.html" class="ext u-like-of" rel="noopener">Fritz Schwimbeck (1889-1972)</a><br>
A german artist’s “dark, psychological pen and ink images from before 1920”</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>App Defaults - 2024</title>
        
          
            <category term="defaults" />
          
            <category term="personal" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/app-defaults-2024/"/>
        
        <updated>2024-11-19T11:30:00Z</updated>
        <id>https://ttntm.me/blog/app-defaults-2024/</id>
        <content type="html"><![CDATA[
          
            
            <p>It’s that time of the year again: here’s my extended “frozen <a href="https://ttntm.me/uses" class="int">/uses</a> page” for late 2024.</p>
<p>Whenever multiple applications are listed, <code>p</code> marks private use, <code>w</code> marks software that I (have to) use at work. Changes compared to the previous year are highlighted using <em>colored text</em>.</p>
<ul>
<li>
<p><strong>Mail Client</strong>: Proton Mail (<code>p</code>), Outlook PWA (<code>w</code>)</p>
</li>
<li>
<p><strong>Calendar</strong>: Proton Calendar (<code>p</code>), Outlook PWA (<code>w</code>)</p>
</li>
<li>
<p><strong>Notes</strong>: <em>Plain text files, and my personal wiki (TKB)</em></p>
</li>
<li>
<p><strong>Todos</strong>: <em>Plain text files</em> (<code>p</code>), MS To Do (<code>w</code>)</p>
</li>
<li>
<p><strong>Terminal</strong>: WezTerm</p>
</li>
<li>
<p><strong>Shell</strong>: zsh with Oh My Zsh and p10k</p>
</li>
<li>
<p><strong>Browser (Desktop)</strong>: <em>Zen Browser</em>, Ungoogled Chromium (<code>w</code>)</p>
</li>
<li>
<p><strong>Browser (Mobile)</strong>: <em>Cromite</em> (Android), Safari (iOS)</p>
</li>
<li>
<p><strong>Search Engine</strong>: DuckDuckGo</p>
</li>
<li>
<p><strong>Text Editor</strong>: VS Code, micro or nvim (terminal editing), gedit (quick notes, previews; <code>w</code>)</p>
</li>
<li>
<p><strong>Git Client</strong>: terminal</p>
</li>
<li>
<p><strong>Passwords</strong>: Proton Pass (<code>p</code>), KeePassXC (<code>w</code>)</p>
</li>
<li>
<p><strong>File Sharing</strong>: Proton Drive (<code>p</code>), OneDrive (<code>w</code>)</p>
</li>
<li>
<p><strong>Docs/Wiki</strong>: <em>TKB</em> (<code>p</code>), Confluence (<code>w</code>)</p>
</li>
<li>
<p><strong>Spreadsheets</strong>: Google Sheets (<code>p</code>), LibreOffice Calc (<code>w</code>)</p>
</li>
<li>
<p><strong>Photos</strong>: Google Photos</p>
</li>
<li>
<p><strong>RSS</strong>: <em>TBD (they killed Omnivore)</em></p>
</li>
<li>
<p><strong>Chat</strong>: Signal (<code>p</code>), Teams (<code>w</code>)</p>
</li>
<li>
<p><strong>Bookmarks/Links</strong>: <em>TKB, /bookmarks</em></p>
</li>
<li>
<p><strong>Shopping List</strong>: Google Keep</p>
</li>
<li>
<p><strong>Music</strong>: cmus (Desktop), Poweramp (Android)</p>
</li>
<li>
<p><strong>Social Media</strong>: Mastodon via Elk (Desktop), IceCubes (iOS) and <em>Moshidon</em> (Android), Pixelfed (PWA)</p>
</li>
<li>
<p><strong>Ad Blocking</strong>: RethinkDNS (Android), uBlock Origin</p>
</li>
<li>
<p><strong>VPN</strong>: Proton VPN</p>
</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 47, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w47/"/>
        
        <updated>2024-11-18T14:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w47/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://dev.to/jgustavoas/solved-how-to-fully-animate-the-details-html-element-with-only-css-and-no-javascript-55ek" class="ext u-like-of" rel="noopener">How to fully animate the details HTML element with only CSS and no JavaScript</a><br>
An interesting approach, something to keep in mind for future reference</li>
<li><a href="https://jai.me/blog/2024-11-08-til-bracket-notation-object-declaration/" class="ext u-like-of" rel="noopener">Bracket notation during object declaration</a><br>
Not “something that gets used frequently, but still a nice option” - yup, that</li>
<li><a href="https://alistairshepherd.uk/writing/accessibility-overlays-email/" class="ext u-like-of" rel="noopener">My anti-overlay client letter</a></li>
<li><a href="https://molily.de/something-went-wrong/" class="ext u-like-of" rel="noopener">Something went wrong</a><br>
About the “ways out of the JavaScript crisis”</li>
<li><a href="https://jakelazaroff.com/words/a-local-first-case-study/" class="ext u-like-of" rel="noopener">A Local-First Case Study</a></li>
<li><a href="https://frontendmasters.com/blog/you-might-not-need-that-framework/" class="ext u-like-of" rel="noopener">You Might Not Need that Framework</a><br>
Yes, indeed</li>
<li><a href="https://sachachua.com/blog/2024/10/thinking-about-12-aspects-of-personal-information-knowledge-management/" class="ext u-like-of" rel="noopener">Thinking about 12 aspects of personal information/knowledge management</a><br>
Good one, check it out if you write notes that you intend to keep around</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://cookieconsent.orestbida.com" class="ext u-like-of" rel="noopener">CookieConsent</a><br>
A very good “lightweight, GDPR and CCPA compliant Consent Management Tool written in vanilla JS”</li>
<li><a href="https://classic.battle.net/diablo2exp/" class="ext u-like-of" rel="noopener">The Arreat Summit</a><br>
A strategy guide for Diablo II: Lord of Destruction, and most likely the “grandfather of modern game wikis”</li>
<li><a href="https://raindrop.io" class="ext u-like-of" rel="noopener">raindrop.io</a><br>
An “all-in-one bookmark manager”, that I heard lots of good things about - on my list of tools to try</li>
<li><a href="https://european-alternatives.eu/alternatives-to" class="ext u-like-of" rel="noopener">European alternatives for popular services</a></li>
<li><a href="https://webamp.org" class="ext u-like-of" rel="noopener">webamp</a><br>
Winamp 2 reimplemented for the browser</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 37, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w37/"/>
        
        <updated>2024-09-13T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w37/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://daverupert.com/2024/09/dev-tools-performance-monitor-panel/" class="ext u-like-of" rel="noopener">The Dev Tools Performance Monitor Panel</a><br>
Good summary of a feature I haven’t used much so far</li>
<li><a href="https://www.wired.com/story/meredith-whittaker-signal/" class="ext u-like-of" rel="noopener">Signal Is More Than Encrypted Messaging</a><br>
An interview with Meredith Whittaker for Signal’s 10th anniversary</li>
<li><a href="https://www.decarpentier.nl/carpentopod" class="ext u-like-of" rel="noopener">Carpentopod: A walking table project</a><br>
One word: amazing</li>
<li><a href="https://www.howtolovecomics.com/2024/08/25/best-junji-ito-manga-stories/" class="ext u-like-of" rel="noopener">The 12 Best Junji Ito Manga Stories You Should Read</a><br>
A good list indeed</li>
<li><a href="https://build-your-own.org/blog/20240813_css_vertical_center/" class="ext u-like-of" rel="noopener">CSS finally adds vertical centering in 2024</a><br>
“In 2024, browsers have implemented <code>align-content</code> for flow layout” - we shoul keep that in mind for the next site we work on</li>
<li><a href="https://www.markpitblado.me/blog/learning-by-building" class="ext u-like-of" rel="noopener">Learning by building</a><br>
A niece piece about the “lessons learned throughout the process of learning by building”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.haptic.md" class="ext u-like-of" rel="noopener">Haptic</a><br>
A new tool for markdown notes, local-first and privacy-focused</li>
<li><a href="https://www.sitepoint.com/style-html-details-element/" class="ext u-like-of" rel="noopener">20 Simple Ways to Style the HTML details Element</a><br>
Some helpful examples I keep coming back to every once in a while</li>
<li><a href="https://clew.se" class="ext u-like-of" rel="noopener">clew</a><br>
In their own words: “Clew is a web search engine trying to be different from the rest”</li>
<li><a href="https://webcomponents.fyi" class="ext u-like-of" rel="noopener">webcomponents.fyi</a><br>
Web components in plain English</li>
<li><a href="https://www.cookiestatus.com" class="ext u-like-of" rel="noopener">Cookie Status</a><br>
A catalog of the various tracking protection mechanisms implemented by the major browsers and browser engines</li>
<li><a href="https://www.projectwallace.com" class="ext u-like-of" rel="noopener">Project Wallace</a><br>
A set of CSS analyzers that check complexity, specificity, performance and much more</li>
<li><a href="https://pororoca.io" class="ext u-like-of" rel="noopener">Pororoca</a><br>
Another Postman alternative, nice</li>
<li><a href="https://www.surveillancewatch.io" class="ext u-like-of" rel="noopener">Surveillance Watch</a><br>
An interactive map that documents the hidden connections within the opaque surveillance industry</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Millenial Nostalgia</title>
        
          
            <category term="personal" />
          
        
        <link href="https://ttntm.me/blog/millenial-nostalgia/"/>
        
        <updated>2024-08-24T20:30:00Z</updated>
        <id>https://ttntm.me/blog/millenial-nostalgia/</id>
        <content type="html"><![CDATA[
          
            
            <p>Others wrote about their share of the “Millennial nostalgia”, and I enjoyed reading about their memories very much:</p>
<ul>
<li><a class="ext u-in-reply-to" href="https://notes.jeddacp.com/my-millennial-memories/" rel="noopener">My Millennial Memories</a></li>
<li><a class="ext u-in-reply-to" href="https://flamedfury.com/posts/millennial-energy/" rel="noopener">Millennial Energy</a></li>
</ul>
<p>The following is a list of some of the things I remember from growing up in the 1990s and early 2000s in Central Europe.</p>
<h2 id="lifestyle" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#lifestyle"><span>Lifestyle</span></a></h2>
<ul>
<li>Trading cards
<ul>
<li>NBA Basketball in elementary school</li>
<li>Pokémon and MTG in high school</li>
</ul>
</li>
<li>A teenage obsession with skateboarding, thanks to the Tony Hawk games, and failing badly in real life</li>
<li>Jeans full of holes - it got so bad, that grandma once gave me some money and told me to stop looking like a bum, meaning I was supposed to buy “proper” pants and wear them next time, just to prove I used the money for the right thing</li>
<li>Smoking was cool unfortunately - I managed to quit when I was 28, after 15 years</li>
</ul>
<h2 id="music-tv" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#music-tv"><span>Music &amp; TV</span></a></h2>
<ul>
<li>Taping radio to cassette for the car and/or the (used) Walkman I got from some older relative that upgraded to MiniDisc</li>
<li>Taping TV (i.e. Star Wars) to VHS because it ran so long past bedtime that this was the only deal my parents would accept (I was about 6 then)</li>
<li>Watching Dragon Ball Z on TV - 5 mins of new stuff, 5 mins each for recap/preview and a 5 min ad break in between</li>
<li>Watching MTV, when they were still (mostly) a music channel</li>
<li>Watching TV series… on TV - one episode per week, with a repeat late in the night a day or two later, in case you missed it or forgot to tape it</li>
<li>Warez
<ul>
<li>Early mp3 dealings, managing a music library with Winamp</li>
<li>Movie downloads, and burning to multiple CDs just to watch a single film on the slightly bigger TV screen in front of the couch</li>
</ul>
</li>
<li>Video rentals, first on VHS, then on DVD</li>
</ul>
<h2 id="video-games" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#video-games"><span>Video Games</span></a></h2>
<ul>
<li>Reading gaming magazines while offline on vacation, obsessing over things I didn’t have yet, i.e. Diablo 2</li>
<li>Complimentary CDs that came with gaming magazines - they included free/shareware, game demos, and sometimes even full versions</li>
<li>Going to the store to buy PC games that came in beautiful boxes and with a printed manual</li>
<li>Having a shared family PC for internet access
<ul>
<li>Paying Diablo in the living room full of guests during New Year’s Eve</li>
<li>ICQ, and the “fun” of having overly curious family members around</li>
<li>One day, a porn site appeared as our browser start page, and everybody denied responsibility for it - wasn’t me, and I still don’t know who’s to blame a quarter of century later…</li>
</ul>
</li>
<li>LAN parties, and having to make someone drive you (and your bulky PC) there</li>
<li>Summer holidays full of
<ul>
<li>Game Boy Super Mario, without a save function</li>
<li>Age of Empires and Diablo I/II, which ran on old company laptops my father could borrow for us</li>
<li>PlayStation matches with the neighbors kids, who had a Dragon Ball fighting game that was insanely fun</li>
</ul>
</li>
<li>My parents’ reaction to games like GTA 3 or Quake III Arena - not exactly fun conversations to have as a teenager…</li>
<li>Hundreds (thousands?) of hours spent on CounterStrike (the early days, 1.6 and before)</li>
</ul>
<h2 id="tech" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#tech"><span>Tech</span></a></h2>
<ul>
<li>Getting my first email address, which came with 25 MB of webspace (FTP access)</li>
<li>Early mobile phone customization with banners and ringtones</li>
<li>Using all my prepaid phone credit for SMS</li>
<li>Building my first gaming PC - it had a GeForce 3 that could even handle Doom 3 a couple of years later</li>
<li>Building my first websites, feeling like I can do everything on the web</li>
<li>Mobile phones kept getting smaller - I actually broke one of mine when I accidentally scored during a game of basketball, the phone flew through the net together with the ball</li>
<li>My first mp3 player, and the freedom that came with it</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 32, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w32/"/>
        
        <updated>2024-08-08T17:00:00Z</updated>
        <id>https://ttntm.me/likes/2024-w32/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.luminary.com/blog/resolving-rich-text-in-astro-and-next-js" class="ext u-like-of" rel="noopener">Resolving Rich Text in Astro and Next.js</a><br>
A helpful article about using Portable Text to resolve richt text from a headless CMS</li>
<li><a href="https://paulbutler.org/2024/the-haters-guide-to-kubernetes/" class="ext u-like-of" rel="noopener">The hater’s guide to Kubernetes</a><br>
Seems helpful, got to keep it in mind for whenever I’m digging into Kubernetes further</li>
<li><a href="https://tsev.dev/posts/2024-08-03-javascript-didnt-cause-the-great-divide/" class="ext u-like-of" rel="noopener">JavaScript didn’t cause The Great Divide</a><br>
A thoughtful piece about the state of web development</li>
<li><a href="https://ploum.net/2023-08-01-splitting-the-web.html" class="ext u-like-of" rel="noopener">Splitting the Web</a><br>
“Maybe the web is not dying. Maybe the web is only splitting itself in two.”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://gitlab-org.gitlab.io/frontend/fonts/" class="ext u-like-of" rel="noopener">gitlab-org.gitlab.io/frontend/fonts/</a><br>
Cool fonts, also GitLab improved JetBrains Mono a little</li>
<li><a href="https://defrag98.com" class="ext u-like-of" rel="noopener">Win 98 Disk Defragmenter</a><br>
Super nostalgic :D</li>
<li><a href="https://public.work" class="ext u-like-of" rel="noopener">Public Work</a><br>
A search engine for public domain content</li>
<li><a href="https://github.com/mcastillof/FakeTraveler" class="ext u-like-of" rel="noopener">FakeTraveler</a><br>
A tool to mock the device location on Android</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 30, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w30/"/>
        
        <updated>2024-07-23T09:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w30/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3382728/" class="ext u-like-of" rel="noopener">The time-course of single-word reading: Evidence from fast behavioral and brain responses</a><br>
An interesting scientific article about the way our brain works when we’re reading</li>
<li><a href="https://box464.com/posts/salesforce-eleventy-directory/" class="ext u-like-of" rel="noopener">Eleventy Experiment - Salesforce User Directory</a><br>
Interesting, because it reminded me of stuff we do at work</li>
<li><a href="https://piccalil.li/blog/styling-tables-the-modern-css-way/" class="ext u-like-of" rel="noopener">Styling Tables the Modern CSS Way</a><br>
A great resource to prepare for the inevitable</li>
<li><a href="https://nooshu.com/blog/2019/10/02/how-to-read-a-wpt-waterfall-chart/" class="ext u-like-of" rel="noopener">How to read a WebPageTest Waterfall View chart</a><br>
A little older, but helpful nevertheless</li>
<li><a href="https://werd.io/2024/a-computer-can-never-be-held-accountable" class="ext u-like-of" rel="noopener">A computer can never be held accountable</a><br>
About a famous IBM slide…</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.saynotobroscience.com/gzclp-infographic/" class="ext u-like-of" rel="noopener">The GZCLP Program</a><br>
A linear progression workout program that I’ve decided to try</li>
<li><a href="https://github.com/lokesh/color-thief" class="ext u-like-of" rel="noopener">color-thief</a><br>
A JS tool to grab the color palette from an image</li>
<li><a href="https://github.com/darrenburns/posting" class="ext u-like-of" rel="noopener">posting</a><br>
An API client that lives in your terminal</li>
<li><a href="https://how.complexsystems.fail" class="ext u-like-of" rel="noopener">How Complex Systems Fail</a><br>
Still as important as ever</li>
<li><a href="https://www.gov.uk/service-manual/technology/using-progressive-enhancement" class="ext u-like-of" rel="noopener">Building a resilient frontend using progressive enhancement</a><br>
A helpful guideline</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 28, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w28/"/>
        
        <updated>2024-07-11T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w28/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://melkat.blog/p/unsafe-pricing" class="ext u-like-of" rel="noopener">Unsafe Pricing at Any Scale </a><br>
Cool article with some very interesting suggestions for things you might want to consider doing</li>
<li><a href="https://blog.frankmtaylor.com/2024/06/20/a-rant-about-front-end-development/" class="ext u-like-of" rel="noopener">A Rant about Front-end Development</a><br>
A good rant, worth reading</li>
<li><a href="https://danilafe.com/blog/blog_microfeatures/" class="ext u-like-of" rel="noopener">Microfeatures I Love in Blogs and Personal Websites</a><br>
A cool list, glad to have implemented many of them already</li>
<li><a href="https://gomakethings.com/every-website-and-web-app-should-have-a-service-worker/" class="ext u-like-of" rel="noopener">Every website and web app should have a service worker</a><br>
Definitely something to consider</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://penpot.app" class="ext u-like-of" rel="noopener">Penpot</a><br>
A new (to me) “web-based open-source design tool”</li>
<li><a href="https://archive.org/details/on-computer-games-monthly-december-2000-magazine" class="ext u-like-of" rel="noopener">On Computer Games Monthly #2</a><br>
The latest issue (2024-07-01) of “a gaming magazine styled as if it were created in the early 2000s”</li>
<li><a href="https://www.thisiscolossal.com/2024/07/kowlooon-walled-city-illustration/" class="ext u-like-of" rel="noopener">A Rare Cross-Section Illustration Reveals the Infamous Happenings of Kowloon Walled City</a><br>
Phenomenal; don’t forget to view the full panorama in its entirety</li>
<li><a href="https://mastometrics.com" class="ext u-like-of" rel="noopener">MastoMetrics</a><br>
Interesting, didn’t know about that one</li>
<li><a href="https://github.com/kjk/edna" class="ext u-like-of" rel="noopener">edna</a><br>
A pretty cool “note taking app for developers and power users”</li>
<li><a href="https://jdcm.al" class="ext u-like-of" rel="noopener">Johnny.Decimal</a><br>
An interesting system designed to “help you find things quickly, with more confidence, and less stress”</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Website Evolution: Q2 2024</title>
        
          
            <category term="evolution" />
          
            <category term="series" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/website-evolution-q2-2024/"/>
        
        <updated>2024-07-10T17:15:00Z</updated>
        <id>https://ttntm.me/blog/website-evolution-q2-2024/</id>
        <content type="html"><![CDATA[
          
            
            <p>There weren’t quite as many changes in Q2 2024 as there were in Q1, but I managed to do lots of cool things nevertheless.</p>
<p>Part of the website <a href="https://ttntm.me/tags/evolution/" class="int">#evolution</a> series. This post covers versions <a href="https://ttntm.me/changelog/#3-14" class="int">3.14</a> to <a href="https://ttntm.me/changelog/#3-19" class="int">3.19</a>.</p>
<h2 id="content-and-pages" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#content-and-pages"><span>Content and Pages</span></a></h2>
<p>This section provides an overview of content and pages with details on what was added, changed and/or (re)moved.</p>
<h3 id="albums" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#albums"><span>Albums</span></a></h3>
<p>URL: <a href="https://ttntm.me/albums/" class="int">/albums</a></p>
<p>Creating this page was something that I wanted to do for a long time. Thinking about the music I love also lead to the posts in the <a href="https://ttntm.me/tags/albums/" class="int">#albums</a> series, and I had a lot of fun writing them. Some of these posts also lead to interesting email conversations that I enjoyed very much.</p>
<p>Building the page turned into a second use case for my custom filter, and I <a href="https://ttntm.me/blog/building-a-custom-filter-for-eleventy-collections/" class="int">wrote about that</a> in detail recently. I also added a “Layout” toggle to the page that currently supports both list and grid views.</p>
<h3 id="everything" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#everything"><span>Everything</span></a></h3>
<p>URL: <a href="https://ttntm.me/everything/" class="int">/everything</a></p>
<p>A list of every Blog Post, Like and Note published on this website.</p>
<p>The page itself is nothing special, just something I felt was missing. Oh, and there’s also a matching <a href="https://ttntm.me/everything.xml" class="int">feed</a>.</p>
<h3 id="hello" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#hello"><span>Hello</span></a></h3>
<p>URL: <a href="https://ttntm.me/hello/" class="int">/hello</a></p>
<p>A new (kind of) “Contact” page, based on an idea that Alastair Johnson came up with in a post called <a class="ext u-in-reply-to" href="https://alastairjohnston.com/introducing-hello-pages/" rel="noopener">Introducing Hello pages</a>. It’s a little more than the classic “Contact” page however, because it also explains <em>how</em> one prefers to be contacted.</p>
<h3 id="interests" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#interests"><span>Interests</span></a></h3>
<p>URL: <a href="https://ttntm.me/interests/" class="int">/interests</a></p>
<p>Another new page that aims to shed some light on the person behind a (personal) website. Its creation was inspired by Chris Burnell’s <a class="ext" href="https://chrisburnell.github.io/interests-directory/" rel="noopener">/interests Directory</a> that lists pages that have an /interests page, and his post titled <a class="ext u-in-reply-to" href="https://chrisburnell.com/note/slash-interests/" rel="noopener">What are you interested in?</a>.</p>
<h3 id="other-changes" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#other-changes"><span>Other Changes</span></a></h3>
<ul>
<li>Added an <a href="https://ttntm.me/blogroll.opml" class="int">OPML blogroll</a> and joined the <a class="ext" href="https://alexsci.com/rss-blogroll-network/" rel="noopener">Blogroll Network Map</a></li>
<li>Created a <a href="https://ttntm.me/blank/" class="int">/blank</a> page - it’s mostly blank, so don’t expect too much from it</li>
<li>Restored some old blog posts that I’d unpublished a long time ago - there’s nothing wrong with them, and I can’t remember why I hid them, so they’re back</li>
</ul>
<h2 id="features-and-functionality" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#features-and-functionality"><span>Features and Functionality</span></a></h2>
<p>Additional features and changes enhancing the website’s functionality.</p>
<h3 id="blog-posts" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#blog-posts"><span>Blog Posts</span></a></h3>
<p>I’ve added a new “(Table of) Contents” section to my blog posts which should help navigate the longer ones. It’s a <code>details/summary</code> element that’s collapsed by default. I’m using the <code>eleventy-plugin-toc</code> to create the ToC, and there’s also a bit of modern CSS that hides it if it should ever be empty, which can happen for shorter posts:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.toc-wrapper</span> <span class="token punctuation">{</span><br>  <span class="token selector">&amp;:not(:has(.toc))</span> <span class="token punctuation">{</span><br>    <span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>Implementing linkable headings for Markdown content was a prerequisite for that, and I’m happy I finally did that (using <code>markdown-it-anchor</code>) - it’s been on my list of website improvements for a very long time. I’ve also added an override front matter key - <code>showToc</code> - to my <code>post</code> layout. It can be used to hide the ToC, even when it’s not empty, because it’d be a bit silly to have one there (i.e. posts with only one heading).</p>
<p>Furthermore, my blog post template (and the RSS feed) now support a “last update” date. It gets displayed in the post header, right underneath the paragraph with the publication date and reading time.</p>
<p>In addition to that, blog posts are using the new <a href="https://ttntm.me/everything.xml#reading-progress" class="int">reading progress indicator</a> now (see below for further details) and I added a couple of buttons to the post footer:</p>
<ul>
<li>“Copy Link”, which can be found below “Reply With Email”</li>
<li>“View All Posts/Tags” at the very end</li>
</ul>
<h3 id="navigation" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#navigation"><span>Navigation</span></a></h3>
<p>Some cool stuff happened to my website’s navigation.</p>
<p>I added the option of making the header sticky, using persistence in <code>localStorage</code>. It’s a cool feature that leaves the decision of whether the header should be fixed at the top of the page in the hands of the user.</p>
<p>In addition to that, the links in the main navigation were also updated:</p>
<ul>
<li>A sitemap icon was added to the header</li>
<li>/likes was added to the header and footer navigation, replacing /work</li>
</ul>
<p>There’s also been a significant change of how (external) links are handled: I removed <code>target=&quot;_blank&quot;</code> from wherever it was used, and decided that external links should display with a dedicated icon instead.<br>
I also wrote a post called <a href="https://ttntm.me/blog/external-links-and-target-blank/" class="int">External Links and target=“_blank”</a> about my recent insight regarding this topic.</p>
<h3 id="reading-progress" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#reading-progress"><span>Reading Progress</span></a></h3>
<p>Adding an indicator for reading progress to blog posts (and some other content) was another thing I wanted to do for a very long time. I finally found the time and motivation to do it, and I’m quite happy with the result.</p>
<p>The progress indicator is enabled for all blog posts, but it can also be used for any other page using a <code>showProgress</code> attribute in the front matter.</p>
<p>The code that manages this progress indicator can be found here: <a class="ext" href="https://github.com/ttntm/ttntm/blob/master/src/_includes/js/useProgress.js" rel="noopener">useProgress.js</a></p>
<p>A nice detail: I’m using <code>window.getComputedStyle()</code> to determine the visibility of the progress indicator and only run the code if the element actually is visible. It’s hidden on mobile devices, for example, and there’s no need to waste resources on an event handler for a hidden element.<br>
More information can be found in a recent note I wrote about it: <a href="https://ttntm.me/notes/#35" class="int">JavaScript: element visibility</a></p>
<h2 id="look-and-feel" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#look-and-feel"><span>Look and Feel</span></a></h2>
<p>Not too much happened here:</p>
<ul>
<li>An SVG wave background image was added to the footer, it’s less boring now</li>
<li><code>position: sticky;</code> was used to put the ever-growing table of contents of the /notes page into a scrollable box</li>
<li>The layout of individual /likes entries was updated</li>
<li>Descriptions for tags were added to pages listing tagged content</li>
<li>The icon before/after the year headings on the blog archive page was changed</li>
</ul>
<h2 id="setup-and-other-technical-stuff" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#setup-and-other-technical-stuff"><span>Setup and Other Technical Stuff</span></a></h2>
<p>I upgraded to Eleventy 3.0.0-alpha.* and wrote about it: <a href="https://ttntm.me/blog/upgraded-to-eleventy-v3/" class="int">Upgraded to Eleventy v3</a></p>
<p>I also restructured my <code>_includes</code> folder because it was getting way too messy.<br>
There are 3 subfolders now:</p>
<ul>
<li><code>/components</code><br>
All the various Nunjucks components</li>
<li><code>/css</code><br>
CSS modules for <code>postcss</code></li>
<li><code>/js</code><br>
JS composables</li>
<li><code>/macros</code><br>
Nunjucks macros</li>
</ul>
<p>The new structure works better for me, but updating all my old <code>include</code> statements manually would’ve been a lot of pain. I’m happy I figured out how to use RegEx-based search and replace with append in VS Code, and I shared that as a note: <a href="https://ttntm.me/notes/#37" class="int">VS Code: RegEx replace and append</a></p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Making Your Own Tools</title>
        
          
            <category term="personal" />
          
            <category term="thoughts" />
          
        
        <link href="https://ttntm.me/blog/making-your-own-tools/"/>
        
        <updated>2024-07-04T13:30:00Z</updated>
        <id>https://ttntm.me/blog/making-your-own-tools/</id>
        <content type="html"><![CDATA[
          
            
            <p>Growing up with computers and the internet, my idea of a <em>tool</em> became equivalent to software at a rather early age. Actual tools, in terms of hammers, power tools or screwdrivers, certainly existed in our household and were also interesting enough for me to learn how to use them at some point. However, none of that came quite close to how it felt to build my first website or to set up a brand-new wireless home network.</p>
<p>Many years later, the sentiment of “tool equals software” is still deeply embedded in my subconscious. I’m ok paying 100+€/year for a software subscription, but I’m somehow struggling to justify the purchase of a torque wrench (a ~120€ one-time payment) to myself. I’d only need that torque wrench maybe 5 times per year, but that’s not really an excuse either, considering that it’s probably a once-in-a-lifetime purchase.</p>
<p>Aside from making physical tool purchases a bit harder, my “tool equals software” mindset has contributed to the fact that I’d usually prefer to make my own tools whenever that made sense (sometimes also when it didn’t, but that’s another story). I’d also choose hand-coding websites into editors without syntax highlighting, while people across the room were cursing at Dreamweaver, and I built lightweight CMS systems from scratch, instead of installing another WordPress instance that’d get hacked at some point anyway.</p>
<p>Over time, most of the tools I built for myself, and others, eventually died or were replaced with other tools that did things better, but sometimes also worse. However, some of the tools I built stood the test of time, and even saw new features, updates and fixes. Let’s talk about them now.</p>
<p>If we ignore this website (more on that later), my oldest actively maintained “make your own” tool is <strong>recept0r</strong>. It’s a lightweight web application that me and my wife use to keep track of recipes we like to cook at home. The whole project is FOSS, the recipes are publicly accessible, but we’re the only users.</p>
<p>My wife and I built the first version of recept0r in 2020, working on it together when the pandemic struck and confined us to our home. About a year later, during the late summer weeks of 2021, I re-wrote it from scratch and with a clear focus on long-term maintainability. I decided to do that, because the first year of usage proved that it’s a very convenient tool that we wouldn’t want to miss in our everyday life anymore. Now, almost 4 years after its initial release, we’re preparing for a v3 with a couple of changes and new features.</p>
<p>A couple of weeks after I was done working on recept0r v1, I started building another tool and called it <strong>watch3r</strong>. The motivation for building it was accidentally re-watching a movie, and noticing lack of a simple tool to track movies and series. Netflix’s horrible watchlist UX also contributed to the decision.</p>
<p>Working on watch3r was (and still is) fun, but it can also be quite a bit of a challenge. Contrary to recept0r, it’s open for public signup, and it has about 50 users. Features and changes shouldn’t break things and require a both careful planning and lots of testing. I haven’t managed to break anything severely during the 3,5 years that watch3r’s been operational so far, and I’m always a little surprised just how maintainable the project actually is, despite my questionable decision not to use Vue 3 with TypeScript.</p>
<p>When I changed jobs in early 2021, I moved from a product company to an agency. I found myself having to manage my time across many client projects, and I noticed that I was in need of a tool to keep track of what I was doing during the day. I could’ve just used a text file, or a spreadsheet, but instead, I sat down one Friday afternoon and started working on a time tracker.</p>
<p>Lucky for me, my wife had some spare design prototypes lying around, and by Sunday evening, I’d turned them into a simple task-based time tracker. We decided to call it <strong>aitrack</strong>, despite the lack of any “AI” in there, but the logo blob simply looked better that way (its original name was supposed to be <em>itrack</em>, without the “a”).</p>
<p>There isn’t too much to say about a time tracker, but it works reliably, and I use it pretty much every day. There are some ideas for future features, but I’m not exactly sure about them. The rather minimal base functionality just works, and at least I myself am not missing additional features that’d gradually move the application into the to-do list territory.</p>
<p>The last and also the newest tool I’d like to mention in this context is <strong>TKB</strong>, my personal knowledge base. I don’t want to get into the details here, because I wrote <a href="https://ttntm.me/blog/personal-knowledge-base/" class="int">a whole article</a> about it 2 months ago, but journaling/note taking/PKM apps simply didn’t feel right to me anymore. Building my own personal knowledge base / wiki, on the other hand, felt (and still feels) like the - preliminary - end of a long process of finding out just what works best for me in terms of documenting and managing the many things I don’t want to forget about.</p>
<p>As mentioned earlier, this website is actually my oldest actively maintained “make your own” tool. However, I’m not quite sure if I’d really call it tool, considering that it’s turned into a multi-faceted creative outlet over the years. Either way, it’s important to me and I love tinkering with it, sometimes even more so, than to use it to write and publish. But after all, it’s a personal website, and there isn’t any obligation to do anything at all. It only really matters how you (or me, in this case) feel about it - I love mine, and I’m really glad to have a tool like that at my disposal.</p>
<p>Feel free to explore the <a href="https://ttntm.me/projects/" class="int">Projects page</a> if you want to know more about the tools I mentioned.</p>
<p>—</p>
<p>This post was written as part of the <a class="ext u-in-reply-to" href="https://jamesg.blog/2024/07/01/indieweb-carnival-tools/" rel="noopener">July 2024 IndieWeb blog carnival</a>.<br>
The carnival is hosted by <a class="ext" href="https://jamesg.blog" rel="noopener">James</a>, and this month’s topic is “Tools”.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 27, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w27/"/>
        
        <updated>2024-07-03T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w27/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://frontendmasters.com/blog/control-javascript-promises-from-anywhere-using-promise-withresolvers/" class="ext u-like-of" rel="noopener">Control JavaScript Promises from Anywhere Using Promise.withResolvers()</a><br>
An insightful article about using Promises</li>
<li><a href="https://martinfowler.com/articles/data-fetch-spa.html" class="ext u-like-of" rel="noopener">Data Fetching Patterns in Single-Page Applications</a><br>
A giant piece, (almost) co-authored by Martin Fowler; uses React for examples</li>
<li><a href="https://alexwlchan.net/2024/scheduled-screenshots/" class="ext u-like-of" rel="noopener">Taking regular screenshots of my website</a><br>
A great idea!</li>
<li><a href="https://anthonyhobday.com/sideprojects/saferules/" class="ext u-like-of" rel="noopener">Visual design rules you can safely follow every time</a><br>
Very helpful indeed</li>
<li><a href="https://82mhz.net/posts/2024/06/will-we-remember-2024-as-the-year-the-internet-died/" class="ext u-like-of" rel="noopener">Will we remember 2024 as the year the internet died?</a><br>
A thought-provoking article with links to many related articles</li>
<li><a href="https://gabrielsieben.tech/2024/05/17/thinking-out-loud-2nd-gen-email/" class="ext u-like-of" rel="noopener">Thinking out loud about 2nd-gen Email</a><br>
Many good points, so many things are painful when working with email</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://dotenvx.com" class="ext u-like-of" rel="noopener">dotenvx</a><br>
Looks gooood, should give this a try asap</li>
<li><a href="https://annas-archive.org" class="ext u-like-of" rel="noopener">Anna’s Archive</a><br>
“The largest truly open library in human history”</li>
<li><a href="https://shouldiuseacarousel.com" class="ext u-like-of" rel="noopener">Should I Use a Carousel?</a><br>
A good question with a simple answer…</li>
<li><a href="https://1x.engineer" class="ext u-like-of" rel="noopener">1x Engineer</a><br>
Definitely not a 10x engineer</li>
<li><a href="https://tools.keithjgrant.com/theme-machine/" class="ext u-like-of" rel="noopener">Theme Machine</a><br>
Two color tools for practical CSS development, a color picker and a palette builder</li>
<li><a href="https://github.com/brittonhayes/pillager" class="ext u-like-of" rel="noopener">pillager</a><br>
A tool to search directories for sensitive information in files</li>
<li><a href="https://github.com/joye61/pic-smaller" class="ext u-like-of" rel="noopener">pic-smaller</a><br>
An(other) image compressor; support batch processing</li>
<li><a href="https://github.com/josdejong/jsoneditor" class="ext u-like-of" rel="noopener">JSON Editor Online</a><br>
A pretty cool open source JSON editor</li>
<li><a href="https://yoan-thirion.gitbook.io" class="ext u-like-of" rel="noopener">Knowledge-base</a><br>
A great personal wiki by Yoan Thirion</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Building a Custom Filter for Eleventy Collections</title>
        
          
            <category term="eleventy" />
          
            <category term="guide" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/building-a-custom-filter-for-eleventy-collections/"/>
        
        <updated>2024-07-01T18:50:00Z</updated>
        <id>https://ttntm.me/blog/building-a-custom-filter-for-eleventy-collections/</id>
        <content type="html"><![CDATA[
          
            
            <p>Collections and lists are a wonderful thing, and filters simply make them better.</p>
<p>On this website, the first use case for a filterable collection was my whisky collection, and I’ve also added a filter to my <a href="https://ttntm.me/albums/" class="int">/albums</a> page in the meantime. Both lists are filterable by different criteria, but use the same code for it.</p>
<p>So, let’s have a look at how one could build such a composable filter for Eleventy collections.</p>
<h2 id="prerequisites" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#prerequisites"><span>Prerequisites</span></a></h2>
<p>There’s only one actual prerequisite: an Eleventy collection that you want to filter.</p>
<p>It doesn’t matter if the collection is based on JSON from the <code>_data</code> directory, or Markdown from <code>config.addCollection()</code>, both should work. In my case, the “albums” collection is a JSON file, and the “whisky” collection consists of many Markdown files.</p>
<h3 id="collection-rendering" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#collection-rendering"><span>Collection Rendering</span></a></h3>
<p>There are 2 things to keep in mind when rendering a collection that should become filterable:</p>
<p>First, there should be a wrapper <code>ul</code> for the filter controls (<code>li</code> elements wrapping a <code>button</code>). I’ve kept that <code>ul</code> in the parent component (template), so there’s full control on how to layout it without any need to interfere with the shortcode that produces the filter UI.</p>
<p>Next, when rendering the list items, some additional information should be included in the HTML:</p>
<ol>
<li>A specific class all list items have in common, i.e. <code>album-listing</code><br>
This class shouldn’t be used for other elements (on the same page at least)</li>
<li>A <code>data-*</code> attribute that is named after, and contains, the data attribute to filter by<br>
Sticking to the albums collection, that’d be the respective list item’s “genre”, rendered as <code>data-genre=&quot;undefined&quot;</code></li>
</ol>
<p>Example (albums collection):</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>albums<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>list-reset post<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  {% for item in albums %}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>album-listing<span class="token punctuation">"</span></span> <span class="token attr-name">data-genre</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ item.genre | slugify }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      {% set released = ["Released: ", item.release] | join %}<br>      {% set titleText = [item.title, " (", item.release, ")"] | join %}<br>      {% imageHeader item.cover, imageHeaderSize, "h3", titleText, released %}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span><br>  {% endfor %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span></code></pre>
<h2 id="the-filter-ui" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-filter-ui"><span>The Filter UI</span></a></h2>
<p>This section expands on a note I wrote back in March: <a href="https://ttntm.me/notes/#33" class="int">11ty: passing collections to shortcodes</a>.</p>
<p>I’m using a shortcode to create the filter UI, which is essentially a list (<code>ul</code>) that contains a couple of <code>button</code> elements:</p>
<pre class="language-js"><code class="language-js"><span class="token function-variable function">customFilter</span><span class="token operator">:</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">collection<span class="token punctuation">,</span> filterKey<span class="token punctuation">,</span> excludeKey <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">,</span> excludeValue <span class="token operator">=</span> <span class="token keyword">null</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> displayLength <span class="token operator">=</span> <span class="token number">0</span><br>  <span class="token keyword">const</span> terms <span class="token operator">=</span> collection<span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">map<span class="token punctuation">,</span> currentItem</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> data <span class="token operator">=</span> currentItem<span class="token punctuation">.</span>data <span class="token operator">??</span> currentItem<br><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span><br>      data<br>      <span class="token operator">&amp;&amp;</span> data<span class="token punctuation">.</span><span class="token function">hasOwnProperty</span><span class="token punctuation">(</span>filterKey<span class="token punctuation">)</span><br>      <span class="token operator">&amp;&amp;</span> data<span class="token punctuation">[</span>filterKey<span class="token punctuation">]</span><span class="token operator">?.</span>length <span class="token operator">></span> <span class="token number">0</span><br>      <span class="token operator">&amp;&amp;</span> data<span class="token punctuation">[</span>excludeKey<span class="token punctuation">]</span> <span class="token operator">!==</span> excludeValue<br>    <span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">const</span> filterTerm <span class="token operator">=</span> data<span class="token punctuation">[</span>filterKey<span class="token punctuation">]</span><br>      <span class="token keyword">const</span> filterKeyCount <span class="token operator">=</span> <span class="token punctuation">(</span>map<span class="token punctuation">[</span>filterTerm<span class="token punctuation">]</span> <span class="token operator">||</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">1</span><br><br>      displayLength<span class="token operator">++</span><br><br>      <span class="token keyword">return</span> <span class="token punctuation">{</span><br>        <span class="token operator">...</span>map<span class="token punctuation">,</span><br>        <span class="token punctuation">[</span>filterTerm<span class="token punctuation">]</span><span class="token operator">:</span> filterKeyCount<br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> map<br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token keyword">const</span> resetBtn <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;li><br>    &lt;button class="filter-btn filter-btn__active shadow" data-reset="true" data-value="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filterKey<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"><br>      All&amp;nbsp;(</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>displayLength<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">)<br>    &lt;/button><br>  &lt;/li></span><span class="token template-punctuation string">`</span></span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">[</span>resetBtn<span class="token punctuation">]</span><br>    <span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span>Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>terms<span class="token punctuation">)</span><br>      <span class="token punctuation">.</span><span class="token function">sort</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>      <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">t</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;li><br>          &lt;button class="filter-btn shadow" data-term="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filterKey<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" data-value="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>_<span class="token punctuation">.</span><span class="token function">kebabCase</span><span class="token punctuation">(</span>t<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"><br>            </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>t<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;nbsp;(</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>terms<span class="token punctuation">[</span>t<span class="token punctuation">]</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">)<br>          &lt;/button><br>        &lt;/li></span><span class="token template-punctuation string">`</span></span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>That’s quite a chunk of code - let’s have a look at it.</p>
<p>This shortcode is a function that accepts 4 parameters and returns HTML.<br>
The parameters are:</p>
<ol>
<li><code>collection</code> (<code>object[]</code>): an Eleventy collection</li>
<li><code>filterKey</code> (<code>string</code>): the front matter or JSON key the filter should be based on, i.e. “genre” for the albums collection</li>
<li><code>excludeKey?</code> (<code>string | null</code>): used together with <code>excludeValue</code>; a way to exclude certain items in a collection from the filter</li>
<li><code>excludeValue?</code> (<code>any | null</code>): used together with <code>excludeKey</code></li>
</ol>
<p>Both <code>excludeKey</code> and <code>excludeValue</code> are optional parameters that default to <code>null</code>. That’s a deliberate choice, because <code>Boolean(o[null] !== null)</code> returns <code>true</code>, which means that the code won’t break if/when either (or both) of them are omitted.</p>
<p>At its core, the shortcode uses <code>Array.prototype.reduce()</code> to flatten all <code>filterKey</code> values obtained from the collection into a map(-like) object that contains only unique <code>filterKey</code> values and their usage counts. The resulting array gets sorted and concatenated with a button that resets the filter (<code>resetBtn</code>), and eventually ends up in the page template as one long string of HTML.</p>
<p>This is how I’m using the shortcode in my page templates:</p>
<p>Albums collection (JSON):</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>...<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  {% customFilter albums, "genre" %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span></code></pre>
<p>Whisky collection (Markdown):</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>...<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  {% customFilter collections.whisky, "region", "status", "wanted" %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span></code></pre>
<p>The above snippets show simple usage (albums), and also how to use the exclusion parameters (whisky).</p>
<h2 id="composable-logic" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#composable-logic"><span>Composable Logic</span></a></h2>
<p>Thanks to the shortcode from above, there’s a UI for the filter. Now, it’s time to make that UI interactive.</p>
<p>This is a high-level - collapsed - view of the <code>useCollectionFilter</code> composable:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">useCollectionFilter</span><span class="token punctuation">(</span><span class="token parameter">config</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">function</span> <span class="token function">updateSearchParams</span><span class="token punctuation">(</span><span class="token parameter">term<span class="token punctuation">,</span> value</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> classActive <span class="token operator">=</span> <span class="token string">'filter-btn__active'</span><br>    <span class="token keyword">const</span> classHidden <span class="token operator">=</span> <span class="token string">'d-none'</span><br>    <span class="token keyword">const</span> filterBtns <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'.filter-btn'</span><span class="token punctuation">)</span><br>    <span class="token keyword">const</span> filterParam <span class="token operator">=</span> config<span class="token punctuation">.</span>filterParam<br>    <span class="token keyword">const</span> listings <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span>config<span class="token punctuation">.</span>listingClass<span class="token punctuation">)</span><br>    <span class="token keyword">const</span> resetBtns <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'[data-reset="true"]'</span><span class="token punctuation">)</span><br><br>    <span class="token keyword">function</span> <span class="token function">applyFilter</span><span class="token punctuation">(</span><span class="token parameter">term<span class="token punctuation">,</span> value</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span><br><br>    <span class="token keyword">function</span> <span class="token function">handleFilterBtnClick</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span><br><br>    <span class="token keyword">function</span> <span class="token function">handleFilterInURL</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span><br><br>    <span class="token keyword">function</span> <span class="token function">resetFilter</span><span class="token punctuation">(</span><span class="token parameter">updateUrl <span class="token operator">=</span> <span class="token boolean">true</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span><br><br>    filterBtns<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">btn</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      btn<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> handleFilterBtnClick<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>    resetBtns<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">btn</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      btn<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> resetFilter<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>    window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'popstate'</span><span class="token punctuation">,</span> handleFilterInURL<span class="token punctuation">)</span><br>    window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'pushState'</span><span class="token punctuation">,</span> handleFilterInURL<span class="token punctuation">)</span><br><br>    <span class="token function">handleFilterInURL</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p><code>useCollectionFilter</code> requires a <code>config</code> object that looks like this:</p>
<pre class="language-js"><code class="language-js"><span class="token punctuation">{</span><br>  <span class="token literal-property property">filterParam</span><span class="token operator">:</span> string<br>  <span class="token literal-property property">listingClass</span><span class="token operator">:</span> string<br><span class="token punctuation">}</span></code></pre>
<p>The wrapper function returns an IIFE that first finds all needed DOM elements and eventually registers all required event listeners - the full code can be found <a class="ext" href="https://github.com/ttntm/ttntm/blob/master/src/_includes/js/useCollectionFilter.js" rel="noopener">at GitHub</a>.</p>
<p><code>applyFilter(term, value)</code> loops through both filter buttons and list items, adding/removing classes to reflect the current filter selection.</p>
<p><code>handleFilterBtnClick(event)</code> handles filter button clicks, manages their active state, and calls <code>applyFilter()</code> as well as <code>updateSearchParams()</code>.</p>
<p><code>resetFilter(updateUrl = true)</code> used to reset the filter, pretty much reverts <code>applyFilter()</code> and also removes query arguments, using <code>updateSearchParams()</code>, when called via the UI (as opposed to automated processing of query arguments, see below).</p>
<p>The two functions <code>updateSearchParams()</code> and <code>handleFilterInURL()</code> are noteworthy for the additional functionality they provide: filter persistence using the History API and dedicated query arguments contained in the URL. That makes direct links to specific list views possible, i.e. <a href="https://ttntm.me/albums/?genre=synthwave" class="int">ttntm.me/albums/?genre=synthwave</a>.</p>
<p><code>updateSearchParams()</code> is a simple and convenient function that manages the query arguments contained in the URL:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">updateSearchParams</span><span class="token punctuation">(</span><span class="token parameter">term<span class="token punctuation">,</span> value</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span>window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>href<span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>value<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    url<span class="token punctuation">.</span>searchParams<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>term<span class="token punctuation">,</span> value<span class="token punctuation">)</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    url<span class="token punctuation">.</span>searchParams<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span>term<span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><br>  window<span class="token punctuation">.</span>history<span class="token punctuation">.</span><span class="token function">pushState</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">,</span> url<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>Further information can be found at MDN:</p>
<ul>
<li><a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/API/History_API" rel="noopener">History API</a></li>
<li><a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams" rel="noopener">URL: searchParams property</a></li>
</ul>
<p><code>handleFilterInURL()</code> is used by the <code>popstate</code> and <code>pushState</code> event listeners. It also gets called whenever <code>useCollectionFilter</code> is initialized (i.e. page load), extracting and processing the query arguments contained in the URL. The function eventually calls either <code>applyFilter()</code>, or <code>resetFilter()</code>, based on a simple IF-statement.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">handleFilterInURL</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> paramName <span class="token operator">=</span> filterParam<br>  <span class="token keyword">const</span> urlParams <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URLSearchParams</span><span class="token punctuation">(</span>window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>search<span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>urlParams<span class="token punctuation">.</span><span class="token function">has</span><span class="token punctuation">(</span>paramName<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> paramValue <span class="token operator">=</span> <span class="token function">String</span><span class="token punctuation">(</span>urlParams<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>paramName<span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token comment">// Find the filter button that matches the URL param's value</span><br>    <span class="token keyword">const</span> paramValueBtn <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">[data-value="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>paramValue<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"]</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br><br>    <span class="token function">applyFilter</span><span class="token punctuation">(</span>paramName<span class="token punctuation">,</span> paramValue<span class="token punctuation">)</span><br><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>paramValueBtn <span class="token keyword">instanceof</span> <span class="token class-name">HTMLElement</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      paramValueBtn<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span>classActive<span class="token punctuation">)</span><br>      paramValueBtn<span class="token punctuation">.</span>disabled <span class="token operator">=</span> <span class="token boolean">true</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token function">resetFilter</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<h3 id="setup-and-configuration" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#setup-and-configuration"><span>Setup and Configuration</span></a></h3>
<p>Using the composable filter should end up looking like this (after including the <code>useCollectionFilter</code> code somewhere before the code in this snippet):</p>
<pre class="language-js"><code class="language-js">document<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'DOMContentLoaded'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> filterOptions <span class="token operator">=</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">filterParam</span><span class="token operator">:</span> <span class="token string">'genre'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">listingClass</span><span class="token operator">:</span> <span class="token string">'.album-listing'</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token function">useCollectionFilter</span><span class="token punctuation">(</span>filterOptions<span class="token punctuation">)</span><br><br>  <span class="token comment">// other code</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>The filter initialization has no other dependencies than the <code>config</code> object (<code>filterOptions</code> in the example above) and can be mixed with other code without causing issues.</p>
<p>My albums and whisky pages both have additional list view controls, and that’s where I manage that code. You can refer to the following files for further context:</p>
<ul>
<li><a class="ext" href="https://github.com/ttntm/ttntm/blob/master/src/_includes/js/albums.js" rel="noopener">albums.js</a></li>
<li><a class="ext" href="https://github.com/ttntm/ttntm/blob/master/_archive/whisky.js" rel="noopener">whisky.js</a></li>
</ul>
<h2 id="summary" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#summary"><span>Summary</span></a></h2>
<p>I’m really happy with this implementation of a reusable custom filter and I will probably create/find additional list pages that I’m eventually going to use it for.</p>
<p>The code is clear and minimal (IMO), making use of vanilla JS and keeping dependencies to a bare minimum (the shortcode makes use of <code>lodash</code>, but that’s just on the server side, and was already used elsewhere a long time ago).</p>
<p>At some point, I’d like to test if multiple instances of the filter on the same page would work. I wrote the code in anticipation of that, but I haven’t gotten around to testing it yet. Using multiple filters might need some changes in the config, and how it is handled though, so keep that in mind if/when you’re trying to do that.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 26, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w26/"/>
        
        <updated>2024-06-26T10:00:00Z</updated>
        <id>https://ttntm.me/likes/2024-w26/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.augmentedmind.de/2024/01/28/benchmark-azure-vm-in-kubernetes/" class="ext u-like-of" rel="noopener">Performance benchmark of over 30 Azure VMs in AKS</a><br>
A pretty interesting article, helpful for when you have no idea what kind of VM to pick</li>
<li><a href="https://prma.dev/posts/blog-from-scratch" class="ext u-like-of" rel="noopener">Blog From Scratch</a><br>
An interesting piece by Perma about rewriting his blog from scratch</li>
<li><a href="https://www.mikejohnson.dev/posts/2024/06/mobx-react-compiler" class="ext u-like-of" rel="noopener">Mobx Memoizes Components (You don’t need React Compiler)</a><br>
Good one, re: “React code […] and the performance effects of simple hooks”</li>
<li><a href="https://moddedbear.com/its-time-to-switch-an-opinionated-linux-guide/" class="ext u-like-of" rel="noopener">It’s Time to Switch: An Opinionated Linux Guide</a><br>
Yes, yes and yes</li>
<li><a href="https://joshcollinsworth.com/blog/fwiw" class="ext u-like-of" rel="noopener">For whatever it’s worth: my advice on job hunting in tech</a><br>
Focused on the US, but a great article nevertheless</li>
<li><a href="https://www.wired.com/story/perplexity-is-a-bullshit-machine/" class="ext u-like-of" rel="noopener">Perplexity Is a Bullshit Machine</a><br>
Who would have thought…</li>
<li><a href="https://solar.lowtechmagazine.com/2020/12/how-and-why-i-stopped-buying-new-laptops" class="ext u-like-of" rel="noopener">How and Why I Stopped Buying New Laptops</a><br>
From 2020, but still worth reading</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://botwiki.org" class="ext u-like-of" rel="noopener">botwiki</a><br>
A nice collection of “examples of interesting and creative online bots”</li>
<li><a href="https://exploringjs.com/js/" class="ext u-like-of" rel="noopener">Exploring JavaScript (ES2024 Edition)</a><br>
A new JS book by Axel Rauschmayer, looks good</li>
<li><a href="https://www.fastcompany.com/91140068/how-the-internet-went-mainstream-in-1994" class="ext u-like-of" rel="noopener">What the internet looked like in 1994, according to 15 webpages born that year</a><br>
Nice, long, and pretty nostalgic - loved it</li>
<li><a href="https://www.bbc.co.uk/programmes/articles/1g84m0sXpnNCv84GpN2PLZG/the-game-30th-anniversary-edition" class="ext u-like-of" rel="noopener">The Hitchhiker’s Guide to the Galaxy game</a><br>
A 30th anniversary edition of the adventure game, pretty cool</li>
<li><a href="https://github.com/immarisabel/PixeeFeeds/" class="ext u-like-of" rel="noopener">PixeeFeeds</a><br>
An open source PHP RSS feed reader</li>
<li><a href="https://2023.stateofjs.com/en-US/" class="ext u-like-of" rel="noopener">State of JS 2023 - Results</a><br>
Just in case you missed it</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 25, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w25/"/>
        
        <updated>2024-06-17T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w25/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://mijndertstuij.nl/posts/how-i-do-todos/" class="ext u-like-of" rel="noopener">How I do to-dos</a><br>
Interesting, I do mine on paper at the moment, might give this a try</li>
<li><a href="https://www.andrewwalpole.com/blog/opinions-for-writing-good-css/" class="ext u-like-of" rel="noopener">Opinions for Writing Good CSS</a><br>
Some good points indeed</li>
<li><a href="https://rebeccatoh.co/small/" class="ext u-like-of" rel="noopener">small unknown complex life</a><br>
A good read</li>
<li><a href="https://christianheilmann.com/2024/06/03/witnessing-the-death-of-the-web-as-a-news-medium/" class="ext u-like-of" rel="noopener">Witnessing the death of the web as a news medium</a><br>
Not a fun read, but good points</li>
<li><a href="https://blog.zgp.org/block-ai-training-on-a-web-site/" class="ext u-like-of" rel="noopener">Block AI training on a web site</a><br>
A very helpful article for website owners</li>
<li><a href="https://hamatti.org/posts/multi-select-filterable-command-line-interface-with-inquirer/" class="ext u-like-of" rel="noopener">Multi-select, filterable command-line interface with inquirer</a><br>
A nice guide to keep in mind for if/when I find myself wanting to do something similar</li>
<li><a href="https://gomakethings.com/a-simpler-way-to-build-dynamic-user-interactions/" class="ext u-like-of" rel="noopener">A simpler way to build dynamic user interactions</a><br>
Another good reason to use web components</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.fffuel.co" class="ext u-like-of" rel="noopener">fffuel</a><br>
A wonderful “collection of color tools and free SVG generators”</li>
<li><a href="https://developer.chrome.com/docs/web-platform/prerender-pages" class="ext u-like-of" rel="noopener">Prerender pages in Chrome for instant page navigations</a><br>
Quite amazing, didn’t know about it</li>
<li><a href="https://solidproject.org" class="ext u-like-of" rel="noopener">Solid</a><br>
An “open standard for structuring data, digital identities, and applications on the Web”</li>
<li><a href="https://whimsica11y.net" class="ext u-like-of" rel="noopener">Whimsica11y</a><br>
Web accessibility guides and advice for personal websites</li>
<li><a href="https://www.eversql.com/sql-optimization-a-comprehensive-developers-guide/" class="ext u-like-of" rel="noopener">SQL Optimization: a comprehensive developer’s guide</a><br>
A couple of “standard practices that you can implement to improve performance” when working with SQL</li>
<li><a href="https://buildless.baldurbjarnason.com" class="ext u-like-of" rel="noopener">Basic, Beautiful &amp; Buildless</a><br>
I’ve done a lot of buildless things, and this looks helpful if you want to do it too</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>My Favorite Albums - The 2000s</title>
        
          
            <category term="albums" />
          
            <category term="personal" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/my-favorite-albums-from-the-2000s/"/>
        
        <updated>2024-05-30T10:30:00Z</updated>
        <id>https://ttntm.me/blog/my-favorite-albums-from-the-2000s/</id>
        <content type="html"><![CDATA[
          
            
            <p>A chronological list of my favorite albums from 2000 to 2009 with reasons why I like them.</p>
<p>Part of the <a href="https://ttntm.me/tags/albums/" class="int">#albums</a> series.</p>
<div class="hr shadow mt2 mb2"></div>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/aop-fdt.jpg" width="125" alt="Altar of Perversion - From Dead Temples" title="Altar of Perversion - From Dead Temples" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="altar-of-perversion-from-dead-temples" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#altar-of-perversion-from-dead-temples"><span>Altar of Perversion - From Dead Temples</span></a>
          </h2>
          <p class="m0">Release: 2001</p>
        </hgroup>
    </div>
<p>An epic, monumental black metal album that’s equally fierce and atmospheric.</p>
<p>I first got this album on CD, but later hunted down the vinyl. Quite interesting, because “From Dead Temples” was released as a 6 track LP that comes with an additional 2 tracks on a 7&quot;, one of them a bonus track that’s not on the CD.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/af-bft.jpg" width="125" alt="Aslan Faction - Blunt Force Trauma" title="Aslan Faction - Blunt Force Trauma" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="aslan-faction-blunt-force-trauma" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#aslan-faction-blunt-force-trauma"><span>Aslan Faction - Blunt Force Trauma</span></a>
          </h2>
          <p class="m0">Release: 2002</p>
        </hgroup>
    </div>
<p>“The Matrix” came out in 1999. By 2002, I’d probably seen it 50 times, and “Blunt Force Trauma” sounded just like it belonged into the movie universe - I loved it!</p>
<p>Oh, almost forgot: I’d probably never have heard of this record, if it wasn’t for a perfectly planned (and definitely parent-supported) home party I hosted. I had to end this very civilized gathering abruptly, and an unknown person forgot their perfectly legal backup copy of this album in my stereo, haha.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/k-k.jpg" width="125" alt="Kaamos - Kaamos" title="Kaamos - Kaamos" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="kaamos-kaamos" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#kaamos-kaamos"><span>Kaamos - Kaamos</span></a>
          </h2>
          <p class="m0">Release: 2002</p>
        </hgroup>
    </div>
<p>Swedish death metal, from the beginning of a(nother?) “new wave of Swedish death metal” that started in the early to mid 2000s.</p>
<p>Kaamos’ self-titled debut album is a brutal and intense record with a thick, powerful production that drives their “Curse of Aeons” right into your ears. It’s one of my favorite death metal albums of all time.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/qotsa-sftd.jpg" width="125" alt="Queens of the Stone Age - Songs for the Deaf" title="Queens of the Stone Age - Songs for the Deaf" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="queens-of-the-stone-age-songs-for-the-deaf" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#queens-of-the-stone-age-songs-for-the-deaf"><span>Queens of the Stone Age - Songs for the Deaf</span></a>
          </h2>
          <p class="m0">Release: 2002</p>
        </hgroup>
    </div>
<p>A great album that absolutely kicks ass!</p>
<p>Like many others, I discovered this album due to Dave Grohl’s involvement in Nirvana, and stayed for songs like “No One Knows”, “Go With the Flow” (which also had a superb music video!) and “First it Giveth”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/v-v.jpg" width="125" alt="Verdunkeln - Verdunkeln" title="Verdunkeln - Verdunkeln" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="verdunkeln-verdunkeln" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#verdunkeln-verdunkeln"><span>Verdunkeln - Verdunkeln</span></a>
          </h2>
          <p class="m0">Release: 2005</p>
        </hgroup>
    </div>
<p>Dark, obscure, and trance-inducing black metal from Germany.</p>
<p>I used to love listening to “Verdunkeln” in darkness, or in a candle-lit room, lying flat on the floor, and becoming totally absorbed in this epic record.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/k-wwe.jpg" width="125" alt="Katharsis - VVorldVVithoutEnd" title="Katharsis - VVorldVVithoutEnd" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="katharsis-v-vorld-v-vithout-end" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#katharsis-v-vorld-v-vithout-end"><span>Katharsis - VVorldVVithoutEnd</span></a>
          </h2>
          <p class="m0">Release: 2006</p>
        </hgroup>
    </div>
<p>“VVorldVVithoutEnd” is a relentless assault that isn’t comparable to anything else out there. It’s a monument of chaos-driven destruction that’s both a marvel of sound engineering and a fierce demonstration of the band’s dedication to their dark craft - the “Grim lorde death, he is once more to ryde!” (“Eden Belovv”).</p>
<p>And if the apocalypse had a soundtrack, this would be it.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/r-eod.jpg" width="125" alt="Repugnant - Epitome of Darkness" title="Repugnant - Epitome of Darkness" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="repugnant-epitome-of-darkness" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#repugnant-epitome-of-darkness"><span>Repugnant - Epitome of Darkness</span></a>
          </h2>
          <p class="m0">Release: 2006</p>
        </hgroup>
    </div>
<p>Most likely the peak of that “new wave of Swedish death metal”, and definitely a great album. It’s full of rather catchy and memorable songs that I can still sing along to almost 20 years later.</p>
<p>Oh, and there’s been a CD-R around back then that’s got a rough mix of the album on it. I’ve still got it lying around somewhere, and it sounds much better than the vinyl record does.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/nc-tir.jpg" width="125" alt="Necros Christos - Trivne Impvrity Rites" title="Necros Christos - Trivne Impvrity Rites" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="necros-christos-trivne-impvrity-rites" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#necros-christos-trivne-impvrity-rites"><span>Necros Christos - Trivne Impvrity Rites</span></a>
          </h2>
          <p class="m0">Release: 2007</p>
        </hgroup>
    </div>
<p>An impressive (concept) album that mixed black and death metal with dark ambient and modern classical guitar pieces.</p>
<p>I love how the various elements work together and how every track itself is outstanding on its own.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/cdg-h.jpg" width="125" alt="Cultes Des Ghoules - Häxan" title="Cultes Des Ghoules - Häxan" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="cultes-des-ghoules-haxan" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#cultes-des-ghoules-haxan"><span>Cultes Des Ghoules - Häxan</span></a>
          </h2>
          <p class="m0">Release: 2008</p>
        </hgroup>
    </div>
<p>Deranged black metal from Poland that’s heavily inspired by the earliest releases of the genre.</p>
<p>The production is raw, dark and obscure and makes this album stand out for its morbid, evil atmosphere.</p>
<p>“Häxan” also made me discover the excellent movie “Possession” by Andrzej Żuławski that was sampled, to great effect, on “The Impure Wedding”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/dc-gota.jpg" width="125" alt="Dead Congregation - Graves of the Archangels" title="Dead Congregation - Graves of the Archangels" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="dead-congregation-graves-of-the-archangels" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#dead-congregation-graves-of-the-archangels"><span>Dead Congregation - Graves of the Archangels</span></a>
          </h2>
          <p class="m0">Release: 2008</p>
        </hgroup>
    </div>
<p>Death metal from Greece, and easily among the best death metal albums of the new millennium.</p>
<p>Its production is thick and dark, and both the songwriting and the musician’s craftsmanship is excellent.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/ir-reoce.jpg" width="125" alt="Impetuous Ritual - Relentless Execution of Ceremonial Excrescence" title="Impetuous Ritual - Relentless Execution of Ceremonial Excrescence" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="impetuous-ritual-relentless-execution-of-ceremonial-excrescence" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#impetuous-ritual-relentless-execution-of-ceremonial-excrescence"><span>Impetuous Ritual - Relentless Execution of Ceremonial Excrescence</span></a>
          </h2>
          <p class="m0">Release: 2009</p>
        </hgroup>
    </div>
<p>Their debut album, and an absolute bulldozer.</p>
<p>Influences from black and death metal are distilled into a thick, dark “Ritual of the Crypt” that gets delivered with such relentless ferocity that it leaves nothing but destruction in its wake.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/sas-lfv.jpg" width="125" alt="Soap&Skin - Lovetune for Vacuum" title="Soap&Skin - Lovetune for Vacuum" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="soap-skin-lovetune-for-vacuum" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#soap-skin-lovetune-for-vacuum"><span>Soap&Skin - Lovetune for Vacuum</span></a>
          </h2>
          <p class="m0">Release: 2009</p>
        </hgroup>
    </div>
<p>A <em>very</em> interesting experimental album.</p>
<p>Words won’t do it any justice, so you better just go and listen to it yourself.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/t-sc.jpg" width="125" alt="Teitanblood - Seven Chalices" title="Teitanblood - Seven Chalices" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="teitanblood-seven-chalices" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#teitanblood-seven-chalices"><span>Teitanblood - Seven Chalices</span></a>
          </h2>
          <p class="m0">Release: 2009</p>
        </hgroup>
    </div>
<p>A truly worthy final entry for this decade.</p>
<p>At the time of its release, this album was the best extreme metal album I’d ever heard, and I haven’t found a better one yet.</p>
<p>“Seven Chalices” is an amalgamation of everything evil, morbid and occult that humanity ever conceived, cast into the shape of extreme black/death metal darkness. The DLP release includes a 16-page booklet, containing drawings and writings in a multitude of languages, that left me speechless for a while back then.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 22, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w22/"/>
        
        <updated>2024-05-29T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w22/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://robmc.dev/blog/look_mum_no_breakpoints/" class="ext u-like-of" rel="noopener">Look Mum, No Breakpoints!</a><br>
A great article about breakpoints and fluid design</li>
<li><a href="https://robindev.substack.com/p/cloudflare-took-down-our-website" class="ext u-like-of" rel="noopener">Cloudflare took down our website after trying to force us to pay 120k$ within 24h</a><br>
Another item on my long list of reasons for self-hosting; will hopefully get to that soon</li>
<li><a href="https://distinctivequality.com/blog/share-links/" class="ext u-like-of" rel="noopener">The only share links you need</a><br>
Nice one, should add that to this website</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://ctrl-c.club/~loghead/ctrl-zine.html" class="ext u-like-of" rel="noopener">Ctrl-ZINE</a><br>
“A Ctrl-c collaborative zine, celebrating tech and the Smol Web. By and for everyone!”</li>
<li><a href="https://accessibility-cookbook.com" class="ext u-like-of" rel="noopener">Web Accessibility Cookbook</a><br>
<a href="https://matuzo.at" class="ext u-like-of" rel="noopener">Manuel’s</a> new book “with dozens of recipes to help you build common components on the web”</li>
<li><a href="https://djr.com/bungee" class="ext u-like-of" rel="noopener">Bungee</a><br>
A pretty cool font</li>
<li><a href="https://thehistoryoftheweb.com/timeline/" class="ext u-like-of" rel="noopener">The History of the Web</a><br>
A great website that’s “unraveling the web’s story”</li>
<li><a href="https://github.com/ItzCrazyKns/Perplexica" class="ext u-like-of" rel="noopener">Perplexica</a><br>
“An open source alternative to Perplexity AI”</li>
<li><a href="https://unpic.pics" class="ext u-like-of" rel="noopener">Unpic</a><br>
A “toolkit for displaying images on the web” that supports many frameworks</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>External Links and target=&quot;_blank&quot;</title>
        
          
            <category term="css" />
          
            <category term="thoughts" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/external-links-and-target-blank/"/>
        
        <updated>2024-05-26T10:30:00Z</updated>
        <id>https://ttntm.me/blog/external-links-and-target-blank/</id>
        <content type="html"><![CDATA[
          
            
            <p>External links are everywhere, and I’m using tons of them on this rather small website. They’re often used as sources of quoted text and for references scattered across articles, but I’m also maintaining lists of bookmarks, projects and even random things I find on the internet.</p>
<p>All of these external links have one thing in common: you leave this website when you click on them (duh).</p>
<p>That’s all pretty basic stuff, yes. But what I used to do, was making sure that all of these links would open in a new browser tab. It’s something that <code>target=&quot;_blank&quot;</code> makes pretty easy, and web developers don’t even have to worry about the <code>window.opener</code> property anymore these days. Thankfully, browsers are taking care of that for us now, so things like adding <code>rel=&quot;noreferrer&quot;</code> to your external links, which you might remember doing at some point in the past, are obsolete now.</p>
<p>A thing that didn’t change, however, is the fact that <code>target=&quot;_blank&quot;</code> does not in fact spawn a new tab, it spawns a new window:</p>
<blockquote>
<p>You cannot, as a developer, force links to open in a new tab. You always and only tell the browser to open a new window. That’s a big difference, even though browsers (with or without addons) allow you to define how to handle those cases, e.g. open a new tab.</p>
<p><small>Ref. <a href="https://ttntm.me/everything.xml#refs" class="int">[1]</a></small></p>
</blockquote>
<p>Once again, it’s the browsers that do the work for us, using a configuration setting that’s often called “Open links in tabs instead of new windows”. Nevertheless, <em>opening a new window</em> is what we’re actually doing when we’re using <code>target=&quot;_blank&quot;</code>, and there’s no way for our code to determine what it is the browser’s actually going to do. In addition to that, there’s no way for a user to override this behavior, i.e. to force <code>target=&quot;_blank&quot;</code> links to open in the current window.</p>
<p>There are many reasons why that can be an issue for a user, but it really all boils down to one thing: <em>it should be the user’s choice where a link target opens</em>. And there’s really just one way to achieve that: avoiding <code>target=&quot;_blank&quot;</code>.</p>
<h2 id="styling-external-links" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#styling-external-links"><span>Styling External Links</span></a></h2>
<p>Having realized the error of my ways, I wondered what I could do to achieve the desired effect of creating a distinction between internal and external links that would also benefit the user (in terms of awareness of the link target).</p>
<p>I remembered doing something similar for links in my personal wiki, but it wasn’t a good solution - just some JS that distinguishes internal/external links and modifies them. It does the job, but it’s a waste of resources, considering that CSS-only solutions are perfectly viable.</p>
<p>So, I first re-wrote an old shortcode from</p>
<pre class="language-js"><code class="language-js"><span class="token function-variable function">ext</span><span class="token operator">:</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">displayText<span class="token punctuation">,</span> link</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;a href="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>link<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" target="_blank"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>displayText<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/a></span><span class="token template-punctuation string">`</span></span><br><span class="token punctuation">}</span></code></pre>
<p>to</p>
<pre class="language-js"><code class="language-js"><span class="token function-variable function">ext</span><span class="token operator">:</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">displayText<span class="token punctuation">,</span> link</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;a class="ext" href="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>link<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>displayText<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/a></span><span class="token template-punctuation string">`</span></span><br><span class="token punctuation">}</span></code></pre>
<p>and then proceeded to add some new rules to my CSS:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">a[href^="https://"].ext,<br>:is(.like) a[href^="https://"]</span> <span class="token punctuation">{</span><br>  <span class="token property">display</span><span class="token punctuation">:</span> inline-block<span class="token punctuation">;</span><br><br>  <span class="token selector">&amp;::after</span> <span class="token punctuation">{</span><br>    <span class="token property">--size</span><span class="token punctuation">:</span> .875em<span class="token punctuation">;</span><br><br>    <span class="token property">display</span><span class="token punctuation">:</span> inline-block<span class="token punctuation">;</span><br>    <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">""</span><span class="token punctuation">;</span><br>    <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--text<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">background-image</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">"data:image/svg+xml,..."</span><span class="token punctuation">)</span></span><span class="token punctuation">;</span><br>    <span class="token property">background-position</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span><br>    <span class="token property">background-size</span><span class="token punctuation">:</span> contain<span class="token punctuation">;</span><br>    <span class="token property">background-repeat</span><span class="token punctuation">:</span> no-repeat<span class="token punctuation">;</span><br>    <span class="token property">margin-left</span><span class="token punctuation">:</span> 2px<span class="token punctuation">;</span><br>    <span class="token property">line-height</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--size<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">width</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--size<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">height</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--size<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>NB: I read about the <code>[href^=&quot;https://&quot;]</code> rule in an article (ref. <a href="https://ttntm.me/everything.xml#refs" class="int">[2]</a>) and found it very useful.</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>I learned a thing or two about not making decisions for others.</p>
<p>I also updated my website, making external link targets much more transparent, and added a bunch of similar update operations for some of my other projects to my to-do list.</p>
<h2 id="refs">References</h2>
<ol>
<li><a class="ext" href="https://stackoverflow.com/questions/4198788/is-it-alright-to-use-target-blank-in-html5" rel="noopener">fabian-mcfly, Apr 1, 2022</a></li>
<li><a class="ext u-in-reply-to" href="https://christianoliff.com/blog/styling-external-links-with-an-icon-in-css/" rel="noopener">Styling External Links with an Icon in CSS</a></li>
</ol>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>My Favorite Albums - The 1990s</title>
        
          
            <category term="albums" />
          
            <category term="personal" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/my-favorite-albums-from-the-1990s/"/>
        
        <updated>2024-05-24T10:30:00Z</updated>
        <id>https://ttntm.me/blog/my-favorite-albums-from-the-1990s/</id>
        <content type="html"><![CDATA[
          
            
            <p>A chronological list of my favorite albums from 1990 to 1999 with reasons why I like them.</p>
<p>Part of the <a href="https://ttntm.me/tags/albums/" class="int">#albums</a> series.</p>
<div class="hr shadow mt2 mb2"></div>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/b-faod.jpg" width="125" alt="Blasphemy - Fallen Angel of Doom...." title="Blasphemy - Fallen Angel of Doom...." decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="blasphemy-fallen-angel-of-doom" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#blasphemy-fallen-angel-of-doom"><span>Blasphemy - Fallen Angel of Doom....</span></a>
          </h2>
          <p class="m0">Release: 1990</p>
        </hgroup>
    </div>
<p>A monument of sonic destruction at the intersection of black and death metal. Also, one of the foundations of a sub-genre that would later be called war metal.</p>
<p>It’s a raw, brutal sounding piece of extreme music that definitely isn’t for everyone. The music, the aesthetics and the whole Ross Bay Cult thing were quite appealing to me in the early 2000s, when I discovered Blasphemy and this record, and influenced a lot of things I was involved in at the time.</p>
<p>The quintessential Blasphemy song: “Ritual”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/jp-p.jpg" width="125" alt="Judas Priest - Painkiller" title="Judas Priest - Painkiller" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="judas-priest-painkiller" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#judas-priest-painkiller"><span>Judas Priest - Painkiller</span></a>
          </h2>
          <p class="m0">Release: 1990</p>
        </hgroup>
    </div>
<p>When I was a kid, I found this one in my father’s record shelf. I picked it up because of its stunning cover, and it’s probably the first metal record I ever listened to.</p>
<p>Tracks that I must have listened to a thousand times by now: “Painkiller”, “Leather Rebel” and “Night Crawler”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/d-laefs.jpg" width="125" alt="Dismember - Like an Ever Flowing Stream" title="Dismember - Like an Ever Flowing Stream" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="dismember-like-an-ever-flowing-stream" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#dismember-like-an-ever-flowing-stream"><span>Dismember - Like an Ever Flowing Stream</span></a>
          </h2>
          <p class="m0">Release: 1991</p>
        </hgroup>
    </div>
<p>In the early 2000s, I was heavily into old school Swedish death metal and dug up even the most obscure bands. Many of them were great, some even released albums. But nobody got close to “Like an Ever Flowing Stream” - ok, Carnage did (“Dark Recollections”), but they were essentially Amott + Dismember anyway.</p>
<p>Playing this record and listening to how “Override of the Overture” builds up is truly stunning, give it a try!</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/n-n.jpg" width="125" alt="Nirvana - Nevermind" title="Nirvana - Nevermind" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="nirvana-nevermind" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#nirvana-nevermind"><span>Nirvana - Nevermind</span></a>
          </h2>
          <p class="m0">Release: 1991</p>
        </hgroup>
    </div>
<p>An outstanding album that’s still amazing today.</p>
<p>Thinking about it makes me wonder how today’s teenagers would react to being given the 50th dub of a cassette with “Nevermind” on it and hearing it for the first time. But on the other hand: why bother - they probably prefer their vertical videos anyway, and might struggle with listening to a whole, continuous album.</p>
<p>My favorite track from this album: “Breed”, for its raw aggression.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/d-abitns.jpg" width="125" alt="Darkthrone - A Blaze in the Northern Sky" title="Darkthrone - A Blaze in the Northern Sky" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="darkthrone-a-blaze-in-the-northern-sky" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#darkthrone-a-blaze-in-the-northern-sky"><span>Darkthrone - A Blaze in the Northern Sky</span></a>
          </h2>
          <p class="m0">Release: 1992</p>
        </hgroup>
    </div>
<p>I knew Darkthrone as a black metal band before I discovered that they’d also been a death metal band at some point (releasing “Soulside Journey”, a decent album). Either way, they’re great, and “A Blaze…” is definitely one of the best Norwegian black metal records - there isn’t a single bad track on this album.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/i-otg.jpg" width="125" alt="Incantation - Onward to Golgotha" title="Incantation - Onward to Golgotha" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="incantation-onward-to-golgotha" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#incantation-onward-to-golgotha"><span>Incantation - Onward to Golgotha</span></a>
          </h2>
          <p class="m0">Release: 1992</p>
        </hgroup>
    </div>
<p>Dark, rotten death metal from the US. Their unique, heavy sound resonated with me so much, that I spent a lot of time and money on trying to create my own version of it.</p>
<p>Highlights: “Devoured Death”, “Unholy Massacre” and “Entrantment of Evil”</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/n-i.jpg" width="125" alt="Necrosanct - Incarnate" title="Necrosanct - Incarnate" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="necrosanct-incarnate" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#necrosanct-incarnate"><span>Necrosanct - Incarnate</span></a>
          </h2>
          <p class="m0">Release: 1992</p>
        </hgroup>
    </div>
<p>If I had to describe it in one word: relentless.</p>
<p>It was a hidden gem when I discovered it, and it probably still is today. Hunting down the vinyl record took me a while (there were no reissues of this album around in ~2010), but I’m glad I did - it’s one of my favorite death metal records of all time.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/b-ddtm.jpg" width="125" alt="Beherit - Drawing Down the Moon" title="Beherit - Drawing Down the Moon" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="beherit-drawing-down-the-moon" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#beherit-drawing-down-the-moon"><span>Beherit - Drawing Down the Moon</span></a>
          </h2>
          <p class="m0">Release: 1993</p>
        </hgroup>
    </div>
<p>A dark and twisted record that perfectly unites black and death metal. Needless to say that it most definitely influenced countless bands over time and that it’s certainly one of the greats of Finnish extreme metal.</p>
<p>Songs like “Nocturnal Evil”, “Sadomatic Rites”, “The Gate of Nanna” and “Unholy Pagan Fire” were burnt into my brain forever.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/m-dmds.jpg" width="125" alt="Mayhem - De Mysteriis Dom Sathanas" title="Mayhem - De Mysteriis Dom Sathanas" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="mayhem-de-mysteriis-dom-sathanas" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#mayhem-de-mysteriis-dom-sathanas"><span>Mayhem - De Mysteriis Dom Sathanas</span></a>
          </h2>
          <p class="m0">Release: 1994</p>
        </hgroup>
    </div>
<p><em>The</em> Mayhem album. I don’t care much for what came after it.</p>
<p>Also, probably the (first?) peak of Norwegian black metal. A dark and evil sounding masterpiece that cuts through your ears like a razor.</p>
<p>There’s nothing quite like the memory of the first time I listened to “Funeral Fog”, immediately followed by the glorious “Freezing Moon”. And that title track, “De Mysteriis Dom Sathanas”, all the way at the end, when I thought this record couldn’t get any better - I was wrong, and it did.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/d-s.jpg" width="125" alt="Death - Symbolic" title="Death - Symbolic" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="death-symbolic" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#death-symbolic"><span>Death - Symbolic</span></a>
          </h2>
          <p class="m0">Release: 1995</p>
        </hgroup>
    </div>
<p>Probably the best Death album, and definitely one of the best death metal albums of the 90s.</p>
<p>Their songwriting is excellent on this one, even though the lyrics are pretty far from the usual death metal topics of the time. And the production is nothing short of amazing - aggressive and powerful, yet clean enough for the listener to discover the technical finesse in their playing.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/d-outc.jpg" width="125" alt="Deicide - Once Upon the Cross" title="Deicide - Once Upon the Cross" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="deicide-once-upon-the-cross" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#deicide-once-upon-the-cross"><span>Deicide - Once Upon the Cross</span></a>
          </h2>
          <p class="m0">Release: 1995</p>
        </hgroup>
    </div>
<p>Picking just one Deicide album from this decade was hard.</p>
<p>Their first three albums are all excellent really, but I chose “Once Upon the Cross” because it’s the one that feels the most like a bulldozer rolling over your face.</p>
<p>If you need reasons, check out these songs: “Once Upon the Cross”, “Kill the Christian” and “They are the Children of the Underworld”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/a-hog.jpg" width="125" alt="Angelcorpse - Hammer of Gods" title="Angelcorpse - Hammer of Gods" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="angelcorpse-hammer-of-gods" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#angelcorpse-hammer-of-gods"><span>Angelcorpse - Hammer of Gods</span></a>
          </h2>
          <p class="m0">Release: 1996</p>
        </hgroup>
    </div>
<p>A marvel of black/death metal that leaves nothing in its path.</p>
<p>Both sound engineering and execution are extremely well done, and songs like “Lord of the Funeral Pyre” or “Black Solstice” are so catchy that you won’t get them out of your head easily.</p>
<p>I still remember seeing them live in a small club around 2007 or so. There was no stage, so we stood about 2 meters away from them. Watching a band up close, playing with such perfection, was really impressive.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/a-etd.jpg" width="125" alt="Asphyx - Embrace the Death" title="Asphyx - Embrace the Death" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="asphyx-embrace-the-death" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#asphyx-embrace-the-death"><span>Asphyx - Embrace the Death</span></a>
          </h2>
          <p class="m0">Release: 1996</p>
        </hgroup>
    </div>
<p>This album was actually recorded in 1990, but not released until '96. As such, it’s their first album, and I also consider it their best.</p>
<p>It’s a dark and obscure mixture of doom and death metal, the exact formula that made (the original) Asphyx stylistically unique.</p>
<p>Highlights: the A-side as a whole.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/mf-itu.jpg" width="125" alt="Mercyful Fate - Into the Unknown" title="Mercyful Fate - Into the Unknown" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="mercyful-fate-into-the-unknown" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#mercyful-fate-into-the-unknown"><span>Mercyful Fate - Into the Unknown</span></a>
          </h2>
          <p class="m0">Release: 1996</p>
        </hgroup>
    </div>
<p>Nothing too special, just a good heavy metal album. I listened to it a lot for a while, and it quickly became my favorite Mercyful Fate record.</p>
<p>Favorite songs: “The Uninvited Guest”, “Into the Unknown” and “Under the Spell”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/i-itirotac.jpg" width="125" alt="Inquisition - Into the Infernal Regions of the Ancient Cult" title="Inquisition - Into the Infernal Regions of the Ancient Cult" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="inquisition-into-the-infernal-regions-of-the-ancient-cult" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#inquisition-into-the-infernal-regions-of-the-ancient-cult"><span>Inquisition - Into the Infernal Regions of the Ancient Cult</span></a>
          </h2>
          <p class="m0">Release: 1998</p>
        </hgroup>
    </div>
<p>An excellent album that managed to combine many styles and different influences from extreme metal into the unique, meditative style that Inquisition became known for.</p>
<p>Highlights: “Those of the Night”, “Empire of Luciferian Race” and “Into the Infernal Regions of the Ancient Cult”.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 21, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w21/"/>
        
        <updated>2024-05-23T10:35:00Z</updated>
        <id>https://ttntm.me/likes/2024-w21/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.jetbrains.com/guide/javascript/tutorials/eleventy-tsx/" class="ext u-like-of" rel="noopener">Better 11ty Development with Tooling</a><br>
A series of tutorials about “getting a component-driven development experience” when working with 11ty</li>
<li><a href="https://benmyers.dev/blog/eleventy-blogroll/" class="ext u-like-of" rel="noopener">Build a Blogroll with Eleventy</a><br>
Nice one, going to keep that in mind in case I ever end up doing something similar</li>
<li><a href="https://alastairjohnston.com/introducing-hello-pages/" class="ext u-like-of" rel="noopener">Introducing Hello pages</a><br>
A great idea that lead to my own <a href="/hello/" class="int">/hello page</a></li>
<li><a href="https://wimvanderbauwhede.codeberg.page/articles/frugal-computing-developer/" class="ext u-like-of" rel="noopener">Frugal computing: developer perspective</a><br>
An interesting article about the “need for low-carbon and sustainable computing and what developers can do about it”</li>
<li><a href="https://blakewatson.com/journal/rebuilding-my-website-with-eleventy/" class="ext u-like-of" rel="noopener">Rebuilding my website with Eleventy</a><br>
A cool article about moving from WordPress to Eleventy</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://uzumaki-manga.online" class="ext u-like-of" rel="noopener">Uzumaki</a><br>
A Japanese horror manga series written and illustrated by Junji Ito, originally published as a series in a weekly magazine from 1998 to 1999</li>
<li><a href="https://shouldideploy.today" class="ext u-like-of" rel="noopener">Should I Deploy Today?</a><br>
Helpful site, it’s always good to check</li>
<li><a href="https://alexsci.com/rss-blogroll-network/" class="ext u-like-of" rel="noopener">Blogroll Network Map</a><br>
A really cool new project that “can help you discover new websites recommended by bloggers you already follow”</li>
<li><a href="https://blessed.rs" class="ext u-like-of" rel="noopener">blessed.rs</a><br>
An unofficial guide to the Rust ecosystem</li>
<li><a href="https://crates.io/crates/monolith" class="ext u-like-of" rel="noopener">monolith</a><br>
A CLI tool for saving web pages as a single HTML file</li>
<li><a href="https://hmpg.net" class="ext u-like-of" rel="noopener">The end of the internet</a><br>
Whew, finally…</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Upgraded to Eleventy v3</title>
        
          
            <category term="eleventy" />
          
            <category term="news" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/upgraded-to-eleventy-v3/"/>
        
        <updated>2024-05-23T10:30:00Z</updated>
        <id>https://ttntm.me/blog/upgraded-to-eleventy-v3/</id>
        <content type="html"><![CDATA[
          
            
            <p>The alpha release of Eleventy v3 became available in December '23. It took me a little while, but I finally upgraded this website to <code>v3.0.0-alpha.10</code> today.</p>
<p><strong>This is what I had to do:</strong></p>
<p>Updated <code>package.json</code> and switched to ESM using <code>&quot;type&quot;: &quot;module&quot;</code>.</p>
<p>The old <code>.eleventy.js</code> config file became <code>eleventy.config.js</code> and all <code>require()</code> statements were turned into <code>import</code>s. The file now uses the <code>export default async function(config) {}</code> syntax, gone is the old CJS <code>module.exports = async function(config) {}</code>.</p>
<p>The <code>filters.js</code> and <code>shortcodes.js</code> files had to be updated too, same procedure changes the config file (imports/export).</p>
<p>Then there were 2 things that broke:</p>
<p>1, Importing <code>*.json</code> files from the file system failed.</p>
<p>Solved by annotating the import with <code>assert { type: 'json' }</code>, which made it work again. But: there’s a warning in the console now:</p>
<pre class="language-txt"><code class="language-txt">(node:17578) ExperimentalWarning: Importing JSON modules is an experimental feature and<br>might change at any time (Use `node --trace-warnings ...` to show where the warning was created)`.</code></pre>
<p>2, Using <code>__dirname</code> isn’t possible anymore.</p>
<p>It was used in my CSS build (<code>page.11ty.js</code>) and broke builds. I actually had to look this up and found a workaround for it:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> path <span class="token keyword">from</span> <span class="token string">'path'</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> fileURLToPath <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'url'</span><br><br><span class="token keyword">const</span> __filename <span class="token operator">=</span> <span class="token function">fileURLToPath</span><span class="token punctuation">(</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>url<span class="token punctuation">)</span><br><span class="token keyword">const</span> __dirname <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">dirname</span><span class="token punctuation">(</span>__filename<span class="token punctuation">)</span><br><br><span class="token comment">//...</span><br><br><span class="token keyword">const</span> cssDir <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">,</span> <span class="token string">'..'</span><span class="token punctuation">,</span> <span class="token string">'_includes'</span><span class="token punctuation">,</span> <span class="token string">'css'</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> rawFilepath <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>cssDir<span class="token punctuation">,</span> <span class="token string">'_page.css'</span><span class="token punctuation">)</span></code></pre>
<p>Other than that, things kept working without any issues, and I’m happily reading console output like this:</p>
<pre class="language-txt"><code class="language-txt">[11ty] Benchmark    643ms  18%   769× (Configuration) "slugify" Nunjucks Filter<br>[11ty] Benchmark    870ms  25%   309× (Configuration) "jsmin" Nunjucks Async Filter<br>[11ty] Benchmark    580ms  16%   200× (Configuration) "htmlmin" Transform<br>[11ty] Copied 171 files / Wrote 137 files in 3.35 seconds (24.5ms each, v3.0.0-alpha.10)</code></pre>
<p>BTW, development builds (via <code>eleventy --serve</code>) skip the HTML/JS minification and are even faster:</p>
<pre class="language-txt"><code class="language-txt">[11ty] Benchmark    647ms  31%   769× (Configuration) "slugify" Nunjucks Filter<br>[11ty] Copied 171 files / Wrote 137 files in 1.88 seconds (13.7ms each, v3.0.0-alpha.10)</code></pre>
<p>All changes I had to do can be found in a PR if you’re interested: <a class="ext" href="https://github.com/ttntm/ttntm/pull/16/files" rel="noopener">ttntm/pull/16/files</a></p>
<p>Overall a smooth upgrade that I recommend doing if your site also runs on Eleventy. And if not, consider giving it a shot - it’s a fantastic tool with a great community!</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>My Favorite Albums - The 1980s</title>
        
          
            <category term="albums" />
          
            <category term="personal" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/my-favorite-albums-from-the-1980s/"/>
        
        <updated>2024-05-21T10:30:00Z</updated>
        <id>https://ttntm.me/blog/my-favorite-albums-from-the-1980s/</id>
        <content type="html"><![CDATA[
          
            
            <p>A chronological list of my favorite albums from 1980 to 1989 with reasons why I like them.</p>
<p>Also, the opening post of my new <a href="https://ttntm.me/tags/albums/" class="int">#albums</a> series.</p>
<div class="hr shadow mt2 mb2"></div>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/bs-hah.jpg" width="125" alt="Black Sabbath - Heaven and Hell" title="Black Sabbath - Heaven and Hell" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="black-sabbath-heaven-and-hell" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#black-sabbath-heaven-and-hell"><span>Black Sabbath - Heaven and Hell</span></a>
          </h2>
          <p class="m0">Release: 1980</p>
        </hgroup>
    </div>
<p>Black Sabbath’s first album with Ronnie James Dio as their singer.</p>
<p>The A-side is nothing short of phenomenal and the title track, “Heaven and Hell”, is definitely one of the greatest Hard Rock (early Heavy Metal) songs of its time.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/bf-d.jpg" width="125" alt="Black Flag - Damaged" title="Black Flag - Damaged" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="black-flag-damaged" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#black-flag-damaged"><span>Black Flag - Damaged</span></a>
          </h2>
          <p class="m0">Release: 1981</p>
        </hgroup>
    </div>
<p>A classic hardcore (punk) record.</p>
<p>Raw, unadulterated musical violence that inspired countless other bands and influenced a multitude of musical genres. And “Rise Above” should definitely be considered one of the best opening tracks ever.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/m-wau.jpg" width="125" alt="Misfits - Walk Among Us" title="Misfits - Walk Among Us" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="misfits-walk-among-us" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#misfits-walk-among-us"><span>Misfits - Walk Among Us</span></a>
          </h2>
          <p class="m0">Release: 1982</p>
        </hgroup>
    </div>
<p>The first Misfits album. I love most of their music up until and including “Famous Monsters”, but “Walk Among Us” is definitely my favorite.</p>
<p>I got into the band in the late 90s and “Famous Monsters” was my first Misfits album. I still love listening to “Scream” sometimes (and watching that fantastic music video!), but when I discovered “Walk Among Us”, it felt so much more honest and raw that it quickly became the soundtrack of my early teenage years.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/d-hnsnsn.jpg" width="125" alt="Discharge - Hear Nothing See Nothing Say Nothing" title="Discharge - Hear Nothing See Nothing Say Nothing" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="discharge-hear-nothing-see-nothing-say-nothing" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#discharge-hear-nothing-see-nothing-say-nothing"><span>Discharge - Hear Nothing See Nothing Say Nothing</span></a>
          </h2>
          <p class="m0">Release: 1982</p>
        </hgroup>
    </div>
<p>Another classic hardcore record.</p>
<p>I bought Discharge’s self-titled album around the time it was released (2002) and loved what I got. I treasured my then-new discovery and ended up with “Hear Nothing See Nothing Say Nothing” shortly after that.</p>
<p>It’s a great record and I don’t think its influence on metal and extreme music in general can be overstated.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/m-rtl.jpg" width="125" alt="Metallica - Ride the Lightning" title="Metallica - Ride the Lightning" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="metallica-ride-the-lightning" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#metallica-ride-the-lightning"><span>Metallica - Ride the Lightning</span></a>
          </h2>
          <p class="m0">Release: 1984</p>
        </hgroup>
    </div>
<p>It’s quite hard to pick just one favorite Metallica album for the whole 1980s.</p>
<p>In my early 20s, I would definitely have picked “Kill 'Em All”, but 35+ years old me went with this one instead. The main reasons for that decision are “Fight Fire With Fire”, “Creeping Death” and “Trapped Under Ice”, but “For Whom the Bell Tolls” and “Fade to Black” are great songs too.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/s-ha.jpg" width="125" alt="Slayer - Hell Awaits" title="Slayer - Hell Awaits" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="slayer-hell-awaits" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#slayer-hell-awaits"><span>Slayer - Hell Awaits</span></a>
          </h2>
          <p class="m0">Release: 1985</p>
        </hgroup>
    </div>
<p>Definitely my favorite Slayer album.</p>
<p>I saw this album described as “ahead of its time” somewhere, and I can only agree. It’s a violent rush all the way to the end and there isn’t a single bad (or even not-so-good) song on this record. The craftsmanship is close to perfection too, and trying to play these riffs with thin teenager skin will make your fingers bleed (trust me, I tried).</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/im-sit.jpg" width="125" alt="Iron Maiden - Somewhere in Time" title="Iron Maiden - Somewhere in Time" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="iron-maiden-somewhere-in-time" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#iron-maiden-somewhere-in-time"><span>Iron Maiden - Somewhere in Time</span></a>
          </h2>
          <p class="m0">Release: 1986</p>
        </hgroup>
    </div>
<p>My favorite Iron Maiden album.</p>
<p>“Powerslave” might be their universally acclaimed masterpiece, but “Somewhere in Time” has “Wasted Years”, the first Iron Maiden song I ever listened to. My brain probably got fried a little that day, I still remember it as if it happened yesterday.</p>
<p>Also, really cool album artwork!</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/k-ptk.jpg" width="125" alt="Kreator - Pleasure to Kill" title="Kreator - Pleasure to Kill" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="kreator-pleasure-to-kill" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#kreator-pleasure-to-kill"><span>Kreator - Pleasure to Kill</span></a>
          </h2>
          <p class="m0">Release: 1986</p>
        </hgroup>
    </div>
<p>My favorite Kreator album.</p>
<p>A furious German thrash metal record that’s intense, relentless and full of aggression. It’s really heavy and intense for '86, bordering on death/thrash, despite death metal being in it’s infancy still (“Seven Churches” was released in '85).</p>
<p>I remember wearing a matching t-shirt to a first aid class and having an interesting conversation about the album title printed on the back of the shirt - fun times!</p>
<p>Fun fact: also featured in one of the earlier episodes of the TV series “Dark”.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/d-sbg.jpg" width="125" alt="Death - Scream Bloody Gore" title="Death - Scream Bloody Gore" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="death-scream-bloody-gore" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#death-scream-bloody-gore"><span>Death - Scream Bloody Gore</span></a>
          </h2>
          <p class="m0">Release: 1987</p>
        </hgroup>
    </div>
<p>The first death metal album.</p>
<p>Possessed’s “Seven Churches” came out a little earlier, but it was still very much rooted in thrash metal. Death definitely crossed that line and delivered a masterpiece that still sounds fresh today.</p>
<p>A long time ago, I hunted down the original Combat Records version on eBay. It’s one of the few records that having a first press somehow mattered to me.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/ma-aom.jpg" width="125" alt="Morbid Angel - Altars of Madness" title="Morbid Angel - Altars of Madness" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="morbid-angel-altars-of-madness" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#morbid-angel-altars-of-madness"><span>Morbid Angel - Altars of Madness</span></a>
          </h2>
          <p class="m0">Release: 1989</p>
        </hgroup>
    </div>
<p>Another death metal masterpiece full of songs I will never forget about. A record that’s pretty much “all killer, no filler”, and definitely - still - one of the best records of the whole genre.</p>
<p>I never got into later Morbid Angel records (after “Blessed are the Sick”), but I’ll always argue that this is their best album anyway.</p>
<div class="image-header flex wrap-mobile align-items-start gap1 gap2-lg mt2 mb1">
      <img class="image-header__cover w100m m0" src="https://ttntm.me/img/albums/t-wd.jpg" width="125" alt="Terrorizer - World Downfall" title="Terrorizer - World Downfall" decoding="async" loading="lazy" eleventy:ignore="">
      <hgroup class="image-header__text w100m">
          <h2 id="terrorizer-world-downfall" class="h3">
            <a class="header-anchor" href="https://ttntm.me/everything.xml#terrorizer-world-downfall"><span>Terrorizer - World Downfall</span></a>
          </h2>
          <p class="m0">Release: 1989</p>
        </hgroup>
    </div>
<p>I suppose that many of the records listed above eventually culminated in this one.</p>
<p>“World Downfall” is a monument of extreme music and leaves behind nothing but sonic destruction.</p>
<p>Some highlights: “After World Obliteration”, “Fear of Napalm” and “Dead Shall Rise”.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Growing Into Full Stack Development</title>
        
          
            <category term="learning" />
          
            <category term="personal" />
          
            <category term="thoughts" />
          
        
        <link href="https://ttntm.me/blog/growing-into-full-stack-development/"/>
        
        <updated>2024-05-16T10:30:00Z</updated>
        <id>https://ttntm.me/blog/growing-into-full-stack-development/</id>
        <content type="html"><![CDATA[
          
            
            <p>This is a note based on a recent email conversation I had with Luke Harris in response to his recent article <a class="ext" href="https://www.lkhrs.com/blog/2024/whats-next/" rel="noopener">Thinking about what’s next</a> from April 2024.</p>
<p>I transitioned to full stack / backend development (and workflow automation) a couple of years ago, after having done mostly web design / frontend work for about 10 years prior to that. I had some experience working on monolithic MVC applications and serverless (hobby-) stuff, but I didn’t think I’d ever end up working as a full stack developer.</p>
<p>When I was looking for a job a while ago, after having spent 8 years at a small company, I got a job offer that I initially declined. I did that, because it looked like it was only about customizing standard software, which I didn’t feel too comfortable with at the time, considering I’d spent those 8 years at an M$-partnered software maker. Thankfully, the HR person didn’t like me declining that offer very much and sent back a couple of links that she got from my future team lead. Curiosity kicked in, I read through a couple of things, found it interesting and scheduled an interview. I got picked from a total of 11 applicants and became part of a team in a larger organization (100+ people in my country, 1000+ in EMEA).</p>
<p>It seems like it was the right decision for me at the time: years passed, and I’m still working at the intersection of full stack development and workflow automation.</p>
<p>So, here’s a short list of things I found helpful when growing into full stack development.</p>
<h2 id="sql" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#sql"><span>SQL</span></a></h2>
<p>Regardless of what programming language you decide to focus on: learn and practice SQL, if you haven’t already. It’s truly invaluable, and you’ll need it almost anywhere.</p>
<p>I can <em>really</em> recommend clicking this link:<br>
<a class="ext" href="https://sqlcrashcourse.com" rel="noopener">SQL Crash Course</a></p>
<p>And Oracle also has a decent online SQL engine that is free (AFAIR, it’s been a while):<br>
<a class="ext" href="https://livesql.oracle.com" rel="noopener">Live SQL</a></p>
<p>I’d recommend focusing on queries, aggregating, grouping and filtering data.</p>
<h2 id="software-architecture" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#software-architecture"><span>Software Architecture</span></a></h2>
<p>What helps one learn software architecture design patterns?<br>
In my case? Getting dropped into freezing cold water…</p>
<p>Seriously: compound knowledge assembled from working on tenders (that often expect a target architecture), estimates for projects and technical “consulting” work for enterprise clients at a former job.</p>
<p>No formal training, aside from bits and pieces at university, just reading and learning from others (thank you, elder colleagues!).</p>
<p>A few pointers:</p>
<ul>
<li>O’Reilly Books
<ul>
<li>“Designing Distributed Systems”</li>
<li>“Reactive Microservices Architecture”</li>
</ul>
</li>
<li>“Distributed Systems” by M. van Steen and A.S. Tanenbaum<br>
<a class="ext" href="https://www.distributed-systems.net" rel="noopener">distributed-systems.net</a></li>
<li>Martin Fowler’s <a class="ext" href="https://martinfowler.com/architecture/" rel="noopener">Software Architecture Guide</a></li>
<li><a class="ext" href="https://c4model.com" rel="noopener">The C4 model for visualising software architecture</a></li>
<li>Khalil Stemmler’s articles about <a class="ext" href="https://khalilstemmler.com/articles/tags/architecture/" rel="noopener">Architecture</a></li>
</ul>
<h2 id="documentation" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#documentation"><span>Documentation</span></a></h2>
<p>Any kind of “hard” documentation skills (i.e. BPMN, UML) are valuable.</p>
<p>Some examples:</p>
<ul>
<li><a class="ext" href="https://demo.bpmn.io" rel="noopener">demo.bpmn.io</a> (Drawing charts)
<ul>
<li>+<a class="ext" href="https://www.bpmn.org" rel="noopener">bpmn.org</a> (Quick Guide, Examples)</li>
</ul>
</li>
<li><a class="ext" href="https://www.planttext.com" rel="noopener">PlantText</a>
<ul>
<li>+<a class="ext" href="https://crashedmind.github.io/PlantUMLHitchhikersGuide/" rel="noopener">The Hitchhiker’s Guide to PlantUML</a></li>
</ul>
</li>
<li>The C4 model listed above</li>
</ul>
<h2 id="what-else" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#what-else"><span>What else?</span></a></h2>
<p>Miscellaneous things that might make you a better developer/person/etc.:</p>
<ul>
<li>Exercising patience</li>
<li>Establishing, operating and optimizing processes that involve different (legacy) systems and interfaces, which leads to tons of glue code</li>
<li>The willingness and courage to fix issues in production</li>
<li>Writing clean, performant server-side code in ES3 JS that serves millions of requests per month</li>
<li>Friday afternoon deployments, followed by Friday night ninja fixes and/or rollbacks</li>
<li>Surviving big bang go-lives</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 20, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w20/"/>
        
        <updated>2024-05-15T10:35:00Z</updated>
        <id>https://ttntm.me/likes/2024-w20/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://mikegrindle.com/posts/self-promotion" class="ext u-like-of" rel="noopener">Do Things and Tell People</a><br>
“if we feel we are doing good, honest things, we shouldn’t fear speaking a little louder about them”</li>
<li><a href="https://branch.climateaction.tech/issues/issue-8/the-perfect-site-doesnt-exist/" class="ext u-like-of" rel="noopener">The perfect site doesn’t exist</a></li>
<li><a href="https://gomakethings.com/why-use-web-components-over-traditional-dom-manipulation/" class="ext u-like-of" rel="noopener">Why use Web Components over traditional DOM manipulation?</a><br>
Good one: “If vanilla JS is a manual screwdriver, WC’s are an electrical drill. You might use them for the same task, but a WC will make some tasks a bit faster/easier (and be overkill for other things).”</li>
<li><a href="https://klinger.io/posts/eu-acc" class="ext u-like-of" rel="noopener">Dear Europe, please wake up</a><br>
Something I never really thought about, but it makes sense</li>
<li><a href="https://stephaniewalter.design/blog/dark-mode-accessibility-myth-debunked/" class="ext u-like-of" rel="noopener">Dark mode &amp; accessibility myth</a><br>
Good to keep this in mind</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://ethicalsource.dev/licenses/" class="ext u-like-of" rel="noopener">Ethical Source Licenses</a></li>
<li><a href="https://terminaltrove.com" class="ext u-like-of" rel="noopener">Terminal Trove</a><br>
A pretty cool website that “curates and showcases all things in the terminal”</li>
<li><a href="https://terminaltrove.com/atac/" class="ext u-like-of" rel="noopener">atac</a><br>
A simple API client for your terminal</li>
<li><a href="https://github.com/MHNightCat/superfile" class="ext u-like-of" rel="noopener">superfile</a><br>
A terminal file manager</li>
<li><a href="https://extension.js.org" class="ext u-like-of" rel="noopener">Extension</a><br>
“A plug-and-play, zero-config, cross-browser extension development tool for browser extensions”</li>
<li><a href="https://chrisburnell.github.io/interests-directory/" class="ext u-like-of" rel="noopener">The /interests Directory</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 19, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w19/"/>
        
        <updated>2024-05-08T10:35:00Z</updated>
        <id>https://ttntm.me/likes/2024-w19/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.baldurbjarnason.com/2024/react-electron-llms-labour-arbitrage/" class="ext u-like-of" rel="noopener">React, Electron, and LLMs have a common purpose: the labour arbitrage theory of dev tool popularity</a><br>
An insightful article about the tools we use</li>
<li><a href="https://medialab.github.io/iwanthue/" class="ext u-like-of" rel="noopener">i want hue</a><br>
A tool to “generate and refine palettes of optimally distinct colors”</li>
<li><a href="https://www.aleksandrhovhannisyan.com/blog/eleventy-code-demos/" class="ext u-like-of" rel="noopener">Interactive HTML Code Demos in Eleventy</a><br>
Interesting, might consider doing that in the future</li>
<li><a href="https://shivering-isles.com/2024/05/growing-home-infrastructure" class="ext u-like-of" rel="noopener">Growing home infrastructure</a><br>
Lessons learned re: self-hosting</li>
<li><a href="https://arstechnica.com/security/2024/05/novel-attack-against-virtually-all-vpn-apps-neuters-their-entire-purpose/" class="ext u-like-of" rel="noopener">Novel attack against virtually all VPN apps neuters their entire purpose</a><br>
Keep that in mind and be careful out there</li>
<li><a href="https://www.takahe.org.nz/heat-death-of-the-internet/" class="ext u-like-of" rel="noopener">Heat Death of the Internet</a><br>
Sad, but true</li>
<li><a href="https://viewsource.info" class="ext u-like-of" rel="noopener">a view source web</a><br>
A great piece about HTML and its transparency</li>
<li><a href="https://thehistoryoftheweb.com/checking-under-the-hood-of-code/" class="ext u-like-of" rel="noopener">Checking “Under the Hood” of Code</a><br>
A little history of web developer tools</li>
<li><a href="https://j3s.sh/thought/drones-run-linux-free-software-isnt-enough.html" class="ext u-like-of" rel="noopener">drones run linux: the free software movement isn’t enough</a><br>
About the death of the free software movement</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://restorativland.org" class="ext u-like-of" rel="noopener">restorativland</a><br>
Wonderful - a project that restores “web ruins” like geocities, making them accessible again</li>
<li><a href="https://webkit.org/blog/15269/help-us-invent-masonry-layouts-for-css-grid-level-3/" class="ext u-like-of" rel="noopener">CSS Grid Level 3, aka “Masonry” layout</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>My Personal Knowledge Base</title>
        
          
            <category term="docs" />
          
            <category term="personal" />
          
            <category term="project" />
          
        
        <link href="https://ttntm.me/blog/personal-knowledge-base/"/>
        
        <updated>2024-05-08T10:30:00Z</updated>
        <id>https://ttntm.me/blog/personal-knowledge-base/</id>
        <content type="html"><![CDATA[
          
            
            <p>I started using Notion in 2018 when it was very new. I was very convenient and fast, which made using it a rather pleasant experience. Over time, I built small personal wiki in there, drafted articles with it and used lists of to-dos and tables to track lots of different things.</p>
<p>At some point last year, both the web application, and the 2 mobile apps (Android/iOS) I was using occasionally, got slower and slower. I suspected some (unwanted, auto-enabled) AI bloat as the cause and had it turned off for my account (via support case). But that didn’t really change anything. On top of that, I felt increasingly uneasy about my data in their walled garden, and the lack of a properly working offline mode didn’t do much good either.</p>
<p>Some hours of research later, I was convinced that tools like anytype, Logseq, Obsidian and many others all look great in their own way and would definitely be suitable for my rather ordinary use case. But they’re all just another layer on top of plain text that I don’t really need at the moment, so I decided against shopping for a new tool and in favor of writing plain text files (ref. <a href="https://ttntm.me/everything.xml#refs" class="int">[1]</a>).</p>
<p>Exporting my data and pages from Notion was not a big deal, but I soon realized that it’d be nice to have a front end for my wiki. What I had in mind was pretty simply really: navigation, search and a clear focus on the content. Static output and at least a bit of customizability should also be possible.</p>
<p>I briefly considered building my own “wiki framework” using a cool new tool, but I really wanted to avoid that. Why? Because I knew that if I did, I’d certainly spend a disproportionate amount of time tinkering with the tech stack, instead of focusing on the content.</p>
<blockquote>
<p>Today’s web stacks gravitate towards infinite complexity. It’s all too natural to pick an “easy” heavy framework at the start, and then get yourself into npm’s bog. Website is about content, and content has gravity. […] Do carefully consider the choice of your web stack.</p>
<p><small>Ref. <a href="https://ttntm.me/everything.xml#refs" class="int">[2]</a></small></p>
</blockquote>
<p>Some more research was done, and I soon discovered <em>mdBook</em> (ref. <a href="https://ttntm.me/everything.xml#refs" class="int">[3]</a>), which looked like a perfect fit for my use case. I gave it a try, customized it a little and had my wiki up and running rather quickly.</p>
<h2 id="toms-knowledge-base" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#toms-knowledge-base"><span>Tom’s Knowledge Base</span></a></h2>
<p>Naming things is notoriously hard, but I didn’t have any issues with that this time. The heading above already gave it away: I chose “knowledge base” over “wiki” and ended up with “TKB”, a rather appealing abbreviation.</p>
<p>TKB’s content is stored in a public Codeberg repository, and gets published to Codeberg Pages using local builds. To get this done, I’m using a <code>.gitignore</code>’d subdirectory (<code>cdbrg-pages</code>) that a script copies the build artifacts into. Running the script is a manual process and gives me full control over what and when to publish new versions. I might automate this process (ref. <a href="https://ttntm.me/everything.xml#refs" class="int">[4]</a>) at some point, but I’m happy with it for the time being.</p>
<p>NB: I never really got into the more personal (diary) aspect of note-taking, so having TKB available in public is fine for me. Whatever really personal stuff I need to write down is not much usually and can easily be stored elsewhere.</p>
<h2 id="did-it-work-out" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#did-it-work-out"><span>Did it work out?</span></a></h2>
<p>I’ve been using, updating and growing TKB for about 5 months now, and I’d say yes, it worked out. I realized that I’ve collected a ton of links, notes and overall information over the years, and I’m happy to have found a suitable new home for all of it.</p>
<p>There’s still some stuff left in Notion, but I hardly use it anymore. The value of outdated lists of to-dos is debatable, but I haven’t decided what to do with it yet. I’m probably not going to delete my account, because my wife’s using Notion a lot and there are many family things we edit collaboratively.</p>
<p>Overall, I’d say I’d recommend creating/maintaining a personal kb/wiki. It feels liberating and will probably turn into a valuable resource that you’ll enjoy taking care of.</p>
<p>PS: Mark wrote a similar article that I can recommend.<br>
<a class="ext u-in-reply-to" href="https://www.markpitblado.me/blog/goodbye-logseq-obsidian-and-vimwiki-hello-mdbook/" rel="noopener">Goodbye Logseq, Obsidian, and Vimwiki. Hello, mdBook!</a></p>
<h2 id="refs">References</h2>
<ol>
<li><a class="ext u-in-reply-to" href="https://sive.rs/plaintext" rel="noopener">Derek Sivers, “Write plain text files”</a></li>
<li><a class="ext u-in-reply-to" href="https://matklad.github.io/2024/03/22/basic-things.html" rel="noopener">Alex Kladov, “Basic Things”</a></li>
<li><a class="ext" href="https://github.com/rust-lang/mdBook" rel="noopener">github/rust-lang/mdBook</a></li>
<li><a class="ext u-in-reply-to" href="https://www.markpitblado.me/blog/deploying-mdbook-to-codeberg-pages-using-woodpecker-ci/" rel="noopener">Deploying mdbook to codeberg pages using woodpecker CI</a></li>
</ol>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 18, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w18/"/>
        
        <updated>2024-05-02T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w18/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://bost.ocks.org/mike/algorithms/" class="ext u-like-of" rel="noopener">Visualizing Algorithms</a><br>
“Algorithms are a fascinating use case for visualization”</li>
<li><a href="https://www.mcsweeneys.net/articles/the-millennial-captcha" class="ext u-like-of" rel="noopener">The Millennial CAPTCHA</a></li>
<li><a href="https://news.itsfoss.com/mastodon-link-problem/" class="ext u-like-of" rel="noopener">Please Don’t Share Our Links on Mastodon: Here’s Why!</a><br>
Interesting, did not know why this was happening; related GitHub issue for future reference: <a href="https://github.com/mastodon/mastodon/issues/23662" class="ext u-like-of" rel="noopener">Reduce load of preview fetching on third-party servers</a></li>
<li><a href="https://dri.es/the-new-old-jamstack-and-mach-journey-towards-traditional-cms-concepts" class="ext u-like-of" rel="noopener">The new old: Jamstack and MACH’s journey towards traditional CMS concepts</a><br>
An opinion piece, worth reading</li>
<li><a href="https://www.markpitblado.me/blog/creating-a-pleasant-blog-reading-experience" class="ext u-like-of" rel="noopener">Creating a pleasant blog reading experience</a><br>
“It is okay to keep things simple, to stay grounded in the primary purpose that people use something for” - absolutely, I agree</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://nimbletext.com/live" class="ext u-like-of" rel="noopener">NimbleText</a> (Text manipulation and code generation)</li>
<li>LibreOffice: <a href="https://help.libreoffice.org/latest/en-US/text/shared/01/qrcode.html" class="ext u-like-of" rel="noopener">QR and Barcode</a><br>
A nice feature</li>
<li><a href="https://nocodb.com" class="ext u-like-of" rel="noopener">NocoDB</a><br>
“Databases as Spreadsheets”, looks quite useful</li>
<li><a href="https://digital-services-act-whistleblower.integrityline.app/" class="ext u-like-of" rel="noopener">DSA whistleblower tool</a><br>
An important initiative</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 17, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w17/"/>
        
        <updated>2024-04-24T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w17/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://thehistoryoftheweb.com/the-analog-web/" class="ext u-like-of" rel="noopener">The Analog Web</a><br>
“[…] there is a culture that exists beneath the commercial web that is motivated by the same goal; to create a bulkwark against the erasure of a more human web.”</li>
<li><a href="https://shellsharks.com/notes/2024/04/17/having-a-website-is-about-you" class="ext u-like-of" rel="noopener">Having a website isn’t about blogging, it’s about you</a>
<ul>
<li><a href="https://shellsharks.com/notes/2023/08/15/website-component-checklist" class="ext u-like-of" rel="noopener">Website Component Checklist</a></li>
</ul>
</li>
<li><a href="https://predragtasevski.com/blog/decapcms-and-gitlab-integration-without-netlify" class="ext u-like-of" rel="noopener">DecapCMS and GitLab integration without Netlify</a><br>
An interesting approach, might be helpful at some point</li>
<li><a href="https://www.marginalia.nu/log/61-botspam-apocalypse/" class="ext u-like-of" rel="noopener">Botspam Apocalypse</a><br>
“Bots are absolutely crippling the Internet ecosystem.”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://libgen.li" class="ext u-like-of" rel="noopener">Library Genesis</a><br>
A pretty great online library</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 16, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w16/"/>
        
        <updated>2024-04-17T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w16/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://tigerpajamas.com/blog/what-is-a-web-site/" class="ext u-like-of" rel="noopener">What is a web site?</a></li>
<li><a href="https://spectrum.ieee.org/3m-floppy" class="ext u-like-of" rel="noopener">The Rise and Fall of 3M’s Floppy Disk</a></li>
<li><a href="https://www.lkhrs.com/blog/2024/darkvisitors-hugo/" class="ext u-like-of" rel="noopener">Using the Dark Visitors API with Hugo to opt out of AI data harvesting</a><br>
A cool idea - building <code>robots.txt</code> based on the Dark Visitors API</li>
<li><a href="https://www.theverge.com/c/24070570/internet-cables-undersea-deep-repair-ships" class="ext u-like-of" rel="noopener">The Cloud Under the Sea</a><br>
An article about “the invisible seafaring industry that keeps the internet afloat”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://csszengarden.com" class="ext u-like-of" rel="noopener">CSS Zen Garden</a></li>
<li><a href="https://ctrlvjournal.com" class="ext u-like-of" rel="noopener">ctrl + v journal</a><br>
Just beautiful</li>
<li><a href="https://www.a11yproject.com/checklist/" class="ext u-like-of" rel="noopener">The A11Y Project Checklist</a><br>
A helpful checklist that uses the WCAG as a reference point</li>
<li><a href="https://selfh.st/apps/" class="ext u-like-of" rel="noopener">selfh.st/apps/</a><br>
A directory of self-hosted software</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>How to Export Data from Fauna</title>
        
          
            <category term="database" />
          
            <category term="guide" />
          
            <category term="serverless" />
          
        
        <link href="https://ttntm.me/blog/how-to-export-data-from-fauna/"/>
        
        <updated>2024-04-15T10:30:00Z</updated>
        <id>https://ttntm.me/blog/how-to-export-data-from-fauna/</id>
        <content type="html"><![CDATA[
          
            
            <p>Just this morning, I had a brief online conversation that made me realize something: I’m using Fauna for two live web applications and I didn’t even think about database backups for a second.</p>
<p>All my data has been just fine for the past few years, but today’s realization of having no backups whatsoever wasn’t something that I felt comfortable ignoring any longer. Even more so, considering that each application contains data that I definitely don’t want to lose: one stores 65+ family recipes, and the other one is a movie/series journal with 200+ entries for my own user, and 1000s more belonging to other users.</p>
<p>So, I logged into my Fauna account, and clicked around. I don’t know if it’s any different for paid plans, but the free plan doesn’t seem to offer any - obvious - functionality to export the data stored in collections (= tables). That’s not really a showstopper for me, but I suppose it could be for others - it felt like a rather strange UX choice nevertheless.</p>
<p>I quickly remembered that I already wrote about <a href="https://ttntm.me/blog/track-fauna-metrics-google-sheets" class="int">building an automated database reporting service</a> 3 years ago, and I figured that writing a backup script should not be too much trouble. An hour later, I’d finished writing the code and decided to share it here, hoping it might help out somebody else looking for the exact same thing.</p>
<h2 id="the-script" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-script"><span>The Script</span></a></h2>
<p>This is the code I’m using for watch3r’s database backups (3 collections):</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'dotenv'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> faunadb <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'faunadb'</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> q <span class="token operator">=</span> faunadb<span class="token punctuation">.</span>query<br><br><span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">faunadb<span class="token punctuation">.</span>Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token literal-property property">secret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FAUNA_SECRET</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">domain</span><span class="token operator">:</span> <span class="token string">'db.us.fauna.com'</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getData</span><span class="token punctuation">(</span><span class="token parameter">indexName<span class="token punctuation">,</span> size</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><br>      q<span class="token punctuation">.</span><span class="token function">Paginate</span><span class="token punctuation">(</span><br>        q<span class="token punctuation">.</span><span class="token function">Match</span><span class="token punctuation">(</span><br>          q<span class="token punctuation">.</span><span class="token function">Index</span><span class="token punctuation">(</span>indexName<span class="token punctuation">)</span><br>        <span class="token punctuation">)</span><span class="token punctuation">,</span><br>        <span class="token punctuation">{</span> <span class="token literal-property property">size</span><span class="token operator">:</span> size <span class="token punctuation">}</span><br>      <span class="token punctuation">)</span><br>    <span class="token punctuation">)</span><br>      <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token keyword">const</span> listRefs <span class="token operator">=</span> response<span class="token punctuation">.</span>data<br><br>        console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">INFO | Index: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>indexName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> | </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>listRefs<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> entries found</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br><br>        <span class="token keyword">const</span> getListDataQuery <span class="token operator">=</span> listRefs<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">ref</span> <span class="token operator">=></span> q<span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>ref<span class="token punctuation">)</span><span class="token punctuation">)</span><br>        <span class="token keyword">const</span> records <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>getListDataQuery<span class="token punctuation">)</span><br>        <span class="token keyword">return</span> records<br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><br>      <span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token keyword">throw</span> error<br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>ex<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>ex<span class="token punctuation">.</span>message <span class="token operator">||</span> ex<span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">getTimestamp</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> d <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  <span class="token keyword">return</span> d<span class="token punctuation">.</span><span class="token function">toISOString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'T'</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">'-'</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">indexList</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>      <span class="token string">'tracklist_all'</span><span class="token punctuation">,</span><br>      <span class="token string">'watchlist_all'</span><span class="token punctuation">,</span><br>      <span class="token string">'users_all'</span><br>    <span class="token punctuation">]</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">size</span><span class="token operator">:</span> <span class="token number">500</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> config<span class="token punctuation">.</span>indexList<span class="token punctuation">.</span>length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">try</span> <span class="token punctuation">{</span><br>      <span class="token keyword">const</span> currentIndex <span class="token operator">=</span> config<span class="token punctuation">.</span>indexList<span class="token punctuation">[</span>i<span class="token punctuation">]</span><br><br>      <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getData</span><span class="token punctuation">(</span>currentIndex<span class="token punctuation">,</span> config<span class="token punctuation">.</span>size<span class="token punctuation">)</span><br><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">const</span> now <span class="token operator">=</span> <span class="token function">getTimestamp</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>        <span class="token keyword">const</span> output <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>data<span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span><br><br>        <span class="token keyword">await</span> fs<span class="token punctuation">.</span>promises<span class="token punctuation">.</span><span class="token function">writeFile</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">./backup/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>now<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>currentIndex<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.json</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> output<span class="token punctuation">)</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>ex<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>ex<span class="token punctuation">.</span>message <span class="token operator">||</span> ex<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>Source code: <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/backup/backup-db.js" rel="noopener">backup-db.js@Codeberg</a></p>
<h3 id="notes-and-usage-instructions" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#notes-and-usage-instructions"><span>Notes and Usage Instructions</span></a></h3>
<p><code>FAUNA_SECRET</code> is taken from an environment variable, i.e. stored in an <code>.env</code> file.</p>
<p><code>getData()</code> takes 2 parameters:</p>
<ul>
<li><code>indexName</code>: name of a Fauna index, should return an unfiltered collection for this use case</li>
<li><code>size</code>: Fauna query response size, increased (default: <code>64</code>), so we don’t have to think about pagination when processing smaller collections</li>
</ul>
<p><code>getTimestamp()</code> returns a timestamp like <code>YYYYMMDD</code> used as a prefix for the JSON files written by this script.</p>
<p><code>main()</code> ties it all together, using <code>config.IndexList[]</code> to provide an easy way to process multiple collections sequentially, writing a dedicated JSON backup file for each collection, i.e. <code>20240415-users_all.json</code>.</p>
<h2 id="the-bottom-line" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-bottom-line"><span>The Bottom Line</span></a></h2>
<p>Nothing fancy, just a script that works (for my projects) and gets the job done.</p>
<p>It might work for you too, or at least save you some time and serve as a base for something you’re working on.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Website Evolution: Q1 2024</title>
        
          
            <category term="evolution" />
          
            <category term="series" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/website-evolution-q1-2024/"/>
        
        <updated>2024-04-12T10:30:00Z</updated>
        <id>https://ttntm.me/blog/website-evolution-q1-2024/</id>
        <content type="html"><![CDATA[
          
            
            <p>2024 has been a very productive year so far. I’d planned to write more, and I feel like that’s going quite well at the moment. I’ve also managed to launch <a href="https://ttntm.me/blog/bukmark-club-intro/" class="int">a new side project</a> in February, and shipped a ton of updates and enhancements for this website.</p>
<p>Some of these updates were announced in <a href="https://ttntm.me/tags/website/" class="int">previous blog posts</a>, others evolved into longer deep dive articles. However, I’ve added, changed and updated many more things recently, which lead to the idea of a new “Website Evolution” series taking shape. Right now, you’re reading the first post in this new series, and I’m planning to add more posts to it occasionally.</p>
<p>A quick remark before we get started: this post will try to add context to the changes, updates and enhancements. An uncommented chronology of changes can always be found in the <a href="https://ttntm.me/changelog/" class="int">Changelog</a>. This post covers versions <a href="https://ttntm.me/changelog/#3-6" class="int">3.6</a> to <a href="https://ttntm.me/changelog/#3-13" class="int">3.13</a>.</p>
<h2 id="content-and-pages" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#content-and-pages"><span>Content and Pages</span></a></h2>
<p>This section provides an overview of content and pages with details on what was added, changed and/or (re)moved.</p>
<h3 id="appendix" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#appendix"><span>Appendix</span></a></h3>
<p>URL: <a href="https://ttntm.me/appendix/" class="int">/appendix</a></p>
<p>I wanted to split “about me” and “about this website” for a while, because the combined page felt a little too long and the hierarchy of the headings didn’t really make sense anymore.</p>
<p>This “Appendix” is what I ended up with.</p>
<p>It’s a new page, with an URL inspired by <a class="ext" href="https://kayserifserif.place/appendix/" rel="noopener">Katherine’s website</a>, that contains the “About this Website” section and also a completely new section dedicated to “Intentions and To-Dos” regarding this website.</p>
<h3 id="bookmarks" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#bookmarks"><span>Bookmarks</span></a></h3>
<p>URL: <a href="https://ttntm.me/bookmarks/" class="int">/bookmarks</a></p>
<p>This is my personal collection of bookmarks that I like to think of as some kind of <em>treasure map</em>. I wanted to add such a personal directory to my website since finishing “Halt and Catch Fire” in late November '23, and I’m very happy with the result.</p>
<p>The initial version of the page used Markdown files in a dedicated collection, which I described in the post <a href="https://ttntm.me/blog/bookmarks-are-back/" class="int">Update: Bookmarks Are Back</a>. But in the meantime, I’ve moved the collection into a JSON data file, which makes maintaining it a little easier.</p>
<h3 id="ideas" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#ideas"><span>Ideas</span></a></h3>
<p>URL: <a href="https://ttntm.me/ideas/" class="int">/ideas</a></p>
<p>I came across <a class="ext" href="https://aboutideasnow.com" rel="noopener">aboutideasnow.com</a> not long ago, looked at some other people’s websites, liked a lot of what I saw and decided to add an /ideas page to my own website. It’s a nice idea to share ideas publicly, and I also like that it helps keep /now clean in terms of “doing something now” vs. “thinking about something” in general.</p>
<h3 id="reading" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#reading"><span>Reading</span></a></h3>
<p>URL: <a href="https://ttntm.me/reading/" class="int">/reading</a></p>
<p>Adding a public reading list to my website was something I wanted to do for a really long time, it must’ve been on my list of website to-dos for at least 2 years.</p>
<p>Good that I procrastinated for so long though: discovering Omnivore earlier this year almost made this too easy. I’m really satisfied with the result and I had a lot of fun building this integration. So much actually, that the <a href="https://ttntm.me/blog/creating-a-reading-list-with-eleventy-and-omnivore/" class="int">article describing the integration</a> almost wrote itself.</p>
<h3 id="sitemap" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#sitemap"><span>Sitemap</span></a></h3>
<p>URL: <a href="https://ttntm.me/sitemap/" class="int">/sitemap</a></p>
<p>Remember the old days, when websites offered sitemaps for humans instead of just providing invisible XML ones for search engines?</p>
<p>I missed that and decided to add one to this website. It’s flat and simple, but it should provide a sufficient overview of all the pages this website consists of. Also, using Eleventy’s <code>collections.all</code> iterable made it super easy to build this sitemap. So, there’s really no reason not to have one.</p>
<h3 id="whisky" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#whisky"><span>Whisky</span></a></h3>
<p>URL: <a href="https://ttntm.me/whisky/" class="int">/whisky</a></p>
<p>This one’s a rather personal one: I finally gave my “Scotch Log” a home on this website. So far, I tracked my whisky drinking in a Notion table, which worked well enough, but didn’t feel quite right. Putting it out there publicly, as a part of this personal website, feels much more satisfying already.</p>
<p>Building the page was fun too: I’m using a regular Eleventy collection that consists of Markdown files for each whisky bottle. The page template renders each collection item as a <code>details/summary</code> element and also includes some region-based filters. These filters are rendered by a shortcode, <code>customFilter</code>, that works as described in <a href="https://ttntm.me/notes/#33" class="int">notes#33</a>.</p>
<p>I really like the resulting “list+” UI that the <code>details/summary</code> element offers, and I’m already looking forward to maintaining this page long term.</p>
<h3 id="work" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#work"><span>Work</span></a></h3>
<p>URL: <a href="https://ttntm.me/projects/" class="int">/projects</a></p>
<p>This page was overdue for an update - I don’t think I was ever really satisfied with how it looked like and how the information was presented there. But all of that’s changed now, and I quite like the result.</p>
<p>I got rid of the card-based layout and made the page a proper list with different sections. I’ve also added a <em>lot</em> more content to the list, so there are no more “read more on another website…” links that used to be there, and I’ve also included links to articles that I wrote about specific projects. Project listings also include links to both the source code and the project’s demo site (wherever applicable) now, instead of just one of them.</p>
<h2 id="features-and-functionality" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#features-and-functionality"><span>Features and Functionality</span></a></h2>
<p>Mostly additional features, some of them enhancing this website’s functionality in hopes of providing better user experience.</p>
<h3 id="activity-timeline" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#activity-timeline"><span>Activity Timeline</span></a></h3>
<p>I always liked the way most GIT-based SCM systems offer a visual timeline of a user’s commit activity. A little while ago, I got the idea of providing such an activity timeline scoped to this website’s changes along with the <a href="https://ttntm.me/changelog/" class="int">Changelog</a>.</p>
<p>After a bit of research, I discovered the <a class="ext" href="https://postgraph.rknight.me" rel="noopener">Eleventy Post Graph</a> plugin, that offers exactly what I was looking for. By default, it works a little different than what I had in mind: it processes Eleventy’s content (collections) and doesn’t consider the <code>git log</code> of the underlying repository.</p>
<p>I wanted to have some a kind of “visualized changelog”, so that wasn’t enough. However, there’s also the option to use the plugin with custom data, so I went looking for a way to convert <code>git log</code> output to JSON, which I could then use in a data file rather easily.</p>
<p>Eventually, I found an article about <a class="ext u-in-reply-to" href="https://blog.kellybrazil.com/2022/05/17/easily-convert-git-log-output-to-json/" rel="noopener">easily converting git log output to JSON</a> and got to work. The <code>git log</code> JSON output gets processed by a small Node.js script that does a lot of date stuff (extraction, formatting) and eventually makes use of <code>reduce()</code> to build an object as expected by the postgraph plugin requires it.</p>
<p>A solution that works nicely and produces the <a href="https://ttntm.me/changelog/#timeline" class="int">desired result</a>. However, I encountered a little UI issue that I eventually found a CSS-based workaround for: <a class="ext" href="https://github.com/rknightuk/eleventy-plugin-post-graph/issues/9" rel="noopener">Strange Behaviour: Extra HTML Tags</a></p>
<h3 id="dark-mode" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#dark-mode"><span>Dark Mode</span></a></h3>
<p>Functionality that was overdue for a <em>long</em> time.</p>
<p>Initially, I stuck with this website’s long-lasting no-JS approach and implemented theming purely based on a media query. However, some feedback I received from readers/users changed my mind, and I added a button to switch between themes.</p>
<p>The code currently uses auto-detection based on a media query (<code>window.matchMedia()</code>) and persists the user’s choice in <code>localStorage</code>. There’s also a <code>&lt;noscript&gt;</code> fallback that removes the theme switch button from the UI, making theme selection use <code>prefers-color-scheme</code> only. I don’t know how common it is to do that, which would be rather interesting actually, but displaying a non-functional button for <code>&lt;noscript&gt;</code> users on an otherwise completely functional website just felt wrong.</p>
<h3 id="feeds" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#feeds"><span>Feeds</span></a></h3>
<p>This website has offered a feed of articles and blog posts since 2019. However, my <a href="https://ttntm.me/likes/" class="int">Likes</a> and Notes collections did not offer a feed, despite the fact that they’re also collections of content that are updated regularly.</p>
<p>Feeds for both collections were added, together with matching icons in the respective headings, and a <a href="https://ttntm.me/feeds/" class="int">Feeds</a> page that lists all the available feeds.</p>
<h3 id="old-content-note" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#old-content-note"><span>Old Content Note</span></a></h3>
<p>I don’t really know how to call this feature, or if there even is a proper name for it, so I ended up with this weird heading.</p>
<p>It’s essentially just the name of a shortcode, <code>oldContentNote</code>, that checks each post’s publication date and displays a note if a post is older than 2 years. The resulting note clearly states that the following content is a little older, and that “circumstances may have changes since publication”.</p>
<h3 id="tags" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#tags"><span>Tags</span></a></h3>
<p>Sometime, we mess with things, only to realize that they were fine in the first place. And that’s pretty much what happened to blog post tags on this website. I talked about this functionality in a previous post, <a href="https://ttntm.me/blog/restoring-tags/" class="int">Update: Tags Are Back</a>, that also provides some details explaining how to handle tags in Eleventy.</p>
<p>My current guidelines for using tags on this website are:</p>
<ul>
<li>Tags are used for blog posts only</li>
<li>Tags should be limited to one word</li>
<li>Min. 1 / max. 3 tags per post</li>
</ul>
<p>A list of all tags can be found here: <a href="https://ttntm.me/tags/" class="int">Tags</a></p>
<h2 id="look-and-feel" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#look-and-feel"><span>Look and Feel</span></a></h2>
<p>The only major change in terms of my website’s UI is the redesigned homepage layout. I retired the centered 1-column layout and created a 2-column grid. There’s a new “Collections &amp; Pages” section, and I also added links to the sitemap and all the available RSS feeds. The intro and the most recent posts were already there in the past, so they only moved to different spots in the new layout.</p>
<p>Other than that, these are the minor style changes:</p>
<ul>
<li>Added a background texture and a gradient on top of it to achieve a slight “noisy vignette” effect</li>
<li>Revised blockquotes and code blocks
<ul>
<li>Both have a slight left/right margin when viewed on larger screens</li>
<li>Blockquotes are displayed with quotation marks in <code>::before</code> now</li>
</ul>
</li>
<li>A bit of added whimsy
<ul>
<li>Found some cool GIFs of a white Persian cat and added that to some pages</li>
</ul>
</li>
<li>Reduced <code>letter-spacing</code> for headings and links
<ul>
<li>Felt a little too much, looks better now</li>
</ul>
</li>
</ul>
<h2 id="setup-and-other-technical-stuff" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#setup-and-other-technical-stuff"><span>Setup and Other Technical Stuff</span></a></h2>
<p>Changes of how the source code is organized:</p>
<ul>
<li>Added folders to collection root folders to organize them based on years</li>
<li>Added a <code>pages</code> folder to keep content pages and templates in one place</li>
<li>Added <code>humans.txt</code></li>
<li>Extended and updated <code>robots.txt</code></li>
<li>Updated the Eleventy config: <code>html-min</code> now runs for productive builds only</li>
</ul>
<h2 id="closing-thoughts" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#closing-thoughts"><span>Closing Thoughts</span></a></h2>
<p>Thanks for reading this post! I felt that this period of high activity might be worth remembering, and I’m already curious to see this new series grow over time.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 15, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w15/"/>
        
        <updated>2024-04-09T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w15/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://tomhazledine.com/known-pleasures-svg-line-art/" class="ext u-like-of" rel="noopener">Known Pleasures: SVG line art</a><br>
An article about “recreating classic Joy Division album-art with SVG”</li>
<li><a href="https://www.softwaremaxims.com/blog/not-a-supplier" class="ext u-like-of" rel="noopener">I am not a supplier</a><br>
On FOSS: “You are not buying from a supplier, you are a raccoon digging through dumpsters for free code.”</li>
<li><a href="https://davidwalsh.name/im-so-old-1" class="ext u-like-of" rel="noopener">I’m So Old: Web Edition</a><br>
Remembering doing, experiencing and witnessing most things mentioned in this article (and in the comments!), good times</li>
<li><a href="https://romgrk.com/posts/optimizing-javascript" class="ext u-like-of" rel="noopener">Optimizing JavaScript for fun and for profit</a><br>
A summary of common JS optimization techniques</li>
<li><a href="https://www.tpgi.com/the-top-accessibility-errors-found-in-2023/" class="ext u-like-of" rel="noopener">The Top Accessibility Errors Found in 2023</a></li>
<li><a href="https://ericportis.com/posts/2024/okay-color-spaces/" class="ext u-like-of" rel="noopener">Okay, Color Spaces</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://gist.github.com/bbertolani/df95a82e5963d8375e8617e63ef50ffd" class="ext u-like-of" rel="noopener">Effective Engineer - Notes</a><br>
There’s some good advice in there</li>
<li><a href="https://jampack.divriots.com" class="ext u-like-of" rel="noopener">Jampack</a><br>
A tool that “optimizes static websites for best user experience and best Core Web Vitals scores”</li>
<li><a href="https://xata.io/blog/serverless-postgres-platform" class="ext u-like-of" rel="noopener">A new serverless Postgres platform</a><br>
Serverless Postgres? …for free? Yes, looks like that!</li>
<li><a href="https://css.winterveil.net" class="ext u-like-of" rel="noopener">magick.css</a><br>
“A minimalistic, (mostly) classless CSS framework that is designed to be easy to use and easy to understand”</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 14, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w14/"/>
        
        <updated>2024-04-04T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w14/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://gist.github.com/thesamesam/223949d5a074ebc3dce9ee78baad9e27" class="ext u-like-of" rel="noopener">FAQ on the xz-utils backdoor (CVE-2024-3094)</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.404s.design" class="ext u-like-of" rel="noopener">Page not found</a><br>
A curated collection of 404 pages</li>
<li><a href="https://elysiajs.com" class="ext u-like-of" rel="noopener">ElysiaJS</a><br>
An Express alternative: “Elysia is an ergonomic web framework for building backend servers with Bun”</li>
<li><a href="https://spike.news" class="ext u-like-of" rel="noopener">Spike.News</a><br>
A customizable news aggregator with a minimal UI</li>
<li><a href="https://nohello.net/en/" class="ext u-like-of" rel="noopener">nohello.net</a><br>
Absolutely.</li>
<li><a href="https://www.sveltelab.dev" class="ext u-like-of" rel="noopener">SvelteLab</a><br>
“A supercharged REPL for SvelteKit, or in other words a development environment for quickly crafting and sharing Svelte code”</li>
<li><a href="https://ecss.info" class="ext u-like-of" rel="noopener">ecss</a><br>
“Simple rules for simple styling”</li>
<li><a href="https://fx.wtf" class="ext u-like-of" rel="noopener">fx</a><br>
A terminal JSON viewer &amp; processor</li>
<li><a href="https://hostedsoftware.org" class="ext u-like-of" rel="noopener">Self-Hosted Software List</a></li>
<li><a href="https://gradientor.app" class="ext u-like-of" rel="noopener">Gradientor</a><br>
Create and preview radial gradients</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 13, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w13/"/>
        
        <updated>2024-03-27T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w13/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://spacetimetech.wordpress.com/2024/02/19/the-state-of-the-web-novelty-booms-and-busts-the-joys-of-simple-html-centric-web-design-personal-websites-the-walled-web-vs-decentralization/" class="ext u-like-of" rel="noopener">The state of the web in 2024</a></li>
<li><a href="https://www.alexhyett.com/newsletter/the-indie-web-is-the-new-and-the-old-web/" class="ext u-like-of" rel="noopener">The IndieWeb is the new and the old web</a></li>
<li><a href="https://kld.dev/toc-animation/" class="ext u-like-of" rel="noopener">Table of contents progress animation</a><br>
An article about building an animated ToC progress indicator</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://modernfontstacks.com" class="ext u-like-of" rel="noopener">Modern Font Stacks</a><br>
The fastest fonts available.</li>
<li><a href="https://discourse.writefreesoftware.org/" class="ext u-like-of" rel="noopener">Write Free Software Forum</a></li>
<li><a href="https://css-irl.info" class="ext u-like-of" rel="noopener">CSS { In Real Life }</a><br>
A blog about CSS, front-end development, the web, and beyond</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 12, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w12/"/>
        
        <updated>2024-03-20T10:35:00Z</updated>
        <id>https://ttntm.me/likes/2024-w12/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://seanbarry.dev/posts/switch-true-pattern" class="ext u-like-of" rel="noopener">Using the Switch(true) Pattern in JavaScript</a><br>
Seems useful, should keep that in mind</li>
<li><a href="https://hamatti.org/posts/build-an-idea-bank-and-never-run-out-of-blog-ideas/" class="ext u-like-of" rel="noopener">Build an idea bank and never run out of blog ideas</a><br>
A good idea, useful for when you find yourself wondering “What should I write about?”</li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/curriculum/" class="ext u-like-of" rel="noopener">MDN Curriculum</a><br>
Labeled as “The essential skillset for new front-end developers”, looks good</li>
<li><a href="https://docs.astro.build/en/guides/astro-db/" class="ext u-like-of" rel="noopener">Astro DB</a><br>
Sounds interesting - “a fully-managed SQL database designed exclusively for Astro”</li>
<li><a href="https://hellnet.work/8831/page1.html" class="ext u-like-of" rel="noopener">The 88×31 Archive</a><br>
Buttons!!!</li>
<li><a href="https://www.artsy.net/artist/teng-pu-chun" class="ext u-like-of" rel="noopener">Artworks by Teng Pu-Chun</a><br>
A fascinating artist that I haven’t heard of before</li>
<li><a href="https://unmaintained.tech" class="ext u-like-of" rel="noopener">No Maintenance Intended</a></li>
<li><a href="https://en.wikipedia.org/wiki/Onna-musha" class="ext u-like-of" rel="noopener">Onna-musha</a><br>
Something I did not know about: female warriors in pre-modern Japan</li>
<li><a href="https://pages.casa" class="ext u-like-of" rel="noopener">pages.casa</a><br>
Simple hosting of static websites</li>
<li><a href="https://biomejs.dev" class="ext u-like-of" rel="noopener">Biome</a><br>
On this list to keep it in mind for future projects: “Biome is a fast formatter for JavaScript, TypeScript, JSX, and JSON that scores 97% compatibility with Prettier”</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Creating a Reading List with Eleventy and Omnivore</title>
        
          
            <category term="eleventy" />
          
            <category term="guide" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/creating-a-reading-list-with-eleventy-and-omnivore/"/>
        
        <updated>2024-12-16T09:30:00Z</updated>
        <id>https://ttntm.me/blog/creating-a-reading-list-with-eleventy-and-omnivore/</id>
        <content type="html"><![CDATA[
          
            
            <p><strong>Omnivore was shut down in late 2024</strong><br>
See: <a class="ext" href="https://www.theverge.com/2024/10/29/24283055/one-of-my-favorite-read-later-apps-is-shutting-down" rel="noopener">One of my favorite read-later apps is shutting down</a><br>
This article is probably obsolete now, but the general approach might apply to other services/tools.</p>
<p>–</p>
<p>It’s been a productive year so far - I’ve managed to add a couple of cool new things to this website, which is something I’d been planning to do for a while. This article is about the most recent addition, my <a href="https://ttntm.me/reading/" class="int">personal reading list</a> powered by Omnivore, a free, open source, read-it-later app.</p>
<img src="https://ttntm.me/static/img/reading.png" class="img-fluid img-center" alt="An illustration, taken from undraw.co, showing a female person reading a book while sitting in a chair">
<h2 id="prerequisites" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#prerequisites"><span>Prerequisites</span></a></h2>
<ul>
<li>A website; following this guide will be a little easier if it’s built with Eleventy</li>
<li>An Omnivore account</li>
<li>Optional: some kind of CI/CD that supports deployments triggered by webhooks</li>
</ul>
<h2 id="get-an-api-key" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#get-an-api-key"><span>Get an API Key</span></a></h2>
<p>The first thing we’ll have to take care of is getting an Omnivore API key.</p>
<p>It only takes a couple of clicks and the whole procedure is well documented - you’ll end up with an API key that looks like this: <code>FFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF</code></p>
<h3 id="test-your-api-key" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#test-your-api-key"><span>Test Your API Key</span></a></h3>
<p>This step is optional, but I recommend doing it.</p>
<p>I find it helpful to know what kind of response data to expect and it’s easier to notice if something’s wrong <em>before</em> wrapping the API call with additional code.</p>
<p>Using <code>curl</code> is perfectly fine (example in the docs linked above), but GraphQL in the terminal can get a little confusing. I usually recommend API clients like <a class="ext" href="https://github.com/ArchGPT/insomnium" rel="noopener">Insomnium</a>, which can help make things a little easier.</p>
<p>First, you’ll need a matching GraphQL query, no matter which tool you decide to use. I checked the docs, only found the <a class="ext" href="https://github.com/omnivore-app/omnivore/blob/main/packages/api/src/schema.ts" rel="noopener">schema</a>, and eventually ended up getting a suitable query from the developer tool’s network panel:</p>
<img src="https://ttntm.me/static/img/blog/ov_network_panel.jpg" class="img-center img-fluid auto-invert" alt="A screenshot from the network panel of the browser developer tools that shows a GraphQL query of the Omnivore app">
<p>Next, I extracted the fields I felt were necessary and ran a test:</p>
<img src="https://ttntm.me/static/img/blog/ov_api_test.jpg" class="img-center img-fluid" alt="A screenshot of 2 panes in Insomnium that shows a GraphQL query (left pane) that was executed against the Omnivore API and its results (right pane)">
<p>NB: please excuse the squiggly lines, they’re caused by a bug in the application GUI.</p>
<p>The query variables are used to limit the results we get:</p>
<ul>
<li><code>&quot;first&quot;: 10</code> results in the 10 most recently saved items</li>
<li><code>&quot;query&quot;: &quot;no:subscription&quot;</code> filters for items that were saved intentionally, i.e. excludes feeds and newsletters that can end up in your Omnivore account automatically (a <em>great</em> feature actually, just not what we want here)</li>
</ul>
<h2 id="data-fetching-with-eleventy" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#data-fetching-with-eleventy"><span>Data Fetching with Eleventy</span></a></h2>
<p>Based on the successful test described above, we can proceed and set up the code that pulls the data into Eleventy.</p>
<p>I’m using the <a class="ext" href="https://www.11ty.dev/docs/plugins/fetch/" rel="noopener">eleventy-fetch plugin</a> to <code>POST</code> the GraphQL query to Omnivore’s API. The code is rather simple and stored in <code>./src/_data/reading.js</code>, a <a class="ext" href="https://www.11ty.dev/docs/data-global/" rel="noopener">Global Data File</a>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'dotenv'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> eleventyFetch <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'@11ty/eleventy-fetch'</span><span class="token punctuation">)</span><br><br><span class="token keyword">const</span> ovURL <span class="token operator">=</span> <span class="token string">'https://api-prod.omnivore.app/api/graphql'</span><br><span class="token keyword">const</span> queryData <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token comment">// see below</span><br><span class="token punctuation">}</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">eleventyFetch</span><span class="token punctuation">(</span>ovURL<span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">directory</span><span class="token operator">:</span> <span class="token string">'.cache'</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">duration</span><span class="token operator">:</span> <span class="token string">'1d'</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">fetchOptions</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'POST'</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">OV_API_KEY</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>          <span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'application/json'</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>queryData<span class="token punctuation">)</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>response <span class="token operator">&amp;&amp;</span> response<span class="token operator">?.</span>data<span class="token operator">?.</span>search<span class="token operator">?.</span>edges<span class="token operator">?.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> response<span class="token punctuation">.</span>data<span class="token punctuation">.</span>search<span class="token punctuation">.</span>edges<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token operator">=></span> item<span class="token punctuation">.</span>node<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>ex<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>ex<span class="token punctuation">.</span>message <span class="token operator">||</span> ex<span class="token punctuation">)</span><br>    <span class="token keyword">return</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>The Omnivore API key is stored in a local <code>.env</code> file and is made available using the <code>dotenv</code> package. Don’t forget that the API key should also be configured in whatever CI/CD service you’re using to deploy your website.</p>
<p>Other than that, the code is pretty straightforward: we either get a response, that we then process with a <code>map()</code> function, or we don’t, which leads to an empty array being returned by the data file. That, and the outer try/catch, should make sure that potentials errors, timeouts etc. don’t break the build.</p>
<p>I decided to omit the <code>queryData</code> object in the code snippet above to make it a little more concise. It’s absolutely necessary though, considering that this is where you define the shape of the data returned by the API:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> queryData <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">query</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br>    query Search($after: String, $first: Int, $query: String) {<br>      search(first: $first, after: $after, query: $query) {<br>        ... on SearchSuccess {<br>          edges {<br>            node {<br>              id<br>              title<br>              description<br>              siteName<br>              url<br>              publishedAt<br>              savedAt<br>            }<br>          }<br>          pageInfo {<br>            hasNextPage<br>            endCursor<br>            totalCount<br>          }<br>        }<br>        ... on SearchError {<br>          errorCodes<br>        }<br>      }<br>    }<br>  </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>  <span class="token literal-property property">variables</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">after</span><span class="token operator">:</span> <span class="token string">'0'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">first</span><span class="token operator">:</span> <span class="token number">10</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">query</span><span class="token operator">:</span> <span class="token string">'no:subscription'</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p><code>query Search()</code> defines a couple of variables that are used in the next line (<code>search()</code>). The actual values for the variables are supplied by the included <code>variables</code> object. That means, that you shouldn’t have to touch the GraphQL query, unless you want more (or less) fields included in the response.</p>
<p>NB: there’s an official <a class="ext" href="https://github.com/omnivore-app/omnivore-api" rel="noopener">API Client Library for Node.js</a>, but the eleventy-fetch plugin offers caching, so I decided to use that instead. Otherwise, I could end up hammering the Omnivore API with requests during local development, which is something I’d rather not risk.</p>
<h2 id="rendering-the-reading-list" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#rendering-the-reading-list"><span>Rendering the Reading List</span></a></h2>
<p>It’s nothing special, but this is how I’m rendering my reading list:</p>
<pre class="language-html"><code class="language-html">{% if reading | length %}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">></span></span><br>    {% for item in reading %}<br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span><span class="token punctuation">></span></span>{{ item.title }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><br>            {% if item.publishedAt %}<br>              Published: <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>time</span> <span class="token attr-name">datetime</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ item.publishedAt | date('YYYY-MM-DD') }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ item.publishedAt | date('MMMM DD, YYYY') }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>time</span><span class="token punctuation">></span></span> at {{ item.siteName }}<br>            {% else %}<br>              Published at {{ item.siteName }}<br>            {% endif %}<br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><br>            Saved: <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>time</span> <span class="token attr-name">datetime</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ item.savedAt | date('YYYY-MM-DD') }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ item.savedAt | date('MMMM DD, YYYY') }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>time</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>{{ item.description }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ item.url }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Read Now<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span><br>    {% endfor %}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span><br>{% else %}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>No data :(<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>{% endif %}<br></code></pre>
<p>There’s a check for the availability of the list data (<code>if reading | length</code>) that makes sure we don’t run into any rendering errors. Dates are formatted by a <code>date()</code> filter and wrapped in <code>&lt;time&gt;</code> tags. <code>publishedAt</code> is an optional - it’s wrapped in an IF-statement to avoid 1970 fallback dates.</p>
<h2 id="updating-the-list" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#updating-the-list"><span>Updating the List</span></a></h2>
<p>If you’ve followed this guide so far, you should be in this situation:</p>
<ul>
<li>Local builds use the <code>.cache</code> folder managed by the eleventy-fetch plugin</li>
<li>Remote builds should fetch fresh data every time</li>
<li>Saving items to Omnivore does not have any effect on your site</li>
</ul>
<p>That’s probably fine for many folks out there, some might even have automated builds and deployments in place already, which takes care of fetching new Omnivore data too. And if that applies to you, then you can certainly leave it at that, enjoy your new reading list and move on to other matters.</p>
<p>Otherwise, keep reading and set up automatic deployments using webhooks.</p>
<h3 id="automating-deployments" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#automating-deployments"><span>Automating Deployments</span></a></h3>
<blockquote>
<p>Omnivore allows you to send your reading data in realtime using webhooks. When a new page is saved or updated, a webhook can be triggered.</p>
<p><small>Omnivore docs</small></p>
</blockquote>
<p>Services and Platforms like Cloudflare Pages, Netlify etc. usually allow their users to set up dedicated endpoints to receive webhook data and use them to trigger deployments of the respective site.</p>
<p>So-called Deploy Hooks are available for websites built with and served by Cloudflare Pages, which is what I’m currently using for this website (a rather recent decision that you can read about in <a href="https://ttntm.me/blog/about-diversification/" class="int">a previous post</a>).</p>
<p>Setting up automated deployments for a site like mine is really simple:</p>
<ol>
<li>Create a Deploy Hook as described in the <a class="ext" href="https://developers.cloudflare.com/pages/configuration/deploy-hooks/" rel="noopener">CF Pages documentation</a></li>
<li>Copy our hook’s URL</li>
<li>Create a new webhook in Omnivore:
<ul>
<li>URL: paste your hook’s URL</li>
<li>Event Type: <code>PAGE_CREATED</code></li>
<li>Method: <code>POST</code> (default)</li>
<li>Content Type: <code>application/json</code> (default)</li>
</ul>
</li>
</ol>
<p>When this is done, new items saved to your Omnivore account should trigger a deployment of your site, resulting in an updated reading list.</p>
<h2 id="summary" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#summary"><span>Summary</span></a></h2>
<p>I got to know about Omnivore less than 6 months ago, but I’m already wondering how I lived without it for such a long time. About 10 years ago, I used Pocket for a while, but it didn’t really “click” for me. So, I stopped looking into read-it-later solutions and kept links in my browsers, various text files and later also in Notion.</p>
<p>Right now, I’m really glad that I have a tool like Omnivore back at my disposal. In addition to its read-it-later functionality, subscribing to RSS feeds and email newsletters with the very same application is an absolute game changer for me - it decluttered my email inboxes significantly and made it easier to keep track of things I should actually be focusing on.</p>
<p>Having the possibility of adding a reading list to my personal website based on data that I’m already aggregating anyway is a great bonus and might help others discover articles, posts etc. that they might have missed otherwise. It’s a really convenient solution that can be added to almost any kind of website in a matter of hours.</p>
<h2 id="updates" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#updates"><span>Updates</span></a></h2>
<p>Clayton Errington referenced this article when he built his own /bookmarks page. He also wrote about it: <a class="ext u-in-reply-to" href="https://claytonerrington.com/blog/showing-omnivore-links-as-bookmarks/" rel="noopener">Showing Omnivore links as Bookmarks</a>.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>About Diversification</title>
        
          
            <category term="serverless" />
          
            <category term="thoughts" />
          
        
        <link href="https://ttntm.me/blog/about-diversification/"/>
        
        <updated>2024-05-28T10:30:00Z</updated>
        <id>https://ttntm.me/blog/about-diversification/</id>
        <content type="html"><![CDATA[
          
            
            <p>Sometimes, all it takes to question our own choices is just a headline. Most recently, that headline was: “Netlify just sent me a $104K bill for a simple static site”.</p>
<p>Someone posted this as a question to Reddit about 2 weeks ago (Feb. 27th, <a class="ext" href="https://old.reddit.com/r/webdev/comments/1b14bty/netlify_just_sent_me_a_104k_bill_for_a_simple/" rel="noopener">source</a>), and I rather quickly realized that I’d used Netlify for almost every personal project, demo and PoC I worked on since 2018. As such, not only a “bill shock” like that, but also many other things that can potentially happen to my account there could lead to extremely unpleasant situations.</p>
<p>I don’t want this to come across the wrong way though, so let me just say that I personally never had any issues with Netlify’s service at all. If anything, their (free) tier has been <em>too convenient</em> for me, which made it feel even more natural to add projects and sites to my subscription. However, I never “struck gold” with anything I use their service for, so fees, limits etc. weren’t something I paid much attention to.</p>
<p>Anyway, I ended up looking into alternatives for my projects and sites and started this spring-cleaning operation by having a look at what had accumulated in my account over time.</p>
<p>There were many demo/PoC sites that I created for articles, some personal sites (like this one) and a couple of web applications. Next, I made a list and identified projects that could easily be moved elsewhere:</p>
<ul>
<li>For demos and PoC sites:
<ul>
<li>Codeberg Pages</li>
<li>GitHub Pages</li>
</ul>
</li>
<li>Production sites and projects without Netlify features:
<ul>
<li>Cloudflare Pages</li>
</ul>
</li>
</ul>
<p>That list proved very helpful: of a total of 21 sites in my Netlify account, 6 were moved and 2 were archived and deleted.</p>
<p>Many sites I kept in there were frozen (i.e. disconnected from git), because they’re pretty old; I didn’t want to mess with them any more than necessary. Others were left untouched because they’re using Netlify features like Forms, Functions or Identity.</p>
<p>Migrating TLDs and the respective sites into Cloudflare Pages was really easy though, and they even provide a dedicated article in their documentation: <a class="ext" href="https://developers.cloudflare.com/pages/migrations/migrating-from-netlify/" rel="noopener">Migrating from Netlify to Pages</a>. Also, putting a project’s built files into a <code>/docs</code> folder in the same repository as the source code (GitHub), or into a <code>pages</code> branch (Codeberg), felt like a good idea; I’m actually surprised that I didn’t make use of this feature earlier.</p>
<p>So far, I moved both <a class="ext" href="https://aitrack.work" rel="noopener">aitrack.work</a> and this website to Cloudflare Pages and it was an amazingly smooth process overall. I really like the additional configuration options (complete with matching documentation) and the insights that are offered there by default.</p>
<p>This is a screenshot of this website’s “Overview” in the dashboard:</p>
<img src="https://ttntm.me/static/img/blog/cfp_overview.jpg" class="img-fluid img-center" alt="A screenshot of this website's 'Overview' in the Cloudflare Pages dashboard">
<p>I’m not used to that much information for my personal projects, so that’s a rather pleasant surprise.</p>
<p>Overall, I think reviewing my cloud platform and service provider choices was long overdue. Distributing sites and projects across different platforms feels like a good decision and should help minimize risks - at least until I can commit to self-hosting everything.</p>
<h2 id="updates" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#updates"><span>Updates</span></a></h2>
<p>2024/05: Another side of CF - <a href="https://robindev.substack.com/p/cloudflare-took-down-our-website" class="ext u-like-of" rel="noopener">Cloudflare took down our website after trying to force us to pay 120k$ within 24h</a> incl. the linked stories from HN.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 10, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w10/"/>
        
        <updated>2024-03-05T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w10/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://james-iry.blogspot.com/2009/05/brief-incomplete-and-mostly-wrong.html" class="ext u-like-of" rel="noopener">A Brief, Incomplete, and Mostly Wrong History of Programming Languages</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://github.com/ArchGPT/insomnium" class="ext u-like-of" rel="noopener">ArchGPT/insomnium</a><br>
An awesome API client forked from Insomnia</li>
<li>CSS: <code>mix-blend-mode</code>: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode" class="ext u-like-of" rel="noopener">MDN</a></li>
<li><a href="https://darkvisitors.com" class="ext u-like-of" rel="noopener">Dark Visitors</a><br>
Insight into the hidden ecosystem of autonomous chatbots and data scrapers crawling across the web</li>
<li><a href="https://bits.ashleyblewer.com/halt-and-catch-fire-syllabus/" class="ext u-like-of" rel="noopener">Halt and Catch Fire Syllabus</a><br>
<em>Loved</em> the show, so this is something very interesting</li>
<li><a href="https://github.com/Mugen87/nier" class="ext u-like-of" rel="noopener">Mugen87/nier</a><br>
Play the hacking game from NieR: Automata in your browser</li>
<li><a href="https://thorium.rocks" class="ext u-like-of" rel="noopener">Thorium</a><br>
A very cool Chromium fork</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>How to Build a Serverless Guestbook</title>
        
          
            <category term="eleventy" />
          
            <category term="guide" />
          
            <category term="serverless" />
          
        
        <link href="https://ttntm.me/blog/how-to-build-a-serverless-guestbook/"/>
        
        <updated>2024-03-04T10:30:00Z</updated>
        <id>https://ttntm.me/blog/how-to-build-a-serverless-guestbook/</id>
        <content type="html"><![CDATA[
          
            
            <p>I recently started working on a new side project, BUKMARK.CLUB, which I introduced in the <a href="https://ttntm.me/blog/bukmark-club-intro/" class="int">previous article</a> published here on this website. The MVP launched with a couple of open feature requests, and one of them was adding a guestbook, which I already implemented in the meantime.</p>
<p>So, this article is going to guide you through the process of building a serverless guestbook for a static website.</p>
<img src="https://ttntm.me/static/img/blog/guestbook.png" class="img-fluid img-center" alt="A guestbook illustration taken from undraw.co">
<h2 id="definitions" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#definitions"><span>Definitions</span></a></h2>
<p>Let’s start with some definitions:</p>
<ul>
<li><em>build</em> - the process of converting source code into a static website</li>
<li><em>guestbook</em> - a way for visitors to leave a little “I was here” comment and/or feedback about a website</li>
<li><em>persistence</em> - a means to store data reliably, usually a database</li>
<li><em>serverless</em> - means “not running on your own web server” in this article’s context</li>
<li><em>static website</em> - a web page that is delivered to a web browser exactly as stored, i.e. not a web application, no islands or hydration</li>
</ul>
<h3 id="data-flow" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#data-flow"><span>Data Flow</span></a></h3>
<p>On a high level, the data flow that fills our guestbook looks like this:</p>
<ol>
<li>A user enters their data into a form with the intention of leaving their comment in the guestbook</li>
<li>The form is posted to a backend service that handles the submitted data</li>
<li>The submission gets reviewed and approved, or handled in another way (i.e. deleted, ignored)</li>
<li>When approved: the submission gets published in the guestbook</li>
</ol>
<h2 id="functionality" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#functionality"><span>Functionality</span></a></h2>
<p>Our static website will have to provide the necessary functionality to support the data flow outlined above. This example is based on Eleventy, but other SSGs (static site generators) should offer similar functionality.</p>
<p>Some years ago, I also wrote an article about comparable functionality using Hugo: <a href="https://ttntm.me/blog/static-blog-comments-hugo/" class="int">“Static” Comments with Hugo</a>. It’s been a while, but I think that most of it is still applicable today, so it might offer an idea of where to start.</p>
<h3 id="submissions" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#submissions"><span>Submissions</span></a></h3>
<p>We need a way to collect submissions - a <code>&lt;form&gt;</code> it is:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span> <span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/my-site/success<span class="token punctuation">"</span></span> <span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>POST<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>my-guestbook<span class="token punctuation">"</span></span> <span class="token attr-name">netlify</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hidden<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-name<span class="token punctuation">"</span></span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>my-guestbook<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hidden<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>subject<span class="token punctuation">"</span></span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>New Guestbook Entry (%{submissionId})<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sa<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Name<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sa<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>name<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text<span class="token punctuation">"</span></span> <span class="token attr-name">required</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>eml<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Email Address<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>eml<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>email<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>email<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ws<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Website<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ws<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>website<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>url<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>msg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Message<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>textarea</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>msg<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span> <span class="token attr-name">required</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>textarea</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>submit<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Send Message<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">></span></span></code></pre>
<p>In this example, the attributes <code>class</code>, <code>placeholder</code>, etc. were removed, and what remains is a bare minimum of functional HTML. The full version of the code the above snippet was taken from can be found here: <a class="ext" href="https://github.com/ttntm/bukmark.club/blob/main/src/guestbuk.njk" rel="noopener">bukmark.club/src/guestbuk.njk</a>.</p>
<p>Functionally, the form is nothing special: there are two required fields, <code>name</code> and <code>message</code>, and the other two fields are optional. We manually review and approve each submission, so offering submissions to people who might not want to share their email address is perfectly acceptable.</p>
<p>The attribute <code>method=&quot;POST&quot;</code> means that the submitted data is posted directly to the site that the form is displayed on. The next section will provide details about what’s happening with this data.</p>
<h3 id="form-handler" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#form-handler"><span>Form Handler</span></a></h3>
<p>In the first line of the form above, we see a <code>netlify</code> attribute on the <code>&lt;form&gt;</code> tag. <a class="ext" href="https://docs.netlify.com/forms/setup/" rel="noopener">Netlify forms</a> allows managing forms without extra API calls or additional JavaScript, which is very convenient for our use case. Other services might offer similar features, i.e. Cloudflare’s <a class="ext" href="https://developers.cloudflare.com/pages/tutorials/forms/" rel="noopener">handling form submissions with Pages Functions</a>, and putting together your own form handler in any language / at any service you want to use is also a perfectly viable approach.</p>
<p>I’m using Netlify Forms for convenience reasons: it doesn’t just handle the submissions, it also offers persistence (i.e. storage), so we don’t have to take care of that ourselves. Email notifications are also part of the provided functionality.</p>
<p>If you take an(other) look at the form above, you’ll see an <code>&lt;input&gt;</code> with <code>name=&quot;subject&quot;</code>. It’s set to <code>type=&quot;hidden&quot;</code> and its value is used to define the email notification’s subject line. I’m using the auto-generated <code>%{submissionId}</code> variable to get a unique numerical value for every submission, which is eventually going to be used for a whitelist.</p>
<p><strong>A word reg. Netlify</strong>: it’s definitely not an undisputed choice these days, and I’d advise that you do your research, but I’ve never had any issues with their service (free tier since 2018). My choice for an alternative to what they’re offering would be a serverless function (AWS, CF, Azure etc.) that writes submissions into a database (i.e. Fauna) and uses some email API service (i.e. Mailgun, SendGrid) to notify me.</p>
<h3 id="obtaining-the-data" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#obtaining-the-data"><span>Obtaining the Data</span></a></h3>
<p>Our site has a form for submissions now and we should be receiving them successfully, followed by an email notification. That leaves us with one last to do: rendering the guestbook entries for everyone to see! 🤖</p>
<p>Netlify offers <a class="ext" href="https://docs.netlify.com/api/get-started/#forms" rel="noopener">an API</a> for that at <code>GET /api/v1/forms/{form_id}/submissions</code>. We need a personal access token to use the API though. See <a class="ext" href="https://docs.netlify.com/api/get-started/#authentication" rel="noopener">Authentication</a> for all necessary details.</p>
<p>Once that’s been taken care of, you should be able to use your favorite API client (or good old <code>curl</code>) to get submissions from the API. An example of the response can be found in the docs at <a class="ext" href="https://docs.netlify.com/api/get-started/#get-verified-submissions" rel="noopener"></a>, but here’s one of mine for your convenience:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">[</span><br>  <span class="token punctuation">{</span><br>    <span class="token property">"number"</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br>    <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"Tom"</span><span class="token punctuation">,</span><br>    <span class="token property">"email"</span><span class="token operator">:</span> <span class="token string">"redacted"</span><span class="token punctuation">,</span><br>    <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Tom"</span><span class="token punctuation">,</span><br>    <span class="token property">"first_name"</span><span class="token operator">:</span> <span class="token string">"Tom"</span><span class="token punctuation">,</span><br>    <span class="token property">"last_name"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span><br>    <span class="token property">"company"</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span><br>    <span class="token property">"summary"</span><span class="token operator">:</span> <span class="token string">"&lt;strong>Tom&lt;/strong> First. Have fun signing the Guestbuk everyone :)"</span><span class="token punctuation">,</span><br>    <span class="token property">"body"</span><span class="token operator">:</span> <span class="token string">"First. Have fun signing the Guestbuk everyone :)"</span><span class="token punctuation">,</span><br>    <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token property">"subject"</span><span class="token operator">:</span> <span class="token string">"New Guestbuk Entry (%{submissionId})"</span><span class="token punctuation">,</span><br>      <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Tom"</span><span class="token punctuation">,</span><br>      <span class="token property">"website"</span><span class="token operator">:</span> <span class="token string">"https://ttntm.me"</span><span class="token punctuation">,</span><br>      <span class="token property">"email"</span><span class="token operator">:</span> <span class="token string">"ttntm@pm.me"</span><span class="token punctuation">,</span><br>      <span class="token property">"message"</span><span class="token operator">:</span> <span class="token string">"First. Have fun signing the Guestbuk everyone :)"</span><span class="token punctuation">,</span><br>      <span class="token property">"ip"</span><span class="token operator">:</span> <span class="token string">"redacted"</span><span class="token punctuation">,</span><br>      <span class="token property">"user_agent"</span><span class="token operator">:</span> <span class="token string">"redacted"</span><span class="token punctuation">,</span><br>      <span class="token property">"referrer"</span><span class="token operator">:</span> <span class="token string">"https://bukmark.club/guestbuk/"</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token property">"created_at"</span><span class="token operator">:</span> <span class="token string">"2024-02-25T16:55:15.677Z"</span><span class="token punctuation">,</span><br>    <span class="token property">"human_fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span>...<span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token property">"ordered_human_fields"</span><span class="token operator">:</span> <span class="token punctuation">[</span>...<span class="token punctuation">]</span><span class="token punctuation">,</span><br>    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"65db70f3ab..."</span><span class="token punctuation">,</span><br>    <span class="token property">"form_id"</span><span class="token operator">:</span> <span class="token string">"65db6f29df98..."</span><span class="token punctuation">,</span><br>    <span class="token property">"site_url"</span><span class="token operator">:</span> <span class="token string">"https://bukmark.club"</span><span class="token punctuation">,</span><br>    <span class="token property">"site_name"</span><span class="token operator">:</span> <span class="token string">"bukmarkclub"</span><span class="token punctuation">,</span><br>    <span class="token property">"form_name"</span><span class="token operator">:</span> <span class="token string">"guestbuk"</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">]</span></code></pre>
<p>We really only care for the <code>data</code> key, so I’ve collapsed some duplicated values in <code>human_fields</code> and <code>ordered_human_fields</code> respectively.</p>
<p>Next, we’re going to use the handy <a class="ext" href="https://www.11ty.dev/docs/plugins/fetch/" rel="noopener">eleventy-fetch plugin</a> to make the form submissions returned by the API available through a <a class="ext" href="https://www.11ty.dev/docs/data-global/" rel="noopener">Global Data File</a>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'dotenv'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> eleventyFetch <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'@11ty/eleventy-fetch'</span><span class="token punctuation">)</span><br><br><span class="token keyword">const</span> whitelist <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> reqUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://api.netlify.com/api/v1/forms/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NTL_GUESTBUK</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/submissions</span><span class="token template-punctuation string">`</span></span><br>  <span class="token keyword">let</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">eleventyFetch</span><span class="token punctuation">(</span>reqUrl<span class="token punctuation">,</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">directory</span><span class="token operator">:</span> <span class="token string">'.cache'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">duration</span><span class="token operator">:</span> <span class="token string">'1d'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'json'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">fetchOptions</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NTL_TOKEN</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>response <span class="token operator">&amp;&amp;</span> response<span class="token operator">?.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> data <span class="token operator">=</span> response<br>      <span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">_<span class="token punctuation">,</span> entry</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>whitelist<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>entry<span class="token punctuation">.</span>number<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          _<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>            <span class="token operator">...</span>entry<span class="token punctuation">.</span>data<span class="token punctuation">,</span><br>            <span class="token literal-property property">created</span><span class="token operator">:</span> entry<span class="token punctuation">.</span>created_at<br>          <span class="token punctuation">}</span><span class="token punctuation">)</span><br>        <span class="token punctuation">}</span><br><br>        <span class="token keyword">return</span> _<br>      <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br><br>    <span class="token keyword">return</span> data<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><br>      <span class="token operator">?</span> data<br>      <span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>This code is stored in <code>./src/_data/guestbook.js</code> and is pretty much as “by the <s>book</s> docs” as it can be.</p>
<p>All private configuration is pulled in from environment variables. They should be stored in an <em>untracked</em> local <code>.env</code> file and configured in the Netlify UI for your builds. See <a class="ext" href="https://docs.netlify.com/environment-variables/overview/" rel="noopener">Environment variables overview</a> for all necessary details.</p>
<p>There’s a <code>const whitelist = []</code> at the beginning of the code that determines which guestbook entries should be published. It allows us to add individual <code>%{submissionId}</code> values which correspond to the <code>number</code> keys in the API response. A <code>filter()</code> then takes care of applying the whitelist, followed by a <code>map()</code> call that extracts only the necessary data from the submissions returned by the API.</p>
<p>Implementing a whitelist like that offers the 0 config benefit of automatic rebuilds whenever you push whitelist changes to your deployment branch. It’s the most intuitive approach for a manual review and approval of guestbook submissions that I could come up with, simply because it’s the exact same workflow you’re using for all other website maintenance.</p>
<h3 id="rendering" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#rendering"><span>Rendering</span></a></h3>
<p>With that taken care of, we can proceed and render our guestbook entries:</p>
<pre class="language-html"><code class="language-html">{% if guestbook | length %}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span><span class="token punctuation">></span></span>Guestbook Entries<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span><span class="token punctuation">></span></span><br>    {% for item in guestbook %}<br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>article</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><br>            {% if item.website %}<br>              <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ item.website }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>                {{ item.name }}<br>              <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>            {% else %}<br>              {{ item.name }}<br>            {% endif %}<br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><br>            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>time</span><span class="token punctuation">></span></span>{{ item.created | formatDate }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>time</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><br>          {{ item.message }}<br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>article</span><span class="token punctuation">></span></span><br>    {% endfor %}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span><br>{% endif %}<br></code></pre>
<p>It’s a pretty simple loop that renders the guestbook entries contained in the <code>guestbook.js</code> data file. <code>if guestbook | length</code> makes sure that the whole “Guestbook Entries” section gets hidden when there’s nothing to show. We also have to check for <code>item.website</code> explicitly, because it’s an optional field in the guestbook form and we’d be risking broken links otherwise.</p>
<h2 id="the-result" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-result"><span>The Result</span></a></h2>
<p>If everything works out as described, you should have a brand new guestbook on your static website after a couple of hours. It definitely helps though, if you’re familiar with serverless architecture and the individual bits and pieces working together here.</p>
<p>I haven’t added a guestbook to this website, and I’m not sure I ever will. However, there’s a guestbook over at the <a class="ext" href="https://bukmark.club" rel="noopener">BUKMARK.CLUB</a> that you’re welcome to leave a comment at. That’s also where the code samples used in this article were taken from. Feel free to explore the source code over at GitHub: <a class="ext" href="https://github.com/ttntm/bukmark.club" rel="noopener">ttntm/bukmark.club</a>.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Intro: BUKMARK.CLUB</title>
        
          
            <category term="eleventy" />
          
            <category term="news" />
          
            <category term="project" />
          
        
        <link href="https://ttntm.me/blog/bukmark-club-intro/"/>
        
        <updated>2024-02-21T10:30:00Z</updated>
        <id>https://ttntm.me/blog/bukmark-club-intro/</id>
        <content type="html"><![CDATA[
          
            
            <p>I recently wrote about <a href="https://ttntm.me/blog/bookmarks-are-back/" class="int">adding bookmarks to my website</a>. While planning and doing that, a slightly larger idea came to my mind: building a directory of websites that provide a curated collection of bookmarks and/or links to other websites.</p>
<p>As far as I know, there’s no such directory.</p>
<p>A quick question I posted on Mastodon didn’t surface one either:</p>
<img src="https://ttntm.me/static/img/blog/111891597461871340.png" class="img-fluid img-center" alt="Screenshot of one of Tom's mastodon posts from Feb. 7th 2024. It says: 'I know of nownownow, uses.tech and personalsit.es Do you know any index of sites with a bookmarks or links section? I don't, but I'm considering building one.'">
<p>Original link to the post from Feb. 7th: <a class="ext" href="https://elk.zone/fosstodon.org/@ttntm/111891597461871340" rel="noopener">@ttntm/111891597461871340</a></p>
<p>It took me another week to find a suitable (and cheap enough) TLD for the project and I eventually bought <a class="ext" href="https://bukmark.club" rel="noopener">BUKMARK.CLUB</a> on Feb. 17th.</p>
<p>I used a little spare time on the following weekend and built an MVP using Eleventy while my wife spent a little of her time on the logo:</p>
<img src="https://ttntm.me/static/img/blog/bukmark-club.png" class="img-fluid img-center" alt="BUKMARK.CLUB logo">
<h2 id="building-the-website" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#building-the-website"><span>Building the Website</span></a></h2>
<p>There are 2 sections of code that might be worth highlighting:</p>
<h3 id="generating-the-directory-index" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#generating-the-directory-index"><span>Generating the Directory Index</span></a></h3>
<p>I’ve recycled the logic that builds this website’s blog archive, a bit of code originally taken from a post by Darek Kay called <a class="ext" href="https://darekkay.com/blog/eleventy-group-posts-by-year/" rel="noopener">Group posts by year in Eleventy</a>, and made some changes:</p>
<pre class="language-js"><code class="language-js">config<span class="token punctuation">.</span><span class="token function">addCollection</span><span class="token punctuation">(</span><span class="token string">'directory'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">collection</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> _<span class="token punctuation">.</span><span class="token function">chain</span><span class="token punctuation">(</span>collection<span class="token punctuation">.</span><span class="token function">getFilteredByGlob</span><span class="token punctuation">(</span><span class="token string">'./src/directory/*.md'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">sort</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span> <span class="token operator">=></span> a<span class="token punctuation">.</span>data<span class="token punctuation">.</span>title<span class="token punctuation">.</span><span class="token function">localeCompare</span><span class="token punctuation">(</span>b<span class="token punctuation">.</span>data<span class="token punctuation">.</span>title<span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">groupBy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">String</span><span class="token punctuation">(</span>item<span class="token punctuation">.</span>data<span class="token punctuation">.</span>title<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">toPairs</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>This collection sorts directory entries alphabetically and groups them based on the first letter of their respective title.</p>
<p>The function passed to <code>sort()</code> is using <code>String.prototype.localeCompare()</code> (<a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare" rel="noopener">MDN</a>), <code>groupBy()</code> takes the first letter of each site’s name and uppercases it.</p>
<p>Rendering the directory uses two consecutive loops that process the collection:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span><span class="token punctuation">></span></span>Index<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flex flex-wrap gap1 mb2<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  {% for letter, letterPages in collections.directory %}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#section-{{ letter }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>[{{ letter }}]<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span><br>  {% endfor %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span><br>{% for letter, letterPages in collections.directory %}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>article</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{% if not loop.last %}mb2{% endif %}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>...<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>section-{{ letter }}<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>m0<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">border</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span><br>        [{{ letter }}]<br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>block<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#top<span class="token punctuation">"</span></span> <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Back to top<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>[^]<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>grid grid3 gap2 my1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      {% for page in letterPages %}<br>        {% include "components/card.njk" %}<br>      {% endfor %}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>article</span><span class="token punctuation">></span></span><br>{% endfor %}<br></code></pre>
<h3 id="updating-the-directory-stats" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#updating-the-directory-stats"><span>Updating the Directory Stats</span></a></h3>
<p>I’m using a Node.js script for this task. It was inspired by a post called <a class="ext u-in-reply-to" href="https://brianchildress.co/find-latest-file-in-directory-in-nodejs/" rel="noopener">Find the Newest File in Directory Using NodeJS</a>.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> path <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'path'</span><span class="token punctuation">)</span><br><br><span class="token keyword">function</span> <span class="token function">getMostRecentFile</span><span class="token punctuation">(</span><span class="token parameter">dir</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> files <span class="token operator">=</span> <span class="token function">orderRecentFiles</span><span class="token punctuation">(</span>dir<span class="token punctuation">)</span><br>  <span class="token keyword">return</span> files<span class="token punctuation">.</span>length<br>    <span class="token operator">?</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">total</span><span class="token operator">:</span> files<span class="token punctuation">.</span>length<span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">result</span><span class="token operator">:</span> files<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><br>    <span class="token punctuation">}</span><br>    <span class="token operator">:</span> <span class="token keyword">undefined</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">orderRecentFiles</span><span class="token punctuation">(</span><span class="token parameter">dir</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> fs<span class="token punctuation">.</span><span class="token function">readdirSync</span><span class="token punctuation">(</span>dir<span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">file</span><span class="token punctuation">)</span> <span class="token operator">=></span> fs<span class="token punctuation">.</span><span class="token function">lstatSync</span><span class="token punctuation">(</span>path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>dir<span class="token punctuation">,</span> file<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">isFile</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">file</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span><br>      file<span class="token punctuation">,</span><br>      <span class="token literal-property property">btime</span><span class="token operator">:</span> fs<span class="token punctuation">.</span><span class="token function">lstatSync</span><span class="token punctuation">(</span>path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>dir<span class="token punctuation">,</span> file<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span>birthtime<br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">sort</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span> <span class="token operator">=></span> b<span class="token punctuation">.</span>btime<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> a<span class="token punctuation">.</span>btime<span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span><br>    total<span class="token punctuation">,</span><br>    result<br>  <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">getMostRecentFile</span><span class="token punctuation">(</span><span class="token string">'./src/directory/'</span><span class="token punctuation">)</span><br><br>  fs<span class="token punctuation">.</span><span class="token function">writeFileSync</span><span class="token punctuation">(</span><span class="token string">'./src/_data/info.json'</span><span class="token punctuation">,</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">directorySize</span><span class="token operator">:</span> total<span class="token punctuation">,</span><br>    <span class="token literal-property property">mostRecentUpdate</span><span class="token operator">:</span> result<span class="token punctuation">.</span>btime<br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>The script checks a folder (<code>./src/directory/</code>) and orders the files based on the time they were created (<code>birthtime</code>). It then proceeds to write an <code>info.json</code> data file into Eleventy’s <code>_data</code> folder. Both <code>directorySize</code> and <code>mostRecentUpdate</code> can be used in templates without any further configuration. The only thing I had to do, was to define a custom <code>formatDate</code> filter.</p>
<p>In case you noticed: the lack of error handling is not just lazy - it should alert contributors of the fact that something’s wrong <em>before</em> committing changes that would otherwise lead to missing data.</p>
<p>–</p>
<p>Right now, the BUKMARK.CLUB is a 0 JS, sub 100 kB website that uses less than 500 lines of CSS.</p>
<p>Directory content is stored in Markdown files that Eleventy currently processes in less than 1 second. Thanks to Eleventy, it “just works”, which is great for a small side project like that.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Update: Bookmarks Are Back</title>
        
          
            <category term="eleventy" />
          
            <category term="news" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/bookmarks-are-back/"/>
        
        <updated>2024-04-12T10:35:00Z</updated>
        <id>https://ttntm.me/blog/bookmarks-are-back/</id>
        <content type="html"><![CDATA[
          
            
            <p>My website has a bookmark directory now.</p>
<p>This is something that I wanted to do since finishing “Halt and Catch Fire” in late November '23, but I was working on my personal wiki at that time. You’re welcome to have a look at the new <a href="https://ttntm.me/bookmarks/" class="int">bookmarks page</a> right now, but you could also stay here a little longer and read some more about it first.</p>
<p>A brief summary of where this is coming from:</p>
<p>Bookmark directories were one of my first touchpoints with the new-ish internet when I was a kid in the mid 90s. There were dedicated directory sites (like HCF’s “Comet”) built like phone books, and most regular websites also had a page/section where they would list other websites and link to them. And if you had a website, exchanging links with other sites was a thing that you’d do sooner or later. It was mostly centered around similar topics/interests, at least from what I experienced. You probably remember the wonderful (animated) banners/buttons that sites would provide, and they were also exchanged with others. It went on like this for a couple of years, but eventually, link farms and rampant search engine <s>optimization</s> manipulation needed to be regulated. Both directories and collections of bookmarks published on individual websites mostly disappeared.</p>
<p>I didn’t really know I missed those days, but seeing it happen on TV made me think of it. What I still find most interesting about it, is the aspect of <em>curation</em> behind directories and bookmarks pages. It offers the curator’s unique view at what’s important to them and restores a layer of personality that I feel has gotten a little lost amid search results and endless walls of infinitely scrolling social media drip feeds.</p>
<p><em>So, bookmarks are back</em>.</p>
<p>Right now, I’m planning to create a directory of “sites with a bookmarks page” in the vein of <a class="ext" href="https://nownownow.com" rel="noopener">nownownow</a> and <a class="ext" href="https://uses.tech" rel="noopener">uses.tech</a>. Please let me know if something like that should already exist (couldn’t find it) and send me your site if you have a dedicated bookmarks page. I’ll create a dedicated repository for soon, but I’d like to collect a bunch of websites for an initial publication first.</p>
<h2 id="building-the-directory" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#building-the-directory"><span>Building the Directory</span></a></h2>
<p>Under the hood, my little bookmarks directory is “just” another <a class="ext" href="https://www.11ty.dev/docs/collections/" rel="noopener">Eleventy collection</a>:</p>
<pre class="language-js"><code class="language-js">config<span class="token punctuation">.</span><span class="token function">addCollection</span><span class="token punctuation">(</span><span class="token string">'bookmarks'</span><span class="token punctuation">,</span> <span class="token keyword">async</span><span class="token punctuation">(</span><span class="token parameter">collection</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> collection<span class="token punctuation">.</span><span class="token function">getFilteredByGlob</span><span class="token punctuation">(</span><span class="token string">'./src/bookmarks/**/*.md'</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>I’m using one dedicated <code>*.md</code> file for each category:</p>
<ul>
<li><strong>Blogroll</strong>: blogs that I read regularly</li>
<li><strong>Entertainment</strong>: sites I visit for entertainment, unique finds I think others could find entertaining</li>
<li><strong>Knowledge &amp; Research</strong>: resources I’d consult for specific knowledge and informative sites that might help others</li>
<li><strong>Tools &amp; Utilities</strong>: tools I use regularly, OSS alternatives and recommendations</li>
<li><strong>Website Inspiration</strong>: websites I find inspiring</li>
</ul>
<p>Inside those files, a <a class="ext" href="https://www.11ty.dev/docs/shortcodes/" rel="noopener">shortcode</a> makes sure they open in a new tab:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">displayText<span class="token punctuation">,</span> link</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// shortcode to create external links</span><br>  <span class="token keyword">return</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;a class="ext" href="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>link<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>displayText<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/a></span><span class="token template-punctuation string">`</span></span><br><span class="token punctuation">}</span></code></pre>
<p>I wrote this shortcode as an alternative to a JavaScript-based solution (i.e. rewriting HTTP anchor tags using <code>querySelectorAll</code>) when I committed to a 0-JS website (before <a href="https://ttntm.me/blog/dark-mode/" class="int">dark mode</a>).</p>
<p>The shortcode can be used like this: <code>{% ext &quot;xmpl&quot;, &quot;https://example.com&quot; %}</code></p>
<p>It’s nothing fancy, it works well, and I don’t see any reason to change it for the time being.</p>
<h2 id="updates" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#updates"><span>Updates</span></a></h2>
<p><strong>2024/04</strong>: I’ve moved the collection into a JSON data file, which makes maintaining it a little easier.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Building a Filterable Portfolio with Astro and Vue</title>
        
          
            <category term="astro" />
          
            <category term="template" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/building-portfolio-with-astro-and-vue/"/>
        
        <updated>2024-02-13T10:30:00Z</updated>
        <id>https://ttntm.me/blog/building-portfolio-with-astro-and-vue/</id>
        <content type="html"><![CDATA[
          
            
            <p>A couple of months ago, I started working on a re-build of my wife’s website. This wasn’t the first time I did that, but it’s been the most successful attempt so far, considering that we’re almost done with it. I knew that I was going to stick with Astro, which I’d already used for a previous attempt in late 2022, but I decided to use Contentful over StoryBlok as a headless CMS.</p>
<p>Contentful has been a great choice so far, both in terms of developer experience and end user features. And on top of that, their free tier is <em>really</em> generous if you know what you’re doing (mostly in terms of the data model). I think I got a little side-tracked here, might get back to that topic at some point…</p>
<p>Anyway, we’re here to talk about building a filterable portfolio with Astro and Vue.</p>
<p>First, let’s have a look at a brief summary of the core requirements:</p>
<ul>
<li>Content (i.e. projects) should be rendered as a list</li>
<li>Individual projects should be rendered as pages</li>
<li>project should have tags (1 or more)</li>
<li>The list of projects should be filterable based on a selected tag (single choice)</li>
<li>There should be a way to link to filtered list views</li>
<li>The list view should feel “alive” somehow (i.e. transitions/animations)</li>
</ul>
<p>I suppose the title gave it away already: we’re going to use Astro as our SSG and we’re including a Vue <a class="ext" href="https://docs.astro.build/en/concepts/islands/" rel="noopener">island</a> for the list view incl. transitions and filtering.</p>
<p>The result is going to look like this:</p>
<img src="https://ttntm.me/static/img/blog/astro-portfolio.jpg" class="img-fluid img-center" alt="Screenshot of a portfolio website">
<p>…and has already been published as a <a class="ext" href="https://github.com/ttntm/astro-potfolio-starter/" rel="noopener">template repository at GitHub</a>.</p>
<p>There’s also a <a class="ext" href="https://codesandbox.io/p/github/ttntm/astro-potfolio-starter/main" rel="noopener">live demo</a> in a CodeSandbox that should help when following the rest of this article.</p>
<h2 id="portfolio-content" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#portfolio-content"><span>Portfolio Content</span></a></h2>
<p>We’re working with standalone content here, so our portfolio is a <a class="ext" href="https://docs.astro.build/en/guides/content-collections/" rel="noopener">collection</a> that’s defined in <code>./src/content/config.ts</code>.</p>
<p>The collection pages’ front matter properties have to follow this type definition:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">type</span> <span class="token class-name">PortfolioItemData</span> <span class="token operator">=</span> <span class="token punctuation">{</span><br>  title<span class="token operator">:</span> <span class="token builtin">string</span><br>  slug<span class="token operator">:</span> <span class="token builtin">string</span><br>  cover<span class="token operator">:</span> <span class="token builtin">string</span><br>  description<span class="token operator">?</span><span class="token operator">:</span> <span class="token builtin">string</span><br>  tags<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span><br>  referenceUrl<span class="token operator">?</span><span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span><br>  itemData<span class="token operator">:</span> CollectionEntry<span class="token operator">&lt;</span><span class="token string">'portfolio'</span><span class="token operator">></span><br><span class="token punctuation">}</span></code></pre>
<p>A little note regarding <code>itemData</code>: this prop stores the whole collection item. We’ll be using it in a page template to render the markdown content.</p>
<h3 id="images" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#images"><span>Images</span></a></h3>
<p>Images used as <code>cover</code> are stored in <code>./public/img/</code> and are <em>not</em> being processed by any image optimization.</p>
<p>NB: This is probably not ideal, but <code>getImage()</code> from <code>astro:image</code> (<a class="ext" href="https://docs.astro.build/en/guides/images/" rel="noopener">docs</a>) can only be used on the server side, meaning that conditional rendering based on filtered data will make it break (yes, I tried).</p>
<h2 id="rendering" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#rendering"><span>Rendering</span></a></h2>
<p>Collection pages (in <code>./src/content/portfolio/</code>) are processed in alphabetical order by the <code>[...path].astro</code> page template in <code>./src/pages/</code>. Its <code>getStaticPaths()</code> function processes the whole collection and generates both the index/list page (via <code>path: undefined</code> + <code>props.isListPage</code>) and the individual project pages (via <code>path: page.slug</code> + <code>props.isSinglePage</code>).</p>
<p>I first used an “all in one” <code>getStaticPaths()</code> function based on <a class="ext" href="https://docs.astro.build/en/guides/routing/#rest-parameters" rel="noopener">rest parameters</a> when rendering collections of pages based on Contentful’s API responses. Querying the complete collection, and using the data to render both index and individual pages, offered a significant performance boost (in terms of build times), helped save on API requests and reduced repeated boilerplate code.</p>
<p>The index page gets rendered by the <code>PortfolioList.vue</code> component which can be found in <code>./src/components/interactive/</code>; the single page template rendering the individual project pages - <code>PortfolioLayoutSingle.astro</code> - can be found in <code>./src/layouts/</code>. This template makes use of the <code>itemData</code> prop to render content to HTML (<a class="ext" href="https://docs.astro.build/en/guides/content-collections/#rendering-content-to-html" rel="noopener">docs</a>):</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token punctuation">{</span> pageData <span class="token punctuation">}</span> <span class="token operator">=</span> Astro<span class="token punctuation">.</span>props<br><span class="token keyword">const</span> <span class="token punctuation">{</span><br>  <span class="token comment">// ...</span><br>  itemData<br><span class="token punctuation">}</span> <span class="token operator">=</span> pageData<br><span class="token keyword">const</span> <span class="token punctuation">{</span> Content <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> itemData<span class="token punctuation">.</span><span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>The rendered content can be placed anywhere in the template using the built-in <code>&lt;Content /&gt;</code> component Astro provides.</p>
<h2 id="interactivity" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#interactivity"><span>Interactivity</span></a></h2>
<p>As mentioned earlier, our portfolio list page is a Vue island with 3 components:</p>
<ul>
<li><code>PortfolioList.vue</code>: the outer wrapper and common parent of the other 2 components; manages data and handles events</li>
<li><code>PortfolioListItem.vue</code>: just a display component</li>
<li><code>PortfolioListNav.vue</code>: lists all tags and emits an <code>update:filter</code> event when a filter gets selected or removed</li>
</ul>
<p>Let’s have a closer look at the <code>script setup</code> sction of our <code>PortfolioList.vue</code> component now.</p>
<p>First, our component receives the whole portfolio collection from the <code>[...path].astro</code> page template and proceeds to create a list of unique tags using the <code>getUniqueTags()</code> utility function. You’re welcome to check it out, but that function’s essentially just using <code>new Set()</code> to deduplicate an array of arrays of tags obtained from the portfolio pages.</p>
<p>These unique tags are then passed to the <code>PortfolioListNav.vue</code> component that renders the filter/sub-navigation.</p>
<p>Next, <code>PortfolioList.vue</code> makes use of a <code>workItemsDisplay</code> array which utilizes the <code>currentFilter</code> <code>ref</code> to compute the list of items to show based on the current filter selection (or the lack thereof). We also define event listeners for <code>popstate</code> and <code>pushState</code> events:</p>
<pre class="language-js"><code class="language-js"><span class="token function">onMounted</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token function">applyFilterFromURL</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'popstate'</span><span class="token punctuation">,</span> applyFilterFromURL<span class="token punctuation">)</span><br>  window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'pushState'</span><span class="token punctuation">,</span> applyFilterFromURL<span class="token punctuation">)</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><br><br><span class="token function">onBeforeUnmount</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  window<span class="token punctuation">.</span><span class="token function">removeEventListener</span><span class="token punctuation">(</span><span class="token string">'popstate'</span><span class="token punctuation">,</span> applyFilterFromURL<span class="token punctuation">)</span><br>  window<span class="token punctuation">.</span><span class="token function">removeEventListener</span><span class="token punctuation">(</span><span class="token string">'pushState'</span><span class="token punctuation">,</span> applyFilterFromURL<span class="token punctuation">)</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>We do this, because we’re going to use query parameters to store the currently selected filter (as per the requirements above); both event listeners use the <code>applyFilterFromURL()</code> method.</p>
<p>Finally, we define 2 methods that provide the main functionality of our component: applying and updating the filter.</p>
<p><code>applyFilterFromURL()</code>: a method that checks the query parameters in the URL for a parameter called <code>filter</code> and updates the <code>currentFilter</code> variable accordingly. This in turn triggers the - computed - <code>workItemsDisplay</code> array and updates the view.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">applyFilterFromURL</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> paramName <span class="token operator">=</span> <span class="token string">'filter'</span><br>  <span class="token keyword">let</span> urlParams <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URLSearchParams</span><span class="token punctuation">(</span>window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>search<span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>urlParams<span class="token punctuation">.</span><span class="token function">has</span><span class="token punctuation">(</span>paramName<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    currentFilter<span class="token punctuation">.</span>value <span class="token operator">=</span> <span class="token function">String</span><span class="token punctuation">(</span>urlParams<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>paramName<span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    currentFilter<span class="token punctuation">.</span>value <span class="token operator">=</span> <span class="token string">''</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p><code>updateFilter()</code>: an event handler for the filter selection that is used exclusively by the <code>PortfolioListNav.vue</code> component. It handles incoming values and sets or clears the <code>currentFilter</code> variable. Updating the <code>currentFilter</code> variable right after calling <code>window.history.pushState()</code> is necessary, because this programmatic update of <code>window.history</code> <em>does not</em> trigger the <code>pushState</code> event handler.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">updateFilter</span><span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">newVal</span><span class="token operator">:</span> string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span>window<span class="token punctuation">.</span>location<span class="token punctuation">.</span>href<span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>newVal<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    url<span class="token punctuation">.</span>searchParams<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">'filter'</span><span class="token punctuation">,</span> newVal<span class="token punctuation">)</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    url<span class="token punctuation">.</span>searchParams<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">'filter'</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><br>  window<span class="token punctuation">.</span>history<span class="token punctuation">.</span><span class="token function">pushState</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">,</span> url<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  currentFilter<span class="token punctuation">.</span>value <span class="token operator">=</span> newVal<br><span class="token punctuation">}</span></code></pre>
<h3 id="list-transitions-and-animation" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#list-transitions-and-animation"><span>List Transitions and Animation</span></a></h3>
<p>We’re using a <code>&lt;transition-group&gt;</code> wrapper (<a class="ext" href="https://vuejs.org/guide/built-ins/transition-group.html" rel="noopener">Vue Docs: TransitionGroup</a>) around the list of portfolio items. This enables list transitions via CSS, based on the transition group’s <code>name=&quot;list&quot;</code> property:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.list-move,<br>.list-enter-active,<br>.list-leave-active</span> <span class="token punctuation">{</span><br>  <span class="token property">transition</span><span class="token punctuation">:</span> all .75s<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.list-enter-from,<br>.list-leave-to</span> <span class="token punctuation">{</span><br>  <span class="token property">opacity</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br>  <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateY</span><span class="token punctuation">(</span>-10px<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.list-leave-active</span> <span class="token punctuation">{</span><br>  <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>I was rather happy with the result when I built this feature for the first time. Refactoring it into the <a class="ext" href="https://github.com/ttntm/astro-potfolio-starter/" rel="noopener">astro-potfolio-starter</a> for this article was a nice follow-up and might be of help to someone else out there that’s trying to do something similar.</p>
<p>Using query parameters to provide direct links to filtered lists is a pattern that I don’t see very often anymore. It feels like it was used much more ~10 years ago, but has since moved into various state management patterns and other less transparent abstractions. I think it’s a good choice for a “link to filtered list” requirement, especially considering how simple it is to implement nowadays.</p>
<p>A final note on <s>bloat</s> bundle size: using Vue for such a limited use case might seem like overkill to some, which I can fully understand. It was a deliberate choice though, because it’s also used for other islands of interactivity, and it comes with built-in components for list transitions (= no additional animation library + dependencies required).</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Thoughts About Workflow Engines</title>
        
          
            <category term="learning" />
          
            <category term="thoughts" />
          
        
        <link href="https://ttntm.me/blog/thoughts-about-workflow-engines/"/>
        
        <updated>2024-06-25T19:35:00Z</updated>
        <id>https://ttntm.me/blog/thoughts-about-workflow-engines/</id>
        <content type="html"><![CDATA[
          
            
            <p>A couple of months ago, I started working on a project with the desired outcome of building a large and complex application including a 3rd party workflow engine. A wild mixture of a planned microservice and orchestration (-driven) architecture was written into the requirements documents supplied by the customer, making the whole thing sound a lot like “I want to draw software…” when read between the lines.</p>
<p>I spent a lot of time with the topic since then: lots of discussions and workshops, and I’m also building an ever-growing proof of concept application. One thing I can already say with a bit of confidence is, that in doing so, I’m definitely learning a lot. Consequently, this article is a collection of thoughts about aligning software development and workflow engines.</p>
<img src="https://ttntm.me/static/img/blog/workflow.jpg" class="img-fluid img-center" alt="An illustration of workflow from Undraw">
<h2 id="whats-a-workflow-engine" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#whats-a-workflow-engine"><span>What’s a Workflow Engine?</span></a></h2>
<p>I guess it’s always good to start with a definition. I’m feeling a bit lazy though, so this definition was generated by Perplexity. The LLM claims that the following definition is based on Wikipedia, IBM and a couple of other sources (but it’s been found out to be <a class="ext u-in-reply-to" href="https://www.wired.com/story/perplexity-is-a-bullshit-machine/" rel="noopener">a Bullshit Machine</a> since writing this article):</p>
<blockquote>
<p>A workflow engine is a software application that manages and automates business processes. It typically uses a database server and pre-coded scripts to manage and maintain workflows. The workflow engine manages and monitors the state of activities in a workflow and determines which new activity to transition to according to defined processes. It can execute any arbitrary sequence of steps and facilitates the flow of information, tasks, and events. Workflow engines have utility in numerous industries and applications, from connecting remote workers to approving vacation to invoicing. They can automate routine tasks and procedures in a business environment, allowing employees to focus on more important work. By automating processes, businesses can ensure that tasks are completed accurately, promptly, and efficiently, which improves the overall customer experience, increases customer loyalty, and reduces costs.</p>
<p><small>Perplexity</small></p>
</blockquote>
<p>I didn’t edit that response because I would say that I mostly agree with it. It’s neither too detailed, nor too sales-y for the purpose of this article. Let’s have a closer look at it:</p>
<blockquote>
<p>A software application that manages and automates business processes</p>
</blockquote>
<p>On the surface: kind of yes.</p>
<p>But: I learned that the “business” in any given business process does not necessarily have to be the right level of detail for an <em>executable</em> workflow. There might be a lot of options in specific BPM notations (i.e. <a class="ext" href="https://www.bpmn.org" rel="noopener">BPMN</a>), but it’s up to the respective engine to define which specific decisions, events, tasks, etc. of any given notation it actually supports.</p>
<blockquote>
<p>The workflow engine manages and monitors the state of activities in a workflow and determines which new activity to transition to</p>
</blockquote>
<p>Yes, I agree with that statement.</p>
<blockquote>
<p>It can execute any arbitrary sequence of steps and facilitates the flow of information, tasks, and events.</p>
</blockquote>
<p>Mostly yes, but if we define that <em>facilitate</em> == “to make easy or easier”, then we absolutely have to talk about <em>added</em> complexity due to the use of a workflow engine. I’ll get back to that one in a bit.</p>
<blockquote>
<p>Workflow engines have utility in numerous industries and applications, from connecting remote workers to approving vacation to invoicing.</p>
</blockquote>
<p>Yeah, why not.</p>
<p>NB: approving a thing, invoicing and order processing are very common examples. They can often be thought of as highly automated processes, which works in favor of workflow engines. That’s probably why these use cases are very popular in both vendor documentation and marketing.</p>
<blockquote>
<p>They can automate routine tasks and procedures in a business environment, allowing employees to focus on more important work.</p>
</blockquote>
<p>They absolutely can, yes.</p>
<p>But: it gets tricky when they’re utilized to automate tasks and procedures that are <em>not</em> routine. And once there is a license for a workflow engine available, that’s inevitably going to come up.</p>
<blockquote>
<p>Businesses can ensure that tasks are completed accurately, promptly, and efficiently, which improves the overall customer experience, increases customer loyalty, and reduces costs.</p>
</blockquote>
<p>Well, I guess it’s definitely possible that some or even all of that can happen, but I think the LLM went a little overboard here. The statement doesn’t add anything to the definition, and it rather sounds like some marketing site gibberish that you’d expect to find somewhere near a CTA.</p>
<p>With the definition taken care of, we can now move on and explore a couple of topics that will sooner or later come up when you’re dealing with workflow engines.</p>
<h2 id="the-low-code-promise" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-low-code-promise"><span>The Low-Code Promise</span></a></h2>
<p>In theory, making use a workflow engine sounds like an easy way out of having to develop custom applications or to request changes from software vendors and solution providers. I decided to call this “the low-code promise”, considering that many business user expectations often revolve around making increasingly complex software functionality easy to change (i.e. changing a process flowchart). I wouldn’t go so far as to call this an <em>empty promise</em>, but I’d definitely argue that it is a common pitfall in terms of misaligned expectations. It wouldn’t come as a surprise to notice that this is suitable leverage for many software vendors’ marketing and sales efforts…</p>
<p>What’s likely to cause these misaligned expectations is probably a mixture of both, a lack of technical understanding and the lack of awareness/information regarding the larger landscape of systems and applications a workflow engine gets placed in. Yes, of course, there are interfaces for commonly used communication protocols, systems and applications, but most - if not all - of them expect to be consumed/used in a certain way and won’t be able to support the flexibility that “drawing software” based on flowcharts profusely implies.</p>
<p>So, what a workflow engine ends up being used as eventually, can probably best be described as an <em>orchestrator</em> of functionality that is actually handled by dedicated services, systems and applications. As such, lots of - potential - flexibility gets lost due to constraints of APIs, interfaces, concurrency and the necessary sequence of the actual data processing. Workflow-based orchestration might be a benefit for some use cases (i.e. avoiding hard-coded cron jobs, working with legacy systems), but it also causes an increased overhead in terms of having to maintain the necessary artifacts (i.e. flowcharts) and code (i.e. job workers, interfaces) the workflow engine needs.</p>
<h2 id="workflow-driven-architecture" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#workflow-driven-architecture"><span>Workflow-Driven Architecture</span></a></h2>
<p>Let’s think of a monolithic MVC application with 3 layers like this:</p>
<pre class="language-txt"><code class="language-txt">[   Front end (web) client      ]<br>*-------------------------------*<br>[   Back end (business) logic   ]<br>*-------------------------------*<br>[   Database/s                  ]</code></pre>
<p>In comparison, an application that’s ready for process-driven orchestration could look like this:</p>
<pre class="language-txt"><code class="language-txt">[   Front end (web) client      ]<br>*-------------------------------*<br>[   BFF (or API gateway)        ]<br>*-------------------------------*<br>[   Workflow engine             ]<br>*-------------------------------*<br>[   Back end (business) logic   ]<br>*-------------------------------*<br>[   Database/s                  ]</code></pre>
<p>An architecture like this becomes necessary when a workflow engine is defined as the main driver of “the flow of information, tasks, and events” (as per the definition above). Thinking of case management in customer service, a somewhat realistic example could end up looking like this:</p>
<ol>
<li>A web-to-case form on a public website triggers the creation of a new case in a customer service application (CSA)</li>
<li>A new workflow instance matching this business process starts; this can happen based on a message subscription or an API call for example; either way, the CSA backend has to send an event that includes the new case’s ID</li>
<li>As a first step, the workflow performs a lookup into the CSA database to obtain all case data; there is a standard API for this lookup, but the URL and some credentials have to be made available for the workflow instance</li>
<li>Next, the workflow processes a business rule task to determine where the new case should be routed; this could happen based on case data (i.e. a “topic” the reporter had to select when opening the case), but it could also make another API call to the CSA or another system (i.e. employee availabilities) necessary</li>
<li>The new case gets assigned to an employee by the workflow engine; an API call to the CSA backend happens and the necessary fields on the case record get updated; via its BFF, the front end is now able to show the case in the assigned employees’ “my cases” view</li>
<li>The workflow engine proceeds to a wait step and is actively listening to messages with a matching correlation ID (i.e. the case ID) at the same time; firing that event is based on the UI and thus a responsibility of the BFF
<ol>
<li>Nothing happens, a timer set in the workflow expires: the workflow engine triggers an email notification and circles back to the previous step (wait for event/timeout); that’s another API call to the CSA backend (or another system)</li>
<li>The case status changes, the BFF fires the respective event:
<ol>
<li>Case completed (any resolution): the workflow engine completes the task via an API call to the CSA backend, the workflow instance for this task ends</li>
<li>Case review requested: the workflow engine starts a new instance of the case review sub-process; the parent process stays at this step until the sub-process instance gets completed; whenever that happens, the workflow circles back to a previous step (wait for event/timeout)</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>I hope that this example is detailed enough to illustrate the <em>added</em> complexity that adopting workflow-driven architecture can cause.</p>
<p>As mentioned earlier, I don’t think that developing software the way it would be necessary to enable such architecture makes anything easier at all (re: “facilitates…” from the definition above). I’m saying that specifically due to the following aspects:</p>
<ul>
<li>(Additional) glue code is absolutely necessary to enable the communication between workflow steps and the logic necessary to perform the actual work a task in a workflow represents; this overhead can be reduced slightly by a workflow engine’s own libraries for example, but the issue of redundancy and code duplication persists regardless of that</li>
<li>Instead of talking to their own backend/database directly, applications can have to go through the workflow engine (or at least offer the possibility of doing so), which can make such functionality more complex and much less transparent</li>
<li>Applications, services and systems have to expose additional interfaces to trigger functionality that would normally be handled by the resource itself, which can further increase complexity and complicate engineering and maintenance efforts</li>
<li>Applications, services and systems have to account for workflow steps happening out of (the intended) sequence (control flow), or at least be able to handle the possibility of this happening at some point, which can lead to redundancy and code duplication</li>
<li>Logic that is usually handled by an application itself (i.e. task &gt; employee assignment in the example above) is taken out of context and processed elsewhere; this perceived “flexibility” can easily turn into a maintenance nightmare</li>
</ul>
<p>I’m probably missing something, but that’s a lot already anyway. And what’s probably the best conclusion for this section, is this 2nd hand quote from an older <a class="ext" href="https://martinfowler.com/ieeeSoftware/whoNeedsArchitect.pdf" rel="noopener">Martin Fowler article on software architecture</a> that I think fits very well here:</p>
<blockquote>
<p>There is no theoretical reason that anything is hard to change about software. If you pick any one aspect of software then you can make it easy to change, but we don’t know how to make everything easy to change. Making something easy to change makes the overall system a little more complex, and making everything easy to change makes the entire system very complex. Complexity is what makes software hard to change. That, and duplication.</p>
<p><small>Ralph E. Johnson</small></p>
</blockquote>
<h2 id="possible-benefits" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#possible-benefits"><span>Possible Benefits</span></a></h2>
<p>Workflow engines are a rather popular type of - enterprise - software, so it can’t all be bad, right? …right?</p>
<p>So, yes, of course, there are benefits of using workflow engines:</p>
<ul>
<li>They usually work rather well in event-driven architectures in terms of detecting, consuming and reacting to messages in (near-) real-time, which can start/continue/interrupt/… workflow instances</li>
<li>They can also help when you’re forced to include legacy applications and services in larger, more modern systems because the legacy application’s specific requirements (i.e. in terms of interfaces) can be taken care of using the workflow engine’s functionality</li>
<li>They can actually fulfill the “low-code promise” mentioned above if (and when) new and existing applications, services and systems support it well enough</li>
<li>They offer “documentation as a side effect” in terms of providing - more or less - usable process flowcharts for interfaces and functionality that would otherwise just be (insufficiently documented…) code</li>
</ul>
<h2 id="closing-thoughts" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#closing-thoughts"><span>Closing Thoughts</span></a></h2>
<p>What I had in mind when I decided to write this article and started planning it, was that I wanted to avoid ranting. I can only hope that I managed to live up to my own intention and deliver a somewhat diversified collections of thoughts on the topic.</p>
<p>Like I said in the beginning of this article, I’m still learning a lot about aligning software development with a workflow engine at the moment, so I don’t think I can offer any kind of definitive conclusion at this time. Generally, workflow engines are very complex and powerful pieces of software, and I have a lot of respect for the teams of engineers that work on such products.</p>
<p>And as it is the case with many decisions, the decision of whether a workflow engine offers benefits that outweigh the cost of adopting one is definitely a lengthy process that every team and company that’s thinking about it will have to find their own answer to.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 04, 2024</title>
        
        <link href="https://ttntm.me/likes/2024-w04/"/>
        
        <updated>2024-01-22T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2024-w04/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://uxdesign.cc/58-rules-for-stunning-and-effective-user-interface-design-ea4b93f931f6" class="ext u-like-of" rel="noopener">58 rules for beautiful UI design</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://octopus.com/devops/" class="ext u-like-of" rel="noopener">The DevOps engineer’s handbook</a></li>
<li><a href="https://riseofmachine.com" class="ext u-like-of" rel="noopener">Rise of Machine</a><br>
A curated collection of 300+ AI tools</li>
<li><a href="https://www.autoregex.xyz" class="ext u-like-of" rel="noopener">AutoRegex</a><br>
Effortless conversions from English to RegEx</li>
<li><a href="https://nightshade.cs.uchicago.edu/userguide.html" class="ext u-like-of" rel="noopener">Nightshade User’s Guide</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Update: Tags Are Back</title>
        
          
            <category term="eleventy" />
          
            <category term="news" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/restoring-tags/"/>
        
        <updated>2024-01-16T10:30:00Z</updated>
        <id>https://ttntm.me/blog/restoring-tags/</id>
        <content type="html"><![CDATA[
          
            
            <p>This website used to have tags in the past.</p>
<p>They were gone for about 3 years, but they’re back now!</p>
<img src="https://ttntm.me/static/img/blog/content_structure.jpg" class="img-fluid img-center" alt="An illustration of content structure from Undraw">
<p>I didn’t think too much about to decision to remove them when I <a href="https://ttntm.me/blog/migrating-from-hugo-to-eleventy/" class="int">switched from Hugo to Eleventy</a> back in the spring of 2021, I guess I thought they don’t really add much value to the site and I was also following a very minimalist mindset.</p>
<p>My website kept growing though: I wrote 16 posts about various topics since then. Looking back at that amount of information now made me realize that it’s becoming increasingly difficult to find content about a specific topic. And while I’m still not convinced that I should add a search function to the site, restoring tags seemed like a good idea.</p>
<h2 id="using-tags-in-eleventy" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#using-tags-in-eleventy"><span>Using Tags in Eleventy</span></a></h2>
<p>The following section is a brief “look under the hood” of how I added tags to this website.</p>
<p>Tags are relevant for different parts of the site:</p>
<p><strong>1, A reusable <code>include</code> for post previews</strong></p>
<p>This is a rather simple component, but getting the context (<code>currentPost</code>) right was a bit of a challenge. That’s why I ended up using the Collections API for <code>getCollectionItem(page)</code>.</p>
<pre class="language-html"><code class="language-html">{% if post %}<br>  {% set currentPost = post %}<br>{% else %}<br>  {% set currentPost = collections.blog | getCollectionItem(page) %}<br>{% endif %}<br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>list-reset list-inline flex align-items-center<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>list-inline-item sr-only<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Tags:<span class="token entity named-entity" title="&nbsp;">&amp;nbsp;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span><br>  {% for tag in currentPost.data.tags | sort %}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>tag list-inline-item<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/tags/{{ tag | slugify }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>small<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>#<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span>{{ tag }}<br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span><br>  {% endfor %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span></code></pre>
<p><strong>2, An “All Tags” list page</strong></p>
<p>Nothing too fancy here, I just followed the “grid of cards” structure of my <a href="https://ttntm.me/likes/" class="int">/likes</a> page:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>list-reset grid grid3 gap1 mt2<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  {% for tag in collections.blog | getCollectionTags | sort %}<br>    {% set tagUrl %}<br>      /tags/{{ tag | slugify }}/<br>    {% endset %}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>d-block<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>d-block ...<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ tagUrl }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>small<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>#<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span>{{ tag }}<br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span><br>  {% endfor %}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span></code></pre>
<p><code>getCollectionTags</code> is a custom filter I shamelessly stole from <a class="ext" href="https://github.com/11ty/eleventy-base-blog/blob/main/eleventy.config.js#L69C56-L69C56" rel="noopener">eleventy-base-blog</a>.</p>
<p><strong>3, Individual “Posts Tagged With ‘tag’” pages</strong></p>
<p>This one was the only tricky bit.</p>
<p>The <code>filter</code> config was necessary to prevent duplicate permalink errors (due to draft content) and unintended content getting built into the <code>/tags/</code> folder.</p>
<p>Also, <code>data: collections.blog</code> kept giving me errors. I have no idea why, but I plan to get to a minimal reproducible example to report it if it turns out it isn’t “just” some config that’s specific to this site.</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token punctuation">---</span><br><span class="token key atrule">pagination</span><span class="token punctuation">:</span><br>  <span class="token key atrule">data</span><span class="token punctuation">:</span> collections<br>  <span class="token key atrule">size</span><span class="token punctuation">:</span> <span class="token number">1</span><br>  <span class="token key atrule">alias</span><span class="token punctuation">:</span> tag<br>  <span class="token key atrule">filter</span><span class="token punctuation">:</span><br>    <span class="token punctuation">-</span> all<br>    <span class="token punctuation">-</span> blog<br>    <span class="token punctuation">-</span> likes<br>    <span class="token punctuation">-</span> postsByYear<br>    <span class="token punctuation">-</span> til<br><span class="token key atrule">eleventyComputed</span><span class="token punctuation">:</span><br>  <span class="token key atrule">title</span><span class="token punctuation">:</span> <span class="token string">"Posts Tagged With '{{ tag }}'"</span><br><span class="token key atrule">permalink</span><span class="token punctuation">:</span> /tags/<span class="token punctuation">{</span><span class="token punctuation">{</span> tag <span class="token punctuation">|</span> slugify <span class="token punctuation">}</span><span class="token punctuation">}</span>/<br><span class="token punctuation">---</span><br>&lt;<span class="token tag">!--</span> omitted wrapper html <span class="token punctuation">-</span><span class="token punctuation">-</span><span class="token punctuation">></span><span class="token scalar string"><br>  {% for post in collections[ tag ] | reverse %}<br>    {% include "components/post.preview.njk" %}<br>  {% endfor %}</span><br>&lt;<span class="token tag">!--</span> omitted wrapper html <span class="token punctuation">-</span><span class="token punctuation">-</span><span class="token punctuation">></span></code></pre>
<h3 id="11-ty-docs" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#11-ty-docs"><span>11ty Docs</span></a></h3>
<p>Links that I found helpful were:</p>
<ul>
<li><a class="ext" href="https://www.11ty.dev/docs/quicktips/tag-pages/" rel="noopener">Zero Maintenance Tag Pages for your Blog</a></li>
<li><a class="ext" href="https://www.11ty.dev/docs/pagination/" rel="noopener">Pagination</a></li>
<li><a class="ext" href="https://www.11ty.dev/docs/filters/collection-items/#getcollectionitem" rel="noopener">getCollectionItem()</a></li>
</ul>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>Tags are back, enjoy!</p>
<p>Thanks for reading this update. As always, don’t hesitate to get in touch if you should experience any issues with the new feature.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Update: Dark Mode</title>
        
          
            <category term="news" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/dark-mode/"/>
        
        <updated>2024-01-03T10:30:00Z</updated>
        <id>https://ttntm.me/blog/dark-mode/</id>
        <content type="html"><![CDATA[
          
            
            <p><strong>This website supports dark mode now</strong> 🌙.</p>
<p>Adding styles for dark mode has been on my list for a while and I’m glad I finally found enough time and motivation to take care of it.</p>
<p>I’m only using a CSS media query (<code>@media (prefers-color-scheme: dark) {}</code>) at the moment, so the theme switches based on the user’s browser/OS settings.</p>
<p>I might offer a dedicated dark/light mode toggle in the near future, but I still haven’t decided whether that’s a feature I’d retire this site’s current 0 JS policy for. Maybe let me know if you think that a theme toggle would be a good idea and/or if you think that there are issues with <code>prefers-color-scheme</code> being the only option.</p>
<p>And just in case I broke something: I’d really appreciate it if you could let me know when you find something that looks broken (or could be better) in dark mode - thanks!</p>
<h2 id="update-jan-5th-2024" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#update-jan-5th-2024"><span>Update: Jan. 5th 2024</span></a></h2>
<p>I added a theme toggle button thanks to feedback I received: I did not know that using <code>privacy.resistFingerprinting</code> in FF’s <code>about:config</code> forces light mode.</p>
<p>From the <a href="https://ttntm.me/changelog/" class="int">Changelog</a> for the current version 3.6.1 of this website:</p>
<p>Added a theme toggle button in the header and footer of the site.</p>
<ul>
<li>The selected theme is stored in / restored from <code>localStorage</code></li>
<li>The theme defaults to the user’s browser/OS choice</li>
<li>The toggle button overrides the browser/OS choice</li>
<li>There is also a <code>&lt;noscript&gt;</code> fallback in place: it loads a small CSS file that contains <code>:root {}</code> overrides based on <code>@media (prefers-color-scheme: dark) {}</code>
<ul>
<li>The button used to switch themes gets hidden (via CSS) in this case</li>
</ul>
</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>About the Past Year - 2023</title>
        
          
            <category term="personal" />
          
            <category term="recap" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/about-the-past-year-2023/"/>
        
        <updated>2024-01-01T10:30:00Z</updated>
        <id>https://ttntm.me/blog/about-the-past-year-2023/</id>
        <content type="html"><![CDATA[
          
            
            <p>As another year comes to an end, writing this post feels like something I was actually looking forward to.</p>
<p>I recently changed jobs and went back to an SMB after almost 2 years at a large enterprise. The first 9 months of 2023 were quite a downward spiral: good people left and their replacements came with huge delays and were lacking in many areas. Well performing teams broke up and bad management paired with insufficient internal communication killed most people’s motivation. Needless to say that I’m happy I got out when I did.</p>
<p>In terms of my personal life, I’m quite happy that I managed to increase my overall activity level. Getting a new (to me) bike, purchasing a standup paddleboard and leaving the house more (new office) were all helpful in this regard. I hope I’ll manage to keep it up.</p>
<p>To follow the series’ established format, these are some cool things I did last year:</p>
<p><strong>Work</strong></p>
<ul>
<li>Used Preact to build a pretty cool <a href="https://ttntm.me/blog/building-a-data-viewer-with-preact/" class="int">buildless data viewer SPA</a></li>
<li>Used Astro to build some cool websites</li>
<li>Got to use Contentful again (last time was around 2019)</li>
<li>Published a <a class="ext" href="https://github.com/ttntm/buildless-preact-starter" rel="noopener">buildless-preact-starter</a></li>
<li>Left a large enterprise behind and moved (back) to an SMB</li>
<li>Published a library (<a class="ext" href="https://github.com/ttntm/sfmc-utils" rel="noopener">sfmc-utils</a>) for one of the systems I’m working with</li>
<li>Got back into working with process driven workflow engines</li>
</ul>
<p><strong>FOSS &amp; Side Projects</strong></p>
<ul>
<li>Published a TOR Snowflake Proxy at <a class="ext" href="https://relay.iamover.it/" rel="noopener">relay.iamover.it</a></li>
<li>Published a <a class="ext" href="https://www.npmjs.com/package/jsonresume-theme-rnord" rel="noopener">jsonresume theme</a> to NPM</li>
<li>Published an <a href="https://ttntm.me/blog/astro-tailwind-landing-page-template" class="int">Astro version of my landing page template</a></li>
<li>Started a public personal wiki at <a class="ext" href="https://kb.ttntm.me" rel="noopener">kb.ttntm.me</a></li>
<li>Moved a couple of projects from GitHub to <a class="ext" href="https://codeberg.org/ttntm" rel="noopener">Codeberg</a></li>
</ul>
<p>My GitHub contributions for 2023 look like this:</p>
<img src="https://ttntm.me/static/img/blog/github_2023.jpg" class="img-fluid img-center auto-invert" alt="ttntm's GitHub contributions 2023">
<p><strong>Personal Life</strong></p>
<ul>
<li>Got a new (to me) bike after 15+ years of buyers regret from my previous bike</li>
<li>Learned a ton about bicycle maintenance and DIY’ed everything I needed so far</li>
<li>Got into standup paddleboarding and loved it</li>
<li>Got to know about and started using Omnivore</li>
</ul>
<p><strong>Significant Growth</strong></p>
<ul>
<li><strong>Personal</strong>: increasing my activity level in general and also using the bike for many things I would’ve used the car for 1 year ago</li>
<li><strong>Professional</strong>: standardizing code in a way that makes well known errors disappear reliably</li>
</ul>
<p><strong>Persistent Challenges</strong></p>
<ul>
<li><strong>Personal</strong>: multitasking and context switching</li>
<li><strong>Professional</strong>: dealing with insufficient project management, missing requirements and bad documentation</li>
</ul>
<p><strong>Best _ of 2023</strong></p>
<ul>
<li><strong>Drink</strong>: Lagavulin 16yo</li>
<li><strong>Game</strong>: Armored Core VI</li>
<li><strong>Movie</strong>: John Wick: Chapter 4</li>
<li><strong>Series</strong>: Silo (Beef as a close 2nd)</li>
</ul>
<div class="hr shadow mt2 mb2"></div>
<p><strong>Aside: Games of 2023</strong></p>
<div class="grid grid2 gap1">
<div>
<p>In somewhat chronological order:</p>
<ul>
<li>NFS Unbound *</li>
<li>PoE2: Deadfire</li>
<li>Slay the Spire</li>
<li>No Man’s Sky *</li>
<li>The Ascent * (incl. platinum trophy)</li>
<li>W40k: Boltgun</li>
<li>Signalis</li>
<li>Unpacking</li>
<li>Diablo IV *</li>
<li>Darkest Dungeon</li>
<li>Armored Core VI *</li>
<li>Weird West</li>
<li>CP2077: Phantom Liberty *</li>
<li>Inscryption</li>
<li>Stellaris</li>
<li>Borderlands 2</li>
</ul>
<p><code>*</code> <em>marks finished games (main story).</em></p>
</div>
<div>
<p>Games I tried but dropped (&lt;4h):</p>
<ul>
<li>Code Vein</li>
<li>SW Jedi: Fallen Order</li>
<li>Callisto Protocol</li>
<li>Biomutant</li>
<li>Alan Wake</li>
</ul>
</div>
</div>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Hacking &lt;hr&gt; into vue-quill</title>
        
          
            <category term="guide" />
          
            <category term="project" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/hacking-hr-into-vue-quill/"/>
        
        <updated>2023-12-28T10:30:00Z</updated>
        <id>https://ttntm.me/blog/hacking-hr-into-vue-quill/</id>
        <content type="html"><![CDATA[
          
            
            <p>A long time ago, I decided to use <a class="ext" href="https://github.com/vueup/vue-quill" rel="noopener">vue-quill</a> for recept0r, my personal recipes app.</p>
<p>Not long after that, the missing <code>&lt;hr&gt;</code> tag was noticed, which led to a feature request <a class="ext" href="https://codeberg.org/ttntm/recept0r/issues/15" rel="noopener">Feature: add <code>&lt;hr&gt;</code> tag to editor</a> that I ended up ignoring for almost 2 years.</p>
<p>I’m not quite sure what it was exactly that got me thinking about it again back in September, but I ended up with a rather simple workaround: using a regular expression based on the - escaped - string <code>vue-quill</code> produces when a literal <code>&lt;hr&gt;</code> is used.</p>
<p>The workaround has 2 stages:</p>
<ol>
<li>Displaying an escaped string as a placeholder when editing content (= accepting the literal string <code>&lt;hr&gt;</code> as user input)</li>
<li>Replacing the placeholder with valid HTML before saving content to the database</li>
</ol>
<p>The regular expression used for this workaround looks like this: <code>/\&lt;p\&gt;\&amp;lt\;hr\&amp;gt\;\&lt;\/p\&gt;/gi</code></p>
<p>When editing content, the code uses <code>String.prototype.replaceAll()</code> (see: <a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll" rel="noopener">MDN</a>) to replace the actual HTML with the placeholder:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">updateEditMode</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">input</span><span class="token operator">:</span> Recipe</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">key</span> <span class="token operator">=></span> events<span class="token punctuation">.</span><span class="token function">onUpdateRecipe</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> input<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  editor<span class="token punctuation">.</span>value<span class="token punctuation">.</span><span class="token function">setHTML</span><span class="token punctuation">(</span>recipe<span class="token punctuation">.</span>body<span class="token punctuation">)</span><br>  <span class="token comment">// We've got to add in a marker for &lt;hr> elements that will actually get rendered by Quill</span><br>  <span class="token comment">// `onSaveRecipe()` converts it back to valid HTML</span><br>  editor<span class="token punctuation">.</span>value<span class="token punctuation">.</span><span class="token function">setHTML</span><span class="token punctuation">(</span>recipe<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span><span class="token string">'&lt;hr>'</span><span class="token punctuation">,</span> <span class="token string">'&lt;p>&amp;lt;hr&amp;gt;&lt;/p>'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>Eventually, when saving edited content, the placeholder is converted back to valid HTML using the same regular expression:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token function">onSaveRecipe</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">//...</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>editorHrPattern<span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>recipe<span class="token punctuation">.</span>body<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// Replace &lt;hr> marker with valid HTML</span><br>    recipe<span class="token punctuation">.</span>body <span class="token operator">=</span> recipe<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">replaceAll</span><span class="token punctuation">(</span>editorHrPattern<span class="token punctuation">,</span> <span class="token string">'&lt;hr>'</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br>  <span class="token comment">//...</span><br><span class="token punctuation">}</span></code></pre>
<p>If you’re very curious: check out the <a class="ext" href="https://codeberg.org/ttntm/recept0r/commit/2e96503ce09c54418e0efc60405316ff016a127a#diff-3905b2b27985605800a686b8f8000d0762b6d685" rel="noopener">source code</a> at Codeberg.</p>
<p>I’m fully aware that this is a very hacky workaround, but I also think of it as a “quick win” when considering the amount of time I would have had to spend on properly extending the editor’s functionality (see links in the feature request linked above). So, it’s probably best <em>not</em> to follow this approach in any kind of real world scenario.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>App Defaults - 2023</title>
        
          
            <category term="defaults" />
          
            <category term="personal" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/app-defaults-2023/"/>
        
        <updated>2023-11-14T10:30:00Z</updated>
        <id>https://ttntm.me/blog/app-defaults-2023/</id>
        <content type="html"><![CDATA[
          
            
            <p>I saw a couple of similar posts from people’s websites in my RSS reader recently, so here’s my extended “frozen <a href="https://ttntm.me/uses" class="int">/uses</a> page” for late 2023.</p>
<p>Whenever multiple applications are listed, <code>p</code> marks private use, <code>w</code> marks software that I (have to) use at work.</p>
<ul>
<li>
<p><strong>Mail Client</strong>: Proton Mail (<code>p</code>), Outlook PWA (<code>w</code>)</p>
</li>
<li>
<p><strong>Calendar</strong>: Proton Calendar (<code>p</code>), Outlook PWA (<code>w</code>)</p>
</li>
<li>
<p><strong>Notes</strong>: Notion, considering switching to Cryptee or Obsidian</p>
</li>
<li>
<p><strong>Todos</strong>: Notion (<code>p</code>), MS To Do (<code>w</code>)</p>
</li>
<li>
<p><strong>Terminal</strong>: WezTerm</p>
</li>
<li>
<p><strong>Shell</strong>: zsh with Oh My Zsh and p10k</p>
</li>
<li>
<p><strong>Browser (Desktop)</strong>: Firefox Developer Edt., Ungoogled Chromium (<code>w</code>)</p>
</li>
<li>
<p><strong>Browser (Mobile)</strong>: Mull (Android), Safari (iOS)</p>
</li>
<li>
<p><strong>Search Engine</strong>: DuckDuckGo</p>
</li>
<li>
<p><strong>Text Editor</strong>: VS Code, micro or nvim (terminal editing), gedit (quick notes, previews; <code>w</code>)</p>
</li>
<li>
<p><strong>Git Client</strong>: terminal</p>
</li>
<li>
<p><strong>Passwords</strong>: Proton Pass (<code>p</code>), KeePassXC (<code>w</code>)</p>
</li>
<li>
<p><strong>File Sharing</strong>: Proton Drive (<code>p</code>), OneDrive (<code>w</code>)</p>
</li>
<li>
<p><strong>Docs/Wiki</strong>: Notion (<code>p</code>), Confluence (<code>w</code>)</p>
</li>
<li>
<p><strong>Spreadsheets</strong>: Google Sheets (<code>p</code>), LibreOffice Calc (<code>w</code>)</p>
</li>
<li>
<p><strong>Photos</strong>: Google Photos</p>
</li>
<li>
<p><strong>RSS</strong>: Gnome Feeds</p>
</li>
<li>
<p><strong>Chat</strong>: Signal (<code>p</code>), Teams (<code>w</code>)</p>
</li>
<li>
<p><strong>Bookmarks/Links</strong>: Notion</p>
</li>
<li>
<p><strong>Shopping List</strong>: Google Keep</p>
</li>
<li>
<p><strong>Music</strong>: cmus (Desktop), Poweramp (Android)</p>
</li>
<li>
<p><strong>Social Media</strong>: Mastodon via Elk (Desktop), IceCubes (iOS) and Tusky (Android), Pixelfed (PWA)</p>
</li>
<li>
<p><strong>Ad Blocking</strong>: RethinkDNS (Android), uBlock Origin</p>
</li>
<li>
<p><strong>VPN</strong>: Proton VPN</p>
</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 45, 2023</title>
        
        <link href="https://ttntm.me/likes/2023-w45/"/>
        
        <updated>2023-11-07T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2023-w45/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://pcmaffey.com/counting-ghosts" class="ext u-like-of" rel="noopener">Counting Ghosts - A case for abandoning web analytics</a></li>
<li><a href="https://pcmaffey.com/roll-your-own-analytics" class="ext u-like-of" rel="noopener">Roll Your Own Analytics</a></li>
<li><a href="https://robinrendle.com/notes/why-are-websites-embarrassing/" class="ext u-like-of" rel="noopener">Why are websites embarrassing?</a></li>
<li><a href="https://blakewatson.com/journal/magnoliajs-2023-the-joys-of-home-cooked-apps/" class="ext u-like-of" rel="noopener">The joys of home-cooked apps</a></li>
<li><a href="https://chrisboakes.com/how-a-javascript-debounce-function-works/" class="ext u-like-of" rel="noopener">Understanding how a JavaScript ES6 debounce function works</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 42, 2023</title>
        
        <link href="https://ttntm.me/likes/2023-w42/"/>
        
        <updated>2023-10-18T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2023-w42/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://antfu.me/posts/animated-svg-logo" class="ext u-like-of" rel="noopener">Animated SVG Logo</a></li>
<li><a href="https://www.kryogenix.org/code/browser/everyonehasjs.html" class="ext u-like-of" rel="noopener">Everyone has JavaScript, right?</a></li>
<li><a href="https://renegadeotter.com/2023/09/10/death-by-a-thousand-microservices.html" class="ext u-like-of" rel="noopener">Death by a thousand microservices</a></li>
<li><a href="https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/" class="ext u-like-of" rel="noopener">Speeding up the JavaScript ecosystem - The barrel file debacle</a></li>
<li><a href="https://ludic.mataroa.blog/blog/i-will-fucking-haymaker-you-if-you-mention-agile-again/" class="ext u-like-of" rel="noopener">I Will Fucking Haymaker You If You Mention Agile Again</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 34, 2023</title>
        
        <link href="https://ttntm.me/likes/2023-w34/"/>
        
        <updated>2023-08-23T10:35:00Z</updated>
        <id>https://ttntm.me/likes/2023-w34/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://isc.sans.edu/diary/How+to+Find+Hidden+Cameras+in+your+AirBNB/24834" class="ext u-like-of" rel="noopener">How to Find Hidden Cameras in your AirBNB</a></li>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-20-04" class="ext u-like-of" rel="noopener">How To Set Up a Node.js Application for Production</a></li>
<li><a href="https://thenewstack.io/is-jamstack-toast-some-developers-say-yes-netlify-says-no/" class="ext u-like-of" rel="noopener">Is Jamstack Toast? Some Developers Say Yes, Netlify Says No</a></li>
<li><a href="https://joshcollinsworth.com/blog/antiquated-react" class="ext u-like-of" rel="noopener">Things you forgot (or never knew) because of React</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://pm2.keymetrics.io/docs/usage/quick-start/" class="ext u-like-of" rel="noopener">PM2 Process Management Quick Start</a></li>
<li><a href="https://htmx.org/essays/" class="ext u-like-of" rel="noopener">htmx.org/essays/</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>A Simple Astro Landing Page Template</title>
        
          
            <category term="astro" />
          
            <category term="template" />
          
        
        <link href="https://ttntm.me/blog/astro-tailwind-landing-page-template/"/>
        
        <updated>2023-08-23T10:30:00Z</updated>
        <id>https://ttntm.me/blog/astro-tailwind-landing-page-template/</id>
        <content type="html"><![CDATA[
          
            
            <p>Earlier this week, I ported my “simple landing page” template to <a class="ext" href="https://astro.build" rel="noopener">Astro</a>.</p>
<p>I initially published it for Hugo in July 2020, followed by an 11ty version in March 2021.</p>
<img src="https://ttntm.me/static/img/blog/astro-landing-page.png" class="img-fluid img-center" alt="3 screenshot of a landing page on a blue background">
<p>Working with Astro was quite pleasant; I’d used it before (late summer/autumn 2022), but I had to consult the documentation for some things that changed in the meantime and also for a couple of other things that I simply forgot.</p>
<h2 id="good-things" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#good-things"><span>Good Things</span></a></h2>
<ul>
<li>Excellent <a class="ext" href="https://docs.astro.build/en/getting-started/" rel="noopener">documentation</a></li>
<li>Easy use of existing data files (i.e. JSON)</li>
<li>Great <a class="ext" href="https://docs.astro.build/en/guides/integrations-guide/tailwind/#overview" rel="noopener">Tailwind CSS integration</a> that worked OOTB</li>
<li><a class="ext" href="https://docs.astro.build/en/install/manual/#6-add-typescript-support" rel="noopener">TypeScript support</a> (i.e. component props)</li>
</ul>
<h2 id="other-things" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#other-things"><span>Other Things</span></a></h2>
<ul>
<li>The (JSX-like) template language; probably just takes time to get used to…</li>
<li>Some weirdness with the syntax highlighting in *.astro files (using the official VS Code “Language support for Astro” extension)</li>
</ul>
<img src="https://ttntm.me/static/img/blog/astro-landing-page-browser.jpg" class="img-fluid img-center" alt="Screenshot of a landing page in a browser window">
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#links"><span>Links</span></a></h2>
<p>Astro Template:</p>
<ul>
<li><a class="ext" href="https://github.com/ttntm/astro-landing-page" rel="noopener">ttntm/astro-landing-page</a></li>
<li><a class="ext" href="https://awesomestro.ttntm.me" rel="noopener">Live Demo</a></li>
</ul>
<p>Other versions:</p>
<ul>
<li><a class="ext" href="https://github.com/ttntm/11ty-landing-page" rel="noopener">ttntm/11ty-landing-page</a></li>
<li><a class="ext" href="https://github.com/ttntm/hugo-landing-page" rel="noopener">ttntm/hugo-landing-page</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Building a Data Viewer With Preact</title>
        
          
            <category term="guide" />
          
            <category term="preact" />
          
            <category term="project" />
          
        
        <link href="https://ttntm.me/blog/building-a-data-viewer-with-preact/"/>
        
        <updated>2023-08-04T10:30:00Z</updated>
        <id>https://ttntm.me/blog/building-a-data-viewer-with-preact/</id>
        <content type="html"><![CDATA[
          
            
            <p>I recently published a template/starter intended to showcase how I’m using Preact for SPAs in buildless environments. Making use of it for this article’s demo application should serve as a nice follow up 😎</p>
<p>So, let’s assume the following premise:</p>
<p>You’ve got to show personal activity data to the end users of a mountain resort (= guests that stay there and move around on bike and foot).</p>
<p>Data aggregation, storage etc. is being taken care of by various backend services and you can pull that data into a low code front end environment quite easily. But: you’re not dealing with any kind of infrastructure that’d allow the use of CI/CD, builds, pipelines etc. - your customer wants to keep things simple and efforts to a reasonable minimum, which means you’ll have to work with the systems they’ve got in place.</p>
<p>Let’s also assume that said customer wants to have something slick and good looking that’s also modern and responsive. Tables and lists are off the table, they absolutely want to have icons and a tree for navigation!</p>
<h2 id="scope" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#scope"><span>Scope</span></a></h2>
<p>We’ll focus on the front end in this article; data preparation/fetching and user identification are implied, but intentionally omitted. There’s sample data and an example of a data retrieval function in the demo’s codebase to hint at things that’d usually have to happen for the SPA to have the necessary context and data to display anything at all.</p>
<h2 id="getting-started" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#getting-started"><span>Getting Started</span></a></h2>
<p>All code mentioned and referenced in this article can be found in the demo repository over at GitHub: <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo" rel="noopener">preact-data-viewer-demo</a>. The demo repository is based on my buildless preact starter; context, motivation and considerations for code splitting etc. can be found in the detailed readme inlined in the announcement article: <a href="https://ttntm.me/blog/buildless-preact-starter/" class="int">My Buildless Preact Starter</a></p>
<p>The demo provides a working example that’s also been published to this site: <a class="ext" href="https://ttntm.me/demos/pdv/" rel="noopener">/demos/pdv/</a></p>
<img src="https://ttntm.me/static/img/blog/preact-data-viewer.png" class="img-fluid img-center" alt="Screenshot of a demo app">
<p>Please note that all code mentioned in this article should also show up explicitly (i.e. unprocessed, unminified) in your browser’s dev tools - it is buildless after all.<br>
(<em>A little note on that can be found at the end of the article.</em>)</p>
<h2 id="building-the-app" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#building-the-app"><span>Building the App</span></a></h2>
<p>The following section will follow the data flow from the imagined back end (inside the large enterprise system) to the front end where the app’s going to be displayed to the end user.</p>
<h3 id="index-html" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#index-html"><span>index.html</span></a></h3>
<p>I doubt I could summarize it better than I did for a recent readme:</p>
<blockquote>
<p>[This file is] the shell of your SPA. [It] can be used for static elements (header/footer) and server side code (if your environment provides that).</p>
</blockquote>
<p>Data fetching is explicitly implied here (see “Scope” above): working with the imaginary enterprise system, you’d have the possibility of running server side code in this file. You could use such code to pass raw stringified JSON data obtained from the back end into the JavaScript code that’s evaluated on the client side. For demo purposes, locally sourced sample data (<a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/db_rows.js" rel="noopener">./db_rows.js</a>) and a data retrieval function (<code>serverSideDataRetrieval()</code>, <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/index.html#L38" rel="noopener">index.html#L38</a>) were added to the repository.</p>
<p>What gets things started is a <code>load</code> event listener that first builds a <code>config</code> object and then performs the call to start the application using <code>App(config)</code>:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br>  <span class="token keyword">import</span> App <span class="token keyword">from</span> <span class="token string">'./app.js'</span><br><br>  <span class="token comment">// more code...</span><br><br>  window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'load'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">rawStats</span><span class="token operator">:</span> <span class="token function">serverSideDataRetrieval</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><br><br>    <span class="token function">App</span><span class="token punctuation">(</span>config<span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
<h3 id="the-apps-data" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-apps-data"><span>The App’s Data</span></a></h3>
<p>We’re working with rows obtained from an imaginary database that look like this:</p>
<pre class="language-tsx"><code class="language-tsx"><span class="token punctuation">{</span><br>  Id<span class="token operator">:</span> <span class="token builtin">string</span><br>  DistanceOnFoot<span class="token operator">:</span> <span class="token builtin">number</span><br>  DistanceOnBike<span class="token operator">:</span> <span class="token builtin">number</span><br>  MetersOfHeight<span class="token operator">:</span> <span class="token builtin">number</span><br>  DateOfCalculation<span class="token operator">:</span> <span class="token builtin">string</span><br>  DeviceId<span class="token operator">:</span> <span class="token builtin">string</span><br><span class="token punctuation">}</span></code></pre>
<ul>
<li><code>Id</code> is a GUID assigned to each row - we’re not using that for anything, but it’d probably be present in real world data.</li>
<li><code>Distance…</code> and <code>Meters…</code> properties are the values that the data viewer is going to work with</li>
<li><code>DateOfCalculation</code> is a string representation of an SQL <code>datetime</code> value</li>
<li><code>DeviceId</code> is the id of the device that was used to collect the data; different <code>DeviceId</code> values can appear in records belonging to the same <code>DateOfCalculation</code>, but each <code>DeviceId</code> can only ever appear once per actual <em>day</em> of the year</li>
</ul>
<p>The imaginary back end takes care of handling these constraints and provides the data as shown in <code>db_rows.js</code>. It’s safe to rely on this data coming in just like that: the automated processes handling data preparation are protected well from unintended changes (they never <em>really</em> are, but we’ve got to live with that).</p>
<h3 id="app-js" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#app-js"><span>app.js</span></a></h3>
<p>This file is our SPA’s shell. There’s an <code>export default function App(config)</code> in <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/app.js#L68" rel="noopener">app.js#L68</a> that’s called from <code>index.html</code> which is used to pass the data described in the previous section to the application.</p>
<p>The code immediately runs the (destructured) <code>rawStats</code> variable through a reverse sorting function (<code>objectSort()</code>, <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/app.js#L51" rel="noopener">app.js#L51</a>) based on the <code>DateOfCalculation</code>. The rows obtained from our database are now sorted from newest to oldest record.</p>
<p>What follows is the definition of the app’s <code>Main()</code> component: first, we utilize the <code>useState()</code> <a class="ext" href="https://preactjs.com/guide/v10/hooks" rel="noopener">hook</a> and define the <code>statMap</code> object, then we proceed to a <code>useEffect()</code> call that basically mimics the <code>componentDidMount()</code> method which would have been used if this were a Class Component. Together with the <code>firstTimeRender</code> flag, we make sure that this <code>useEffect()</code> call only ever runs once, when the component gets mounted to the DOM:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>firstTimeRender<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token function">setStatMap</span><span class="token punctuation">(</span><span class="token parameter">current</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> sortedStats <span class="token operator">&amp;&amp;</span> sortedStats<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><br>        <span class="token operator">?</span> <span class="token function">createStatMap</span><span class="token punctuation">(</span>sortedStats<span class="token punctuation">)</span><br>        <span class="token operator">:</span> <span class="token keyword">null</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>    <span class="token function">setFirstTimeRender</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span></code></pre>
<p>NB: a <code>useEffect()</code> call with an empty dependencies array would run for each and every re-render if it wasn’t for the <code>firstTimeRender</code> flag - be careful with that and only ever do this intentionally to avoid unnecessary re-renders, performance degradation and a waste of client CPU resources.</p>
<p>What’s essential to mention here is the call to the <code>createStatMap()</code> function (<a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/app.js#L8" rel="noopener">app.js#L8</a>). This function is the key piece of logic that transforms our flat data rows into a map(like) object with keys representing each day we have data for:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">function</span> <span class="token function">createStatMap</span><span class="token punctuation">(</span><span class="token parameter">input</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> input<span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">dateMap<span class="token punctuation">,</span> currentRow</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> <span class="token punctuation">{</span><br>      DateOfCalculation<span class="token punctuation">,</span><br>      DeviceId<span class="token punctuation">,</span><br>      DistanceOnBike<span class="token punctuation">,</span><br>      DistanceOnFoot<span class="token punctuation">,</span><br>      MetersOfHeight<br>    <span class="token punctuation">}</span> <span class="token operator">=</span> currentRow<br>    <span class="token keyword">const</span> dateKey <span class="token operator">=</span> <span class="token function">formatDateTime</span><span class="token punctuation">(</span>DateOfCalculation<span class="token punctuation">)</span><br>    <span class="token keyword">const</span> currentDate <span class="token operator">=</span> dateMap<span class="token punctuation">[</span>dateKey<span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><br>    <span class="token keyword">const</span> currentDayCount <span class="token operator">=</span> <span class="token function">Number</span><span class="token punctuation">(</span>currentDate<span class="token punctuation">.</span>length <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">)</span><br>    <span class="token keyword">const</span> currentTotals <span class="token operator">=</span> dateMap<span class="token punctuation">.</span>_totals <span class="token operator">??</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br><br>    <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">AssetName</span><span class="token operator">:</span> DeviceId<span class="token punctuation">,</span><br>      <span class="token literal-property property">DistanceOnBike</span><span class="token operator">:</span> DistanceOnBike<span class="token punctuation">,</span><br>      <span class="token literal-property property">DistanceOnFoot</span><span class="token operator">:</span> DistanceOnFoot<span class="token punctuation">,</span><br>      <span class="token literal-property property">MetersOfHeight</span><span class="token operator">:</span> MetersOfHeight<br>    <span class="token punctuation">}</span><br><br>    <span class="token keyword">const</span> newTotals <span class="token operator">=</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">DistanceOnBike</span><span class="token operator">:</span> <span class="token punctuation">(</span>currentTotals<span class="token punctuation">.</span>DistanceOnBike <span class="token operator">||</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">+</span> DistanceOnBike<span class="token punctuation">,</span><br>      <span class="token literal-property property">DistanceOnFoot</span><span class="token operator">:</span> <span class="token punctuation">(</span>currentTotals<span class="token punctuation">.</span>DistanceOnFoot <span class="token operator">||</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">+</span> DistanceOnFoot<span class="token punctuation">,</span><br>      <span class="token literal-property property">MetersOfHeight</span><span class="token operator">:</span> <span class="token punctuation">(</span>currentTotals<span class="token punctuation">.</span>MetersOfHeight <span class="token operator">||</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">+</span> MetersOfHeight<span class="token punctuation">,</span><br>      <span class="token literal-property property">NumberOfDays</span><span class="token operator">:</span> <span class="token punctuation">(</span>currentTotals<span class="token punctuation">.</span>NumberOfDays <span class="token operator">||</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">+</span> currentDayCount<br>    <span class="token punctuation">}</span><br><br>    <span class="token keyword">return</span> <span class="token punctuation">{</span><br>      <span class="token operator">...</span>dateMap<span class="token punctuation">,</span><br>      <span class="token punctuation">[</span>dateKey<span class="token punctuation">]</span><span class="token operator">:</span> currentDate<span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span><span class="token punctuation">[</span>data<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">_totals</span><span class="token operator">:</span> newTotals<br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>We are using the powerful <code>Array.prototype.reduce()</code> method here, which “executes a user-supplied “reducer” callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element” (<a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce" rel="noopener">MDN</a>).</p>
<p>Let’s start right at the top: looking at <code>input.reduce((dateMap, currentRow) =&gt; { ... }</code>, <code>dateMap</code> is our <em>accumulator</em> that first contains the (optional) <em>initialValue</em> (an empty object literal <code>{}</code>) and then carries the value of each previous call of the callback function, i.e. what’s wrapped by the <code>return { ... }</code> statement in the code snippet above.</p>
<p>The callback function does the following:</p>
<ol>
<li>It assigns a new <code>dateKey</code> to each database row it processes</li>
<li>It creates a <code>data</code> object containing the day’s records for a specific <code>DeviceId</code> (re-mapped to the key <code>AssetName</code>)</li>
<li>It updates the incoming <code>currentTotals</code> using a <code>newTotals</code> object</li>
<li>It returns a compound object containing
<ol>
<li>All previous data</li>
<li>New or additional data for the current <code>dateKey</code></li>
<li>Updated data for the <code>_totals</code> key</li>
</ol>
</li>
</ol>
<p>The resulting object looks like this:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token punctuation">{</span><br>  <span class="token literal-property property">_totals</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">DistanceOnBike</span><span class="token operator">:</span> <span class="token number">304830.64999999997</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">DistanceOnFoot</span><span class="token operator">:</span> <span class="token number">155892.08000000002</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">MetersOfHeight</span><span class="token operator">:</span> <span class="token number">41611.490000000005</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">NumberOfDays</span><span class="token operator">:</span> <span class="token number">7</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token string-property property">'06/01/2023'</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token punctuation">{</span><br>      <span class="token literal-property property">AssetName</span><span class="token operator">:</span> <span class="token string">'1-218-0815'</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">DistanceOnBike</span><span class="token operator">:</span> <span class="token number">7305.09</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">DistanceOnFoot</span><span class="token operator">:</span> <span class="token number">4314.43</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">MetersOfHeight</span><span class="token operator">:</span> <span class="token number">802.8</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">{</span><br>      <span class="token literal-property property">AssetName</span><span class="token operator">:</span> <span class="token string">'1-218-2001'</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">DistanceOnBike</span><span class="token operator">:</span> <span class="token number">75643.9</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">DistanceOnFoot</span><span class="token operator">:</span> <span class="token number">41343.67</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">MetersOfHeight</span><span class="token operator">:</span> <span class="token number">6704.38</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token comment">// More data...</span><br><span class="token punctuation">}</span></code></pre>
<p>With the data preparation taken care of, we now proceed to mount the app’s core component <code>Stats</code> (<a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/app.js#L94" rel="noopener">app.js#L94</a>) which consumes the newly created <code>statMap</code> as its one and only prop:</p>
<p><code>&lt;${Stats} statMap=&quot;${statMap}&quot; /&gt;</code></p>
<p>Considering that <code>Stats</code> was imported from the components file (<a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/app.js#L4" rel="noopener">app.js#L4</a> and <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/app.js#L65" rel="noopener">app.js#L65</a>), we’ll proceed to have a closer look at that one now.</p>
<h3 id="components-js" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#components-js"><span>components.js</span></a></h3>
<p>I prefer keeping components in a separate file. It keeps things clean(er) and helps (me) reduce side effects to a minimum required to make things work. It also makes it easier to find the source of errors that will happen sooner or later. As a quick real world example:</p>
<p>An application like this one might end up having multiple views and a form somewhere eventually. Considering that forms are (hopefully…) standardized in some way and most likely used elsewhere already, there’s probably an existing <code>formComponents.js</code> file you can reuse without having to duplicate some/all of these components into the code of this particular application, creating redundancy and, as a consequence, a maintenance nightmare. On the other hand, putting your components into separate files may make small(er) apps more complicated than they’d have to be, so it’s really just an “as needed” suggestion.</p>
<p>Anyway, back to our app’s components:</p>
<h4 id="stats" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#stats"><span>Stats</span></a></h4>
<p>Component definition: <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/components.js#L187" rel="noopener">components.js#L187</a></p>
<p>This is our data viewer’s wrapper component which makes use of a tree (<code>StatsTree</code>) and a view (<code>StatsView</code>) sub-component. The main responsibility of this component is to handle the navigation events (via the tree) and to manage the state and data for the view using the <code>displayData</code> object. It also calculates the averages (<a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/components.js#L197" rel="noopener">components.js#L197</a> &gt; <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/components.js#L18" rel="noopener">components.js#L18</a>) for the “Stats Overview”.</p>
<p>Props:</p>
<ul>
<li><code>statMap</code>: an object produced by the <code>createStatMap()</code> reducer (an example of the data can be found above where the function is explained in detail)</li>
</ul>
<h4 id="stats-tree" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#stats-tree"><span>StatsTree</span></a></h4>
<p>Component definition: <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/components.js#L41" rel="noopener">components.js#L41</a></p>
<p>A nav tree component that renders nodes based on the date-keyed entries of the <code>statMap</code> object.</p>
<p>Props:</p>
<ul>
<li><code>activeNode</code>: a string value containing the id of the active tree node; intentionally left blank for when the overview should be shown</li>
<li><code>treeData</code>: the <code>statMap</code> object</li>
<li><code>onTreeNodeClick</code>: a method defined in the parent component (<code>Stats</code>, <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/components.js#L209" rel="noopener">components.js#L209</a>) that handles the clicks onto the tree nodes</li>
</ul>
<p>At its core, the tree component renders an <code>&lt;ul&gt;</code> with a <code>&lt;li&gt;</code> for each one of the date keys in the <code>statMap</code> object (which itself is an array with 1 or more records). It then loops through the value of these keys and renders a <code>&lt;li&gt;</code> for each one of them into a nested <code>&lt;ul&gt;</code> inside the respective main node. The 2 nested loops make use of the <code>activeNode</code> prop to determine which one of the main nodes and which one of its children is currently active and should be highlighted (via CSS classes).</p>
<p>Each node has an unique id built from <code>key</code> and <code>AssetName</code> that looks like this: <code>07/01/2023_1-218-0815</code>. These ids are used by the <code>onTreeNodeClick</code> method which handles the navigation:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> onTreeNodeClick <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">evt</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> nodeId <span class="token operator">=</span> evt<span class="token punctuation">.</span>target<span class="token punctuation">.</span>id<br>  <span class="token keyword">const</span> showOverview <span class="token operator">=</span> nodeId <span class="token operator">===</span> <span class="token string">'overview'</span><br><br>  <span class="token function">setActiveNode</span><span class="token punctuation">(</span>showOverview <span class="token operator">?</span> <span class="token string">''</span> <span class="token operator">:</span> nodeId<span class="token punctuation">)</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>showOverview<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token function">setDisplayData</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      <span class="token literal-property property">averages</span><span class="token operator">:</span> avgData<span class="token punctuation">,</span><br>      <span class="token literal-property property">content</span><span class="token operator">:</span> statMap<span class="token punctuation">.</span>_totals<br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> nodePath <span class="token operator">=</span> nodeId<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'_'</span><span class="token punctuation">)</span><br>    <span class="token function">setDisplayData</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      <span class="token literal-property property">date</span><span class="token operator">:</span> nodePath<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">device</span><span class="token operator">:</span> nodePath<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">content</span><span class="token operator">:</span> statMap<span class="token punctuation">[</span>nodePath<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token parameter">o</span> <span class="token operator">=></span> o<span class="token punctuation">.</span>AssetName <span class="token operator">==</span> nodePath<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>avgData<span class="token punctuation">,</span> statMap<span class="token punctuation">]</span><span class="token punctuation">)</span></code></pre>
<p>As you can see, the wrapper component first grabs the <code>nodeId</code> from the vDOM event to determine what kind of view to show (overview vs. day view) and then proceeds to update the <code>displayData</code> accordingly.</p>
<p>The function is wrapped with a <code>useCallback()</code> hook which helps avoid unnecessary re-renders of child components that depend on it. It uses both <code>avgData</code> and <code>statMap</code> as dependencies to make sure the app’s properly initialized on the first render (both are only ever set once when the <code>Stats</code> component mounts and don’t get any updates).</p>
<p>NB: you can go ahead and try removing <code>avgData</code> from the dependencies and see what happens…<br>
Hint: <code>useCallback()</code> runs before <code>setAvgData()</code> is completed, so <code>avgData</code> will stay at its initial <code>null</code> value.</p>
<h4 id="stats-view" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#stats-view"><span>StatsView</span></a></h4>
<p>Component definition: <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/components.js#L86" rel="noopener">components.js#L86</a></p>
<p>This is the main view component which controls the rendering if the icon tiles. It’s a rather simple component that does not use any complex logic. It does, however, control whether or not to display multiple instances of the <code>StatsViewTiles</code> component (overview = summary + averages), or just one (day view). This decision is based on the respective current value of the (local) <code>averages</code> and <code>showOverview</code> flags.</p>
<p>Props:</p>
<ul>
<li><code>displayData</code>: the data for the current view as determined by the <code>Stats</code> wrapper component</li>
</ul>
<h4 id="stats-view-tiles" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#stats-view-tiles"><span>StatsViewTiles</span></a></h4>
<p>Component definition: <a class="ext" href="https://github.com/ttntm/preact-data-viewer-demo/blob/main/components.js#L121" rel="noopener">components.js#L121</a></p>
<p>A component that renders a grid of icons for the provided data based on a fixed set of (locally defined) <code>tiles</code>. Objects in the <code>tiles</code> array can have a <code>transform</code> property that determines if and how the respective value should be transformed and formatted.</p>
<p>Props:</p>
<ul>
<li><code>containerClass</code>: determined by the <code>StatsView</code> component; controls which CSS class to assign to the <code>div</code> containing the tiles</li>
<li><code>data</code>: the current <code>displayData</code> for the component instance</li>
<li><code>formatAll</code>: a boolean flag that controls whether or not values should be processed by the <code>formatNumber()</code> function if the respective tile lacks the <code>transform</code> property</li>
</ul>
<h3 id="assets-styles" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#assets-styles"><span>Assets &amp; Styles</span></a></h3>
<p>Just like the starter this demo is based on, the styles used here were originally based on <a class="ext" href="https://github.com/kognise/water.css" rel="noopener">water.css</a> but they got modified quite extensively. I neither minified nor purged them, so there’s a bunch of stuff in there that’s not being used at all (i.e. all the form related styles).</p>
<p>Assets (i.e. icons) are stored in their own folder and were sourced from TablerIcons.</p>
<h2 id="production-deployment" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#production-deployment"><span>Production Deployment</span></a></h2>
<p>As mentioned in the beginning of the article, the code is running in your browser just like it’s displayed in your editor. Considering production use in customer environments, this might not be ideal. But we’re still constrained to a buildless approach…</p>
<p>A tool that could help in this situation is <a class="ext" href="https://esbuild.github.io/" rel="noopener">esbuild</a>. It allows creating one (or more) mini/uglyfied *.js files that can be pasted into our enterprise system. This way, we avoid using unminified code there which some curious user might end up playing around with.</p>
<p>Once installed, esbuild can be used with Preact like this:</p>
<pre class="language-json"><code class="language-json"><span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"build:prod"</span><span class="token operator">:</span> <span class="token string">"npx esbuild ./src/app.js '--define:process.env.NODE_ENV=\"production\"' --jsx-factory=preact.h --jsx-fragment=preact.Fragment --minify --outfile=./dist/app.min.js"</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span></code></pre>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>If you were following along and/or playing around with a demo app of your own, you should have arrived at a working SPA by now. It’s built on a solid foundation and you shouldn’t have any issues extending the functionality with additional views and/or logic.</p>
<p>This is just one example of buildless Preact; I’ve built many cool things with it in recent years of working in and around enterprise systems like the one imagined for this article. Some of the resulting SPAs are similar to the one built for this article, others are providing web access to custom functionality, some were form generators or (limited) landing page builders.</p>
<p>I wouldn’t call it something I really love doing, but it’s definitely far ahead of having to deliver similar functionality using 1000s of lines of static HTML with tons of jQuery on top of it for example. And yes, that’s definitely still happening out there, no matter how much you’d like to believe that it’s not - esp. when considering that it’s 2023 at the time of writing this article…</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>My Buildless Preact Starter</title>
        
          
            <category term="preact" />
          
            <category term="template" />
          
        
        <link href="https://ttntm.me/blog/buildless-preact-starter/"/>
        
        <updated>2023-07-27T10:30:00Z</updated>
        <id>https://ttntm.me/blog/buildless-preact-starter/</id>
        <content type="html"><![CDATA[
          
            
            <p>The opening paragraph of the last article published on this site already gave it away: I’m going to be writing about Preact a little more. In fact, the purpose of this article is really “just” an announcement:</p>
<p>I’ve published a template/starter intended to showcase how I’m using Preact for SPAs in buildless environments.</p>
<p>It’s probably going to seems a little lazy, but I’m going to publish the <a class="ext" href="https://github.com/ttntm/buildless-preact-starter/blob/main/README.md" rel="noopener">project’s readme</a> here. Quite a bit of time went into documenting it properly, so it might be worth reading before heading over to GitHub to <a class="ext" href="https://github.com/ttntm/buildless-preact-starter/" rel="noopener">browse the code</a>.</p>
<p>There’s also a demo which can be found here: <a href="https://ttntm.me/demos/bps/" class="int">ttntm.me/demos/bps/</a></p>
<img src="https://ttntm.me/static/img/blog/buildless-preact.png" class="img-fluid img-center" alt="Screenshot of a demo app">
<h2 id="preface" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#preface"><span>Preface</span></a></h2>
<p>In the official docs, this approach is called the <a class="ext" href="https://preactjs.com/guide/v10/getting-started#no-build-tools-route" rel="noopener">No build tools route</a>; it leverages <a class="ext" href="https://github.com/developit/htm" rel="noopener">HTM</a>:</p>
<blockquote>
<p>HTM is a JSX-like syntax that works in standard JavaScript. Instead of requiring a build step, it uses JavaScript’s own Tagged Templates syntax, which was added in 2015 and is supported in all modern browsers. This is an increasingly popular way to write Preact apps, since there are fewer moving parts to understand than a traditional front-end build tooling setup.</p>
</blockquote>
<h2 id="prerequisites" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#prerequisites"><span>Prerequisites</span></a></h2>
<ol>
<li>At least 1 HTML file</li>
<li>Browser support for <code>&lt;script type=&quot;module&quot;&gt;</code></li>
<li>A server (or component in some low code system) that can execute and serve the code</li>
</ol>
<h2 id="project-structure" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#project-structure"><span>Project Structure</span></a></h2>
<h3 id="index-html" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#index-html"><span>index.html</span></a></h3>
<p>Source code: <a class="ext" href="https://github.com/ttntm/buildless-preact-starter/blob/main/index.html" rel="noopener">./index.html</a></p>
<p>The shell of your SPA. Can be used for static elements (header/footer) and server side code (if your environment provides that).</p>
<p>For the most minimal setup, this is where it ends: your Preact SPA is inlined in this file (please refer to the official docs linked above for an example).</p>
<p><code>index.html</code> is also using some <code>&lt;link rel=&quot;modulepreload&quot;&gt;</code> statements in <code>&lt;head&gt;</code> to “significantly reduce the overall download and processing time” of the necessary dependencies listed below (see: <a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/modulepreload" rel="noopener">MDN</a>).</p>
<h3 id="app-js" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#app-js"><span>app.js</span></a></h3>
<p>Source code: <a class="ext" href="https://github.com/ttntm/buildless-preact-starter/blob/main/app.js" rel="noopener">./app.js</a></p>
<p>Your SPA’s entry point, kept in a separate file. Follow this approach if you can (i.e. if your environment supports it).</p>
<p>The file uses a default export (<code>export default function App(config)</code>) that consumes a <code>config</code> object. A practical example: <code>config</code> could be used to pass data obtained from the server side to the SPA if the target system provides the functionality to run server side code in <code>index.html</code>.</p>
<h3 id="components-js" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#components-js"><span>components.js</span></a></h3>
<p>Source code: <a class="ext" href="https://github.com/ttntm/buildless-preact-starter/blob/main/components.js" rel="noopener">./components.js</a></p>
<p>Uses a default export to provide reusable components. Use <a class="ext" href="https://preactjs.com/guide/v10/hooks" rel="noopener">Preact Hooks</a> if you want to make use of stateful components.</p>
<p>NB: using components in a separate file (or even multiple files) is a nice way to follow the separation of concerns and to remove (reusable) component code from <code>app.js</code>. It is not required though - components can (also) be defined in <code>app.js</code> and/or inline (<code>&lt;script type=&quot;module&quot;&gt;</code>) without any issues - check out the <code>Heading</code> component in <code>app.js</code> for an example.</p>
<h3 id="config-js" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#config-js"><span>config.js</span></a></h3>
<p>Source code: <a class="ext" href="https://github.com/ttntm/buildless-preact-starter/blob/main/config.js" rel="noopener">./config.js</a></p>
<p>Entirely optional. I’m often using such config files for translations, lists used to populate <code>&lt;select&gt;</code> elements etc.</p>
<p>Follows the same intention as <code>app.js</code> and <code>components.js</code> - use if your environment supports doing it and if your app has the complexity/size to justify doing it.</p>
<h3 id="styles-css" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#styles-css"><span>styles.css</span></a></h3>
<p>Source code: <a class="ext" href="https://github.com/ttntm/buildless-preact-starter/blob/main/styles.css" rel="noopener">./styles.css</a></p>
<p>Not sure if this needs to be mentioned separately, but you probably want your site (and SPA) not to look like browser defaults.</p>
<p>The styles used for this project were originally based on <a class="ext" href="https://github.com/kognise/water.css" rel="noopener">water.css</a> but they got modified, extended and tinkered with over time.</p>
<h2 id="using-this-starter-for-development" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#using-this-starter-for-development"><span>Using This Starter for Development</span></a></h2>
<p>Fork, clone and start building your thing basically. Nothing to install, no <code>node_modules</code>, no dependencies for you to manage.</p>
<p>Well, that’s not entirely true, but they’re all loaded from <a class="ext" href="https://esm.sh/" rel="noopener">ESM CDN</a>:</p>
<ul>
<li><code>esm.sh/preact@10.4.7</code></li>
<li><code>esm.sh/preact@10.4.7/hooks</code></li>
<li><code>esm.sh/htm@3.0.4</code></li>
</ul>
<p>I’ve included one recommendation for VS Code users in the <code>.vscode</code> folder though:</p>
<ul>
<li><a class="ext" href="https://open-vsx.org/extension/ms-vscode/live-server" rel="noopener">Live Preview</a></li>
</ul>
<p>I’ve come to appreciate this extension for a variety of things, so go check it out if you haven’t used it yet.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 29, 2023</title>
        
        <link href="https://ttntm.me/likes/2023-w29/"/>
        
        <updated>2023-07-21T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2023-w29/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://stackoverflow.com/questions/14249506/how-can-i-wait-in-node-js-javascript-l-need-to-pause-for-a-period-of-time" class="ext u-like-of" rel="noopener">How can I wait In Node.js (JavaScript)?</a></li>
<li><a href="https://blog.risingstack.com/mastering-async-await-in-nodejs/" class="ext u-like-of" rel="noopener">Async Await in Node.js – How to Master it?</a></li>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-launch-child-processes-in-node-js" class="ext u-like-of" rel="noopener">How To Launch Child Processes in Node.js</a></li>
<li><a href="https://sleibrock.xyz/blog/lineage-os/" class="ext u-like-of" rel="noopener">An article about Lineage OS</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://wezfurlong.org/wezterm/index.html" class="ext u-like-of" rel="noopener">WezTerm</a></li>
<li><a href="https://stackoverflow.com/questions/44663864/correct-try-catch-syntax-using-async-await" class="ext u-like-of" rel="noopener">Correct Try…Catch Syntax Using Async/Await</a></li>
<li><a href="https://anytype.io" class="ext u-like-of" rel="noopener">anytype.io</a></li>
<li><a href="https://notabug.org/dfca/DFCA/src/main" class="ext u-like-of" rel="noopener"> Digital Feudalism Counter Action (big tech blocker for linux) </a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 22, 2023</title>
        
        <link href="https://ttntm.me/likes/2023-w22/"/>
        
        <updated>2023-05-31T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2023-w22/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://explainshell.com" class="ext u-like-of" rel="noopener">explainshell.com</a><br>
“write down a command-line to see the help text that matches each argument” - simply delightful.</li>
<li><a href="https://vanjs.org" class="ext u-like-of" rel="noopener">vanjs.org</a></li>
<li><a href="https://martinfowler.com/articles/eurogames/?filter=player-count-2" class="ext u-like-of" rel="noopener">Martin Fowler’s Eurogames for 2 players</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 15, 2023</title>
        
        <link href="https://ttntm.me/likes/2023-w15/"/>
        
        <updated>2023-04-14T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2023-w15/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.mikestreety.co.uk/blog/making-sure-youre-not-doing-everything-at-work/" class="ext u-like-of" rel="noopener">Making sure you’re not doing everything at work</a></li>
<li><a href="https://kentindell.github.io/2023/04/03/can-injection/" class="ext u-like-of" rel="noopener">CAN Injection: keyless car theft</a></li>
<li><a href="https://abdulapopoola.com/2015/08/23/y-combinator-demystified/" class="ext u-like-of" rel="noopener">How to implement the Y-combinator in JavaScript</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.kuketz-blog.de/empfehlungsecke/" class="ext u-like-of" rel="noopener">kuketz-blog.de/empfehlungsecke</a> (German)</li>
<li><a href="https://quicktype.io" class="ext u-like-of" rel="noopener">quicktype.io</a></li>
<li><a href="https://github.com/hagezi/dns-blocklists" class="ext u-like-of" rel="noopener">github.com/hagezi/dns-blocklists</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Building an Autocomplete Input with Preact</title>
        
          
            <category term="guide" />
          
            <category term="preact" />
          
        
        <link href="https://ttntm.me/blog/building-an-autocomplete-input-with-preact/"/>
        
        <updated>2023-02-17T10:30:00Z</updated>
        <id>https://ttntm.me/blog/building-an-autocomplete-input-with-preact/</id>
        <content type="html"><![CDATA[
          
            
            <p>“Building something with Preact…” is probably going to sound like an unusual topic if you look at my other articles, but let me explain: I consider it a very powerful yet lightweight tool for building stateful/interactive experiences when <a class="ext" href="https://modern-web.dev/guides/going-buildless/getting-started/" rel="noopener">going buildless</a> (together with <a class="ext" href="https://github.com/developit/htm#example" rel="noopener">developit’s htm</a>). I mostly use it for stuff I do at work (forms, data display, mini-SPAs etc.) and I’ve come to appreciate it quite a bit. So, this article won’t be the last one about building stuff with Preact… 🤓</p>
<p>With that out of the way, the topic at hand is an <code>&lt;input&gt;</code> that can support auto-complete/suggestions. I built it as an alternative to a <code>&lt;select&gt;</code> which would have been too restrictive - users should be able to use a list of pre-configured values while retaining the option of entering another value not contained in the list.</p>
<img src="https://ttntm.me/static/img/blog/autocomplete.png" class="img-fluid img-center" alt="Screenshot of a demo app">
<p><strong>Demo</strong>: <a href="http://ttntm.me/demos/pas/" class="int">ttntm.me/demos/pas/</a></p>
<p><em>Hint: type the letter “o” into the</em> <code>&lt;input&gt;</code> <em>to see all available values. Not really intended, but works in this case.</em></p>
<h2 id="setup-configuration-usage" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#setup-configuration-usage"><span>Setup, Configuration &amp; Usage</span></a></h2>
<p>Decidedly simple, as per the “buildless” remark above:</p>
<ul>
<li>An HTML file (<a class="ext" href="https://github.com/ttntm/ttntm/blob/master/src/static/demos/pas/index.html" rel="noopener">index.html</a>) containing a <code>&lt;script type=&quot;module&quot;&gt;</code> tag</li>
<li>A stylesheet (<a class="ext" href="https://github.com/ttntm/ttntm/blob/master/src/static/demos/pas/page.css" rel="noopener">page.css</a>)</li>
<li>A Preact module (<a class="ext" href="https://github.com/ttntm/ttntm/blob/master/src/static/demos/pas/app.js" rel="noopener">app.js</a>)</li>
</ul>
<p>We’re going to focus on <code>app.js</code> for this article.</p>
<p>First things first: import all necessary dependencies - Preact, its hooks package and <code>htm</code>.</p>
<p>Options available as suggestions are defined right at the start:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> AutoSuggestList <span class="token operator">=</span> <span class="token punctuation">[</span><br>  <span class="token string">'Systems Administrator'</span><span class="token punctuation">,</span><br>  <span class="token string">'Front End Developer'</span><span class="token punctuation">,</span><br>  <span class="token string">'Back End Developer'</span><span class="token punctuation">,</span><br>  <span class="token string">'Solution Architect'</span><span class="token punctuation">,</span><br>  <span class="token string">'Consultant'</span><span class="token punctuation">,</span><br>  <span class="token string">'Other'</span><br><span class="token punctuation">]</span></code></pre>
<p>The component wrapper for the <code>&lt;input&gt;</code> and the list of suggestions is called <code>FormInputSuggest</code>. This is its rendering function:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;div class="form-group w-100" style="position: relative;"><br>  &lt;label class="text-muted" for="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>htmlAttrs<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" style="margin-bottom: 0.25rem;"></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>label<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/label><br>  &lt;input onInput="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>onInput<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" onChange="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>onChange<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" ...</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>htmlAttrs<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> /><br>  </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>hasTargetVal <span class="token operator">&amp;&amp;</span> suggestions<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span> <span class="token operator">&amp;&amp;</span> showSuggestions <span class="token operator">?</span> <span class="token punctuation">(</span><br>    html`<span class="token operator">&lt;</span>ul <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"list-suggest"</span><span class="token operator">></span><br>      $<span class="token punctuation">{</span>suggestions<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item<span class="token punctuation">,</span> index</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token keyword">return</span> html<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;li key="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>index<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" class="item-suggest" data-name="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" onClick="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>onSuggestionClick<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"><br>          </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>item<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br>        &lt;/li></span><span class="token template-punctuation string">`</span></span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br>    &lt;/ul></span><span class="token template-punctuation string">`</span></span><br>  <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">}</span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>`</code></pre>
<p>It includes a bunch of props; the most important ones for the functionality are:</p>
<ul>
<li><code>name</code>: maps to a key on the <code>formData</code> state object</li>
<li><code>showSuggestions</code>: a boolean flag that gets passed down from the parent component and controls whether or not suggestions should be shown for the respective component instance</li>
<li><code>suggestionList</code>: the list of suggestion values; see <code>const AutoSuggestList</code> above</li>
<li><code>target</code>: specifies the target state object for read/write operations, <code>formData</code> in this case</li>
<li>The event handlers:
<ul>
<li><code>onChange</code>: makes sure that suggestions are hidden when the associated <code>&lt;input&gt;</code> loses focus</li>
<li><code>onInput</code>: handles writes into the <code>formData</code> state object</li>
<li><code>onSuggestionClick</code>: writes into <code>formData</code> and hides the suggestion list after processing the click event</li>
</ul>
</li>
</ul>
<p>The remaining props are used to control how the <code>&lt;input&gt;</code> renders by setting the text for the label, class names, the <code>required</code> flag etc.</p>
<p>The handlers for <code>change</code> and <code>click</code> events both make use of <code>setTimeout()</code>. This was necessary due to some unpredictable/glitchy behavior: the suggestion list seemed to disappear before the click event was completed, leading to the selected value getting lost. There were also some differences in the click event timings between mouse and touchpad that the timeout smoothed out.</p>
<p>The component handles filtering the available suggestions internally, using a simple <code>Array.prototype.filter()</code> based on its internal <code>hasTargetVal</code> flag:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">let</span> suggestions <span class="token operator">=</span> hasTargetVal<br>    <span class="token operator">?</span> suggestionList<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">item</span> <span class="token operator">=></span> item<span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span>target<span class="token punctuation">[</span>name<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><br>    <span class="token operator">:</span> suggestionList</code></pre>
<p>This check prevents errors that could happen when using the component to add/change <code>formData</code> keys without initializing them first. Something that could happen when adding fields=keys to an object obtained from outside the scope of the app (i.e. from a database), i.e. <code>setFormData({ ...objectFromDB })</code> on initial render.</p>
<p>Component usage in a form is rather simple:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token operator">&lt;</span>$<span class="token punctuation">{</span>FormInputSuggest<span class="token punctuation">}</span><br>  name<span class="token operator">=</span><span class="token string">"role"</span><br>  isRequired<span class="token operator">=</span><span class="token string">"${true}"</span><br>  min<span class="token operator">=</span><span class="token string">"5"</span><br>  label<span class="token operator">=</span><span class="token string">"Job or Role"</span><br>  placeholder<span class="token operator">=</span><span class="token string">"Job/role"</span><br>  showSuggestions<span class="token operator">=</span><span class="token string">"${activeControl === 'role'}"</span><br>  suggestionList<span class="token operator">=</span><span class="token string">"${AutoSuggestList}"</span><br>  target<span class="token operator">=</span><span class="token string">"${formData}"</span><br>  type<span class="token operator">=</span><span class="token string">"text"</span><br>  onChange<span class="token operator">=</span><span class="token string">"${onChangeInputSuggest}"</span><br>  onInput<span class="token operator">=</span><span class="token string">"${handleFormInput}"</span><br>  onSuggestionClick<span class="token operator">=</span><span class="token string">"${onClickInputSuggest}"</span><br><span class="token operator">/</span><span class="token operator">></span></code></pre>
<p>The combination of <code>name</code>, <code>showSuggestions</code>, <code>suggestionList</code> and <code>target</code> make sure that multiple instances of the component can be used from within the same parent (here: <code>const Form()</code>) without any detrimental side effects; for example, when using the component in a form rendered from a JSON config file containing the field definitions.</p>
<p>The (mocked) handling of form submissions and the <code>FormStatus</code> component are essentially just a gimmick because they don’t really add anything to the demo. However, they do complete the picture, so I left them in the code.</p>
<p>Feel free to <a class="ext" href="https://github.com/ttntm/preact-auto-suggest-demo" rel="noopener">fork the demo repository</a> and play around with it. As always, feedback is encouraged and much appreciated - even more so, considering that this is my first article about Preact.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 07, 2023</title>
        
        <link href="https://ttntm.me/likes/2023-w07/"/>
        
        <updated>2023-02-16T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2023-w07/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://crockford.com/javascript/private.html" class="ext u-like-of" rel="noopener">Private Members in JavaScript</a></li>
<li><a href="https://www.zachleat.com/web/react-criticism/" class="ext u-like-of" rel="noopener">A Historical Reference of React Criticism</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://modern-web.dev/guides/going-buildless/getting-started/" class="ext u-like-of" rel="noopener">Going Buildless</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Using a Serverless Function as a Sitemap</title>
        
          
            <category term="guide" />
          
            <category term="serverless" />
          
        
        <link href="https://ttntm.me/blog/using-a-serverless-function-as-a-sitemap/"/>
        
        <updated>2023-01-15T10:30:00Z</updated>
        <id>https://ttntm.me/blog/using-a-serverless-function-as-a-sitemap/</id>
        <content type="html"><![CDATA[
          
            
            <img src="https://ttntm.me/static/img/blog/sitemap.png" class="img-fluid img-center" alt="Illustration of a sitemap">
<p>Having a sitemap might not be an immediate priority when working on a small side project. Even more so, if it’s an SPA with dynamic routes that display content from a database - there’s no SSR at all and no builds/deployments to hook into.</p>
<p>Thinking about the somewhat similar scenario of SSGs and user submitted content (i.e. comments), my first idea of how to tackle this feature was automating builds/deployments on a schedule which would create an opportunity for (re-)creating the sitemap as a part of the build process.</p>
<p>This approach didn’t feel right though, so I started thinking about how to generate the sitemap on demand, leveraging the app’s (serverless) back end.</p>
<p>I eventually came across an old <a class="ext" href="https://answers.netlify.com/t/returning-content-type-xml-from-calling-a-function/1107" rel="noopener">Netlify support topic</a> about returning XML from a serverless function - this looked and felt much better than the “automated rebuilds” approach mentioned above, so I gave it a try.</p>
<h2 id="implementing-the-sitemap" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#implementing-the-sitemap"><span>Implementing the Sitemap</span></a></h2>
<p>The app’s sitemap contains 2 static routes (<code>/home</code> and <code>/about</code>) and an ever-changing amount of user generated content.</p>
<p>The first thing the sitemap function has to take care of is initializing a new XML response with the hard coded static routes. The resulting sitemap template object is split into <code>start/mid/end</code> keys to allow adding further <code>&lt;url&gt;</code> elements before returning the final sitemap:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">initSitemap</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">start</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;?xml version="1.0" encoding="UTF-8"?><br>    &lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token literal-property property">mid</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;url><br>      &lt;loc></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>site<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/&lt;/loc><br>      &lt;changefreq>weekly&lt;/changefreq><br>      &lt;priority>1.0&lt;/priority><br>    &lt;/url>&lt;url><br>      &lt;loc></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>site<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/about&lt;/loc><br>      &lt;changefreq>yearly&lt;/changefreq><br>      &lt;priority>0.7&lt;/priority><br>    &lt;/url></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token literal-property property">end</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;/urlset></span><span class="token template-punctuation string">`</span></span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>Next, the function queries the database for all (published) content, using the same API endpoint that the app itself uses to get its front page content. If there’s a usable response, the function’s code loops through the response, adding the individual content page’s <code>&lt;url&gt;</code> objects to the sitemap:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> sitemap <span class="token operator">=</span> <span class="token function">initSitemap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br><span class="token keyword">const</span> rRequest <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>site<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>readAll<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'GET'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token keyword">let</span> rResponse <span class="token operator">=</span> <span class="token keyword">await</span> rRequest<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br><span class="token keyword">if</span> <span class="token punctuation">(</span>rResponse <span class="token operator">&amp;&amp;</span> rResponse<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  rResponse<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">el</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> itemURL <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>site<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/recipe/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>el<span class="token punctuation">.</span>data<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>el<span class="token punctuation">.</span>ref<span class="token punctuation">[</span><span class="token string">'@ref'</span><span class="token punctuation">]</span><span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><br><br>    sitemap<span class="token punctuation">.</span>mid <span class="token operator">+=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;url><br>      &lt;loc></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>itemURL<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/loc><br>      &lt;changefreq>yearly&lt;/changefreq><br>      &lt;priority>0.7&lt;/priority><br>    &lt;/url></span><span class="token template-punctuation string">`</span></span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>After that’s done, the final sitemap response gets concatenated from the <code>start/mid/end</code> keys of the sitemap template object (via <code>initSitemap()</code> above) and returned by the function:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> sitemapFinal <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>sitemap<span class="token punctuation">.</span>start<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>sitemap<span class="token punctuation">.</span>mid<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>sitemap<span class="token punctuation">.</span>end<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><br><br><span class="token keyword">return</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">headers</span><span class="token operator">:</span> xmlHeaders<span class="token punctuation">,</span><br>  <span class="token literal-property property">body</span><span class="token operator">:</span> sitemapFinal<br><span class="token punctuation">}</span></code></pre>
<p>Finally, don’t forget to add a redirect to your app’s config:</p>
<pre class="language-toml"><code class="language-toml">/sitemap<span class="token punctuation">.</span>xml /<span class="token punctuation">.</span>netlify/functions/sitemap <span class="token number">200</span></code></pre>
<p>That’s it basically; here’s some additional context:</p>
<ul>
<li>The merged PR: <a class="ext" href="https://codeberg.org/ttntm/recept0r/pulls/21/files" rel="noopener">feat #20: sitemap@Codeberg</a></li>
<li>The final function: <a class="ext" href="https://codeberg.org/ttntm/recept0r/src/branch/main/functions/sitemap.js" rel="noopener">sitemap.js</a></li>
</ul>
<p>I’m pretty happy with the result and I have to say that using a(nother) serverless function here feels right to me, certainly much better than the idea of using some sort of automated rebuilds on a schedule just to create an XML file. It’s also in line with the rest of the app’s back end and it properly leverages existing functionality to keep things DRY.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>About the Past Year - 2022</title>
        
          
            <category term="personal" />
          
            <category term="recap" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/about-the-past-year-2022/"/>
        
        <updated>2023-01-03T10:35:00Z</updated>
        <id>https://ttntm.me/blog/about-the-past-year-2022/</id>
        <content type="html"><![CDATA[
          
            
            <p>2nd entry in this series that I’ve decided to call “About the Past Year” from now on.</p>
<p>2022 wasn’t an easy year: we lost a family member to ALS (about 18 months after the diagnosis) and had to cope with loss, an international move and a lot of other things that needed taking care of given this new situation. It’s been getting easier lately, but there’s a lot left to take care of.</p>
<p>Work was really supportive in this situation and I had absolutely no problems working remotely and taking days off as needed. A bunch of new projects started recently though, so I’m hoping for an additional team member within the next few weeks right now.</p>
<p>Anyway, these are some cool things I did last year:</p>
<p><strong>Work</strong></p>
<ul>
<li>Built a transactional SMS sending service with blackout functionality (i.e. considering holidays, weekends, nighttime hours etc. and delaying the messages accordingly)</li>
<li>Worked on a larger Nuxt codebase and built a front end for a tour booking solution</li>
<li>Built an SMS based MFA service with basic session management</li>
<li>Implemented a multichannel personalization tool</li>
<li>Architected and implemented an interface that processes Facebook leads, checks their data in CRM and triggers automated marketing communication (multiple FB pages, one CRM, multiple campaigns in different languages)</li>
<li>Lead <a class="ext" href="https://mjml.io" rel="noopener">MJML</a> adoption in my team</li>
<li>Built a media library SPA with Preact</li>
<li>Switched to Fedora on my work laptop - 0 regrets</li>
</ul>
<p><strong>Other</strong></p>
<ul>
<li>Added support for <code>prefers-color-scheme</code> in <a class="ext" href="http://aitrack.work" rel="noopener">aitrack.work</a> (<a class="ext" href="https://codeberg.org/ttntm/itrack/pulls/20" rel="noopener">PR#20@Codeberg</a>)</li>
<li>Did some theming
<ul>
<li>Firefox: <a class="ext" href="https://github.com/ttntm/minimal-one-line-firefox" rel="noopener">minimal-one-line-firefox</a></li>
<li>cmus: <a class="ext" href="https://github.com/ttntm/cmus-tokyo-night" rel="noopener">Tokyo Night Theme for cmus</a></li>
</ul>
</li>
<li>Implemented drag’n’drop sorting in 2 of my personal projects
<ul>
<li>Also wrote about it recently: <a href="https://ttntm.me/blog/building-an-editable-list-with-drag-and-drop-sorting-in-vue3/" class="int">Building an Editable List with Drag and Drop Sorting in Vue3</a></li>
</ul>
</li>
<li>Managed to beat Malenia in less than 10 attempts</li>
</ul>
<p><strong>Most Significant Growth</strong></p>
<ul>
<li>Personal: quit preemptively helping people that can’t acknowledge that they have a problem (and learning to live with the frustration of things being that way in the first place)</li>
<li>Professional: substituting lacking project management more or less efficiently and learning to tolerate it</li>
</ul>
<p>TBH, not sure if either one of those is necessarily a good thing.</p>
<p><strong>Most Persistent Challenges</strong></p>
<ul>
<li>Personal: learning to cope with loss</li>
<li>Professional: context switching, multitasking, learning not to care too much</li>
</ul>
<p><strong>Best _ of 2022</strong></p>
<ul>
<li>Game: Elden Ring (Stray as a close 2nd)</li>
<li>Movie: Bullet Train</li>
<li>Series: Severance</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Building an Editable List with Drag and Drop Sorting in Vue3</title>
        
          
            <category term="guide" />
          
            <category term="project" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/building-an-editable-list-with-drag-and-drop-sorting-in-vue3/"/>
        
        <updated>2022-12-28T10:30:00Z</updated>
        <id>https://ttntm.me/blog/building-an-editable-list-with-drag-and-drop-sorting-in-vue3/</id>
        <content type="html"><![CDATA[
          
            
            <p>The feature request that got me looking into the use case mentioned in the title popped up as a “convenience feature” when re-writing my <a class="ext" href="https://recept0r.com" rel="noopener">family recipes app</a> last year. I didn’t tackle it immediately within the re-write’s initial scope (i.e. migrating the app from Vue2 to Vue3 + TypeScript), but got it done a couple of months later nevertheless. Here’s a link to the merged pull request if you’re curious: <a class="ext" href="https://codeberg.org/ttntm/recept0r/pulls/18" rel="noopener">add: drag &amp; drop for ingredients</a>.</p>
<p>There’s been a couple of fixes and refactoring efforts since then, most notably <a class="ext" href="https://codeberg.org/ttntm/recept0r/commit/7ab9d99d567d017d05df55025db093cfc21bb48b" rel="noopener">RecipeIngredients &gt;&gt; WritableComputedRef()</a> which I took care of when preparing this article’s demo repository. It’s essentially based on what’s been elaborated in detail for another article, <a href="https://ttntm.me/blog/vue-computed-properties-the-most-important-feature/" class="int">Vue’s Computed Properties - The Most Important Feature?</a>, which I wrote earlier this year.</p>
<p>Anyway, the use case is pretty simple: we needed an editable list (i.e. ingredients required to prepare a meal) that should not only be convenient to use, but also offer sorting. Sorting based on pre-defined criteria and tiny buttons somewhere in the UI did not sound very appropriate, so I looked into drag and drop sorting and immediately liked how it felt elsewhere.</p>
<p>Here’s a screenshot of how it looks in the app:</p>
<img src="https://ttntm.me/static/img/blog/recept0r-edit-recipe.png" class="img-fluid img-center" alt="Screenshot of recept0r.com">
<p>Initial research into “building drag and drop from scratch” was soon deemed pointless (i.e. would take me too much time; feel free to call me lazy) when I remembered <a class="ext" href="https://github.com/SortableJS/vue.draggable.next" rel="noopener">SortableJS’ vuedraggable</a> which I ended up using to implement this feature.</p>
<h2 id="getting-started" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#getting-started"><span>Getting Started</span></a></h2>
<p>Let’s dig in - here are 2 links to get things started:</p>
<ol>
<li>The demo’s GitHub repo: <a class="ext" href="https://github.com/ttntm/vue-dnd-demo" rel="noopener">ttntm/vue-dnd-demo</a></li>
<li>A live sandbox: <a class="ext" href="https://stackblitz.com/github/ttntm/vue-dnd-demo?file=src/App.vue" rel="noopener">stackblitz.com/github/ttntm/vue-dnd-demo?file=src/App.vue</a></li>
</ol>
<p>The setup is pretty straightforward: a parent component (<code>App.vue</code>) is passing the list down to a child component (<code>DndList.vue</code>) that takes care of editing and sorting it. The child emits events (<code>update:list</code>, see <code>defineEmits</code> in line 10) up to the parent whenever the list and/or the list items change.</p>
<p>I’m using a <code>WritableComputedRef()</code> (<code>listItems</code>, line 19) to leverage Vue’s powerful computed getters/setters in my code as well as making them usable for the <code>draggable</code> component’s <code>v-model</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token literal-property property">listItems</span><span class="token operator">:</span> WritableComputedRef<span class="token operator">&lt;</span>SortableEl<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token operator">></span> <span class="token operator">=</span> <span class="token function">computed</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> SortableEl<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token function">objectify</span><span class="token punctuation">(</span>props<span class="token punctuation">.</span>input<span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token function">set</span><span class="token punctuation">(</span>newVal<span class="token operator">:</span> SortableEl<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span><br>    <span class="token function">emit</span><span class="token punctuation">(</span><span class="token string">'update:list'</span><span class="token punctuation">,</span> <span class="token function">valuefy</span><span class="token punctuation">(</span>newVal<span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<ul>
<li><code>get()</code> is processing the incoming list (<code>props.input</code>) and passes it through the function <code>objectify()</code> (line 37) which makes sure that the incoming <code>string[]</code> gets converted to <code>SortableEl[]</code> required by vuedraggable.</li>
<li><code>set()</code> emits the <code>update:list</code> event after converting <code>SortableEl[]</code> back to <code>string[]</code> through the function <code>valuefy()</code> (line 43) which is then caught by the parent component.</li>
</ul>
<h2 id="focus-management" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#focus-management"><span>Focus Management</span></a></h2>
<p>Autofocus of the next list item’s input right after adding it through button use or pressing enter in the previous item’s input is a necessary feature too. The implementation is based on the <code>ref()</code> <code>inputs</code> (line 29) and so-called “Function Refs” (<a class="ext" href="https://vuejs.org/guide/essentials/template-refs.html#function-refs" rel="noopener">Vue docs</a>, essentially “<code>ref</code> within <code>v-for</code>”) managed for <code>&lt;input&gt;</code> elements from within the <code>draggable</code> component’s <code>template</code> (from line 91 onward):</p>
<pre class="language-js"><code class="language-js"><span class="token operator">&lt;</span>template #item<span class="token operator">=</span><span class="token string">"{ element, index }"</span><span class="token operator">></span><br>  <span class="token operator">&lt;</span>li <span class="token operator">:</span><span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"{ 'grabbing' : drag }"</span> <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"flex flex-row items-center border border-transparent px-1 py-2 mb-1"</span><span class="token operator">></span><br>    <span class="token operator">&lt;</span>span <span class="token operator">:</span><span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"{ 'text-gray-900' : drag }"</span> <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"handle mr-2"</span> title<span class="token operator">=</span><span class="token string">"Move element"</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span>GripVertical <span class="token operator">/</span><span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>span<span class="token operator">></span><br>    <span class="token operator">&lt;</span>input type<span class="token operator">=</span><span class="token string">"text"</span><br>      v<span class="token operator">-</span>model<span class="token punctuation">.</span>trim<span class="token operator">=</span><span class="token string">"element.name"</span><br>      <span class="token operator">:</span>placeholder<span class="token operator">=</span><span class="token string">"`Ingredient ${index + 1}`"</span><br>      <span class="token operator">:</span>ref<span class="token operator">=</span><span class="token string">"el => { if (el) inputs[index] = el }"</span><br>      <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"inline-block form-control text-sm"</span><br>      @input<span class="token operator">=</span><span class="token string">"events.onChangeItem"</span><br>      @keydown<span class="token punctuation">.</span>enter<span class="token operator">=</span><span class="token string">"events.onAddItem(index)"</span><br>    <span class="token operator">></span><br>    <span class="token operator">&lt;</span>ButtonX size<span class="token operator">=</span><span class="token string">"20"</span> <span class="token keyword">class</span><span class="token operator">=</span><span class="token string">"rounded-full text-gray-700 hover:text-gray-900 focus:text-gray-900 ml-2"</span> @click<span class="token operator">=</span><span class="token string">"events.onRemoveItem(index)"</span> <span class="token operator">/</span><span class="token operator">></span><br>  <span class="token operator">&lt;</span><span class="token operator">/</span>li<span class="token operator">></span><br><span class="token operator">&lt;</span><span class="token operator">/</span>template<span class="token operator">></span></code></pre>
<p>What’s important here is line 99: <code>:ref=&quot;el =&gt; { if (el) inputs[index] = el }&quot;</code></p>
<p>This is the backbone of focus management from inside <code>events.onAddItem()</code> (line 46):</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token function">onAddItem</span><span class="token punctuation">(</span><span class="token parameter">index<span class="token operator">?</span><span class="token operator">:</span> number</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> currentEl <span class="token operator">=</span> <span class="token keyword">null</span><br>  <span class="token keyword">let</span> inputEls <span class="token operator">=</span> inputs<span class="token punctuation">.</span>value<br>  <span class="token keyword">let</span> listEls <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token operator">...</span>listItems<span class="token punctuation">.</span>value<span class="token punctuation">]</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>index <span class="token operator">!==</span> <span class="token keyword">undefined</span> <span class="token operator">&amp;&amp;</span> index <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    listEls<span class="token punctuation">.</span><span class="token function">splice</span><span class="token punctuation">(</span>index <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> index<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">''</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    listItems<span class="token punctuation">.</span>value <span class="token operator">=</span> listEls<br>    <span class="token keyword">await</span> <span class="token function">nextTick</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>    currentEl <span class="token operator">=</span> inputEls<span class="token punctuation">[</span>index<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">]</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    listItems<span class="token punctuation">.</span>value <span class="token operator">=</span> listEls<span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">id</span><span class="token operator">:</span> listEls<span class="token punctuation">.</span>length<span class="token punctuation">,</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">''</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token keyword">await</span> <span class="token function">nextTick</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>    currentEl <span class="token operator">=</span> inputEls<span class="token punctuation">[</span>inputEls<span class="token punctuation">.</span>length<span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>currentEl<span class="token punctuation">)</span> currentEl<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>We still have to make sure to use <code>await nextTick()</code>, but we’re able to access specific <code>&lt;input&gt;</code> elements which allows focusing the next and/or most recently added list item programmatically (see line 62). As such, using the “Add List Item” button will always focus the added (last) item and using the enter key will automatically focus the item at <code>currentIndex+1</code> no matter which list item input is being used.</p>
<h2 id="everything-else" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#everything-else"><span>Everything Else</span></a></h2>
<p>Everything not mentioned specifically is pretty much “just” implementing vuedraggable - there’s the <code>drag</code> flag in line 18, <code>dragOptions</code> in line 31 and the whole <code>draggable</code> component’s setup starting from line 78. Their <a class="ext" href="https://github.com/SortableJS/vue.draggable.next" rel="noopener">documentation at GitHub</a> was very helpful and I can only recommend it if/when you end up using it in one of your projects.</p>
<p>I hope this article and the demo can provide some insight when working with simple editable lists and maybe offer some motivation to try drag and drop for convenient sorting. Feel free to fork the demo repository and play around with it. As always, feedback is encouraged and much appreciated.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 46, 2022</title>
        
        <link href="https://ttntm.me/likes/2022-w46/"/>
        
        <updated>2022-11-16T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2022-w46/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://lemmy.ml/post/31487" class="ext u-like-of" rel="noopener">lemmy.ml/post/31487</a></li>
<li><a href="https://jasonformat.com/islands-architecture/" class="ext u-like-of" rel="noopener">Islands Architecture</a></li>
<li><a href="https://jasonformat.com/reusable-array-search-predicates/" class="ext u-like-of" rel="noopener">Reusable Array search predicates</a></li>
<li><a href="https://tattoocoder.com/recommending-vscode-extensions-within-your-open-source-projects/" class="ext u-like-of" rel="noopener">Recommending VSCode extensions within your Open Source projects</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://yellowlab.tools" class="ext u-like-of" rel="noopener">yellowlab.tools</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 29, 2022</title>
        
        <link href="https://ttntm.me/likes/2022-w29/"/>
        
        <updated>2022-07-23T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2022-w29/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://austingil.com/resilient-applications-progressive-enhancement/" class="ext u-like-of" rel="noopener">Make Beautifully Resilient Apps With Progressive Enhancement</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://privacy.sexy" class="ext u-like-of" rel="noopener">privacy.sexy</a></li>
<li><a href="https://themarkup.org/blacklight" class="ext u-like-of" rel="noopener">Blacklight - A Real-Time Website Privacy Inspector </a></li>
<li><a href="https://darktheme.club" class="ext u-like-of" rel="noopener">The Darktheme Club</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 05, 2022</title>
        
        <link href="https://ttntm.me/likes/2022-w05/"/>
        
        <updated>2022-02-02T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2022-w05/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.dopefly.com/techblog/405/I-Wrote-My-Own-Vue-js" class="ext u-like-of" rel="noopener">I Wrote My Own Vue.js</a></li>
<li><a href="https://www.tim-kleyersburg.de/articles/from-vue-to-alpinejs" class="ext u-like-of" rel="noopener">From Vue to Alpine.js: Our journey</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://craftinginterpreters.com" class="ext u-like-of" rel="noopener">Crafting Interpreters</a></li>
<li><a href="https://capacitorjs.com/docs" class="ext u-like-of" rel="noopener">Capacitor: Cross-platform Native Runtime for Web Apps</a></li>
<li><a href="https://fullstackopen.com/en/" class="ext u-like-of" rel="noopener">Full Stack open 2021 - Deep Dive Into Modern Web Development</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Vue&#39;s Computed Properties - The Most Important Feature?</title>
        
          
            <category term="thoughts" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/vue-computed-properties-the-most-important-feature/"/>
        
        <updated>2022-01-25T10:30:00Z</updated>
        <id>https://ttntm.me/blog/vue-computed-properties-the-most-important-feature/</id>
        <content type="html"><![CDATA[
          
            
            <p>Coincidences sometimes happen, and last night was one such occasion. I was casually checking my inbox and read Michael Thiessen’s newsletter, one of my favorites when it comes to Vue. This particular issue dealt with Vue’s “most important feature”, computed properties - a statement that I tend to agree with and also one that made me write better code when working on version 2 of <a class="ext" href="https://aitrack.work" rel="noopener">aitrack.work</a>, my FOSS time tracking companion app.</p>
<p>I was implementing drag and drop sorting for the tasks the app uses to track time and I chose <a class="ext" href="https://github.com/SortableJS/vue.draggable.next" rel="noopener">vue.draggable.next</a> for that based on a positive experience I had using it for another project. Getting it to work as intended wasn’t too much trouble, but the resulting code felt a little off somehow. Best to have a look yourself though:</p>
<pre class="language-js"><code class="language-js"><span class="token operator">&lt;</span>script setup<span class="token operator">></span><br>  <span class="token keyword">import</span> <span class="token punctuation">{</span> ref<span class="token punctuation">,</span> watch <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vue'</span><br><br>  <span class="token keyword">const</span> localList <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><br><br>  <span class="token comment">// ...</span><br><br>  <span class="token function">watch</span><span class="token punctuation">(</span>tasklist<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> localList<span class="token punctuation">.</span>value <span class="token operator">=</span> tasklist<span class="token punctuation">.</span>value<span class="token punctuation">)</span><br><br>  <span class="token keyword">const</span> events <span class="token operator">=</span> <span class="token punctuation">{</span><br>    <span class="token function">onDragChange</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">const</span> <span class="token function-variable function">ordered</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">arr</span><span class="token punctuation">)</span> <span class="token operator">=></span> arr<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">el<span class="token punctuation">,</span> index</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        el<span class="token punctuation">.</span>order <span class="token operator">=</span> index<br>        <span class="token keyword">return</span> el<br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>      <span class="token function">setState</span><span class="token punctuation">(</span><span class="token string">'tasklist'</span><span class="token punctuation">,</span> <span class="token function">ordered</span><span class="token punctuation">(</span>localList<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br><br>	<span class="token comment">// ...</span><br>  <span class="token punctuation">}</span><br><br>  localList<span class="token punctuation">.</span>value <span class="token operator">=</span> tasklist<span class="token punctuation">.</span>value<br><span class="token operator">&lt;</span><span class="token operator">/</span>script<span class="token operator">></span></code></pre>
<p>As you can see, there are 3 different parts of code that all take care of setting/updating the <code>localList</code> <code>ref()</code> that’s used by the drag and drop component. It’s based on an array called <code>tasklist</code> that’s obtained (read-only) from the app’s centralized (composition API) store. That works, but it’s prone to errors when making further changes due to the code being spread all over the place. I’m talking about the <a class="ext" href="https://codeberg.org/ttntm/itrack/src/branch/main/src/components/TaskList.vue" rel="noopener">‘TaskList’ component</a> here and that’s basically the centerpiece of the whole application. As such, changes are very likely to occur in the future and the code should be kept as clean and maintainable as possible - one more reason the code shown above felt a little off to me.</p>
<p>Anyway, I left it like that for the day until I read the newsletter mentioned earlier. That got me thinking about the code again and I remembered Vue’s <a class="ext" href="https://v3.vuejs.org/guide/computed.html#computed-setter" rel="noopener">‘Computed Setters’</a> - something that wasn’t even mentioned in the newsletter, but also something I’d occasionally used in the past. With that in mind, the new drag and drop feature was quickly up for refactoring, even before it ever made it to production…</p>
<p>Eventually, this is what I ended up with:</p>
<pre class="language-js"><code class="language-js"><span class="token operator">&lt;</span>script setup<span class="token operator">></span><br>  <span class="token keyword">import</span> <span class="token punctuation">{</span> computed <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vue'</span><br><br>  <span class="token keyword">const</span> localList <span class="token operator">=</span> <span class="token function">computed</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token function-variable function">get</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">[</span><span class="token operator">...</span>tasklist<span class="token punctuation">.</span>value<span class="token punctuation">]</span><span class="token punctuation">,</span><br>    <span class="token function-variable function">set</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">val</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">const</span> <span class="token function-variable function">ordered</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">arr</span><span class="token punctuation">)</span> <span class="token operator">=></span> arr<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">el<span class="token punctuation">,</span> index</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        el<span class="token punctuation">.</span>order <span class="token operator">=</span> index<br>        <span class="token keyword">return</span> el<br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>      <span class="token function">setState</span><span class="token punctuation">(</span><span class="token string">'tasklist'</span><span class="token punctuation">,</span> <span class="token function">ordered</span><span class="token punctuation">(</span>val<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>  <span class="token comment">// ...</span><br><span class="token operator">&lt;</span><span class="token operator">/</span>script<span class="token operator">></span></code></pre>
<p>Now there’s a single <code>computed()</code> call that defines everything that should happen to <code>localList</code> which itself is referenced once in the drag and drop component’s <code>v-model</code> directive. Messing up the code is definitely less likely this way and we’re also making better use of Vue’s internals, i.e. <a class="ext" href="https://v3.vuejs.org/guide/computed.html#computed-caching-vs-methods" rel="noopener">‘Computed Caching’</a> instead of manually taking care of setting and updating the component’s state with <code>watch()</code> when that’s not really necessary. On top of that, it’s less code that’s also much easier to understand compared to the initial version without using <code>computed()</code>.</p>
<p>I’ll end this with the quote from Michael’s newsletter that I shamelessly paraphrased into this little article’s title:</p>
<blockquote>
<p>Computed properties are the most important feature of Vue.<br>
<small>Michael Thiessen</small></p>
</blockquote>
<p>And please don’t get me wrong: I’m not trying to promote someone else and/or their newsletter here, but I think it’s a really helpful resource for people working with Vue, even just to get you to remember about things you might not have considered initially (i.e. computed setters in my case).</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>About the Past Year - 2021</title>
        
          
            <category term="personal" />
          
            <category term="recap" />
          
            <category term="series" />
          
        
        <link href="https://ttntm.me/blog/2021-in-review/"/>
        
        <updated>2022-01-10T10:30:00Z</updated>
        <id>https://ttntm.me/blog/2021-in-review/</id>
        <content type="html"><![CDATA[
          
            
            <p>Despite living with a global pandemic (i.e. “the new normal”), the past year was a rather successful one for me.</p>
<p>I started 2021 with a new (= my current) job as a developer in the Salesforce universe and quickly found my bearings within my team. It’s an interesting position that’s pushed me into a somewhat unexpected full stack direction, meaning that I’ve been working with server side code, SQL databases, APIs and interfaces a lot more than I’d expected. That provides a lot of freedom when building customer specific solutions and learning/practicing something new has also been quite satisfying on a personal level.</p>
<p>It wasn’t all fun and games though, especially when dealing with thousands, tens of thousands or even millions of records on a daily basis. Challenges like debugging legacy code, lack of test instances and monstrous relational databases occasionally lead me to the brink of exhaustion and/or frustration. I suppose that’s quite normal overall and I could always count on the support of my teammates in these few situations.</p>
<p>But enough of what I do at work now. Let’s have a look at what else I’ve managed to do last year. Here’s a somewhat chronological list:</p>
<ul>
<li>Coded and launched <a class="ext" href="http://aitrack.work" rel="noopener">aitrack.work</a> on a weekend in early February. It’s a simple task-based time tracker that I’m using for work every day.</li>
<li><a href="https://ttntm.me/blog/migrating-from-hugo-to-eleventy/" class="int">Migrated my personal website from Hugo to Eleventy</a> in March.</li>
<li>Switched <a class="ext" href="http://watch3r.app" rel="noopener">watch3r.app</a> <a href="https://ttntm.me/blog/switch-vue-cli-to-vite/" class="int">from Vue CLI to Vite</a> in early April, wrote about it and eventually got that article featured in Michael Thiessen’s Vue newsletter (which made me smile).</li>
<li>Used Eleventy to build a <a class="ext" href="https://github.com/ttntm/startpage" rel="noopener">custom browser start page</a> in April.</li>
<li>Built a <a href="https://ttntm.me/blog/track-fauna-metrics-google-sheets/" class="int">serverless reporting service</a> that writes into Google Sheets in May</li>
<li>Gained a better understanding of <a href="https://ttntm.me/blog/using-netlify-functions-as-a-backend/" class="int">architecting serverless back ends</a> with Netlify functions and wrote about it in July</li>
<li>Rewrote my <a class="ext" href="https://codeberg.org/ttntm/recept0r" rel="noopener">personal recipes app</a> from scratch using Vue 3 with TypeScript in August and September. Motivated by how shitty the old code was (one of my first SPAs) and supported by “TypeScript in 50 Lessons”. Looking at the code makes me happier now and I’m not going to avoid adding new features anymore.</li>
<li>Took care of the front end/styling for <a class="ext" href="https://www.ethicsinapps.eu" rel="noopener">Ethics in an App</a> in November/December.</li>
</ul>
<p><em>For what it’s worth, that’s a lot more than I thought I managed to get done.</em></p>
<p>Anyway, I suppose I should stop rambling and wrap this up. So, here’s some final thoughts:</p>
<p><strong>Most Significant Growth</strong></p>
<ul>
<li>Personal: moving from “no” to “maybe” more often and getting it done in half of the estimated time eventually</li>
<li>Professional: contributing to a team and/or larger projects, i.e. writing much more code that can be used and understood by others</li>
</ul>
<p><strong>Most Persistent Challenges</strong></p>
<ul>
<li>Personal: being active, i.e. leaving the notebook/tv/apartment (not kidding)</li>
<li>Professional: context switching and multitasking</li>
</ul>
<p><strong>Best _ of 2021</strong></p>
<ul>
<li>Game: Returnal</li>
<li>Movie: Nobody</li>
<li>Series: Squid Game</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 45, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w45/"/>
        
        <updated>2021-11-11T10:35:00Z</updated>
        <id>https://ttntm.me/likes/2021-w45/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://medium.com/@marcel.leusch/use-vue-3-single-file-components-without-compilation-ac2ccb5f15c2" class="ext u-like-of" rel="noopener">Use Vue 3 Single File Components without compilation</a></li>
<li><a href="https://antfu.me/posts/reimagine-atomic-css" class="ext u-like-of" rel="noopener">Reimagine Atomic</a></li>
<li><a href="https://daverupert.com/2021/09/my-notion-blogging-kanban/" class="ext u-like-of" rel="noopener">My Notion Blogging Kanban</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.freecodecamp.org/news/how-to-start-a-blog-book/" class="ext u-like-of" rel="noopener">How to Start a Blog</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 37, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w37/"/>
        
        <updated>2021-09-20T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w37/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://markus.oberlehner.net/blog/goodbye-webpack-building-vue-applications-without-webpack" class="ext u-like-of" rel="noopener">Building Vue.js Applications Without webpack</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://yomguithereal.github.io/mnemonist/" class="ext u-like-of" rel="noopener">Mnemonist - Curated collection of data structures for the JavaScript language</a></li>
<li><a href="https://www.kuketz-blog.de/firefox-umatrix-firefox-kompendium-teil9/" class="ext u-like-of" rel="noopener">Firefox: uMatrix – Firefox-Kompendium Teil9</a></li>
<li><a href="https://github.com/gorhill/httpswitchboard/wiki/How-to-use-HTTP-Switchboard:-Two-opposing-views" class="ext u-like-of" rel="noopener">How to use HTTP Switchboard: Two opposing views</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 34, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w34/"/>
        
        <updated>2021-08-30T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w34/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.netlify.com/blog/2021/01/29/deep-dive-into-the-vue-composition-apis-watch-method/" class="ext u-like-of" rel="noopener">Deep dive into the Vue Composition API’s watch() method</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://whatdevsneed.com" class="ext u-like-of" rel="noopener">what devs need</a></li>
<li><a href="https://deta.space" class="ext u-like-of" rel="noopener">Deta Space</a>
<ul>
<li><a href="https://docs.deta.sh/docs/home/" class="ext u-like-of" rel="noopener">Docs</a></li>
<li><a href="https://space.deta.dev" class="ext u-like-of" rel="noopener">For developers</a></li>
</ul>
</li>
<li><a href="https://github.com/jorgebucaran/hyperapp" class="ext u-like-of" rel="noopener">Hyperapp</a></li>
<li><a href="https://webcrate.app" class="ext u-like-of" rel="noopener">WebCrate</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 31, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w31/"/>
        
        <updated>2021-08-09T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w31/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://slawkolodziej.com/posts/what-is-function-currying-in-javascript-and-when-to-use-it" class="ext u-like-of" rel="noopener">What is Function Currying in JavaScript and When to Use It</a></li>
<li><a href="https://medium.com/jspoint/typescript-module-system-5022cac310f6" class="ext u-like-of" rel="noopener">A comprehensive guide to “Module System” in TypeScript</a></li>
<li><a href="https://medium.com/jspoint/typescript-type-declaration-files-4b29077c43" class="ext u-like-of" rel="noopener">A quick introduction to “Type Declaration” files and adding type support to your JavaScript packages</a></li>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-create-custom-types-in-typescript" class="ext u-like-of" rel="noopener">How To Create Custom Types in TypeScript</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://adamtheautomator.com/robocopy/" class="ext u-like-of" rel="noopener">The Ultimate Guide to Robocopy</a></li>
<li><a href="https://typescript-exercises.github.io" class="ext u-like-of" rel="noopener">TypeScript Excercises</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 30, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w30/"/>
        
        <updated>2021-08-01T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w30/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://javascript.plainenglish.io/frontend-clean-architecture-ca2592bd9d58" class="ext u-like-of" rel="noopener">Moving Away from React and Vue.js</a></li>
<li><a href="https://goinbigdata.com/using-curl-for-ad-hoc-testing-of-restful-microservices/" class="ext u-like-of" rel="noopener">Using Curl For Ad Hoc Testing Of RESTful Microservices</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.webdesignmuseum.org" class="ext u-like-of" rel="noopener">Web Design Museum</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Using Netlify Functions as a Back End</title>
        
          
            <category term="guide" />
          
            <category term="project" />
          
            <category term="serverless" />
          
        
        <link href="https://ttntm.me/blog/using-netlify-functions-as-a-backend/"/>
        
        <updated>2021-07-24T10:30:00Z</updated>
        <id>https://ttntm.me/blog/using-netlify-functions-as-a-backend/</id>
        <content type="html"><![CDATA[
          
            
            <img src="https://ttntm.me/static/img/blog/backend.png" class="img-fluid img-center" alt="Illustration of a back end">
<p>Serverless functions are great - they provide instant superpowers to front end focused developers and they’re also quite addictive once you’ve gotten into using them. There’s also a ton of tutorials, proof of concept articles etc. that make it really easy to get started.</p>
<p>Many of these resources, including some of my own, are sometimes missing the aspect of security though. That’s probably ok when building a quick demo or proof of concept site, but there’s always a chance of that code ending up in production apps. And when considering that serverless functions work just like any other API’s endpoints, responding to requests and doing CRUD operations with data, security should probably get mentioned a little more often.</p>
<p>For additional context: the following article is based on the notes and resources I collected when refactoring the serverless back end of one of my side projects. It probably won’t end up being a complete tutorial, but it will hopefully provide some helpful information for those looking to secure their serverless functions.</p>
<h2 id="general-remarks" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#general-remarks"><span>General Remarks</span></a></h2>
<p>This article is based on a web application that’s built into the Netlify ecosystem. As such, the findings and practices outlined here might end up being helpful in general, but might not apply 1:1 to other services.</p>
<p>Furthermore, I’d like to point out that a lot of excellent information on the topic of serverless functions can be found here: <a class="ext" href="https://github.com/DavidWells/netlify-functions-workshop" rel="noopener">DavidWells/netlify-functions-workshop</a>. It’s the most complete resource on the topic I came across and I haven’t seen it mentioned quite as much as I thought it deserves to be.</p>
<h2 id="architecting-a-serverless-back-end" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#architecting-a-serverless-back-end"><span>Architecting a Serverless Back End</span></a></h2>
<p>When I launched my application’s first ‘finished version’ (side projects never really are…), I basically had a <code>./functions</code> folder full of individual files that did CRUD operations. It worked, but it was a bit of a mess to work with.</p>
<p>Adopting the <a class="ext" href="https://github.com/DavidWells/netlify-functions-workshop/tree/master/lessons-code-complete/use-cases/1-rest-api" rel="noopener">REST API pattern</a> from the functions workshop mentioned above, I ended up with a central <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/functions/api.js" rel="noopener">api.js function</a>. That’s not just better looking, it also offers some additional benefits:</p>
<ul>
<li>Calls from the front end all hit the same endpoint, eliminating possible errors of sending requests to the wrong endpoint and/or having to keep track of all of them somewhere</li>
<li>User authentication gets verified in one place for all API operations</li>
<li>Unrecognized HTTP methods get rejected before hitting any ‘real’ functionality</li>
<li>Actual functionality is split up into individual files that are much easier to maintain (see: <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/functions/api-methods" rel="noopener">./functions/api-methods</a></li>
<li>If you prefer tidier paths, you could even work with a redirect to change your API’s path from <code>your.app/.netlify/functions/api</code> to <code>your.app/api</code> - see <a class="ext" href="https://twitter.com/Netlify/status/1417937708203249665" rel="noopener">this Tweet</a> for the ‘official’ recommendation</li>
</ul>
<p>I’m probably missing some benefits, but that’s probably already well worth the effort of refactoring - it was to me at least.</p>
<h3 id="users-and-authentication" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#users-and-authentication"><span>Users and Authentication</span></a></h3>
<p>I’m using Netlify Identity for my app’s user accounts. Thanks to that, taking care of protecting my API endpoints is basically just making use of built-in functionality. I’m working with <code>context.clientContext.user</code> which contains the claims of the respective user. <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/functions/api.js#L11-L14" rel="noopener">Checking that in api.js</a> (and any other non-public function that’s used by the app) makes sure that only authenticated users can use the application. See <a class="ext" href="https://github.com/DavidWells/netlify-functions-workshop/tree/master/lessons-code-complete/core-concepts/5-authenication" rel="noopener">Protecting Endpoints</a> for further details and explanations.</p>
<p>PS: you’ll have to make sure that your application <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/src/store/modules/user.js#L167-L178" rel="noopener">takes care of refreshing user tokens</a> (if they’re expired) and that it <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/src/helpers/shared.js#L12-L28" rel="noopener">sends them along with the requests</a> to your API.</p>
<h3 id="headers" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#headers"><span>Headers</span></a></h3>
<p>A good overview: <a class="ext" href="https://security.stackexchange.com/a/147559" rel="noopener">Security Headers for a web API</a></p>
<p>Custom Netlify headers defined in <code>netlify.toml</code> (see: <a href="https://ttntm.me/notes/#18" class="int">Notes#18</a>) will only apply to the HTML responses of requests to your site; functions need to return their own (secure) headers - see: <a class="ext" href="https://answers.netlify.com/t/is-it-possible-to-fetch-a-netlify-function-from-another-domain/26256/5" rel="noopener">Is it possible to fetch a netlify function from another domain</a>.</p>
<p>I’ve opted for adding the basic headers to my functions from a single file, so I can keep things DRY: used as an import from a file like <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/functions/_shared/headers.js" rel="noopener">_shared/headers.js</a>, it can be used easily in your functions:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">const</span> fnHeaders <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'./_shared/headers.js'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token operator">...</span><br><span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>	<span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>	<span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token operator">...</span>fnHeaders <span class="token punctuation">}</span><span class="token punctuation">,</span><br>	<span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>apiData<span class="token punctuation">)</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>That’s making sure that the base headers stay the same while there’s still enough flexibility to add further headers to individual functions if necessary.</p>
<p>A note on Netlify’s event triggered functions (i.e. user signup): they’re secured via JSON web signature (JWS) by default: <a class="ext" href="https://docs.netlify.com/functions/trigger-on-events/" rel="noopener">docs.netlify.com/functions/trigger-on-events/</a>.</p>
<p><strong>Where to check Headers:</strong></p>
<ul>
<li><a class="ext" href="https://observatory.mozilla.org/" rel="noopener">observatory.mozilla.org</a></li>
<li><a class="ext" href="https://securityheaders.com/" rel="noopener">securityheaders.com</a></li>
</ul>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>Working through these notes and transforming a list of bullet points (mostly URLs) into prose might not be up to the same standard as some of the other articles on this site. Nevertheless, I hope that these notes might be useful to someone else out there.</p>
<p>As far as I can tell, following the practices outlined above made my application a better and more secure one than it was before. The code became easier to maintain as well - there’s less repetition (i.e. headers) now and also a reduced risk of errors related to endpoints and user authentication.</p>
<p>Please don’t hesitate and contact me in case there’s something wrong or missing in these notes. 😉</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 29, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w29/"/>
        
        <updated>2021-07-23T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w29/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.louderthanten.com/coax/design-machines" class="ext u-like-of" rel="noopener">Design machines - How to sur­vive the dig­i­tal apocalypse</a></li>
<li><a href="https://partlycloudy.blog/2020/06/05/wsl2-making-windows-10-the-perfect-dev-machine/" class="ext u-like-of" rel="noopener">WSL2: Making Windows 10 the perfect dev machine</a></li>
<li><a href="https://betterprogramming.pub/javascript-design-patterns-25f0faaaa15" class="ext u-like-of" rel="noopener">JavaScript Design Patterns</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://github.com/sileshn/ManjaroWSL" class="ext u-like-of" rel="noopener">ManjaroWSL</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 27, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w27/"/>
        
        <updated>2021-07-12T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w27/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://lachlan-miller.me/articles/vue-3-reactivity-from-scratch" class="ext u-like-of" rel="noopener">Building Vue 3 Reactivity from Scratch</a></li>
<li><a href="https://www.forbes.com/sites/zakdoffman/2021/07/03/stop-using-google-chrome-on-your-iphone-android-mac-and-pc-after-privacy-backlash/" class="ext u-like-of" rel="noopener">Why Delayed Google Chrome Update Is A Reason To Switch</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 25, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w25/"/>
        
        <updated>2021-06-27T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w25/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.freecodecamp.org/news/learn-typescript-basics/" class="ext u-like-of" rel="noopener">Learn TypeScript Basics in this Beginner’s Guide</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://d3ward.github.io/toolz/src/adblock.html" class="ext u-like-of" rel="noopener">Ad Blocker Test</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 24, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w24/"/>
        
        <updated>2021-06-20T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w24/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://hackwild.com/article/web-worker-timers/" class="ext u-like-of" rel="noopener">More Accurate JavaScript Timers with Web Workers</a></li>
<li><a href="https://mikerogers.io/2021/05/12/moving-from-tailwind-to-css-variables" class="ext u-like-of" rel="noopener">Moving From Tailwind To CSS Variables</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://ojoy.zaps.dev" class="ext u-like-of" rel="noopener">Ojoy - image upscaling</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 23, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w23/"/>
        
        <updated>2021-06-13T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w23/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://www.blackdragon.se/index.php/articles/87-google-pixel-4a-5g-and-grapheneos" class="ext u-like-of" rel="noopener">Google Pixel 4A-5G and GrapheneOS</a></li>
<li><a href="https://mizik.sk/blog/howto-degoogle-your-android-phone/" class="ext u-like-of" rel="noopener">Howto degoogle your Android phone</a></li>
<li><a href="https://calendar.perfplanet.com/2020/html-and-css-techniques-to-reduce-your-javascript/" class="ext u-like-of" rel="noopener">HTML and CSS techniques to reduce your JavaScript</a></li>
<li><a href="https://digiday.com/marketing/as-ad-tech-firms-test-ways-to-connect-googles-floc-to-other-data-privacy-watchers-see-fears-coming-true/" class="ext u-like-of" rel="noopener">As ad tech firms test ways to connect Google’s FLoC to other data, privacy watchers see fears coming true</a></li>
<li><a href="https://austingil.com/watching-changes-vue-js-component-slot-content/?ck_subscriber_id=860809457" class="ext u-like-of" rel="noopener">Watching for Changes in Vue.js Component Slot Content</a></li>
<li><a href="https://gomakethings.com/dont-disable-buttons-while-submitting-forms-with-ajax/" class="ext u-like-of" rel="noopener">Don’t disable buttons while submitting forms with ajax</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://www.cardesignhistory.com" class="ext u-like-of" rel="noopener">Car Design History</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 22, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w22/"/>
        
        <updated>2021-06-06T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w22/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://css-tricks.com/what-i-learned-building-a-word-game-app-with-nuxt-on-google-play/" class="ext u-like-of" rel="noopener">What I Learned Building a Word Game App With Nuxt on Google Play</a></li>
<li><a href="https://techika.com/2021/05/29/build-hackernews-reader-vuejs-tailwind-p3/" class="ext u-like-of" rel="noopener">Build the HackerNews Reader with VueJS 3 — Part 3, infinite loading with ES2018 syntax</a><br>
Check the infinite scroll implementation</li>
<li><a href="https://blog.asayer.io/discovering-vue-composition-api-with-examples" class="ext u-like-of" rel="noopener">Discovering Vue Composition API with examples</a><br>
Vue + TypeScript</li>
<li><a href="https://www.maxpou.fr/vue-js-module-architecture" class="ext u-like-of" rel="noopener">Vue.js: Let’s Scale!</a></li>
<li><a href="https://chriscoyier.net/2021/06/04/40-for-40/" class="ext u-like-of" rel="noopener">40 for 40</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Week 21, 2021</title>
        
        <link href="https://ttntm.me/likes/2021-w21/"/>
        
        <updated>2021-05-30T10:30:00Z</updated>
        <id>https://ttntm.me/likes/2021-w21/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="articles" tabindex="-1"><a class="header-anchor int" href="#articles"><span>Articles</span></a></h2>
<ul>
<li><a href="https://garba.org/posts/2021/uml/" class="ext u-like-of" rel="noopener">Has UML died without anyone noticing?</a></li>
</ul>
<h2 id="links" tabindex="-1"><a class="header-anchor int" href="#links"><span>Links</span></a></h2>
<ul>
<li><a href="https://stackoverflow.com/a/44078785" class="ext u-like-of" rel="noopener">stackoverflow.com/a/44078785</a><br>
Needed somewhat reliable IDs in JS this week and ended up there</li>
<li><a href="https://reactbootcamp.com/react-cheatsheet-2021/" class="ext u-like-of" rel="noopener">React Cheatsheet for 2021</a><br>
Working with Preact more and more these days, so this might come in handy</li>
<li><a href="https://www.mailmask.me" class="ext u-like-of" rel="noopener">mailmask.me</a><br>
Unique and random forwarding addresses</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>How to Track Fauna Metrics in Google Sheets</title>
        
          
            <category term="database" />
          
            <category term="guide" />
          
            <category term="serverless" />
          
        
        <link href="https://ttntm.me/blog/track-fauna-metrics-google-sheets/"/>
        
        <updated>2021-05-10T10:30:00Z</updated>
        <id>https://ttntm.me/blog/track-fauna-metrics-google-sheets/</id>
        <content type="html"><![CDATA[
          
            
            <p>It’s been about 6 months since I built and launched <a class="ext" href="https://watch3r.app" rel="noopener">WATCH3R, an open source movie journal</a> that’s using <a class="ext" href="https://fauna.com" rel="noopener">Fauna</a> as its database. I kept (and I am keeping it) small, so the amount of users and the overall database size is rather manageable. However, the user base is slowly growing and I’ve become interested in observing the corresponding growth of the application’s data over time.</p>
<p>Focusing my development efforts primarily on adding features and functionality, I took care of the data growth observation manually so far (if and when I had the time to think about that). I had nowhere to track my findings either, so what I ended up doing was mostly just looking at my app’s data curiously, thinking things like “aah nice, …” whenever I discovered at a bunch of new rows in a collection (= database table).</p>
<p>Eventually, the idea of tracking these observations somewhere consistently matured and I quickly decided that Google Sheets would be a suitable tool to collect the data. I also thought that it’d be nice to automate this data collection process somehow, so I don’t have to take care of it manually.</p>
<p>I proceeded with some research and finished building an automated reporting service 2 evenings later. This service now queries the application’s database for metrics and writes them into Google Sheets for tracking and analysis. It’s scheduled to run automatically once a week, so I can’t forget about it; in fact, I <em>can</em> actually forget about it and it still does its job.</p>
<p>But let’s get started on the details now: this article is going to provide a high level overview first, followed by implementation details offering code snippets, explanations and links to resources I found helpful.</p>
<h2 id="service-overview" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#service-overview"><span>Service Overview</span></a></h2>
<p>Let’s start with a flowchart:</p>
<img src="https://ttntm.me/static/img/blog/ServiceArchitecture.jpg" class="img-fluid img-center auto-invert" alt="Flowchart showing the service architecture">
<p>Based on a schedule, automated <code>POST</code> requests to the URL the service runs at happens. These requests contain an <code>action</code> that determines what logic (database queries, etc.) the service will execute. Finally, the queried information gets transformed and written into a spreadsheet for tracking and further analysis.</p>
<p>From an architectural point of view, the service itself is making use of multiple (micro-) services to get things done. That’s not exactly keeping things simple, but it’s an approach that’s powerful, modular and flexible enough for future growth.</p>
<p>The tools I used to automate the reporting process are:</p>
<ol>
<li>IFTTT for scheduling and making the <code>POST</code> requests</li>
<li><a class="ext" href="https://functions.netlify.com/playground/" rel="noopener">Netlify Functions</a> as host and runtime environment</li>
<li>A Google Spreadsheet as target to collect the reporting data in</li>
</ol>
<p>The following sections will provide details regarding the setup of each one of these services. The finished code for the reporting service can be found in step 5, but I’m linking it here as well - just in case you’d want to have a look at it right away and/or keep it open while reading on: <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/functions/get-app-stats.js" rel="noopener">get-app-stats.js@Codeberg</a></p>
<h2 id="step-1-serverless-what-else" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#step-1-serverless-what-else"><span>Step 1: Serverless. What Else?</span></a></h2>
<p>My application’s back end functionality was already using Netlify’s functions and their identity management service, so deploying the reporting service as a(nother) serverless function was both easy and convenient at the same time; especially considering that the service should be a part of the existing codebase and that it also makes use of some of the existing environment variables.</p>
<p>It may not be obvious immediately, but a serverless function can be used like any other API endpoint once deployed to an URL like <code>https://app.io/my-function</code>. That means that your functions = endpoints can respond to any incoming HTTP requests (like <code>POST</code>), but it also implies that you should probably take good care of securing them (see <a class="ext" href="https://github.com/DavidWells/netlify-functions-workshop/tree/master/lessons-code-complete/core-concepts/5-authenication" rel="noopener">this tutorial</a> for example).</p>
<p>The serverless function I set up as the service’s shell looks like this:</p>
<pre class="language-js"><code class="language-js">exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context<span class="token punctuation">,</span> callback</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>	<span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">faunadb<span class="token punctuation">.</span>Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">secret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FAUNA_SECRET</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>body<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// get data from request payload</span><br>  <span class="token keyword">const</span> q <span class="token operator">=</span> faunadb<span class="token punctuation">.</span>query<span class="token punctuation">;</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>httpMethod <span class="token operator">!==</span> <span class="token string">'POST'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">405</span><span class="token punctuation">,</span> <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">'Method Not Allowed'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>data<span class="token punctuation">.</span>action <span class="token operator">||</span> data<span class="token punctuation">.</span>grant <span class="token operator">!==</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">GRANT</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">400</span><span class="token punctuation">,</span> <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">'Bad Request'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token comment">// actual service logic here</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>It’s accepting <code>POST</code> requests exclusively, responding with a 405 error otherwise. That’s explicitly intended; I’m going to make use of <code>POST</code> requests to the reporting service’s URL when taking care of automating it in step 5 of this article.</p>
<p>The function also checks for <code>data.action</code> and <code>data.grant</code>, both values that should be coming in as the request’s payload. <code>action</code> is required to tell the service what to do while <code>grant</code> is a security measure that makes sure that only authenticated requests make it through to the service’s actual functionality.</p>
<h2 id="step-2-query-the-metrics-out-of-fauna" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#step-2-query-the-metrics-out-of-fauna"><span>Step 2: Query the Metrics out of Fauna</span></a></h2>
<p>The data you’re going to use for your own reporting service will depend on your database configuration and the respective use case/s - I went for the following two metrics:</p>
<ol>
<li>Collection growth over time</li>
<li>Collection growth per user account</li>
</ol>
<p>I recommend developing and testing your queries in Fauna’s web shell, at least until you’re satisfied with the result. That makes it much easier to debug the code compared to running it inside a serverless function right away.</p>
<p><a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/collections" rel="noopener">Collections</a> are Fauna’s equivalent of tables, essentially containers that store your application’s data. Observing their size provides sufficient information for my rather simple use case that you could sum up as: “I’d like to watch my application grow over time”.</p>
<p>Getting the collection sizes is not very complicated; there’s only 2 lists (= collections with user created content) in the application and there’s a handy <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/functions/count" rel="noopener">Count</a> function. I’m using that function to get the amount of items in each collection with the help of an <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/indexes" rel="noopener">index</a>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">getIndexItemCount</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">indexName</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> req <span class="token operator">=</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Count</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Match</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Index</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>indexName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">let</span> response <span class="token operator">=</span> <span class="token keyword">await</span> req<span class="token punctuation">;</span><br>  <span class="token keyword">return</span> response<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>The reporting service receives a number from this function which will then be used to create a new row in the Google Spreadsheet.</p>
<p>Querying the collection growth per user is a little more complex - it requires iterating over a list of active user accounts and performing a <code>Count()</code> like above for each one of the respective account’s lists.</p>
<p>I experimented with a bunch of different approaches (based on <a class="ext" href="https://docs.fauna.com/fauna/current/start/fql_for_sql_users" rel="noopener">SQL operations</a>…) but I couldn’t really get it to work properly until I came across this discussion: <a class="ext" href="https://forums.fauna.com/t/sql-group-by-having-counterpart-in-faunadb/434" rel="noopener">SQL group by, having counterpart in Fauna</a>. It turned out to be an extremely helpful resource that made me get a better understanding of how to approach a query like this in FQL. This is the function I put together based on the linked discussion:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> <span class="token function-variable function">getUserListSizes</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> req <span class="token operator">=</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span><br>    q<span class="token punctuation">.</span><span class="token function">Map</span><span class="token punctuation">(</span><br>      q<span class="token punctuation">.</span><span class="token function">Paginate</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Match</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Index</span><span class="token punctuation">(</span><span class="token string">'allUsersById'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>      q<span class="token punctuation">.</span><span class="token function">Lambda</span><span class="token punctuation">(</span><br>        <span class="token punctuation">[</span><span class="token string">'userId'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>        q<span class="token punctuation">.</span><span class="token function">Let</span><span class="token punctuation">(</span><br>          <span class="token punctuation">{</span><br>            <span class="token literal-property property">tracklistCount</span><span class="token operator">:</span> q<span class="token punctuation">.</span><span class="token function">Count</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Match</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Index</span><span class="token punctuation">(</span><span class="token string">'tracklist_user'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> q<span class="token punctuation">.</span><span class="token function">Var</span><span class="token punctuation">(</span><span class="token string">'userId'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>            <span class="token literal-property property">watchlistCount</span><span class="token operator">:</span> q<span class="token punctuation">.</span><span class="token function">Count</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Match</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Index</span><span class="token punctuation">(</span><span class="token string">'watchlist_user'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> q<span class="token punctuation">.</span><span class="token function">Var</span><span class="token punctuation">(</span><span class="token string">'userId'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>          <span class="token punctuation">}</span><span class="token punctuation">,</span><br>          <span class="token punctuation">{</span><br>            <span class="token literal-property property">User</span><span class="token operator">:</span> q<span class="token punctuation">.</span><span class="token function">Var</span><span class="token punctuation">(</span><span class="token string">'userId'</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>            <span class="token literal-property property">Tracklist</span><span class="token operator">:</span> q<span class="token punctuation">.</span><span class="token function">Var</span><span class="token punctuation">(</span><span class="token string">'tracklistCount'</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>            <span class="token literal-property property">Watchlist</span><span class="token operator">:</span> q<span class="token punctuation">.</span><span class="token function">Var</span><span class="token punctuation">(</span><span class="token string">'watchlistCount'</span><span class="token punctuation">)</span><br>          <span class="token punctuation">}</span><br>        <span class="token punctuation">)</span><br>      <span class="token punctuation">)</span><br>    <span class="token punctuation">)</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">let</span> response <span class="token operator">=</span> <span class="token keyword">await</span> req<span class="token punctuation">;</span> <span class="token comment">// returns a response like this: '{ data: Object[] }'</span><br>  <span class="token keyword">return</span> response<span class="token punctuation">.</span>data<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>The query works similar to JavaScript’s <a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map" rel="noopener">map()</a> Array method: it’s based on the index <code>allUsersById</code> that contains all active user accounts. It then runs a <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/functions/lambda" rel="noopener">Lambda</a> (= an anonymous function that executes custom code) on each item = user ID in the index and finally returns an array of objects. Inside the Lambda, <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/functions/let" rel="noopener">Let</a> takes care of binding multiple <code>Count()</code> statements to a single response object. Doing this makes sure that the query returns one distinct object = table row per user ID and that this row contains the correct count = size for both lists.</p>
<h2 id="step-3-hello-google-sheets-api" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#step-3-hello-google-sheets-api"><span>Step 3: Hello Google Sheets API</span></a></h2>
<p>Access to Google Sheets from within the reporting service is based on this API wrapper for JavaScript: <a class="ext" href="https://github.com/theoephraim/node-google-spreadsheet" rel="noopener">github/theoephraim/node-google-spreadsheet</a>. I’ve also found a lot of useful information on this topic in an article called <a class="ext" href="https://www.swyx.io/netlify-google-sheets/" rel="noopener">Google Sheets v4 API with Netlify Dev</a> that’s also based on this API wrapper.</p>
<p>Authentication is the first thing to rake care of. I’ve used the recommended approach of <a class="ext" href="https://theoephraim.github.io/node-google-spreadsheet/#/getting-started/authentication?id=service-account" rel="noopener">connecting to the Google Sheets API as a service account</a> and it worked right away.</p>
<p>Make sure you take good care of your service account’s credentials and don’t forget to add them to the Netlify UI (in case you’re using that). Contrary to Shawn’s linked article, I dind’t use a separate <code>.env</code> file in the function’s directory - I’ve added the necessary values for authentication to my application’s existing (local) <code>.env</code> file instead.</p>
<p>Let’s have a look at the service’s code now:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> doc <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">GoogleSpreadsheet</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">GOOGLE_SPREADSHEET_ID</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token comment">// authenticate as Service Account</span><br><span class="token keyword">await</span> doc<span class="token punctuation">.</span><span class="token function">useServiceAccountAuth</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token literal-property property">client_email</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">GOOGLE_SERVICE_ACCOUNT</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">private_key</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">GOOGLE_PRIVATE_KEY</span><span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\\n</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">'\n'</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">await</span> doc<span class="token punctuation">.</span><span class="token function">loadInfo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// loads document properties and worksheets; required.</span></code></pre>
<p>These lines take care of initializing and authenticating the API connection with the service account’s credentials. The <code>private_key</code> is matched against a RegEx to process the line breaks; that’s taken 1:1 from Shawn’s article and might have contributed to things working properly right away (I didn’t test for different outcomes when removing it).</p>
<p>With the API set up, the reporting service’s code proceeds into a <code>switch()</code> statement based on the incoming request’s <code>action</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">switch</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>action<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">case</span> <span class="token string">'indexSize'</span><span class="token operator">:</span><br>    <span class="token keyword">const</span> listGrowthSheet <span class="token operator">=</span> doc<span class="token punctuation">.</span>sheetsByIndex<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>    <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> listname <span class="token keyword">of</span> lists<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">let</span> addedRow <span class="token operator">=</span> <span class="token keyword">await</span> listGrowthSheet<span class="token punctuation">.</span><span class="token function">addRow</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>        <span class="token literal-property property">Date</span><span class="token operator">:</span> dateVal<span class="token punctuation">,</span><br>        <span class="token literal-property property">List</span><span class="token operator">:</span> listname<span class="token punctuation">,</span><br>        <span class="token literal-property property">ListItemCount</span><span class="token operator">:</span> <span class="token keyword">await</span> <span class="token function">getIndexItemCount</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>listname<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">_all</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      logger<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">row</span><span class="token operator">:</span> addedRow<span class="token punctuation">.</span>_rowNumber<span class="token punctuation">,</span> <span class="token literal-property property">data</span><span class="token operator">:</span> addedRow<span class="token punctuation">.</span>_rawData <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>    <span class="token keyword">return</span> <span class="token function">returnVal</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">case</span> <span class="token string">'userListSize'</span><span class="token operator">:</span><br>    <span class="token keyword">const</span> listSizeUserSheet <span class="token operator">=</span> doc<span class="token punctuation">.</span>sheetsByIndex<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> userListSizes <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getUserListSizes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>		<span class="token comment">// calculate length in advance for slightly better performance</span><br>		<span class="token comment">//see: https://stackoverflow.com/a/17989524</span><br>    <span class="token keyword">let</span> len <span class="token operator">=</span> userListSizes<span class="token punctuation">.</span>length<span class="token punctuation">;</span><br>    <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> len<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      userListSizes<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>Date <span class="token operator">=</span> dateVal<span class="token punctuation">;</span><br>      <span class="token keyword">let</span> addedRow <span class="token operator">=</span> <span class="token keyword">await</span> listSizeUserSheet<span class="token punctuation">.</span><span class="token function">addRow</span><span class="token punctuation">(</span>userListSizes<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      logger<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">row</span><span class="token operator">:</span> addedRow<span class="token punctuation">.</span>_rowNumber<span class="token punctuation">,</span> <span class="token literal-property property">data</span><span class="token operator">:</span> addedRow<span class="token punctuation">.</span>_rawData <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>    <span class="token keyword">return</span> <span class="token function">returnVal</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">default</span><span class="token operator">:</span><br>    <span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">400</span><span class="token punctuation">,</span> <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">"Unknown action, aborting."</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>The database queries from step 2 get executed from inside the <code>for(listname of lists)</code> loop in case of the <code>indexSize</code> action or provide input for another <code>for</code>-loop in case of the <code>userListSize</code> action. The latter <code>for</code> is written using older syntax for the sake of slightly better performance (see: <a class="ext u-in-reply-to" href="https://www.incredible-web.com/blog/performance-of-for-loops-with-javascript/" rel="noopener">Performance of for loops with JavaScript</a>).</p>
<p>Both <code>listGrowthSheet</code> and <code>listSizeUserSheet</code> represent individual sheets in the spreadsheet accessed via their index (<a class="ext" href="https://theoephraim.github.io/node-google-spreadsheet/#/?id=the-basics" rel="noopener">other options</a>). The API method <code>addRow()</code> then uses these variables to write the data into a new row of the respective sheet.</p>
<p><code>logger</code> and <code>returnVal()</code> are ordinary abstractions for code shared across the different <code>action</code> routes inside the <code>switch()</code> statement.</p>
<h2 id="step-4-testing-the-service" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#step-4-testing-the-service"><span>Step 4: Testing the Service</span></a></h2>
<p>The code snippets in the previous sections left out a bunch of things, so you best head over to <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/functions/get-app-stats.js" rel="noopener">get-app-stats.js@Codeberg</a> to have a look at the complete code of the service - just in case you haven’t done that already.</p>
<p>I definitely recommend testing the service locally before deploying it to your site - I’m using <a class="ext" href="https://github.com/netlify/cli" rel="noopener">netlify-cli</a> for most of my projects which makes functions run locally thanks to <a class="ext" href="https://cli.netlify.com/netlify-dev" rel="noopener">Netlify Dev</a>. It’s a great tool that makes testing and debugging serverless functions much easier.</p>
<p>Once you’ve got a local development server running, you can use <code>cURL</code> to make <code>POST</code> requests to the new function like this:</p>
<pre class="language-bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-X</span> POST <span class="token parameter variable">-H</span> <span class="token string">"Content-Type: application/json"</span> <span class="token parameter variable">-d</span> <span class="token string">'{"action": "test", "grant": "1234"}'</span> http://localhost:8888/.netlify/functions/my-function</code></pre>
<p>If you stuck with the verbosity of my code, you should see the values stored in the <code>logger</code> variable (or some error) showing up in your terminal window while the function execution gets logged by the running <code>netlify dev</code> process in another terminal window (i.e. the one in VS Code).</p>
<h2 id="step-5-automating-requests" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#step-5-automating-requests"><span>Step 5: Automating Requests</span></a></h2>
<p>There’s many ways of sending automated <code>POST</code> requests to an endpoint using its URL. Some of the options I considered before making a decision were:</p>
<ul>
<li>GitHub actions; something like <a class="ext" href="https://github.com/marketplace/actions/fetch-api-data" rel="noopener">this</a> on an <a class="ext" href="https://docs.github.com/en/actions/reference/events-that-trigger-workflows" rel="noopener">event that triggers a workflow</a>
<ul>
<li>Not predicatable enough, in terms of a recurring schedule</li>
</ul>
</li>
<li>IFTTT
<ul>
<li>Max. of 3 applets = automations for free accounts</li>
</ul>
</li>
<li>Netlify’s <a class="ext" href="https://docs.netlify.com/site-deploys/notifications/#outgoing-webhooks" rel="noopener">outgoing webhooks</a>
<ul>
<li>Does <em>not</em> allow a custom paypload</li>
</ul>
</li>
<li>Zapier
<ul>
<li>Triggering webhooks is a premium feature</li>
</ul>
</li>
</ul>
<p>I chose IFTTT - their free features (max. 3 applets with max. 2 steps each) are enough to send 2 payloads (one for each <code>action</code> the service currently performs) to my service every week. I can also use my third free applet for another request in the future, which is cool too.</p>
<p>Working with IFTTT is pretty simple, so I’m not going to cover that in depth here. You basically chose a trigger (a schedule for example) for your applet = workflow and configure a webhook to make a <code>POST</code> request (incl. a payload like you did with <code>cURL</code>) to the service’s URL.</p>
<h2 id="summary-and-findings" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#summary-and-findings"><span>Summary and Findings</span></a></h2>
<p>With the final step of automation taken care of, your reporting service should work just like mine now. It’ll respond to incoming requests and contribute to filling up your Google spreadsheet with valuable information obtained from your database.</p>
<p>Building this little reporting service was fun and it made me realize once more how powerful serverless (micro-) service architectures can be. There were no mayor headaches and I managed to proceed from an idea to a working service rather quickly.</p>
<p>Using Netlify and its serverless functions is always a great choice that offers a lot of convenience that’s far too easy to get used to. I have to admit though that I don’t think that their competitors are any less capable of providing a similar experience - I have yet to give Vercel a try, but I already implemented similar architectures successfully with Azure.</p>
<p>Another thing I definitely have to keep in mind is how powerful Fauna’s FQL can be. It’ll take me a while to learn it properly (instead of bit by bit as I have until now), but I think it will be well worth it to get out of the SQL comfort zone a bit more in the future.</p>
<p>In conclusion, building this little reporting service based on a spontaneous “need” (if you can even call it that…) taught me a bit more about how different services can work together and will certainly set the bar higher for the next time I’ll come across a mundane use case that could be automated somehow.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Switching from Vue CLI to Vite</title>
        
          
            <category term="guide" />
          
            <category term="project" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/switch-vue-cli-to-vite/"/>
        
        <updated>2021-04-04T10:30:00Z</updated>
        <id>https://ttntm.me/blog/switch-vue-cli-to-vite/</id>
        <content type="html"><![CDATA[
          
            
            <p>I used Vite to build <a class="ext" href="http://aitrack.work" rel="noopener">aitrack.work</a> about 2 months ago and it left me with a really solid experience.  Since then, launching any Vue CLI dev server felt painfully slow; this feeling kept getting worse over time, leading to the idea of switching a bigger project over to Vite in order to get some comparable before/after data.</p>
<p>Considering that I wasn’t using any Vue CLI specific (Webpack) features for a <a href="https://ttntm.me/blog/watch3r-movie-watchlist-journal-app/" class="int">movie journal app</a> that I launched last year, the choice of using that project as a test case for the switch of tools was an easy one.</p>
<p>We’ll start looking at the numbers before heading into the <em>how</em>; the following numbers are based on Netlify’s deployment logs and present the same set of features and functionality (= same source code) with different build tooling.</p>
<h3 id="vue-cli" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vue-cli"><span>Vue CLI</span></a></h3>
<ul>
<li>Build &amp; deploy time (average of the last 5 builds): <strong>51,4 seconds</strong></li>
<li>Built files (CSS &amp; JS): <strong>42</strong></li>
<li>Built files size: <strong>460,65 KB</strong></li>
</ul>
<h3 id="vite" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vite"><span>Vite</span></a></h3>
<ul>
<li>Build &amp; deploy time (average of the last 5 builds): <strong>40,8 seconds</strong></li>
<li>Built files (CSS &amp; JS): <strong>28</strong></li>
<li>Built files size: <strong>445,69 KB</strong></li>
</ul>
<p>These numbers don’t show a very drastic difference, but Vite builds the app about 10 seconds faster, produces less files and has better compression. Here’s some additional details:</p>
<ul>
<li>As far as I could tell from the logs, the Vue CLI build <em>does not</em> include the app’s locally sourced font files in the reported numbers while Vite’s build log does</li>
<li>Vue CLI uses <em>gzip</em> (71,8 KB → 25,76 KB = 35,87%) for compression, Vite uses <em>brotli</em> (188,71 KB → 53,6 KB  = 28,4%)</li>
<li>Less files = less HTTP requests; both tools built a bunch of sub 5 KB files, but Vite just built less of them (might be due to CSS; details below)</li>
</ul>
<p>These number based on rather small sample sizes certainly aren’t worth much scientifically, but I found them interesting enough to share anyway. Enough about numbers though, let’s have a look at what I did in order to switch from Vue CLI to Vite now.</p>
<h2 id="installation-and-configuration" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#installation-and-configuration"><span>Installation and Configuration</span></a></h2>
<p>There’s 2 packages you’ll have to install for a Vue project:</p>
<ul>
<li><code>vite</code></li>
<li><code>@vitejs/plugin-vue</code></li>
</ul>
<p>With that out of the way, you’ll want to create a <code>vite.config.js</code> in your project root directory. See <a class="ext" href="https://vitejs.dev/config/" rel="noopener">Configuring Vite</a> for config options; my very minimal config file looks like this:</p>
<pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> defineConfig <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'vite'</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> vue <span class="token keyword">from</span> <span class="token string">'@vitejs/plugin-vue'</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">defineConfig</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token literal-property property">build</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">cssCodeSplit</span><span class="token operator">:</span> <span class="token boolean">false</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">optimizeDeps</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">include</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>      <span class="token string">'vue'</span><span class="token punctuation">,</span><br>      <span class="token string">'vue-router'</span><span class="token punctuation">,</span><br>      <span class="token string">'vuex'</span><span class="token punctuation">,</span><br>      <span class="token comment">// etc.</span><br>    <span class="token punctuation">]</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">plugins</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token function">vue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  <span class="token punctuation">]</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>You’ll also have to move your <code>index.html</code> from <code>./public/</code> to the project root directory; see <a class="ext" href="https://vitejs.dev/guide/#index-html-and-project-root" rel="noopener">vitejs.dev/guide</a> for details on that.</p>
<p>Next thing to take care of: <strong>environment variables</strong>. See <a class="ext" href="https://vitejs.dev/guide/env-and-mode.html" rel="noopener">vitejs.dev/guide/env-and-mode</a> for details on that and change your code accordingly. Be careful when using search and replace - I’m using a bunch of environment variables in the app’s serverless functions and there’s no Vite there (duh) - make sure you don’t update those accidentally.</p>
<p>In case you previously used Webpack-specific <strong>component imports</strong> from paths like <code>@/components/buttons/someButton.vue</code> they’ll have to be changed - Vite will not be able to work with those and give you errors instead. You can also use this housekeeping activity to get rid of any <code>/* webpack... */</code> magic comments while you’re at it.</p>
<p>If you’re not using any other tools or specific configurations, then it’s time to <strong>update your scripts</strong> in <code>package.json</code> now:</p>
<pre class="language-json"><code class="language-json"><span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>  <span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"vite"</span><span class="token punctuation">,</span><br>  <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"vite build"</span><br><span class="token punctuation">}</span></code></pre>
<p>And that’s it - running your <em>start</em> script should serve the app at <code>localhost:3000</code>, probably before you can even finish saying “Wow that was really fast and I can’t believe how long it takes Vue CLI to do the same thing”. Oh, and if everything works, you can now remove all the Vue CLI stuff (config files, plugins, etc.) you no longer need.</p>
<h3 id="resolving-issues" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#resolving-issues"><span>Resolving Issues</span></a></h3>
<p>There were 2 small issues I ran into when switching to Vite.</p>
<p>If you’re using <a class="ext" href="https://github.com/netlify/cli" rel="noopener">netlify-cli</a>: Vite does not (yet…) get auto-detected, so you’ll have to update your <code>netlify.toml</code>:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token punctuation">[</span>dev<span class="token punctuation">]</span><br>  command = "npm run start"<br>  framework = "<span class="token comment">#custom"</span><br>  functions = "functions"<br>  publish = "dist"<br>  targetPort = 3000</code></pre>
<p><code>framework</code> and <code>targetPort</code> (set to Vite’s dev server port) are key here. Also make sure that <code>npm run dev</code> is mapped to <em>netlify-cli</em>:</p>
<pre class="language-json"><code class="language-json"><span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"dev"</span><span class="token operator">:</span> <span class="token string">"netlify dev"</span><span class="token punctuation">,</span><br>    <span class="token comment">// etc.</span><br><span class="token punctuation">}</span></code></pre>
<p>Netlify Functions and Identity will properly work on <code>localhost:8888</code> that way; see <a class="ext" href="https://cli.netlify.com/netlify-dev" rel="noopener">cli.netlify.com/netlify-dev</a> for further details.</p>
<p>The second issue I had was a missing file in my production build. It took me a while to find it, but here’s what seems to have happened:</p>
<ul>
<li>I imported <code>index.css</code> in my <code>main.js</code></li>
<li>Vite split the CSS as per it’s <code>build.cssCodeSplit</code> config option (default = <code>true</code>)</li>
<li>The built global <code>index.css</code> got referenced by the code but somehow wasn’t there - Netlify returned a 404 for the URL</li>
<li>The app wouldn’t load due to a missing module</li>
</ul>
<p>Workaround: set <code>cssCodeSplit: false</code> in your Vite config file. That’ll bundle up all the CSS into one file; 36 KB in my case, so I’d argue that it actually makes sense to do that instead of having many small files around = more HTTP requests.</p>
<p>It felt a little painful and I still don’t have a clue as to why that happened; will try to reproduce but haven’t had the motivation to do that yet - will update this article accordingly.</p>
<h2 id="summary" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#summary"><span>Summary</span></a></h2>
<p>Moving a production app to Vite worked well for me and offers better DX than Vue CLI did so far. Builds are faster and the build size is a little lower than it was using Vue CLI. I didn’t encounter any road blocks and the whole switch took me about 2-3 hours (incl. finding and working around the 2 small issues I ran into).</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Migrating from Hugo to Eleventy</title>
        
          
            <category term="eleventy" />
          
            <category term="guide" />
          
            <category term="hugo" />
          
        
        <link href="https://ttntm.me/blog/migrating-from-hugo-to-eleventy/"/>
        
        <updated>2021-03-29T10:30:00Z</updated>
        <id>https://ttntm.me/blog/migrating-from-hugo-to-eleventy/</id>
        <content type="html"><![CDATA[
          
            
            <img src="https://ttntm.me/static/img/blog/11ty.jpg" class="img-fluid img-center mb1" alt="Eleventy logo splash screen">
<h2 id="why" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#why"><span>Why?</span></a></h2>
<p>This site is live since 2018 and there’s now been 3 major versions of it. So, I’m going to start this off with the same one word question that also started one of last year’s post: “<strong>why?</strong>”</p>
<p>Last week I noticed another one of Hugo’s more or less monthly releases and I soon started wondering whether it’d finally be time to update this site. For additional context: <code>ttntm.me</code> was running based on Hugo 0.58.0 while their current release is 0.82 as of writing this…</p>
<p>The idea of updating Hugo did not spark joy - mostly due to changes in their Markdown rendering engine dating back to 2019 or so that were one of the reasons I didn’t install anything newer than 0.58 in the first place. After thinking about it some more, I was more or less certain that there’d be a bunch of other issues on top of that, so I quickly abandoned that idea.</p>
<p>On the other hand though, I kept wondering whether this wasn’t just an opportunity in disguise…</p>
<p>I was aware of <a class="ext" href="https://www.11ty.dev/" rel="noopener">Eleventy</a> for quite a while, certainly since Google talked about having used it to build their <em>web.dev</em> site, but I hadn’t used it for any production sites so far. The idea of switching this site over to a JavaScript based SSG was certainly tempting; I slept on it and decided to give it shot on <strong>Friday</strong> (3 days ago).</p>
<blockquote>
<p>It’s Monday now and you’re reading this article on a site built by Eleventy 🕺</p>
</blockquote>
<p>I’ve spent about 12 hours on this migration; that’s probably more time than the Hugo update would have needed, but it’s safe to assume that tinkering with this site (in addition to writing the occasional articles) sparks joy again.</p>
<h2 id="how" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#how"><span>How?</span></a></h2>
<p>Getting back to the point of writing an article like this: I’d like to give you a brief overview of how one could accomplish such a migration from Hugo to Eleventy.</p>
<p>I can recommend reading <a class="ext u-in-reply-to" href="https://www.smashingmagazine.com/2021/03/eleventy-static-site-generator/" rel="noopener">this article</a> for preparation; it’s pretty detailed and you’ll get a working mini-site out of it, but it’s probably best suited for people that already have experience working with SSGs. I know that I had to scroll up and down a bit in order to collect all the necessary answers to questions that arose while doing things.</p>
<p>The <a class="ext" href="https://www.11ty.dev/docs/" rel="noopener">official 11ty docs</a> are also a great place to get acquainted with and having a look at some <a class="ext" href="https://www.11ty.dev/docs/starter/" rel="noopener">starter projects</a> won’t hurt either - most universal one I came across was one called <a class="ext" href="https://github.com/maxboeck/eleventastic" rel="noopener">eleventastic</a>. I ended up re-building this site from scratch 'cause I feel that I’m generally doing better that way compared to having to deal with what other people might prefer in terms of configuration, folder structure etc.</p>
<p>The following notes are a slightly improved version of my working notes; I’m glad if they can help “as is” but I’d also appreciate questions in case there’s too much I didn’t explicitly write down.</p>
<h3 id="project-setup" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#project-setup"><span>Project Setup</span></a></h3>
<ul>
<li><code>.eleventy.js</code> config file =&gt; configure <code>return {}</code> in terms of <code>dir</code> and <code>templateFormats</code></li>
<li>Contents of Hugo’s <code>Data</code> directory =&gt; <code>src/_data/</code></li>
<li>Site data from Hugo’s <code>config.toml</code> =&gt; <code>src/_data/site.json</code> as an Object with the respective keys; available via <code>site.ParamName</code> in 11ty</li>
</ul>
<h3 id="collections-and-content" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#collections-and-content"><span>Collections and Content</span></a></h3>
<ul>
<li>Single pages =&gt; <code>src</code> - check and cleanup for Markdown files, anything with Go template logic in it needs to be converted to the template language of your choice (I went with Nunjucks)</li>
<li>Collections (i.e. Blog) =&gt; <code>src/CollectionName/</code> - Markdown files go in there, <a class="ext" href="https://www.11ty.dev/docs/data-template-dir/" rel="noopener">directory based Front Matter</a> can be defined for all of them via <code>CollectionName.json</code></li>
<li>Each collection that’s not based on Front Matter <code>tags</code> needs to be defined in <code>.eleventy.js</code>; see <a class="ext" href="https://www.11ty.dev/docs/collections/" rel="noopener">11ty.dev/docs/collections/</a></li>
<li>Front Matter <code>permalink</code> sets/overrides the URL = build target for the respective piece of content; set <code>permalink: false</code> for content in individual files that should <em>not</em> be rendered as individual pages (like my <a href="https://ttntm.me/notes" class="int">/notes page</a>)</li>
<li>Deal with Markdown errors (in case you’ve got Go template code in your code blocks for example): <a class="ext" href="https://stackoverflow.com/questions/3426182/how-to-escape-liquid-template-tags" rel="noopener">so.com/questions/3426182/how-to-escape-liquid-template-tags</a></li>
</ul>
<h3 id="migrate-templates" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#migrate-templates"><span>Migrate Templates</span></a></h3>
<ul>
<li>Contents of Hugo’s <code>layouts</code> directory =&gt; <code>src/_layouts/</code> (<em>except for partials and shortcodes</em>)</li>
<li>Hugo partials =&gt; <code>src/_includes/</code></li>
<li>Create an Archive page for your posts: <a class="ext u-in-reply-to" href="https://darekkay.com/blog/eleventy-group-posts-by-year/" rel="noopener">darekkay.com/blog/eleventy-group-posts-by-year/</a></li>
<li>Implement <code>active</code> navigation state: <a class="ext u-in-reply-to" href="https://bryanlrobinson.com/blog/using-nunjucks-if-expressions-to-create-an-active-navigation-state-in-11ty/" rel="noopener">using-nunjucks-if-expressions-to-create-an-active-navigation-state-in-11ty/</a></li>
<li>Don’t forget about:
<ul>
<li>Sitemap</li>
<li>OG/Twitter/… Metadata</li>
<li>404 page</li>
</ul>
</li>
</ul>
<h3 id="migrate-static-content" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#migrate-static-content"><span>Migrate Static Content</span></a></h3>
<pre class="language-js"><code class="language-js">eleventyConfig<span class="token punctuation">.</span><span class="token function">addPassthroughCopy</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token string-property property">'./src/static/'</span><span class="token operator">:</span> <span class="token string">'/'</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<ul>
<li>Do the above to mimic Hugo’s <code>static</code> folder and its functionality; see <a class="ext" href="https://www.11ty.dev/docs/copy/" rel="noopener">11ty.dev/docs/copy/</a></li>
<li>Copies the folder content incl. subfolders like <code>img</code></li>
</ul>
<h4 id="handling-css" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#handling-css"><span>Handling CSS</span></a></h4>
<p>I’ve ditched Gulp, now using PostCSS directly. In order to do that, I had to get 11ty to process my CSS via <code>src/css/page.11ty.js</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> path <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'path'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> postcss <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'postcss'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token keyword">class</span> <span class="token punctuation">{</span><br>  <span class="token keyword">async</span> <span class="token function">data</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> cssDir <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">,</span> <span class="token string">'..'</span><span class="token punctuation">,</span> <span class="token string">'_includes'</span><span class="token punctuation">,</span> <span class="token string">'css'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> rawFilepath <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>cssDir<span class="token punctuation">,</span> <span class="token string">'_page.css'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">return</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">permalink</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">css/page.css</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      rawFilepath<span class="token punctuation">,</span><br>      <span class="token literal-property property">rawCss</span><span class="token operator">:</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span>rawFilepath<span class="token punctuation">)</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">async</span> <span class="token function">render</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> rawCss<span class="token punctuation">,</span> rawFilepath <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token keyword">await</span> <span class="token function">postcss</span><span class="token punctuation">(</span><span class="token punctuation">[</span><br>      <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'postcss-import'</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>      <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'cssnano'</span><span class="token punctuation">)</span><br>    <span class="token punctuation">]</span><span class="token punctuation">)</span><br>      <span class="token punctuation">.</span><span class="token function">process</span><span class="token punctuation">(</span>rawCss<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">from</span><span class="token operator">:</span> rawFilepath <span class="token punctuation">}</span><span class="token punctuation">)</span><br>      <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">result</span><span class="token punctuation">)</span> <span class="token operator">=></span> result<span class="token punctuation">.</span>css<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<ul>
<li><code>src/_includes/css/</code> contains all the CSS files that get imported into <code>_page.css</code> which is the blueprint for PostCSS processing</li>
<li>Needs <code>11ty.js</code> in <code>templateFormats: []</code> in the 11ty config file; otherwise 11ty will ignore this template = no CSS</li>
</ul>
<p>Idea taken from: <a class="ext u-in-reply-to" href="https://florian.ec/blog/cache-busting-eleventy-postcss/" rel="noopener">florian.ec/blog/cache-busting-eleventy-postcss/</a></p>
<h3 id="plugins-and-shortcodes" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#plugins-and-shortcodes"><span>Plugins and Shortcodes</span></a></h3>
<ul>
<li>Find and replace broken Hugo shortcodes; see <a class="ext" href="https://www.11ty.dev/docs/shortcodes/" rel="noopener">11ty.dev/docs/shortcodes/</a> for creating new shortcodes in 11ty</li>
<li>Configure RSS output =&gt; <a class="ext" href="https://www.11ty.dev/docs/plugins/rss/" rel="noopener">11ty.dev/docs/plugins/rss/</a></li>
<li>Configure Syntax highlighting =&gt; <a class="ext" href="https://www.11ty.dev/docs/plugins/syntaxhighlight/" rel="noopener">11ty.dev/docs/plugins/syntaxhighlight/</a></li>
</ul>
<h3 id="finishing-touches" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#finishing-touches"><span>Finishing Touches</span></a></h3>
<ul>
<li>Minify HTML =&gt; <a class="ext" href="https://www.11ty.dev/docs/config/#transforms-example-minify-html-output" rel="noopener">11ty.dev/docs/config/#transforms-example-minify-html-output</a></li>
</ul>
<h2 id="summary" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#summary"><span>Summary</span></a></h2>
<p>Love how “light” the site feels now, probably due to the “new toy” effect if that’s a thing.</p>
<p>I also removed some bloat like comments (didn’t get that many anyway; might add it again at some point) and pages listing tags. I kept the display of tags though, 'cause it may be of help when quickly browsing the content.</p>
<p>Getting into 11ty was rather easy compared to when I got started with Hugo back in the day - but that may very well be due to years of experience working with SSGs and the JAMstack in general.</p>
<p>A last remark: Hugo is by no means a bad choice and I’m in no way “done with it” or anything like that. I just felt like I needed to try something new which seems to have been the right decision for the moment.</p>
<p>PS: loving the possibility of fetching API data via JavaScript files in <code>src/_data/</code>, gotta give that a shot some time!</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Lessons Learned Building WATCH3R</title>
        
          
            <category term="learning" />
          
            <category term="project" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/lessons-learned-building-watch3r/"/>
        
        <updated>2020-12-18T10:30:00Z</updated>
        <id>https://ttntm.me/blog/lessons-learned-building-watch3r/</id>
        <content type="html"><![CDATA[
          
            
            <p>When Vue 3 was released in September, I started brainstorming for something new and useful I could build with it - in my opinion, real life use cases are often the best approach for getting acquainted with shiny new toys.</p>
<p>Around the same time, me and my wife accidentally re-watched a movie we’d already seen 2 or 3 years ago. Based on its title and the 5 word Netflix summary, we couldn’t quite remember that we’d already seen it. Not too bad an accident though, because that’s basically where the idea for building a movie watchlist and journal app came from.</p>
<p>I formally announced the project on this site some weeks ago (see: <a href="https://ttntm.me/blog/watch3r-movie-watchlist-journal-app" class="int">Introducing: WATCH3R</a>), but didn’t really go into the details there. This article will take care of that, providing a collection of things I learned building and working on that application.</p>
<h2 id="application-architecture" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#application-architecture"><span>Application Architecture</span></a></h2>
<p>Much like the recipes app I built earlier this year, WATCH3R follows a similar client-serverless pattern. Vue takes care of all front end matters and a bunch of serverless functions act as a lightweight back end.</p>
<p>User accounts and user authentication are handled by Netlify Identity, which I consider exceptionally convenient for a small project like this - even more so, when it comes to user metadata (like preferences or user IDs) - more on that below.</p>
<p>Based on my exceptionally positive experience with Fauna DB so far, I stuck with their service for data storage. It’s reliable, fast and also offers great scaling options to handle future needs if and when necessary.</p>
<p>Last but not least important, WATCH3R makes use of 2 different (free) APIs that are queried for necessary title data, poster images, ratings and recommendations.</p>
<h3 id="handling-application-state" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#handling-application-state"><span>Handling Application State</span></a></h3>
<p>Even though it might be overkill for such a small application, I’m using Vuex as centralized state management. Vue 3 and its Composition API offer a lot of possibilities to handle global application state (see <a class="ext u-in-reply-to" href="https://dev.to/blacksonic/you-might-not-need-vuex-with-vue-3-52e4" rel="noopener">this article</a> for example), but I got quite used to the way Vuex handles things when building recept0r, which made this a somewhat biased decision.</p>
<p>I’ll talk about it some more later on, but I believe I’ve managed to use it quite well this time around. That means using Vuex to provide a true separation of concerns and making sure that individual views and components <em>do not</em> directly interact with global application state without having to go through Vuex actions.</p>
<h2 id="lessons-learned" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#lessons-learned"><span>Lessons Learned</span></a></h2>
<p>Getting into the details now, I’d like to mention that WATCH3R is not just free, it’s also <a class="ext" href="https://codeberg.org/ttntm/watch3r" rel="noopener">open source</a>. Having said that, it might be interesting to have a look at the source code while reading the rest of this article - I’ll also link to the respective code where appropriate.</p>
<p>Aside from that, you’re more than welcome to have a look at the live application at <a class="ext" href="https://watch3r.app" rel="noopener">watch3r.app</a>.</p>
<h3 id="composition-api-is-a-game-changer" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#composition-api-is-a-game-changer"><span>Composition API is a Game Changer</span></a></h3>
<p>I didn’t use Vue 2 for a very long time - I’ve only built 2 or 3 demo applications and 2 real applications with it, so I wouldn’t call myself an expert on that matter at all. However, I immediately noticed how much cleaner using the Composition API and its <code>setup()</code> function felt. You import what you need and <code>return{}</code> what’s necessary. Inside <code>setup()</code>, things are allowed to follow the flow you deem appropriate and that makes them feel a lot less convoluted than they felt using <code>data()</code>, <code>methods()</code>, etc. in Vue 2.</p>
<p>This <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/src/views/List.vue" rel="noopener">list view</a> could be considered WATCH3R’s “heart”. It’s pretty easy to understand what it does, even though it’s complex in its functionality (providing both the watchlist and the journal view) and also pieces together a ton of other components.</p>
<p>Getting to the state of things you can see in the linked code took a while though. At first, I had separate components for each list mode (one for the watchlist, one for the journal) but that felt like an antipattern quickly. The “key” to making a shared list view component work properly though, was adding a <code>:key</code> property to the router view in order for Vue to completely re-render the shared component when navigating between list views. There was also a lot of logic concerning the modals in the component which I eventually extracted and put into Vuex (more on that below).</p>
<p>As you can see it now, it’s immediately evident that there are dependencies to both <code>route</code> and <code>store</code> in this view - none of this is hidden behind abstractions (i.e. what <code>mapGetters</code> etc. used to do) and looking at the <code>return{}</code> statement, it’s also quite obvious which data and/or functions are relevant for the view and which ones are “just” used internally in <code>setup()</code> (i.e. <code>getListData()</code>).</p>
<p>What I mentioned here is nice for sure, but it’s still mostly component internals and formatting sugar. When it comes to reusable code though, Vue’s Composition API is quite capable of some more.</p>
<p>Take <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/src/components/list/ListAddModal.vue" rel="noopener">this modal</a> for example: it takes care of querying a serverless function and it also displays the resulting data. Which function it queries depends on the context though - searching for a specific title (<code>doSearch()</code>) or processing recommendations (<code>processRecommendation()</code>). Both cases result in a <code>searchResult</code> and a <code>searchStatus</code> which are then used to display the data. In order to keep the component clean (and independent of the API calls), the code that populates those 2 pieces of reactive state was extracted into a Vue 3 composable (see <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/src/helpers/get-omdb.js" rel="noopener">get-omdb.js</a>).</p>
<p>There are tons of other things you can do with this composable pattern (i.e. the “build your own Vuex” article linked above) and I would probably have used it more if I hadn’t committed to Vuex in the first place.</p>
<h3 id="vue-hooks" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vue-hooks"><span>Vue Hooks</span></a></h3>
<p>If you’ve ever used Vue 2, you’ve probably come across Vue hooks like <code>onCreated()</code> that were used to execute functions (i.e. load data) at a certain point in Vue’s <a class="ext" href="https://vuejs.org/v2/guide/instance.html#Lifecycle-Diagram" rel="noopener">instance lifecycle</a> (the linked diagram refers to Vue 2).</p>
<p>Using Vue 3 with the Composition API, <code>setup()</code> takes care of most of that already - any code that would have been placed in <code>onCreated()</code> gets put in there, executed and ends up working the same way.</p>
<p>Working with other hooks like <code>onMounted()</code> or <code>onUpdated()</code> is possible from inside <code>setup()</code> (see: <a class="ext" href="https://v3.vuejs.org/guide/composition-api-introduction.html#lifecycle-hook-registration-inside-setup" rel="noopener">Vue 3 docs</a>) and can be very useful sometimes (i.e. <a href="https://ttntm.me/notes/#10" class="int">handling page refresh</a>). Other times though, it can end up causing you a massive headache…</p>
<p>A quick briefing on what I tried to achieve: new items added to a list should trigger a sorting function. Users are able to set their own sorting preference which means that adding and removing list items might require re-sorting the respective list.</p>
<p>At first, that seemed like a perfect use case for an <code>onBeforeUpdate()</code> or <code>onUpdated()</code> hook (see: <a class="ext" href="https://v3.vuejs.org/guide/composition-api-lifecycle-hooks.html#lifecycle-hooks" rel="noopener">Vue 3 docs</a> for details). It worked well and I didn’t really think about it anymore until I noticed that there was a crazy amount of CPU activity whenever I used more than one tab (possible thanks to <a class="ext" href="https://github.com/gabrielmbmb/vuex-multi-tab-state" rel="noopener">vuex-multi-tab-state</a>). I immediately suspected that there was something going on with that Vuex module and even opened an <a class="ext" href="https://github.com/gabrielmbmb/vuex-multi-tab-state/issues/23" rel="noopener">issue</a> regarding my observations (CPU load, crippled browser)…</p>
<p>Long story short: thanks to in-browser debugging tools (like “stop on caught exceptions”), I was eventually able to understand what’s happening. With multi-tab shared state and more than one tab open, a change of the respective list would trigger an infinite sorting loop - tab 1 being updated, <code>onUpdated()</code> calling the sorting function, tab 2 interpreting that as an update, calling the sorting function and so on.</p>
<p>I didn’t really feel too good about this whole mess and it took me way too long to find and understand it. In the end, the solution was really simple though - removing the Vue hook from the list component and calling the sorting function right after getting the list data <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/src/store/modules/list.js#L136" rel="noopener">in the respective Vuex action</a> instead.</p>
<p>Sometimes it seems like getting simple things (a list…) right should not be underestimated. Despite the fact that this infinite sorting loop cost me a day, I do think that I was able to improve my application and gain some valuable experience (esp. in-browser debugging tools) - both reasons this topic has found its way into this collection of lessons learned.</p>
<h3 id="vuex-is-your-friend" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vuex-is-your-friend"><span>Vuex is your Friend</span></a></h3>
<p>When I started working on WATCH3R, I already had a basic understanding of working with Vuex based on a previous Vue 2 application. In terms of how things work, Vue 3 and Vuex 4 don’t really change too much, so I got my store set up rather quickly.</p>
<p>Compared to working with Vuex in Vue 2 and frequently using things like <code>mapActions</code> and <code>mapGetters</code>, the way things are done using Vue 3 and its Composition API provide a lot more transparency. That’s in line with <a class="ext u-in-reply-to" href="https://medium.com/@stephane.souron/making-a-large-scale-app-with-vue-js-part-1-modularize-your-store-bf9066436502" rel="noopener">this excellent article</a>, especially the section called “Avoid helper calls to the store” - something I’d now consider a better practice due to the clarity it provides.</p>
<p>Let me give you some details on that: working with Vuex inside <code>setup()</code> requires something like <code>const store = useStore()</code>. As a result, every interaction with your Vuex store (like <code>store.dispatch('module/actionName')</code>) is immediately obvious, instead of being hidden behind obscured helper calls that can easily be confused with in-component methods and imported function calls. It may not seem like a real “wow effect”, might even be obvious for many out there, but for me, it made writing and debugging my own code much easier.</p>
<p>Another Vuex win I’d like to point out here is related to modals. The way I used to implement them frequently resulted in a parent-child dependency, meaning that the respective modal’s parent component (i.e. <code>App.vue</code> for a global application menu) was made responsible for toggling the modal’s display. That’s certainly fine if your application has one or 2 modals, but it becomes quite messy when there are 5 or more, resulting in modal related code being spread all over your application.</p>
<p>I already had all of this toggle modal code in Vuex due to the need of auto-closing open modals when navigating to another route and/or opening another modal. Having written this code step by step though, there was a separate action and separate state for each modal, resulting in a severe violation of the DRY pattern.</p>
<p>Once I noticed the mess I’d made, I came up with something like an ENUM - a <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/src/store/modules/app.js#L22" rel="noopener">piece of numerical state</a> that would control which modal’s currently being shown. I knew that this would work well because my application wasn’t supposed to be showing more than one modal at the same time anyway. It’s an approach that worked well for me, leaving me with only one global <code>action</code>, <code>getter</code> and <code>mutation</code> to handle all my app’s modals. The code’s also flexible enough to deal with any amount of further modals added in the future.</p>
<h3 id="netlify-identity-provides-convenience" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#netlify-identity-provides-convenience"><span>Netlify Identity Provides Convenience</span></a></h3>
<p>The first time I used <a class="ext" href="https://docs.netlify.com/visitor-access/identity" rel="noopener">Netlify Identity</a> was when I built recept0r earlier this year. It’s a really cool service that’s free for up to a 1000 active users per month, so if you’re already using Netlify for deployment/hosting, there’s hardly a good reason to ignore it.</p>
<p>In its current state, WATCH3R isn’t just using the service for user authentication - it’s also storing the <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/src/store/modules/user.js#L10" rel="noopener">user preferences</a> set by the app’s users themselves. When a user logs in, the application receives a user object and updates the preferences in the Vuex store accordingly. I consider this extremely convenient as it results in all user related information coming from a single source.</p>
<p>Another convenience feature that Netlify Identity provides is user ids. I wasn’t aware of that when using it the first time, but I wouldn’t want to miss it anymore. It makes really easy to store and work with user specific content in your database without having to deal with this matter in your own code.</p>
<h3 id="ap-is-and-serverless-work-well-together" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#ap-is-and-serverless-work-well-together"><span>APIs and Serverless Work Well Together</span></a></h3>
<p>When dealing with APIs, related secrets and tokens, security should certainly be a major concern. Fortunately, going for a serverless back end provides an ideal opportunity of both working with the necessary API requests and keeping them safe at the same time, essentially separating the respective logic and all related secrets/tokens from the front end code running in the browser. If you’ve ever worked with serverless functions, that shouldn’t be too much of a surprise though.</p>
<p>I’ve included this section for to a slightly different reason - interoperability between different APIs. Initially, I implemented WATCH3R based on the free <a class="ext" href="https://www.omdbapi.com" rel="noopener">OMDb API</a> which can be queried for all the data I wanted to have available in the application. I also made use of <a class="ext" href="https://www.themoviedb.org/documentation/api" rel="noopener">TMDb’s API</a> when I added recommendations to the application, meaning that I now had to deal with 2 different APIs providing different data. After thinking about it for a bit, I tried <a class="ext" href="https://codeberg.org/ttntm/watch3r/src/branch/master/functions/tmdb-omdb-get.js" rel="noopener">a single serverless function</a> that first queries API 1 to get an id that it then uses to query API 2 in order to get the final response data. It worked well and was surprisingly fast too, so I’ll definitely keep that in mind for the future.</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>Getting started with Vue 3 was a really good experience for me and I found everything was either working as it should already or at least well documented; <a class="ext" href="https://github.com/vuejs/vue-next/issues/2325" rel="noopener">except for one little bug I found</a>. I definitely prefer working with the Composition API to how things were done in Vue 2, just my personal preference though. Using the same stack I already used for another app this year certainly made things easier too, but it’s hard to ignore the power and convenience it provides.</p>
<p>WATCH3R’s already got the first bunch of users and will be actively maintained and improved for the foreseeable future. So, if you’re interested to give it a shot, just head over to <a class="ext" href="https://watch3r.app/invite" rel="noopener">watch3r.app/invite</a> and try it yourself.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Introducing: WATCH3R</title>
        
          
            <category term="news" />
          
            <category term="project" />
          
        
        <link href="https://ttntm.me/blog/watch3r-movie-watchlist-journal-app/"/>
        
        <updated>2020-10-31T10:30:00Z</updated>
        <id>https://ttntm.me/blog/watch3r-movie-watchlist-journal-app/</id>
        <content type="html"><![CDATA[
          
            
            <img src="https://ttntm.me/static/img/blog/watch3r.jpg" class="img-fluid img-center mb1" alt="WATCH3R logo splash screen">
<p>Nothing fancy, just a small announcement for an app I recently built - WATCH3R is a free service that provides basic information about movies and TV shows. It allows you to create a dedicated watchlist and journal to collect your thoughts after watching something.</p>
<p>Both lists are capable of search and sorting, making sure you don’t lose track of your listed titles. WATCH3R also includes recommendations based on titles in your journal (via TMDb’s API).</p>
<p>More info and screenshots can be found here: <a class="ext" href="https://watch3r.app/about" rel="noopener">About WATCH3R</a></p>
<p>Once again, it’s a free app, so you’re welcome to <a class="ext" href="https://watch3r.app/invite" rel="noopener">request an invite</a> anytime 😎</p>
<h2 id="technical-details" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#technical-details"><span>Technical Details</span></a></h2>
<p>On top of watching the same movie twice within 3 years, making use of Vue 3 was the main reason I started working on this application. I’ll publish a separate article on my developer experience soon; for the time being, here’s a quick summary of the technical details:</p>
<ul>
<li>Front end: Vue 3 + Tailwind CSS</li>
<li>State mgt.: Vuex 4 beta + plugins</li>
<li>Content API: OMDb (title data) + TMDb (recommendations)</li>
<li>Back end: serverless Netlify functions</li>
<li>Identity: Netlify (GoTrue)</li>
<li>Database: Fauna DB</li>
</ul>
<p>I decided to stick to the approach that already worked well for my <a href="https://ttntm.me/blog/serverless-recipes-app-faunadb-vuejs/" class="int">recipes app</a> earlier this year. It’s a really powerful stack and as of writing this, I feel quite confident using it - Vue 3 (and its Composition API in particular) is a huge step forward and an absolute pleasure to work with.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>How I Built a Serverless Recipes App with FaunaDB and Vue.js</title>
        
          
            <category term="guide" />
          
            <category term="project" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/serverless-recipes-app-faunadb-vuejs/"/>
        
        <updated>2020-09-09T10:30:00Z</updated>
        <id>https://ttntm.me/blog/serverless-recipes-app-faunadb-vuejs/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="introduction" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#introduction"><span>Introduction</span></a></h2>
<p>I had already heard of client-serverless architectures and read a bunch of articles about the subject when I came up with the idea of creating a <a class="ext" href="https://recept0r.com" rel="noopener">personal use recipes app</a> earlier this year - I hadn’t done anything like that before though.</p>
<img src="https://ttntm.me/static/img/blog/client-serverless-credit-faunadb.png" class="img-fluid img-center auto-invert" alt="client-serverless application model">
<p>What I had in mind at this point was a minimal web application that would serve me and <a class="ext" href="https://sahar.design" rel="noopener">my wife</a> (who took care of the design) as a recipes collection for food that we enjoy cooking. Convenient bonus features like PWA functionality (i.e. using the app on the phone or tablet when standing in the kitchen) and user authentication should be available too, making sure it’s not just functional, but also enjoyable to use.</p>
<p>At first, I was tempted to go for a static site + headless CMS approach that worked really well for many other projects I did - it could be considered serverless, but it wouldn’t have been a challenge, nor anything I hadn’t done before; and that’s precisely the reason I went looking for something else, eventually going for Vue.js + Fauna for this project.</p>
<h3 id="why-fauna" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#why-fauna"><span>Why Fauna?</span></a></h3>
<p>The first time I came across Fauna was when I was looking for SQL alternatives for a project at work last year. We didn’t go for it (PostgreSQL was chosen in the end), but I kept it in mind for the future. Back then, it may have sounded a little “too good to be true” for the majority of the decision makers involved, but marketing statements like “add a global datastore to your app in minutes” and “don’t worry about database correctness, sharding, provisioning, latency, or scale” left me with an overall positive impression of their service.</p>
<p>So, when I had to pick a database for my recipes app, I found myself looking at <a class="ext" href="https://fauna.com" rel="noopener">Fauna’s website</a> again, checking out their pricing model this time. The “Always Free” plan seemed generous enough to support a small personal use app, so I didn’t hesitate much and signed up right away.</p>
<p>To be honest, I didn’t really bother looking for alternatives too much - self-hosted databases or things like AWS/Azure/Firebase were not quite what I had in mind.</p>
<h2 id="basic-app-functionality" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#basic-app-functionality"><span>Basic App Functionality</span></a></h2>
<p>The recipes app I built can be described as a basic CRUD (create-read-update-delete) application - there’s no intense computing or sophisticated algorithms. The recipes are available in read-only mode to the public, whereas creating, editing and deleting them requires an authenticated user (i.e. the author). Other than that, there are smaller convenience features like search, filtering and a dedicated page to see your own recipes.</p>
<h3 id="vue-app-setup" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#vue-app-setup"><span>Vue App Setup</span></a></h3>
<p>Vue.js was on my list of “frameworks I’d like to work with” for a while, so the decision of going for it was a rather easy one. If you’re looking for some good reasons in favor of using Vue in general, some of them can be found here: <a class="ext u-in-reply-to" href="https://michaelnthiessen.com/underdog-framework" rel="noopener">michaelnthiessen.com/underdog-framework</a></p>
<p>What I ended up building can be described as a classic SPA with multiple routes (i.e. pages) for different functions. For anonymous users, it loads a page of recipe cards that can be searched and an “About” page. Each recipe card can be clicked, which opens its respective details page containing the actual cooking instructions and a nice image. There’s a login button that can be used to both sign up and sign in - public signup is currently disabled though, as this is an invite only service at the moment.</p>
<p>Once logged in, registered users get 2 additional routes: “Create” and “My Recipes”. As the respective title suggests, these pages can be used to either create additional recipes or to view a sortable list of the current user’s recipes. Editing and deleting recipes is a part of each recipe’s details page when logged in as the recipe’s author.</p>
<p>Each of the app’s individual routes=pages was implemented as its own Vue <a class="ext" href="https://vuejs.org/v2/guide/single-file-components.html" rel="noopener">SFC</a> file, shared functionality (i.e. navbar, toast messages, etc.) makes use of reusable components. To tie it all together, Vue extensions like <code>vue-router</code> and <code>vuex</code> were used to manage rounting and application state more efficiently - you’re welcome to browse the full list of dependencies <a class="ext" href="https://codeberg.org/ttntm/recept0r/src/branch/main/package.json" rel="noopener">on Codeberg</a> if you’re interested in what other packages I used.</p>
<h3 id="fauna-setup" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#fauna-setup"><span>Fauna Setup</span></a></h3>
<p>Setting up a database in Fauna is surprisingly easy - log in to your account, create a database and finally create a collection for your data (i.e. recipes). Their <a class="ext" href="https://docs.fauna.com/fauna/current/start/cloud" rel="noopener">documentation</a> regarding “getting started” is quite good and there’s also an interactive tutorial that provides a practical introduction once you signed up.</p>
<p>As Fauna is schema-less and close to zero-config, the structure of my app’s data organically grew from its needs. An example can probably help to clarify what I mean here: initially, I didn’t really think much about where to store the images for the recipes. Fauna is technically able to store Base64 encoded images inside the recipe objects, so I went for that approach initially. As images tend to be large though, this inflated my database, added a lot of bandwidth consumption and crippled loading times on top of that - I can assure you that it’s not a good idea (also <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/documents#limits" rel="noopener">not recommended</a> by FaunaDB themselves).</p>
<p>That’s not the point though - my app wrote the Base64 images into the database without any specific configuration and later <a href="https://ttntm.me/blog/how-to-use-cloudinary-with-vue-app/" class="int">replaced them with links to the actual images</a> just like that as well. Fauna simply adjusts to the data you provide, even if not all data inside a collection has the same set of properties (i.e. some recipes with picture, others without).</p>
<p><strong>To sum it up</strong>: as far as my rather simple application is concerned, Fauna was quick and easy to set up and configure, no matter what data I provided or how I ended up transforming and manipulating it.</p>
<h2 id="serverless-functionality" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#serverless-functionality"><span>Serverless Functionality</span></a></h2>
<p>It would have been possible to implement the necessary database operations directly in the Vue app (see <a class="ext" href="https://docs.fauna.com/fauna/current/drivers/javascript.html" rel="noopener">Fauna’s JS driver</a>, but that would have been a severe security concern. I decided to add a 3rd layer here, forcing database operations to go through Netlify functions. These serverless functions provide a clear separation of concerns and added security for the database access token.</p>
<p>But what are Netlify functions?</p>
<p>Here’s an explanatory paragraph from <a class="ext" href="https://functions.netlify.com" rel="noopener">their website</a>:</p>
<blockquote>
<p>Functions are scripts that you write and deploy with Netlify. The function’s code is hidden from the public, but you can interact with it just like any other API service.</p>
</blockquote>
<p>For my app, I am using a couple of these functions for what would otherwise have to be backend or server-side functionality - more specifically for all database operations and user identity management (via <a class="ext" href="https://docs.netlify.com/visitor-access/identity" rel="noopener">Netlify Identity</a>).</p>
<h3 id="local-development-configuration" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#local-development-configuration"><span>Local Development Configuration</span></a></h3>
<p>It was my first time using Netlify functions and as such, I based my choice of Node modules and configuration on seemingly outdated information; my functions returned errors instead of data…</p>
<p>After some hours of less successful trial and error cycles, I stumbled upon this article recommending the <code>netlify-cli</code> module: <a class="ext u-in-reply-to" href="https://alligator.io/nodejs/solve-cors-once-and-for-all-netlify-dev" rel="noopener">Solve CORS once and for all with Netlify Dev</a></p>
<p>So, if you’re going to use Netlify functions, this is as good as it gets - really simple configuration and immediate success. Just keep in mind or bookmark <code>localhost:8888</code> - your terminal output (based on Vue CLI and Webpack) will continue to direct you to port 8080 instead where functions don’t work and none of the success is visible.</p>
<h3 id="user-authentication" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#user-authentication"><span>User Authentication</span></a></h3>
<p>The user signup and login procedures I used for my app are based on a library called <a class="ext" href="https://github.com/netlify/gotrue-js" rel="noopener">gotrue-js</a> that in itself “is a client library for the <a class="ext" href="https://github.com/netlify/gotrue" rel="noopener">GoTrue</a> API” (both by Netlify).</p>
<blockquote>
<p>GoTrue is a small open-source API written in golang, that can act as a self-standing API service for handling user registration and authentication for JAM projects.</p>
</blockquote>
<p>On top of that, large parts of the code I used for my app’s user authentication process are based on <a class="ext" href="https://github.com/chiubaca/vue-netlify-fauna-starter-kit" rel="noopener">this repository</a> where <code>gotrue-js</code> was successfully implemented for a Vue.js based application. A truly helpful resource indeed.</p>
<h3 id="functions" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#functions"><span>Functions</span></a></h3>
<p>With both the Vue app and the FaunaDB instance up and running, the following serverless functions can be considered the app’s backbone. To make them work, FaunaDB’s JavaScript Driver, the client secret and <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/" rel="noopener">Fauna Query Language</a> are used.</p>
<h4 id="read-recipes-from-the-database" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#read-recipes-from-the-database"><span>Read Recipes from the Database</span></a></h4>
<p>Recipes are stored in the database as an array of JSON data. In order to display those recipes to the app’s users, they have to be obtained from the database when one of these things happens:</p>
<ol>
<li>User navigates to the app’s front page</li>
<li>User navigates to an individual recipe’s details page</li>
<li>User navigates to the “My Recipes” page</li>
</ol>
<p>These cases are implemented as a separate Netlify function each. First, we’ll have a look at the function called <code>all-recipes.js</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> faunadb <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'faunadb'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context<span class="token punctuation">,</span> callback</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">faunadb<span class="token punctuation">.</span>Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">secret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FAUNADB_SECRET</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token keyword">const</span> q <span class="token operator">=</span> faunadb<span class="token punctuation">.</span>query<br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Function `all-recipes` invoked"</span><span class="token punctuation">)</span><br>  <span class="token keyword">return</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Paginate</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Match</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Ref</span><span class="token punctuation">(</span><span class="token string">"indexes/all_recipes"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> recipeRefs <span class="token operator">=</span> response<span class="token punctuation">.</span>data<br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Recipe refs"</span><span class="token punctuation">,</span> recipeRefs<span class="token punctuation">)</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>recipeRefs<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> recipes found</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>    <span class="token keyword">const</span> getAllRecipeDataQuery <span class="token operator">=</span> recipeRefs<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">ref</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> q<span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>ref<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token keyword">return</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>getAllRecipeDataQuery<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">ret</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>ret<span class="token punctuation">)</span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>Once a connection is established, the function queries for the <code>all_recipes</code> index (manually generated for the recipes collection when setting up the database) which returns a <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/functions/paginate?lang=javascript" rel="noopener">Page</a> of results. These results - essentially an array of recipe IDs - are then processed by the function <code>getAllRecipeDataQuery()</code> which eventually returns an array of all recipes complete which each one of their individual properties (name, description, image, etc.).</p>
<p>The Netlify function <code>recipe-get.js</code> queries the database for a single recipe and looks like this:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> faunadb <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'faunadb'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">getId</span><span class="token punctuation">(</span><span class="token parameter">urlPath</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> urlPath<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">([^\/]*)\/*$</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><br><span class="token punctuation">}</span><br><br>exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context<span class="token punctuation">,</span> callback</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">faunadb<span class="token punctuation">.</span>Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">secret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FAUNADB_SECRET</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token keyword">const</span> q <span class="token operator">=</span> faunadb<span class="token punctuation">.</span>query<br>  <span class="token keyword">const</span> id <span class="token operator">=</span> <span class="token function">getId</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>path<span class="token punctuation">)</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Function 'recipe-get' invoked. Read id: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>  <span class="token keyword">return</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Ref</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">collections/recipes/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"success"</span><span class="token punctuation">,</span> response<span class="token punctuation">)</span><br>    <span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>Loading a single recipe’s details page depends on the respective recipe’s ID. That’s why there’s the function <code>getId()</code> which essentially extracts the ID from the request URL. With that ID, the function then checks the recipes collection for the matching recipe data and returns that as JSON.</p>
<p>In order to save database read operations, I set up Vuex in a way that caches the recipes (in local storage) whenever <code>all-recipes.js</code> is executed. That means, that if a user visits the app’s front page and then proceeds to view a certain recipe’s details page, only one database query is necessary. The recipe’s details get returned by Vuex from the data available in local storage.</p>
<p>If a user decides to view a list of their own recipes, <code>all-recipes-user.js</code> gets executed:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> faunadb <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'faunadb'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">getUsr</span><span class="token punctuation">(</span><span class="token parameter">urlPath</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> urlPath<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">([^\/]*)\/*$</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><br><span class="token punctuation">}</span><br><br>exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context<span class="token punctuation">,</span> callback</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">faunadb<span class="token punctuation">.</span>Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">secret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FAUNADB_SECRET</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token keyword">const</span> q <span class="token operator">=</span> faunadb<span class="token punctuation">.</span>query<br>  <span class="token keyword">const</span> usr <span class="token operator">=</span> <span class="token function">getUsr</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>path<span class="token punctuation">)</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Function `all-recipes-user` invoked"</span><span class="token punctuation">)</span><br>  <span class="token keyword">return</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Paginate</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Match</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Index</span><span class="token punctuation">(</span><span class="token string">'recipes_by_owner'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>usr<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> recipeRefs <span class="token operator">=</span> response<span class="token punctuation">.</span>data<br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Recipe refs"</span><span class="token punctuation">,</span> recipeRefs<span class="token punctuation">)</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>recipeRefs<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> recipes found</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>    <span class="token keyword">const</span> getAllRecipeDataQuery <span class="token operator">=</span> recipeRefs<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">ref</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> q<span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>ref<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token keyword">return</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>getAllRecipeDataQuery<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">ret</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>ret<span class="token punctuation">)</span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>The correct (=current) user is included in the function call and then used as the variable for the index <code>recipes_by_owner</code>. FaunaDB uses indexes for the “retrieval of documents by attributes other than their References” (see: <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/indexes?lang=javascript" rel="noopener">Indexes</a>). That means that you can use them to implement search within the records stored in your collection (based on pre-defined parameters, i.e. the username).</p>
<p>In my Fauna dashboard, this specific index looks like this:</p>
<img src="https://ttntm.me/static/img/blog/fauna-dashboard.png" class="img-fluid img-center auto-invert" alt="FaunaDB dashboard">
<p>The rest of this Netlify function follows the same logic as <code>all-recipes.js</code> described above and eventually returns an array of recipes belonging to the current user that gets displayed on the “My Recipes” page of the app. If the user just came from the front page (and Vuex has a current local version of all recipes), the app saves bandwidth and displays the cached data instead of executing the function.</p>
<h4 id="write-recipes-to-the-database" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#write-recipes-to-the-database"><span>Write Recipes to the Database</span></a></h4>
<p>This Netlify function called <code>recipe-create.js</code> handles database writes that happen when authenticated users create and save new recipes:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> faunadb <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'faunadb'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context<span class="token punctuation">,</span> callback</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">faunadb<span class="token punctuation">.</span>Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">secret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FAUNADB_SECRET</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token keyword">const</span> q <span class="token operator">=</span> faunadb<span class="token punctuation">.</span>query<br>  <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>body<span class="token punctuation">)</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Function `recipe-create` invoked"</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span><br>  <span class="token keyword">const</span> newRecipe <span class="token operator">=</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">data</span><span class="token operator">:</span> data<br>  <span class="token punctuation">}</span><br>  <span class="token keyword">return</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Create</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Ref</span><span class="token punctuation">(</span><span class="token string">"collections/recipes"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> newRecipe<span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"success"</span><span class="token punctuation">,</span> response<span class="token punctuation">)</span><br>    <span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>Adding new recipes is accomplished by <code>q.Create</code> (see <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/functions/create?lang=javascript" rel="noopener">Create</a>) and the recipe’s data included in the function call. The response contains the newly created recipe’s ID and the Vue app is using that to automatically navigate to the new recipe’s details page (if the create operation was successful).</p>
<h4 id="update-existing-recipes" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#update-existing-recipes"><span>Update Existing Recipes</span></a></h4>
<p>Whenever an authenticated user decides to edit one of their recipes, the data stored in the database has to be updated as well. The Netlify function called <code>recipe-edit.js</code> is responsible for doing exactly that:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> faunadb <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'faunadb'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">getId</span><span class="token punctuation">(</span><span class="token parameter">urlPath</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> urlPath<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">([^\/]*)\/*$</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><br><span class="token punctuation">}</span><br><br>exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context<span class="token punctuation">,</span> callback</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">faunadb<span class="token punctuation">.</span>Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">secret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FAUNADB_SECRET</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token keyword">const</span> q <span class="token operator">=</span> faunadb<span class="token punctuation">.</span>query<br>  <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>body<span class="token punctuation">)</span><br>  <span class="token keyword">const</span> id <span class="token operator">=</span> <span class="token function">getId</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>path<span class="token punctuation">)</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Function 'recipe-edit' invoked. update id: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>  <span class="token keyword">return</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Update</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Ref</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">collections/recipes/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>data<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"success"</span><span class="token punctuation">,</span> response<span class="token punctuation">)</span><br>    <span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>Much like <code>recipe-get.js</code>, this function works with the respective recipe’s ID (based on its URL) in order to find the correct record to update. The data sent into the function is the complete updated recipe object, but keep in mind that in Fauna, “Updates are partial, and only modify values that are specified in the param_object” (see <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/functions/update?lang=javascript" rel="noopener">q.update</a>).</p>
<h4 id="delete-existing-recipes" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#delete-existing-recipes"><span>Delete Existing Recipes</span></a></h4>
<p>Probably the least used Netlify function; <code>recipe-delete.js</code> does exactly what its name suggests - it permanently deletes existing recipes from the database:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> faunadb <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'faunadb'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">getId</span><span class="token punctuation">(</span><span class="token parameter">urlPath</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> urlPath<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">([^\/]*)\/*$</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><br><span class="token punctuation">}</span><br><br>exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context<span class="token punctuation">,</span> callback</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">faunadb<span class="token punctuation">.</span>Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">secret</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">FAUNADB_SECRET</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token keyword">const</span> q <span class="token operator">=</span> faunadb<span class="token punctuation">.</span>query<br>  <span class="token keyword">const</span> id <span class="token operator">=</span> <span class="token function">getId</span><span class="token punctuation">(</span>event<span class="token punctuation">.</span>path<span class="token punctuation">)</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Function 'recipe-delete' invoked. delete id: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>  <span class="token keyword">return</span> client<span class="token punctuation">.</span><span class="token function">query</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Delete</span><span class="token punctuation">(</span>q<span class="token punctuation">.</span><span class="token function">Ref</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">collections/recipes/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"success"</span><span class="token punctuation">,</span> response<span class="token punctuation">)</span><br>    <span class="token keyword">return</span> <span class="token function">callback</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>Deleting recipes requires the recipe’s ID (again…) and doesn’t really do much else. Function failures result in the recipe not being deleted; the app displays a toast message in that case and stays on the respective recipe’s details page. Otherwise (successful delete operation), it forcefully navigates the user to the front page.</p>
<h4 id="thats-it" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#thats-it"><span>That’s it?</span></a></h4>
<p>Indeed, the function to delete recipes concludes the serverless “backend” functionality. If we ignore user identity management, everything else like search, filters and sorting is done client-side only and doesn’t involve the database (yet).</p>
<h2 id="summary-and-outlook" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#summary-and-outlook"><span>Summary and Outlook</span></a></h2>
<p>Considering that this was my first “real” application (not counting tutorial ToDo apps…) built with this tech stack, I’d like to mention that it was an overall pleasant and reassuring (learning) experience. Yes, there were a few times that exhaustion, frustration and tunnel vision occurred, but I guess that’s just normal. None of these “little things” made me regret my choices and all of them were eventually resolved by reading the docs (again) or simply having some rest and having another go at it the next day.</p>
<p>The cloud services I used for this project (Cloudinary, Fauna and Netlify) all have a very generous free tier without any noticeable throttling or service restrictions. That means, that as of writing this, I haven’t had to pay a single cent to keep my application online and functional. However, this may change if the app’s ever going to be publicly accessible (i.e. anyone being able to sign up for an account). Right now, there’s only a handful of users, basically no SEO and (probably) hardly any traffic (there’s no analytics service running and I don’t plan on adding one).</p>
<p>In terms of <strong>future improvements</strong>, I definitely see the amount of data (=recipes) as a key “risk” over time. If you read until here, you’ll probably know that my app’s currently loading <em>all</em> recipes whenever navigating to the front page. It’s still really fast, but there’s only 12 recipes at the moment. A growth in content over time will probably have me working on either infinite scroll or pagination, which in turn will require a “real” (database) search function (instead of the simple <code>filter()</code> I’m currently using. Other than that, there’s probably going to be some sort of import/export of recipe data at some point and maybe also a larger refactoring effort when Vue 3.0 is officially released (announced as Q3 2020 as of writing this article).</p>
<p>A final remark: there are a lot of helpful resources out there that supported me when getting started with this project. I myself ended up <a href="https://ttntm.me/blog/vuejs-getting-started-in-2020/" class="int">writing about my learning experience</a> too, which might offer some guidance to anyone out there doing something similar at the moment or trying to get started with (any of) the services and tools I used for this application.</p>
<p><strong>The tech stack the app’s based on:</strong></p>
<ul>
<li>Cloudinary</li>
<li>Fauna</li>
<li>Netlify (Hosting, Functions &amp; User Identity)</li>
<li>Vue.js</li>
</ul>
<p><strong>Update Aug. 29th 2021</strong>:</p>
<p>The app this article is based on has been rewritten from scratch using Vue 3 and TypeScript - the current source code can be found at <a class="ext" href="https://codeberg.org/ttntm/recept0r" rel="noopener">Codeberg</a>.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>How to use Cloudinary with your Vue App</title>
        
          
            <category term="guide" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/how-to-use-cloudinary-with-vue-app/"/>
        
        <updated>2020-08-05T10:30:00Z</updated>
        <id>https://ttntm.me/blog/how-to-use-cloudinary-with-vue-app/</id>
        <content type="html"><![CDATA[
          
            
            <p>My Vue app needed user uploaded images; storing Base64 encoded images inside FaunaDB was my first idea, but that proved both slow and resource heavy (which is probably why Fauna’s docs <a class="ext" href="https://docs.fauna.com/fauna/current/api/fql/documents#limits" rel="noopener">advise against</a> doing that…).</p>
<p>Taking into consideration that a recipe app’s images will hardly ever change, an image CDN seemed more appropriate. I had already heard of Cloudinary somewhere, so I decided to try that service. They offer a generous free tier that can even be extended (permanently!) by inviting other people; here we go, just in case you’d like to check it out and do something nice for me at the same time (in case you end up creating an account there): <a class="ext" href="https://cloudinary.com/invites/lpov9zyyucivvxsnalc5/hrdkuf3mpvpupvr4vdh9" rel="noopener">Cloudinary invite</a></p>
<h2 id="getting-started" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#getting-started"><span>Getting Started</span></a></h2>
<p>This guide assumes that you’ve got a Vue.js application configured and running. I won’t cover much of the app’s setup or architecture here, but you could head over to another article I wrote about <a href="https://ttntm.me/blog/vuejs-getting-started-in-2020/" class="int">getting started with Vue.js</a> to get a better understanding of the app this approach is based on.</p>
<h3 id="the-app-and-its-data" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-app-and-its-data"><span>The App and its Data</span></a></h3>
<p>Let’s talk about our starting point: we’ve got a CRUD application that stores data in a database. The data itself is an array of objects with properties; each one of them can be manipulated by the app’s users. One such property is called <code>image</code> and should contain an URL to an image resource hosted on Cloudinary. In our app, the respective piece of content (i.e. a recipe) can then be presented with a nice looking image.</p>
<h3 id="cloudinary-image-upload" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#cloudinary-image-upload"><span>Cloudinary Image Upload</span></a></h3>
<p>There’s multiple ways of uploading images to your Cloudinary account; I needed something that works with a serverless app and I didn’t want to install any dependencies - I wanted my app to send a <code>POST</code> request to an URL and be done with it.</p>
<p>This approach is called <em>unsigned upload</em> and you can <a class="ext" href="https://cloudinary.com/documentation/upload_images#unsigned_upload" rel="noopener">read all about it in their documentation</a>. Yes, there could be security concerns obviously - you might want to take another approach here if you’re working on something with public image upload for example. My semi private app has only got a few users, there’s no public image upload and therefore no security concerns that would outweigh the benefits of using unsigned uploads.</p>
<p>Before taking a look at the implementation, you should take a moment to think about how you’d like to process your uploaded images. Users shouldn’t really be expected to have image processing software available that could crop and compress whatever photo they took with their phone to some predefined limits. Our app needs optimized images though, and that’s why I recommend using so-called <em>Upload Manipulations</em> - Cloudinary doesn’t just offer image storage, they also offer image processing - very convenient.</p>
<p>If you had a look at the linked documentation, you probably came across the term <em>upload preset</em> already - that’s basically an ID that can has to be referenced when communicating with the API that tells Cloudinary what to do with your image, including these manipulations (see: <a class="ext" href="https://cloudinary.com/documentation/upload_presets" rel="noopener">Cloudinary docs</a>).</p>
<img src="https://ttntm.me/static/img/blog/cloudinary-manipulations.png" class="img-fluid img-center auto-invert" alt="Upload Manipulations">
<p>As you can see, I’m auto converting anything that comes in to <code>webp</code>. Images also get resized to <code>w_1280</code> and compressed with <code>q_auto:good</code> - the result is good looking images at reasonable file size (= faster page loads) and big enough dimensions.</p>
<h2 id="implementation" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#implementation"><span>Implementation</span></a></h2>
<p>Now that we know what we want and can do with Cloudinary, let’s look at how this can be implemented in Vue.</p>
<p>The image upload component I built had to work for both creating and editing recipes in my app; that’s 2 individual routes and also 2 separate SFCs (<a class="ext" href="https://vuejs.org/v2/guide/single-file-components.html" rel="noopener">Single File Components</a>) due to different layouts and functionality.</p>
<h3 id="component-setup" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#component-setup"><span>Component Setup</span></a></h3>
<p>This is how the initial setup of the component’s <code>&lt;script&gt;</code> section looks like:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"recipe-image"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">recipe</span><span class="token operator">:</span> Object<br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token function">data</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">imageStatus</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">""</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">""</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">uPreset</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">VUE_APP_CDNRY_UPRESET</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">computed</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token function">isUploaded</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">const</span> checkImgSrc <span class="token operator">=</span> <span class="token function">RegExp</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^https:\/\/</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token keyword">return</span> checkImgSrc<span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>recipe<span class="token punctuation">.</span>image<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">methods</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>As you can see, the component inherits the <code>recipe</code> object from its respective parent (create or edit). It only has an object <code>imageStatus</code> and the upload preset <code>uPreset</code> necessary for Cloudinary in its own <code>data()</code> function, both of which are used inside the component itself exclusively. There’s also a <a class="ext" href="https://vuejs.org/v2/api/#computed" rel="noopener">computed property</a> that checks whether a recipe’s image was uploaded already.</p>
<p>We’ll get into the actual functionality in a minute, let’s first have a look at the HTML in the <code>&lt;template&gt;</code> section though:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>template</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>edit-image<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">v-if</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>!recipe.image<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>...<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>...<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>w-8 h-8<span class="token punctuation">"</span></span> <span class="token attr-name">fill</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>currentColor<span class="token punctuation">"</span></span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 20 20<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>path</span> <span class="token attr-name">d</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>...<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Select Image<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span><br>          <span class="token attr-name">@change</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>addImage<span class="token punctuation">"</span></span><br>          <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hidden<span class="token punctuation">"</span></span><br>          <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>file<span class="token punctuation">"</span></span><br>          <span class="token attr-name">accept</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/*<span class="token punctuation">"</span></span><br>        <span class="token punctuation">/></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><br>      <span class="token attr-name">v-if</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>imageStatus.type !== ''<span class="token punctuation">"</span></span><br>      <span class="token attr-name">v-html</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>imageStatus.body<span class="token punctuation">"</span></span><br>      <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-sm ml-4 mb-4<span class="token punctuation">"</span></span><br>      <span class="token attr-name">:class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{<br>        'error': imageStatus.type === 'error',<br>        'text-blue-500': imageStatus.type === 'info',<br>      }<span class="token punctuation">"</span></span><br>    <span class="token punctuation">/></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>...<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">@click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>uploadImage<span class="token punctuation">"</span></span> <span class="token attr-name">v-blur</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn btn-green<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Upload Image<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">v-if</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>recipe.image<span class="token punctuation">"</span></span> <span class="token attr-name">@click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>removeImage<span class="token punctuation">"</span></span> <span class="token attr-name">v-blur</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn btn-red<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Remove Image<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>template</span><span class="token punctuation">></span></span></code></pre>
<p>I’ve used Tailwind CSS for my app and the <code>&lt;input type=&quot;file&quot;&gt;</code> is based on this component I found online: <a class="ext" href="https://tailwindcomponents.com/component/tailwind-file-upload" rel="noopener">tailwind-file-upload</a></p>
<p>The <code>&lt;input&gt;</code> is wrapped in a <code>&lt;div&gt;</code> and there’s a <code>v-if=&quot;!recipe.image&quot;</code> in there which makes sure that it is only shown when there’s no image (=URL to an image) set for the respective recipe. This is also related to the “Remove Image” button in the bottom of the code snippet that is only shown when there is an image. The flow for the user could look like this:</p>
<ol>
<li>Use the <code>&lt;input&gt;</code> to select an image</li>
<li>See the image now (inserted and displayed based on the code of the parent (create or edit) component); shown instead of the previously displayed <code>&lt;input&gt;</code> element</li>
<li>Decide whether to upload or change (=remove; <code>&lt;input&gt;</code> would come back) the image</li>
</ol>
<p>In order to prevent errors and misunderstandings, there’s also a conditional <code>&lt;p&gt;</code> that displays status messages to the user. The messages are coming from the component’s methods <code>addImage</code>, <code>removeImage</code>, and <code>uploadImage</code> which we’ll have a look at now.</p>
<h3 id="component-methods" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#component-methods"><span>Component Methods</span></a></h3>
<p>Our component is observing the file input for changes with <code>@change=&quot;addImage&quot;</code> which in turn triggers the <code>addImage(e)</code> method:</p>
<pre class="language-js"><code class="language-js"><span class="token function">addImage</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> selectedImage <span class="token operator">=</span> e<span class="token punctuation">.</span>target<span class="token punctuation">.</span>files<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">//get the first file</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>selectedImage<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> reader <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FileReader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    reader<span class="token punctuation">.</span><span class="token function-variable function">onload</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">$emit</span><span class="token punctuation">(</span><span class="token string">"image:update"</span><span class="token punctuation">,</span> e<span class="token punctuation">.</span>target<span class="token punctuation">.</span>result<span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">"info"</span><span class="token punctuation">;</span><br>      <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>body <span class="token operator">=</span> <span class="token string">'Image received, please press "Upload Image" now.'</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>    reader<span class="token punctuation">.</span><span class="token function">readAsDataURL</span><span class="token punctuation">(</span>selectedImage<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token comment">// cancel if there's no image or if the image is removed</span><br>    <span class="token keyword">return</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>This method uses the <a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/API/FileReader" rel="noopener">File Reader API</a>, more specifically <a class="ext" href="https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL" rel="noopener">readAsDataURL()</a> which returns a <code>data:</code> URL representing the file’s data. This image data is then emitted to the image uploader component’s parent component with the line <code>this.$emit(&quot;image:update&quot;, e.target.result);</code> which means that the image can both be stored in the respective recipe object and displayed; we’ll have a quick look at this part of the parent component’s code later.</p>
<p>The lines referring to <code>this.imageStatus</code> are responsible for displaying the respective status messages to the user; in this case, the app’s letting the user know that the image was received and is waiting for a click on the “Upload Image” button.</p>
<p>Then there’s the <code>removeImage()</code> method, undoing what we just saw:</p>
<pre class="language-js"><code class="language-js"><span class="token function">removeImage</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>recipe<span class="token punctuation">.</span>image<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">$emit</span><span class="token punctuation">(</span><span class="token string">"image:update"</span><span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">"info"</span><span class="token punctuation">;</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>body <span class="token operator">=</span> <span class="token string">"Image removed."</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">"error"</span><span class="token punctuation">;</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>body <span class="token operator">=</span> <span class="token string">"Please select an image first"</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>This method is as simple as it looks, replacing the recipes’ image with <code>null</code> and therefore making the image uploader component’s <code>&lt;input&gt;</code> element to come back. The status messages are speaking for themselves, again notifying the user of what’s happening. The <code>else {}</code> path is in there just in case - it’s more than unlikely to ever be reached due to the <code>v-if</code> on the “Remove Image” button (i.e. the button only being displayed when there is an image).</p>
<p>Now that we know how to add and remove images locally, let’s have a look at the <code>uploadImage()</code> method that actually handles the image upload to Cloudinary:</p>
<pre class="language-js"><code class="language-js"><span class="token function">uploadImage</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> vm <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">;</span><br>  <span class="token keyword">function</span> <span class="token function">postImage</span><span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>recipe<span class="token punctuation">.</span>image <span class="token operator">&amp;&amp;</span> <span class="token operator">!</span><span class="token keyword">this</span><span class="token punctuation">.</span>isUploaded<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">let</span> spinner <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"@/assets/loading.svg"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">"info"</span><span class="token punctuation">;</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>body <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;img src="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>spinner<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" class="...">&lt;span class="...">Uploading...&lt;/span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>    <span class="token keyword">let</span> uData <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FormData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    uData<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">"upload_preset"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>uPreset<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    uData<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">"tags"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>recipe<span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    uData<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">"file"</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>recipe<span class="token punctuation">.</span>image<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token function">postImage</span><span class="token punctuation">(</span>uData<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">response</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token comment">// check for the response first - otherwise the current data: image</span><br>      <span class="token comment">// would be cleared and set to an error response, forcing the user have to select it again</span><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>response<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">let</span> temp <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> response<span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">$emit</span><span class="token punctuation">(</span><span class="token string">"image:update"</span><span class="token punctuation">,</span> temp<span class="token punctuation">.</span>secure_url<span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>isUploaded<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">"error"</span><span class="token punctuation">;</span><br>      <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>body <span class="token operator">=</span> <span class="token string">"This image was uploaded already. Please remove it first if you want to change it."</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>      <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">"error"</span><span class="token punctuation">;</span><br>      <span class="token keyword">this</span><span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>body <span class="token operator">=</span> <span class="token string">"Please select an image first"</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>We have got a method with a nested function here which is why that <code>const vm</code> is necessary - it passes down Vue’s <code>this</code> into the function <code>postImage()</code>.</p>
<p>If we have an image for a recipe which has not been uploaded already (i.e. in case of editing existing recipes), we’ll format the data for the Cloudinary API (yes, it needs <code>FormData()</code> which took me a minute to figure out…) and pass that into <code>postImage()</code>. We’ll then take the <code>response</code> we get from the Cloudinary API, extract the <code>secure_url</code> to our image and <code>$emit</code> that URL to the parent just like we did in <code>addImage()</code> and <code>removeImage()</code> before.</p>
<p>Error handling is done in <code>else {}</code> here which displays 2 different messages based on the state of the recipe’s image (missing vs. already uploaded). This is necessary because the same (already uploaded) image could be uploaded again and again otherwise.</p>
<p>While the image is uploading, we’ll display a little animated SVG and “Uploading…” as the status message for the user; the success message will be triggered from within <code>postImage()</code> if communication with the API was successful:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">postImage</span><span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">"https://api.cloudinary.com/v1_1/USERNAME/image/upload"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> data<span class="token punctuation">,</span><br>    <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">response</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      vm<span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">"info"</span><span class="token punctuation">;</span><br>      vm<span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>body <span class="token operator">=</span> <span class="token string">"Image successfully uploaded"</span><span class="token punctuation">;</span><br>      <span class="token keyword">return</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token parameter">error</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"CDNRY API error"</span><span class="token punctuation">,</span> error<span class="token punctuation">)</span><span class="token punctuation">;</span><br>      vm<span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">"error"</span><span class="token punctuation">;</span><br>      vm<span class="token punctuation">.</span>imageStatus<span class="token punctuation">.</span>body <span class="token operator">=</span> <span class="token string">"Error uploading image"</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>You can see the API path here - simply substitute your username and it should work. All other settings like the image manipulations mentioned earlier are defined in the upload preset you’re using.</p>
<h3 id="parent-components" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#parent-components"><span>Parent Components</span></a></h3>
<p>Now we have a working image uploader component - let’s see how the parent components handle the emitted data.</p>
<p>In the parent components, the image uploader can be used like this:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>recipe-image</span> <span class="token attr-name">:recipe</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>recipe<span class="token punctuation">"</span></span> <span class="token attr-name"><span class="token namespace">@image:</span>update</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>imageUpdate<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
<p>The recipe object is passed into the component and the method <code>imageUpdate()</code> is attached to the <code>image:update</code> event that it emits from within multiple methods.</p>
<p>For the parent components, the <code>imageUpdate()</code> method looks like this:</p>
<pre class="language-js"><code class="language-js"><span class="token function">imageUpdate</span><span class="token punctuation">(</span><span class="token parameter">url</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">this</span><span class="token punctuation">.</span>recipe<span class="token punctuation">.</span>image <span class="token operator">=</span> url<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>Pretty basic, huh?</p>
<p>There’s basically only one thing you’ll have to keep in mind here: if it’s possible that your data’s images are optional, adding them at a later point in time (editing previously existing data) can lead to problems with Vue’s reactivity. To avoid this possibility, <code>imageUpdate()</code> looks a little different for my app’s edit mode component:</p>
<pre class="language-js"><code class="language-js"><span class="token function">imageUpdate</span><span class="token punctuation">(</span><span class="token parameter">url</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">this</span><span class="token punctuation">.</span>recipe <span class="token operator">=</span> Object<span class="token punctuation">.</span><span class="token function">assign</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token keyword">this</span><span class="token punctuation">.</span>recipe<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">image</span><span class="token operator">:</span> url<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>This makes sure that reactivity works properly in case of a recipe being created without an image first and that image being added later on - unlikely, but just in case. For more details, please have a look at this resource: <a class="ext" href="https://vuejs.org/v2/guide/reactivity.html" rel="noopener">guide/reactivity</a></p>
<p>Careful when sending your data off to your database: the parent component/s should check whether the image was actually uploaded (i.e. also use the RegEx used in the image uploader’s computed <code>isUploaded</code> property) - otherwise you’ll write the whole image <code>data:</code> returned by the File Reader API into your database.</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>I tried to write some sort of “guide I wish I had” when I built this image uploader component. Cloudinary’s documentation is alright and helpful enough, but if you don’t want to use their packages you have to figure out your own implementation - or maybe I just didn’t click on the right search results…</p>
<p>A note re: future improvements: my app’s image URLs are now hard-locked to Cloudinary. If I ever (have) to change that configuration, it’ll be a bit of work. There’s good article I came across related to this (potential) issue for those using Netlify: <a class="ext" href="https://markus.oberlehner.net/blog/using-netlify-redirects-to-proxy-images-hosted-on-a-third-party-image-provider-or-a-headless-cms" rel="noopener">Using Netlify Redirects to Proxy Images Hosted on a Third Party Image Provider</a></p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Using Tailwind CSS with Vue.js</title>
        
          
            <category term="guide" />
          
            <category term="tailwind" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/tailwind-css-with-vuejs/"/>
        
        <updated>2020-07-09T10:30:00Z</updated>
        <id>https://ttntm.me/blog/tailwind-css-with-vuejs/</id>
        <content type="html"><![CDATA[
          
            
            <p><strong>This article was written a long time ago</strong>. It applies to Vue 2 which has reached its end of life (EOL) on December 31st, 2023.</p>
<p>This article is based on first-hand experience of <a href="https://ttntm.me/blog/vuejs-getting-started-in-2020" class="int">getting started with Vue.js</a>.</p>
<h2 id="prerequisites" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#prerequisites"><span>Prerequisites</span></a></h2>
<p>You should have gone through your Vue app’s setup procedure before setting up Tailwind CSS for the project. Make sure your new app’s available at <code>localhost:8080</code> after running <code>npm run serve</code>.</p>
<h2 id="installation" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#installation"><span>Installation</span></a></h2>
<p>We’re obviously going to need Tailwind CSS, so we’re going to install it now:</p>
<p><code>npm install tailwindcss</code></p>
<p>If you started with a new project from <code>vue create projectName</code>, your <code>package.json</code> should look like this now:</p>
<pre class="language-json"><code class="language-json">...<br><span class="token property">"dependencies"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>  <span class="token property">"core-js"</span><span class="token operator">:</span> <span class="token string">"^3.6.5"</span><span class="token punctuation">,</span><br>  <span class="token property">"tailwindcss"</span><span class="token operator">:</span> <span class="token string">"^1.4.6"</span><span class="token punctuation">,</span><br>  <span class="token property">"vue"</span><span class="token operator">:</span> <span class="token string">"^2.6.11"</span><br><span class="token punctuation">}</span><span class="token punctuation">,</span><br>...</code></pre>
<p>Next, we’re going to create a brand new <a class="ext" href="https://tailwindcss.com/docs/configuration/" rel="noopener">configuration file</a>:</p>
<p><code>npx tailwindcss init --full</code></p>
<p>The <code>--full</code> flag makes sure that we get the entire <a class="ext" href="https://github.com/tailwindcss/tailwindcss/blob/master/stubs/defaultConfig.stub.js" rel="noopener">default configuration file</a> and not just a couple of placeholders.</p>
<h2 id="configuration" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#configuration"><span>Configuration</span></a></h2>
<p>In order to use of Tailwind CSS for our project, we’re going to have to do a bit of configuration.</p>
<p>First, let’s create <code>index.css</code> in <code>./src/assets/styles/</code> with the following content:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@tailwind</span> base<span class="token punctuation">;</span></span><br><br><span class="token atrule"><span class="token rule">@tailwind</span> components<span class="token punctuation">;</span></span><br><br><span class="token atrule"><span class="token rule">@tailwind</span> utilities<span class="token punctuation">;</span></span></code></pre>
<p>This will import Tailwind’s <code>base</code>, <code>components</code> and <code>utilities</code> styles into your new Vue app’s main CSS.</p>
<p>This stylesheet then needs to be imported into the app; open <code>./src/main.js</code> and add this line:</p>
<p><code>import('@/assets/styles/index.css');</code></p>
<p>The whole file should then look like this:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> Vue <span class="token keyword">from</span> <span class="token string">'vue'</span><br><span class="token keyword">import</span> App <span class="token keyword">from</span> <span class="token string">'./App.vue'</span><br><br><span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'@/assets/styles/index.css'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>Vue<span class="token punctuation">.</span>config<span class="token punctuation">.</span>productionTip <span class="token operator">=</span> <span class="token boolean">false</span><br><br><span class="token keyword">new</span> <span class="token class-name">Vue</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token function-variable function">render</span><span class="token operator">:</span> <span class="token parameter">h</span> <span class="token operator">=></span> <span class="token function">h</span><span class="token punctuation">(</span>App<span class="token punctuation">)</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">$mount</span><span class="token punctuation">(</span><span class="token string">'#app'</span><span class="token punctuation">)</span></code></pre>
<p>When running <code>npm run serve</code> now, you’ll get an error; this is due to a missing <code>postcss.config.js</code>, so let’s create that file in the app’s root directory:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// ./postcss.config.js</span><br><span class="token keyword">const</span> autoprefixer <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'autoprefixer'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> tailwindcss <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'tailwindcss'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">plugins</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    tailwindcss<span class="token punctuation">,</span><br>    autoprefixer<span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>Pretty simple and now <code>npm run serve</code> will work again (because Tailwind CSS gets properly injected into <code>styles/index.css</code>).</p>
<h2 id="using-tailwind-css-with-vue" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#using-tailwind-css-with-vue"><span>Using Tailwind CSS with Vue</span></a></h2>
<p>If you haven’t touched your Vue app yet, you’ll notice the Vue logo has moved all the way to the left; it’s not centered anymore due to Tailwind’s reset styles.</p>
<p>Let’s change that! Head over to your app’s <code>App.vue</code> and change this line: <code>&lt;img alt=&quot;Vue logo&quot; src=&quot;./assets/logo.png&quot;&gt;</code></p>
<p>Simply adding Tailwind’s <code>class=&quot;mx-auto&quot;</code> will center the Vue logo and prove that the base configuration worked.</p>
<p>There are 2 likely use cases now:</p>
<ol>
<li>creating global styles in <code>index.css</code></li>
<li>creating local/scoped styles inside of Vue’s components</li>
</ol>
<p>Both of these use cases will make use of Tailwind’s <code>@apply</code> directive (see: <a class="ext" href="https://tailwindcss.com/docs/functions-and-directives/#apply" rel="noopener">docs</a>).</p>
<pre class="language-css"><code class="language-css"><span class="token comment">/* index.css */</span><br><span class="token atrule"><span class="token rule">@tailwind</span> base<span class="token punctuation">;</span></span><br><br><span class="token atrule"><span class="token rule">@tailwind</span> components<span class="token punctuation">;</span></span><br><br><span class="token selector">hr</span> <span class="token punctuation">{</span><br>  <span class="token atrule"><span class="token rule">@apply</span> border-gray-500 my-8<span class="token punctuation">;</span></span><br><span class="token punctuation">}</span><br><br><span class="token atrule"><span class="token rule">@tailwind</span> utilities<span class="token punctuation">;</span></span></code></pre>
<p>Also helpful when defining styles <strong>in components</strong> (global or <code>scoped</code>):</p>
<pre class="language-js"><code class="language-js"><span class="token operator">&lt;</span>style lang<span class="token operator">=</span><span class="token string">"postcss"</span><span class="token operator">></span><br>  <span class="token punctuation">.</span>btn <span class="token punctuation">{</span><br>    @apply inline<span class="token operator">-</span>block font<span class="token operator">-</span>bold rounded<span class="token operator">-</span>lg shadow<span class="token operator">-</span>sm px<span class="token operator">-</span><span class="token number">6</span> py<span class="token operator">-</span><span class="token number">2</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token operator">&lt;</span><span class="token operator">/</span>style<span class="token operator">></span></code></pre>
<p>Please note <code>lang=&quot;postcss&quot;</code> - that’s necessary for <code>@apply</code> to work here (and also provides autocomplete for Tailwind’s classes in VS Code).</p>
<h2 id="cleaning-up-purge-css" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#cleaning-up-purge-css"><span>Cleaning up - PurgeCSS</span></a></h2>
<p>As mentioned at the beginning of this article, Tailwind CSS offers built-in PurgeCSS now - very convenient and an absolute game changer IMO!</p>
<p>You may have noticed it already; in order to configure PurgeCSS, we’ll need to get back to our <code>tailwind.config.js</code> - there’s an empty array in there that will accept our desired configuration:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// ./tailwind.config.js</span><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">purge</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token operator">...</span></code></pre>
<p>Let’s change that according to where our styling might be located in a Vue app:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// ./tailwind.config.js</span><br>module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token comment">// purge: { //ENABLE TO TEST LOCALLY</span><br>  <span class="token comment">//   enabled: true,</span><br>  <span class="token comment">//   content: ['./public/**/*.html', './src/**/*.html', './src/**/*.vue',],</span><br>  <span class="token comment">// },</span><br>  <span class="token literal-property property">purge</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token string">'./public/**/*.html'</span><span class="token punctuation">,</span><br>    <span class="token string">'./src/**/*.html'</span><span class="token punctuation">,</span><br>    <span class="token string">'./src/**/*.vue'</span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token operator">...</span></code></pre>
<p>The <a class="ext" href="https://tailwindcss.com/docs/controlling-file-size/" rel="noopener">PurgeCSS configuration</a> only affects production builds which is why there are some commented lines - I like to use them to test manually before deployment (if necessary and/or curious).</p>
<h2 id="recommended-vs-code-extensions" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#recommended-vs-code-extensions"><span>Recommended VS Code Extensions</span></a></h2>
<p>Aside from <a class="ext" href="https://marketplace.visualstudio.com/items?itemName=octref.vetur" rel="noopener">Vetur</a>, which will make your life with Vue a lot easier, there are some extensions that make working with Tailwind CSS even more pleasant:</p>
<ul>
<li><a class="ext" href="https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss" rel="noopener">Tailwind CSS IntelliSense</a></li>
<li><a class="ext" href="https://marketplace.visualstudio.com/items?itemName=Zignd.html-css-class-completion" rel="noopener">IntelliSense for CSS class names</a></li>
</ul>
<h2 id="and-thats-it" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#and-thats-it"><span>…and that’s it!</span></a></h2>
<p>As of writing this article, setting up Tailwind CSS for use with Vue.js is a rather simple process. Considering that there’s basically 0 configuration needed for PurgeCSS is an absolute convenience feature and one more reason to give Tailwind CSS a go for your next Vue.js project.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Getting Started with Vue.js in 2020</title>
        
          
            <category term="guide" />
          
            <category term="learning" />
          
            <category term="vue" />
          
        
        <link href="https://ttntm.me/blog/vuejs-getting-started-in-2020/"/>
        
        <updated>2020-07-04T10:30:00Z</updated>
        <id>https://ttntm.me/blog/vuejs-getting-started-in-2020/</id>
        <content type="html"><![CDATA[
          
            
            <p>This article is about <a class="ext" href="https://vuejs.org" rel="noopener">Vue.js</a>, but it is not going to be a Vue.js tutorial. There’s enough good ones out there and I don’t think I’ll be able to compete with them. Instead, this is going to be a summary of my personal lessons learned when picking up Vue.js and starting to build something useful with it.</p>
<p>Now, what might be something useful? No offense to the ones advocating its teaching effect, but probably not the 10000st todo list app. After a bit of deliberation, I decided to go for a <strong>recipes app</strong> intended for real world use, i.e. collecting all the magazine clippings and grandma’s notes in one place and making sure that it’s also available as a PWA while standing in the kitchen and actually cooking.</p>
<p><strong>TL;DR</strong> - I built a Vue.js based recipes app without ever having touched Vue before; you’re going to read about the process of doing that (incl. all the helpful resources I came across!), the obstacles that came up and how I overcame them here.</p>
<p>The app can be found over at <a class="ext" href="https://recept0r.com" rel="noopener">recept0r.com</a>.</p>
<h2 id="preface" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#preface"><span>Preface</span></a></h2>
<p>My motivation for writing this article is to share my experience with others and to make their journey into Vue a little more convenient. As such, the following article represents a somewhat chronological (thought) process. There’s no affiliation to any of the linked resources and I don’t stand to gain anything from recommending or even mentioning them.</p>
<p>Please note that this article is based on Vue.js 2.9.6 / June 2020.</p>
<h3 id="1-initial-research-and-decision-making" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#1-initial-research-and-decision-making"><span>1. Initial Research and Decision Making</span></a></h3>
<p>The decision for Vue.js was easy; some good reasons can be found here: <a class="ext u-in-reply-to" href="https://michaelnthiessen.com/underdog-framework" rel="noopener">michaelnthiessen.com/underdog-framework</a></p>
<p>Other than that, there’s always a long list of “technologies I’d like to work with”; for this project, I ended up going for <a class="ext" href="https://functions.netlify.com" rel="noopener">serverless functions</a> (Netlify) and <a class="ext" href="https://fauna.com" rel="noopener">Fauna</a> as the app’s data store. Both seemed powerful (and free) enough to build a multiuser CRUD app on top of while offering good documentation and development experience.</p>
<p>Most other decisions for (and against) modules/libraries/etc. happened along the way and we’ll get to that; the whole list of dependencies can be found in recept0r’s <code>package.json</code> at <a class="ext" href="https://codeberg.org/ttntm/recept0r/src/branch/main/package.json" rel="noopener">Codeberg</a> though, just in case you’re curious already.</p>
<h3 id="2-learning-by-doing" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#2-learning-by-doing"><span>2. Learning by Doing</span></a></h3>
<p>I had heard of Vue.js before and I knew I wanted to use it for this project. I hadn’t actually <em>used</em> it before though, so I thought it would be a good idea to start with some sort of tutorial to get my head around the basics before going for my actual project.</p>
<p>After a bit of research, I came across this one here: <a class="ext u-in-reply-to" href="https://www.taniarascia.com/getting-started-with-vue" rel="noopener">taniarascia.com/getting-started-with-vue</a></p>
<p>This tutorial provided a very good “code along” kind of introduction from basic project setup (Vue CLI) to accessing external APIs - I stopped before the API part though, as I felt comfortable enough already, eager to dive into my own project. Overall a decent and well written tutorial.</p>
<p>There were one or two things in the tutorial app that needed a bit of tinkering/thinking for yourself to make it work as intended, nothing serious though if you’re into JavaScript far enough to be dealing with Vue.</p>
<h3 id="3-start-small" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#3-start-small"><span>3. Start Small</span></a></h3>
<p>Building on top of what I had learned in both the tutorial mentioned above and the (inevitable) trial and error cycles, I set out to build a first prototype of what would later become <em>recept0r</em>.</p>
<p>I decided to limit myself to the essential CRUD functionality only using local data at first, meaning I consciously decided in favor of later refactoring, which in turn lead to the benefit of first architecting/understanding my own app and going for the (serverless) backend and database after that.</p>
<p>What do I mean with “local data”? Instead of querying an API/database, the Vue app will be initialized with an array of predefined data, recipes in my case:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// App.vue</span><br><span class="token function">data</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">recipes</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>      <span class="token punctuation">{</span><br>        <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token number">001</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">title</span><span class="token operator">:</span> <span class="token string">"First Recipe"</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">ingredients</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>          <span class="token string">"one"</span><span class="token punctuation">,</span><br>          <span class="token string">"two"</span><span class="token punctuation">,</span><br>          <span class="token string">"three"</span><br>        <span class="token punctuation">]</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">"Preliminary recipe body text..."</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span><br>      <span class="token comment">// more recipe objects if needed</span><br>    <span class="token punctuation">]</span><br>  <span class="token punctuation">}</span></code></pre>
<p>This array of recipes (objects) provided enough flexibility to develop the necessary components (incl. styling) and their methods (the core CRUD functionality) before ever having to deal with the more delicate matters like user authentication, database queries and serverless functions.</p>
<p><em>This step is entirely optional of course</em>; not needed at all if you’re already comfortable with the necessary backend functionality or have done it before. As this was my first time doing anything like this (and there was no limit in terms of time/budget), I opted for this slower approach.</p>
<h3 id="4-rtfm" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#4-rtfm"><span>4. RTFM</span></a></h3>
<p>We’ve all been there - after some time spent looking up a solution to what seemed like a trivial task, the answer presents itself in the form of the official documentation for the respective tool/module/library/etc.</p>
<p>So, this isn’t necessarily a “lesson learned”, maybe more of a reminder to actually go and <em>read the manual</em>.</p>
<p>One such case was <a class="ext" href="https://router.vuejs.org" rel="noopener">vue-router</a>, an essential building block in any vue application that has more than a single route the user can navigate to. It took me a while to set it up the way I had intended, including the so-called <em>Navigation Guards</em> as well as <em>Programmatic Navigation</em>.</p>
<p>You’ll notice an issue with routing immediately when your app keeps acting as if there was a page refresh (i.e. resetting your local data) after clicking on a simple link. In my case, I had all the links in my navigation defined as regular <code>&lt;a href=&quot;...&quot;&gt;</code> elements when I should actually have used <code>&lt;router-link :to=&quot;...&quot;&gt;</code> instead.</p>
<p>Another such case was <strong>configuring Webpack</strong>. Things work differently when using <a class="ext" href="https://cli.vuejs.org" rel="noopener">Vue CLI</a> - Webpack needs to be configured inside of <code>vue.config.js</code> and sooner or later you’ll have to deal with it (i.e. <a class="ext" href="https://cli.vuejs.org/core-plugins/pwa.html" rel="noopener">PWA</a>, code splitting). It would have saved me quite some time and some install/uninstall procedures if I had read and understood that in the first place. Don’t make the same mistake and read up on what Vue CLI does and how you can configure it to do what you want it to do.</p>
<h3 id="5-make-your-tools-work-for-you" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#5-make-your-tools-work-for-you"><span>5. Make Your Tools Work for You</span></a></h3>
<p>I had some issues when it came to setting up <strong>Netlify functions</strong> to run locally; research I based my choice of node modules and configuration on seemingly outdated information and my functions returned errors instead of data…</p>
<p>Eventually, I stumbled upon this article recommending <code>netlify-cli</code>: <a class="ext u-in-reply-to" href="https://alligator.io/nodejs/solve-cors-once-and-for-all-netlify-dev" rel="noopener">Solve CORS once and for all with Netlify Dev</a></p>
<p>If you’re going the Netlify route, this is as good as it gets basically - simple configuration and immediate success. Just memorize <code>localhost:8888</code>, as you terminal output will still direct you to 8080 instead where none of the success is visible.</p>
<p>Another case that belongs in this category was <strong>toast messages</strong>. Yes, there are a lot (!) of modules out there that can handle every imaginable notification use case including respective customization. I won’t recommend any of them, because I didn’t use any - I opted against adding yet another dependency to my app just for the sake of an occasional notification and decided to build it from scratch.</p>
<p>I came across <a class="ext u-in-reply-to" href="https://laravel-news.com/building-a-flash-message-component-with-vue-js-and-tailwind-css" rel="noopener">this article</a> that quickly provided a working solution that was rather easy to follow without adding any more dependencies to my app.</p>
<h3 id="6-refactoring-is-good-keep-doing-it" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#6-refactoring-is-good-keep-doing-it"><span>6. Refactoring is Good, Keep Doing it</span></a></h3>
<p>I started building <em>recept0r</em> based on Bootstrap. I don’t know why exactly, it just happened - probably 70% of why Bootstrap is used, haha. This added jQuery and some more JS bloat which adds nothing to a Vue app. It made the app look decent from the start though and especially forms/inputs and their flavor of <code>normalize.css</code> were convenient.</p>
<p>It all ended up becoming unnecessarily heavy and I found myself working around it too much (overwriting CDN CSS in components). I then discovered that there is <a class="ext" href="https://bootstrap-vue.org" rel="noopener">BootstrapVue</a>, a seemingly convenient way to make better use of Bootstrap together with Vue. I didn’t go for that after some deliberation, but instead spent my time on switching to <a class="ext" href="https://tailwindcss.com" rel="noopener">TailwindCSS</a>.</p>
<p>I’ve used TaiwlindCSS for 5 or more projects in the last 2 years, so this ended up making me faster and more flexible than I could ever have imagined. Using Tailwind with Vue is surprisingly easy in 2020, you can read all about it in <a href="https://ttntm.me/blog/tailwind-css-with-vuejs/" class="int">another blog post I wrote about that</a>.</p>
<p>Remember the <strong>toast notifications</strong> mentioned earlier? I ended up refactoring them eventually…</p>
<p>The <em>Event Bus</em> based approach linked above worked well and did its job, but my app was already using <a class="ext" href="https://vuex.vuejs.org" rel="noopener">vuex</a> for global state management anyway (mostly due to user authentication, but also for filtering and general global state mgt.). Following the principles of unified state management, it clearly made more sense to use vuex compared to having a separate Vue instance (named <em>Event Bus</em>) initialized just for the toast notifications.</p>
<h3 id="7-the-devil-is-in-the-detail" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#7-the-devil-is-in-the-detail"><span>7. The Devil is in the Detail</span></a></h3>
<p>Between setting up FaunaDB and getting the <code>focus()</code> right on procedurally added <code>input</code> elements, what do you think took me more time?</p>
<p>Yes, <code>focus()</code>. To be honest though, I’m rather sure that the answer is <em>not</em> going to surprise anyone that has ever worked in software development. I guess this is not a real “lesson learned” then, sorry for that.</p>
<p>What was it all about, anyway? Recipes have ingredients and <em>recept0r</em> stores them as strings in an array. When creating a new recipe or editing an existing one, there’s obviously the possibility of adding/removing ingredients. Adding them should work in two ways:</p>
<ol>
<li>“Add ingredient” button -&gt; adds an input below all others, essentially <code>ingredients.push('')</code></li>
<li>“Enter” pressed inside an existing ingredient’s input -&gt; adds an input below <em>the respective</em> element and <em>before</em> the next one (or at the end, in case it is the last one): <code>ingredients.splice(index + 1, 0, '')</code></li>
</ol>
<p>The whole method looked like this then:</p>
<pre class="language-js"><code class="language-js"><span class="token function">addIngredient</span><span class="token punctuation">(</span><span class="token parameter">index</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> ing <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>recipe<span class="token punctuation">.</span>ingredients<span class="token punctuation">;</span><br>  <span class="token keyword">if</span><span class="token punctuation">(</span>index <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    ing<span class="token punctuation">.</span><span class="token function">splice</span><span class="token punctuation">(</span>index <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    ing<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>Simple, huh?</p>
<p>Now what was the issue with <code>focus()</code>? I already had a <a class="ext" href="https://vuejs.org/v2/guide/custom-directive.html" rel="noopener">global directive</a> in my app that applied focus this way:</p>
<pre class="language-js"><code class="language-js">Vue<span class="token punctuation">.</span><span class="token function">directive</span><span class="token punctuation">(</span><span class="token string">'focus'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>  <span class="token function-variable function">inserted</span><span class="token operator">:</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">el</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    el<span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>That didn’t help with case #2 though; instead, it always applied focus to the last input in my ingredients list. I already knew about <a class="ext" href="https://vuejs.org/v2/api/#ref" rel="noopener">$refs</a>, but I couldn’t get them to work the way I wanted after generating them for my inputs:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text<span class="token punctuation">"</span></span><br>  <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block form-control text-sm mb-4<span class="token punctuation">"</span></span><br>  <span class="token attr-name">v-model.trim</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>recipe.ingredients[index]<span class="token punctuation">"</span></span><br>  <span class="token attr-name">v-focus</span><br>  <span class="token attr-name">@keydown.enter</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>addIngredient(index)<span class="token punctuation">"</span></span><br>  <span class="token attr-name">:placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>`Ingredient ${index+1}`<span class="token punctuation">"</span></span><br>  <span class="token attr-name">:ref</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>`input${index}`<span class="token punctuation">"</span></span><br><span class="token punctuation">></span></span></code></pre>
<p>After hours of trial and error and the emerging “tunnel vision”, I took a break. After the break, I randomly found this <a class="ext" href="https://stackoverflow.com/questions/54306581/this-refsp-index-focus-is-not-a-function" rel="noopener">Stack Overflow question</a>. 5 minutes later I had it working, the only thing different to my own previous approach being the added <code>[0]</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token function">addIngredient</span><span class="token punctuation">(</span><span class="token parameter">index</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> ing <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span>recipe<span class="token punctuation">.</span>ingredients<span class="token punctuation">;</span><br>  <span class="token keyword">if</span><span class="token punctuation">(</span>index <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    ing<span class="token punctuation">.</span><span class="token function">splice</span><span class="token punctuation">(</span>index <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">$nextTick</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token comment">// focus the spliced element instead of the last one as per directive</span><br>      <span class="token comment">// somehow vue returns a 1 element array here</span><br>      <span class="token comment">// see -> https://stackoverflow.com/questions/54306581/this-refsp-index-focus-is-not-a-function</span><br>      <span class="token keyword">this</span><span class="token punctuation">.</span>$refs<span class="token punctuation">[</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">input</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>index <span class="token operator">+</span> <span class="token number">1</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">focus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    ing<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>I would have been able to get to the bottom of this myself eventually, carefully reading each line of the errors in my console; that’s not the point though - tunnel vision can cost you a lot of time. Take a step back, do something else for a while and look at your problem again later once you’ve gained some distance; no matter how you get to the solution, it’s all better than staring at your screen, obsessing over it and getting stuck.</p>
<h2 id="final-remarks" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#final-remarks"><span>Final Remarks</span></a></h2>
<p>This list could go on for a while, in varying degrees of detail. I tried to pick the larger/more important bits that people researching their own problems might stumble upon. I’ll probably also end up writing a second part at some point and/or update it, comments and feedback are much appreciated.</p>
<h2 id="additional-resources-further-reading" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#additional-resources-further-reading"><span>Additional Resources / Further Reading</span></a></h2>
<p>All of these articles were helpful to me in one way or another:</p>
<ul>
<li><a class="ext" href="https://notes-on-vue.netlify.app" rel="noopener">Notes on Vue</a> (notes based on first-hand personal learning experience, recommended!)</li>
<li><a class="ext" href="https://v2.vuejs.org/v2/guide/reactivity.html" rel="noopener">Reactivity in Depth</a> (must read; the basics of reactivity in vue)</li>
<li><a class="ext u-in-reply-to" href="https://itnext.io/anyway-heres-how-to-create-a-multiple-layout-system-with-vue-and-vue-router-b379baa91a05" rel="noopener">How to create a multiple layout system with Vue and Vue-router</a> (re: vue-router)</li>
<li><a class="ext u-in-reply-to" href="https://www.netlify.com/blog/2018/07/09/building-serverless-crud-apps-with-netlify-functions-faunadb/" rel="noopener">Building Serverless CRUD apps with Netlify Functions &amp; FaunaDB</a> (esp. useful for the detailed explanations re: serverless functions)</li>
<li><a class="ext u-in-reply-to" href="https://dev.to/chiubaca/build-a-serverless-crud-app-using-vue-js-netlify-and-faunadb-5dno" rel="noopener">Build a serverless CRUD app with authentication using Vue.js Netlify and FaunaDB</a> (must read; user authentication with Netlify Identity/GoTrueJS)</li>
<li><a class="ext u-in-reply-to" href="https://alligator.io/vuejs/vue-lazy-load-images/" rel="noopener">Lazy-Load Images In Vue.js with vue-clazy-load</a> (convenient and recommended)</li>
<li><a class="ext u-in-reply-to" href="https://medium.com/@stephane.souron/making-a-large-scale-app-with-vue-js-part-1-modularize-your-store-bf9066436502" rel="noopener">Making a large scale app with vue.js (part 1): modularize your store!</a> (very well written and useful for a better understanding of how to properly use vuex)</li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Boilerplate: Hugo, TailwindCSS and gulp</title>
        
          
            <category term="hugo" />
          
            <category term="tailwind" />
          
            <category term="template" />
          
        
        <link href="https://ttntm.me/blog/hugo-tailwind-boilerplate/"/>
        
        <updated>2020-04-12T10:30:00Z</updated>
        <id>https://ttntm.me/blog/hugo-tailwind-boilerplate/</id>
        <content type="html"><![CDATA[
          
            
            <p>So far, I’ve used the combination of Hugo and <a class="ext" href="https://tailwindcss.com" rel="noopener">Tailwind CSS</a> for more than 5 different projects. It’s an extremely versatile zero dependency toolkit that “just works” without the added bulk of unnecessary pre-made components, jQuery and many lines of (mostly) unused code.</p>
<p>In doing so, I’ve started using local template project folders that just needed an <code>npm install</code> command to be up and running. Turns out that might be useful for others too (duh), so I’ve created a boilerplate repository of this setup today.</p>
<h2 id="hugo-tailwind-boilerplate" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#hugo-tailwind-boilerplate"><span>hugo-tailwind-boilerplate</span></a></h2>
<p>So, here’s the rundown.</p>
<p>Trusty old <a class="ext" href="https://gulpjs.com" rel="noopener">gulp</a> handles Tailwind builds via PostCSS with 2 gulp tasks called <code>dev-css</code> and <code>build-css</code>. I’m sticking to gulp as my “swiss army knife”; it’s small, powerful and overall another one of those tools that “just work”, no matter what you throw at them.</p>
<p>Then, there’s 2 npm scripts defined in <code>package.json</code> that make use of these tasks:</p>
<ol>
<li><code>start</code></li>
<li><code>deploy</code></li>
</ol>
<p><code>start</code> is meant for local development (and the whole power of Tailwind + autocomplete classes), <code>deploy</code> will build the site into <code>./public</code> (using <code>hugo --minify</code>), ready for deployment (i.e. drag and drop into Netlify).</p>
<p>This boilerplate also includes <a class="ext" href="https://purgecss.com" rel="noopener">purgecss</a> to remove any unused styles from the final stylesheet that gets written to <code>static/css</code> for deployment. I had to play around with the config a bit to make it work for me though - I’m a bit lazy, so I like using things like this:</p>
<pre class="language-css"><code class="language-css"><span class="token selector">*:hover</span> <span class="token punctuation">{</span><br>  <span class="token property">transition</span><span class="token punctuation">:</span> all .35s ease-in-out<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>In order for this <em>not</em> to get purged (as it’s technically an unused selector), my <code>purgecss</code> config looks like this:</p>
<pre class="language-js"><code class="language-js"><span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">purgecss</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'./layouts/**/*.html'</span><span class="token punctuation">,</span><span class="token string">'./content/**/*.md'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token function-variable function">defaultExtractor</span><span class="token operator">:</span> <span class="token parameter">content</span> <span class="token operator">=></span> content<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">[\w-/:]+(?&lt;!:)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token comment">//save most things that should not be purged</span><br>  <span class="token literal-property property">whitelist</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">':hover'</span><span class="token punctuation">,</span><span class="token string">':focus-visible'</span><span class="token punctuation">,</span> <span class="token string">'button'</span><span class="token punctuation">,</span> <span class="token string">'button:focus-visible'</span><span class="token punctuation">]</span> <span class="token comment">//preserve the rest</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre>
<p>For testing/troubleshooting etc., un-purged CSS can be built using <code>gulp dev-css</code> whereas purged CSS comes out of <code>gulp build-css</code>.</p>
<p>The Hugo setup is basic at best; should be enough to build upon though. See <code>layouts/_default/baseof.html</code> to get an idea what the default template looks like.</p>
<blockquote>
<p>Here’s the <a class="ext" href="https://github.com/ttntm/hugo-tailwind-boilerplate" rel="noopener">GitHub repository</a></p>
</blockquote>
<h2 id="usage" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#usage"><span>Usage</span></a></h2>
<p>In order to work with this repository, only 2 things are required:</p>
<ul>
<li>Hugo</li>
<li>node/npm</li>
</ul>
<p>If that’s set up, then all you need is to get the repository, run <code>npm install</code> and then <code>npm run start</code>. Go to <code>localhost:1313</code> and you should see a one-page demo site you can start working with in your browser.</p>
<p>PS: the CSS in <code>./static/css</code> has been purged, so you may want to run <code>gulp dev-css</code> in order to make all of Tailwind available for your project and/or customize it through <code>tailwind.config.js</code> (see <a class="ext" href="https://tailwindcss.com/docs/configuration" rel="noopener">their docs</a> for details).</p>
<h2 id="addendum" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#addendum"><span>Addendum</span></a></h2>
<p>This setup (esp. the choice of using gulp) is older than <a class="ext" href="https://gohugo.io/hugo-pipes/" rel="noopener">Hugo Pipes</a>; but I’m also relying on gulp as the main actor for <a href="https://ttntm.me/blog/static-blog-comments-hugo/" class="int">processing comments</a>, so I’ll stick with it for the foreseeable future.</p>
<p>If <em>you</em> don’t want to use gulp though, Hugo has PostCSS built in by now - more about that in the <a class="ext" href="https://gohugo.io/hugo-pipes/postcss/" rel="noopener">Hugo Docs</a>.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Hugo&#39;s &#39;.plain&#39; Function Ignores Page Reources</title>
        
          
            <category term="hugo" />
          
            <category term="learning" />
          
        
        <link href="https://ttntm.me/blog/hugo-plain-function-ignores-page-resources/"/>
        
        <updated>2020-03-30T10:30:00Z</updated>
        <id>https://ttntm.me/blog/hugo-plain-function-ignores-page-resources/</id>
        <content type="html"><![CDATA[
          
            
            <p>When <a class="ext" href="https://ttntm.me/gist.github.com/eddiewebb/735feb48f50f0ddd65ae5606a1cb41ae" rel="noopener">adding a search function to your Hugo site</a>, one thing that’s required is a JSON index of the respective site’s content. If your site is also making use of <a class="ext" href="https://ttntm.me/gohugo.io/content-management/page-resources/" rel="noopener">Page Resources</a>, there can be issues when creating this index. This short article is meant to document a possible workaround for such cases.</p>
<p><em>This is/was an issue in Hugo 0.58; I assume it won’t be different in newer releases, but that’s not a 100% certain.</em></p>
<h2 id="problem-description" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#problem-description"><span>Problem Description</span></a></h2>
<p>According to the linked gist, the template for the JSON index <code>index.json</code> is really the opposite of spectacular:</p>
<pre class="language-go"><code class="language-go"><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $<span class="token punctuation">.</span>Scratch<span class="token punctuation">.</span>Add <span class="token string">"index"</span> slice <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> <span class="token keyword">range</span> <span class="token punctuation">.</span>Site<span class="token punctuation">.</span>RegularPages <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>  <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $<span class="token punctuation">.</span>Scratch<span class="token punctuation">.</span>Add <span class="token string">"index"</span> <span class="token punctuation">(</span>dict <span class="token string">"title"</span> <span class="token punctuation">.</span>Title <span class="token string">"tags"</span> <span class="token punctuation">.</span>Params<span class="token punctuation">.</span>tags <span class="token string">"categories"</span> <span class="token punctuation">.</span>Params<span class="token punctuation">.</span>categories <span class="token string">"contents"</span> <span class="token punctuation">.</span>Plain <span class="token string">"permalink"</span> <span class="token punctuation">.</span>Permalink<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> end <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $<span class="token punctuation">.</span>Scratch<span class="token punctuation">.</span>Get <span class="token string">"index"</span> <span class="token operator">|</span> jsonify <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span></code></pre>
<p>The template sets up a <code>.Scratch</code> (<a class="ext" href="https://gohugo.io/functions/scratch" rel="noopener">Hugo docs</a>) and then ranges through all <code>RegularPages</code>, meaning every page that isn’t a section, taxonomy list etc. - pages that are supposed to contain “real” content (see <a class="ext" href="https://gohugo.io/variables/site/#site-pages" rel="noopener">Hugo docs</a> for further details). It collects the <code>.Title</code>, tags, categories and the respective <code>.Permalink</code> of each page and it also makes sure that the actual content is added to the index via <code>.Plain</code>.</p>
<p>All of that information/content then gets added to <code>index.json</code> in the last line, thus providing enough “food” for the respective full text search functionality (Fuse.js in this case).</p>
<p>When it comes to <code>RegularPages</code> that consist of a mixture of pure Markdown content <em>and</em> Page Resources though, <code>.Plain</code> fails (and so do other things like <code>.Content | plainify</code>) - the “content” key for those pages stays blank and/or contains only whatever Markdown was actually found, ignoring the content coming from the Page Resources.</p>
<h2 id="why-use-page-resources" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#why-use-page-resources"><span>Why use Page Resources?</span></a></h2>
<p>Simply talking about that term might be enough for some, but let’s just take a look at an example here: <a class="ext" href="https://process4.biz/en/services/" rel="noopener">a ‘Services’ page</a> rendered almost exclusively from Page Resources.</p>
<p>On the top, right underneath the header, there are 2 lines of “real” Markdown content. Then there’s a grid of icons with short pieces of text and a larger section with the actual services on offer.</p>
<p>Each one of these pieces of content is its own *.md Page Resource, mostly due to an easy <code>range</code> based approach when creating the template and a high degree of extensibility in case something has to be added, changed or removed in either one of these blocks. Page Resources provide this flexibility here.</p>
<h2 id="workaround" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#workaround"><span>Workaround</span></a></h2>
<p>Coming back to the problem with the JSON search index, the Hugo community wasn’t of any help unfortunately - here’s the respective post from earlier this year: <a class="ext" href="https://discourse.gohugo.io/t/building-a-json-search-index-trouble-with-plain/" rel="noopener">discourse.gohugo.io</a></p>
<p>After a while of messing around with the <code>index.json</code> template, I ended up with the following approach to get the Page Resources included in the index:</p>
<pre class="language-go"><code class="language-go"><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $<span class="token punctuation">.</span>Scratch<span class="token punctuation">.</span>Add <span class="token string">"index"</span> slice <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> <span class="token keyword">range</span> <span class="token punctuation">.</span>Site<span class="token punctuation">.</span>RegularPages <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>  <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> <span class="token keyword">if</span> and eq <span class="token punctuation">.</span>Layout <span class="token string">"services"</span> <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>    <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $md <span class="token operator">:=</span> <span class="token punctuation">.</span>Resources<span class="token punctuation">.</span>Match <span class="token string">"**.md"</span> <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>    <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $sc <span class="token operator">:=</span> newScratch <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>      <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $sc<span class="token punctuation">.</span>Add <span class="token string">"ct"</span> <span class="token punctuation">.</span>Description <span class="token operator">|</span> markdownify <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>      <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $sc<span class="token punctuation">.</span>Add <span class="token string">"ct"</span> <span class="token string">", "</span> <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>      <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> with $<span class="token punctuation">.</span>Params<span class="token punctuation">.</span>intro <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>        <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $sc<span class="token punctuation">.</span>Add <span class="token string">"ct"</span> <span class="token punctuation">.</span>Params<span class="token punctuation">.</span>intro <span class="token operator">|</span> markdownify <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>        <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $sc<span class="token punctuation">.</span>Add <span class="token string">"ct"</span> <span class="token string">", "</span> <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>      <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> end <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>      <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> <span class="token keyword">range</span> $md <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>        <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $sc<span class="token punctuation">.</span>Add <span class="token string">"ct"</span> <span class="token punctuation">.</span>Title <span class="token operator">|</span> markdownify <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>        <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $sc<span class="token punctuation">.</span>Add <span class="token string">"ct"</span> <span class="token string">", "</span> <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>        <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $sc<span class="token punctuation">.</span>Add <span class="token string">"ct"</span> <span class="token punctuation">.</span>Content <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>    <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> end <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>    <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $content <span class="token operator">:=</span> $sc<span class="token punctuation">.</span>Get <span class="token string">"ct"</span> <span class="token operator">|</span> plainify <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>    <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $<span class="token punctuation">.</span>Scratch<span class="token punctuation">.</span>Add <span class="token string">"index"</span> <span class="token punctuation">(</span>dict <span class="token string">"title"</span> <span class="token punctuation">.</span>Title <span class="token string">"tags"</span> <span class="token punctuation">.</span>Params<span class="token punctuation">.</span>tags <span class="token string">"content"</span> $content <span class="token string">"permalink"</span> <span class="token punctuation">.</span>Permalink<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>  <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> <span class="token keyword">else</span> <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>    <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $<span class="token punctuation">.</span>Scratch<span class="token punctuation">.</span>Add <span class="token string">"index"</span> <span class="token punctuation">(</span>dict <span class="token string">"title"</span> <span class="token punctuation">.</span>Title <span class="token string">"tags"</span> <span class="token punctuation">.</span>Params<span class="token punctuation">.</span>tags <span class="token string">"content"</span> <span class="token punctuation">.</span>Plain <span class="token string">"permalink"</span> <span class="token punctuation">.</span>Permalink<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br>  <span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> end <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> end <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span><br><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token operator">-</span> $<span class="token punctuation">.</span>Scratch<span class="token punctuation">.</span>Get <span class="token string">"index"</span> <span class="token operator">|</span> jsonify <span class="token operator">-</span><span class="token punctuation">}</span><span class="token punctuation">}</span></code></pre>
<p>This is a simplified version of the deployed code; the most important bit to highlight here is the part starting with <code>range $md</code>. That’s where we leave the page context and instead range through all the Page Resources (see line 4; <code>$md := .Resources.Match &quot;**.md&quot;</code>) in order to get their <code>.Title</code> and <code>.Content</code> added to the index. All that happens inside the context of the key of the “services” page, so that all search queries finding a match lead to the correct page.</p>
<h2 id="lessons-learned" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#lessons-learned"><span>Lessons Learned</span></a></h2>
<p>Hugo is a very powerful tool, but every once in a while there’s something that hasn’t been thought of yet. Aside from some headache and some trial and error based on the community support, this hasn’t been a major issue though. The above code may not be the best or prettiest solution to this issue, but it works - you can test it yourself in the real world example site linked above.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Reboot/Catharsis</title>
        
          
            <category term="css" />
          
            <category term="news" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/reboot-catharsis/"/>
        
        <updated>2020-03-24T10:30:00Z</updated>
        <id>https://ttntm.me/blog/reboot-catharsis/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="why" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#why"><span>Why?</span></a></h2>
<p>A one word question seems like a good way to start off this article. But before we go into further details, maybe a word or two to shed some light on what’s actually happened here.</p>
<p>If you’ve been here before (much appreciated!), you’ll notice that the site looks somehow different. You may also have noticed that it loads much faster, depending on your connection of course. And in case you’re reading this with JavaScript disabled, that’s also some news - there’s none of that here anymore. <strong>So, this is basically a new website you’re looking at.</strong></p>
<p>Getting back to answering the initial question of <em>why</em> now, the answer could be any of the following:</p>
<ul>
<li>Coronavirus quarantine</li>
<li>Bootstrap overdose</li>
<li>Nord theme</li>
<li>Design exercise</li>
<li>I felt like it</li>
<li>Spring</li>
<li>…</li>
</ul>
<p>There’s a second part to <em>why</em> though, but we’ll get to that in a bit.</p>
<h2 id="what" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#what"><span>What?</span></a></h2>
<p>Now that we’ve questioned my motivation, let’s have a look at what I’ve done here exactly.</p>
<p>Previously, this website loaded at least 1.1 MB of resources/assets in 17 requests. This was due to Bootstrap 4 and its dependencies (i.e. jQuery), that <code>particles.js</code> library (was fun!) and some vain images to show off what I worked on (nothing wrong with that).</p>
<p>The copy of Bootstrap I used was cut down to the “essentials” over time, heavily customized in terms of colors, button styles etc. and way too bulky for this nimble website. jQuery wasn’t utilized at all, except for a bunch of smaller UI things and <code>particles.js</code> was basically just a gimmick I wanted to try back in the day when this site was originally created.</p>
<p>What did I do with it, you ask? <code>Shift</code> + <code>Del</code></p>
<p>I proceeded to write my own CSS from scratch (based on Eric Meyer’s CSS reset) and tried to run the site without any client-side JavaScript. To be honest though, there are 4 lines of JS in here:</p>
<pre class="language-js"><code class="language-js">window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'load'</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'JavaScript: 404'</span><span class="token punctuation">)</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Check the repo here: https://github.com/ttntm/ttntm'</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<h2 id="again-why" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#again-why"><span>Again: Why?</span></a></h2>
<p>It’s certainly nostalgic somehow, but writing my own CSS without any framework made it feel like 20 years ago. I wanted a rather simple website, reduced to the necessary functionality to present content while feeling “whole” in a way.</p>
<p>It’s not like this wouldn’t have been achievable with most CSS frameworks out there, but I doubt many of them could do it for less than the 10 KB this site’s (minified) CSS now has. To be honest, I was tempted to pick up Tailwind once again, mostly because of its high degree of customizability and its “everything’s possible” approach. I’m glad I didn’t though, for the sake of my HTML <code>class</code> attributes and the beauty of clean code.</p>
<p>To sum it up, my CSS consists of the above-mentioned CSS reset, some custom styles for all “static” elements (i.e. <code>nav</code>, <code>footer</code>) and a bunch of “utilities” (i.e. <code>padding</code>, <code>margin</code>, shadows, etc.) now. It’s still processed by trusty old <code>gulp</code>, which is the last JS bastion still standing here.</p>
<p>As mentioned above, all other JS was completely removed - it’s simply not needed for a nimble site like this. Hugo and Netlify are an extremely powerful tool set and the one bit of JS I feel like I need (comments) is handled by <code>gulp</code> and Netlify’s API - details <a href="https://ttntm.me/blog/static-blog-comments-hugo/" class="int">here</a>.</p>
<h2 id="ok-done" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#ok-done"><span>Ok, Done</span></a></h2>
<p>Thanks for reading this, might give you some ideas. I’m already curious how long this version will last.</p>
<p>I’ll go looking for some speed test results now, curious how this site performs “out there” now.</p>
<p>Honorable mention: glad I came across Nord Theme, super sleek colors.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Building a Store Locator Based on Leaflet</title>
        
          
            <category term="guide" />
          
            <category term="hugo" />
          
        
        <link href="https://ttntm.me/blog/store-locator-leaflet-hugo/"/>
        
        <updated>2020-03-15T10:30:00Z</updated>
        <id>https://ttntm.me/blog/store-locator-leaflet-hugo/</id>
        <content type="html"><![CDATA[
          
            
            <p>There’s probably a lot of good tutorials on building a store locator out there, but I couldn’t find one matching my client’s requirements 100%.</p>
<p>For context: they’re running a static site built by Hugo that uses forestry as a CMS.</p>
<p>Not exactly rocket science, but they’re normal business users (if such a thing exists). That means that content maintenance should be rather streamlined and convenient - forestry’s pretty sweet in terms of that, especially when it comes to maintaining JSON data in a way that looks and feels more natural to said users than any editor GUI would.</p>
<h2 id="requirements" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#requirements"><span>Requirements</span></a></h2>
<ul>
<li>list of stores maintained as JSON (in the CMS)</li>
<li>search function (within the list of stores)</li>
<li>map with custom markers</li>
<li>clustering of markers depending on the map’s zoom level</li>
<li>“find my location” button</li>
</ul>
<p>In terms of the visual representation, this store locator was requested as some kind of bulletin board/window looking thing, basically a list of stores with a map next to it that reflects the clicks on the respective list entry.</p>
<h2 id="introductory-remarks" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#introductory-remarks"><span>Introductory Remarks</span></a></h2>
<p>The approach described in this article is also available as a fully functional Hugo site. The site structure is defined in <code>layouts/_default/baseof.html</code> - the page content resides in <code>layouts/index.html</code>, all JavaScript is either pulled in from external (gulp-built) bundles or stored in <code>layouts/partials/js.html</code>.</p>
<p>Best have a quick look at the repository and its structure now, it’ll make the following explanations easier to understand: <a class="ext" href="https://github.com/ttntm/hugo-leaflet-store-locator" rel="noopener">GitHub Repository</a></p>
<h2 id="location-data" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#location-data"><span>Location Data</span></a></h2>
<p>In order to make this work, you’ll need data - stores and their address/location. I’ve used McDonald’s locations in Vienna for the demo site, but anything else would also work.</p>
<p>The raw JSON for one store looks like this then:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"shopName"</span><span class="token operator">:</span> <span class="token string">"McDonald's Schwedenplatz"</span><span class="token punctuation">,</span><br>  <span class="token property">"shopAddress"</span><span class="token operator">:</span> <span class="token string">"Rotenturmstraße 29"</span><span class="token punctuation">,</span><br>  <span class="token property">"shopPLZ"</span><span class="token operator">:</span> <span class="token string">"1010 Vienna"</span><span class="token punctuation">,</span><br>  <span class="token property">"shopCountry"</span><span class="token operator">:</span> <span class="token string">"Austria"</span><span class="token punctuation">,</span><br>  <span class="token property">"shopLatitude"</span><span class="token operator">:</span> <span class="token string">"48.211787"</span><span class="token punctuation">,</span><br>  <span class="token property">"shopLongitude"</span><span class="token operator">:</span> <span class="token string">"16.375875"</span><span class="token punctuation">,</span><br>  <span class="token property">"shopActive"</span><span class="token operator">:</span> <span class="token boolean">true</span><br><span class="token punctuation">}</span></code></pre>
<p>It should all be rather self-explanatory - name, address, postcode/city and country are based on data you’ll either have or easily find. The latitude/longitude can be a bit tricky to obtain, but I found <a class="ext u-in-reply-to" href="https://www.businessinsider.de/international/how-to-find-coordinates-on-google-maps/" rel="noopener">this article</a> helpful in case you’re looking for something similar.</p>
<p>The <code>shopActive</code> key is simply a toggle: show/hide the respective store in the list/map. Not absolutely necessary, but certainly convenient.</p>
<h2 id="page-template" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#page-template"><span>Page Template</span></a></h2>
<p>Before we’re going to dig into the JavaScript, we’ll need both map and data rendered for our site.</p>
<p>The following code samples are all part of <code>layouts/index.html</code>.</p>
<h3 id="rendering-the-data" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#rendering-the-data"><span>Rendering the Data</span></a></h3>
<p>If you’re not familiar with the way Hugo handles (JSON) data files, best head over there for a moment: <a class="ext" href="https://gohugo.io/templates/data-templates/" rel="noopener">Hugo Docs - Data Templates</a></p>
<p>So, we’re basically going to loop (<code>range</code>) through the data, rendering it as a list. We’re also going to add the necessary information as HTML <code>data-*</code> attributes to the respective list item:</p>
<pre class="language-html"><code class="language-html">{{ $shops := .Site.Data.stores }}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>shop-container d-flex flex-column flex-nowrap align-content-start px-md-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>{{ range sort $shops "shopPLZ" "asc" }}<br>    {{ if .shopActive }}<br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sItem flex-grow-0 px-0 py-2 p-md-2<span class="token punctuation">"</span></span> <span class="token attr-name">data-name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ .shopName }}<span class="token punctuation">"</span></span> <span class="token attr-name">data-add</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ .shopAddress }}<span class="token punctuation">"</span></span> <span class="token attr-name">data-plz</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ .shopPLZ }}<span class="token punctuation">"</span></span> <span class="token attr-name">data-cty</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ .shopCountry }}<span class="token punctuation">"</span></span> <span class="token attr-name">data-lat</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ .shopLatitude }}<span class="token punctuation">"</span></span> <span class="token attr-name">data-lon</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ .shopLongitude }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sItem--offline rounded-lg shadow-sm px-3 py-2<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h5</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>h6 mb-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ .shopName }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h5</span><span class="token punctuation">></span></span><br>                <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>small mt-1 mb-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ .shopAddress }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>br</span><span class="token punctuation">></span></span>{{ .shopPLZ }}, {{ .shopCountry }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>    {{ end }}<br>{{ end }}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>The HTML <code>data-*</code> attributes will be helpful as they’re going to supply the necessary data for the search functionality for each <code>sItem</code> as a whole, making it easy to show/hide the correct elements.</p>
<h3 id="search-input" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#search-input"><span>Search Input</span></a></h3>
<p>Above this list of shops, we’d like to have a search bar:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>p-4<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>input-group shadow-sm mt-3<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-control border-0<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>storefinder<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">onkeyup</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value javascript language-javascript"><span class="token function">findStore</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Area code, i.e. '1010'<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>input-group-append<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn btn-secondary bg-white text-secondary border-0 py-0<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>submit<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Find<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>result<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>small text-center mt-3 mb-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<h3 id="map" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#map"><span>Map</span></a></h3>
<p>Not much to do here, the map is going to fill the remaining <code>col-7</code> left behind by the list in order for both to be displayed side by side.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col-12 col-md-7 col-lg-8 map-container order-1 order-md-2 px-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>map<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">width</span><span class="token punctuation">:</span>100%<span class="token punctuation">;</span><span class="token property">height</span><span class="token punctuation">:</span>100%<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<h2 id="java-script" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#java-script"><span>JavaScript</span></a></h2>
<p>Now that we have our content rendered, it’s time to have a look at the actual functionality:</p>
<ul>
<li>creating the map</li>
<li>clustering</li>
<li>search</li>
<li>navigating based on search results, i.e. finding the marker that belongs to the clicked store</li>
<li>reset function</li>
</ul>
<p>All code samples listed here can be found in <code>layouts/partials/js.html</code> unless stated otherwise.</p>
<h3 id="prerequisites" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#prerequisites"><span>Prerequisites</span></a></h3>
<p>We have some dependencies (other than Bootstrap 4/jQuery) that we have to keep in mind. They’re all included in the <code>src</code> folder of the repository, so you don’t have to go looking unless you want to change something.</p>
<ul>
<li>Leaflet 1.2.0</li>
<li>Leaflet.markercluster 1.4.1: <a class="ext" href="https://github.com/Leaflet/Leaflet.markercluster" rel="noopener">GitHub</a></li>
<li>leaflet-locatecontrol 0.70: <a class="ext" href="https://github.com/domoritz/leaflet-locatecontrol" rel="noopener">GitHub</a></li>
</ul>
<h3 id="creating-the-map" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#creating-the-map"><span>Creating the Map</span></a></h3>
<p>First off, we’re going to need some definitions:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> items <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token string">'.sItem'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// all shops in the list</span><br><span class="token keyword">const</span> item <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token string">'.sItem--offline'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// each shop</span><br><span class="token keyword">const</span> startZoom <span class="token operator">=</span> <span class="token number">11</span><span class="token punctuation">;</span> <span class="token comment">//Define zoom level - 13 = default | 19 = max</span><br><span class="token keyword">const</span> startLat <span class="token operator">=</span> <span class="token number">48.208726</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> startLon <span class="token operator">=</span> <span class="token number">16.372644</span><span class="token punctuation">;</span></code></pre>
<p>Now we’ll create the map and add OpenStreetMap tiles:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">var</span> mymap <span class="token operator">=</span> <span class="token constant">L</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token string">'map'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token literal-property property">scrollWheelZoom</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">setView</span><span class="token punctuation">(</span><span class="token punctuation">[</span>startLat<span class="token punctuation">,</span> startLon<span class="token punctuation">]</span><span class="token punctuation">,</span> startZoom<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token comment">// Add tiles</span><br><span class="token constant">L</span><span class="token punctuation">.</span><span class="token function">tileLayer</span><span class="token punctuation">(</span><span class="token string">'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">attribution</span><span class="token operator">:</span> <span class="token string">'&amp;copy; &lt;a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap&lt;/a> contributors | &lt;a href="javascript:resetMap();">Reset map&lt;/a>'</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addTo</span><span class="token punctuation">(</span>mymap<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Take note of the <code>scrollWheelZoom: false</code> option. It makes sure that the mouse wheel doesn’t get hijacked by the map and instead changes the map’s behavior in a way that makes it necessary to click into it in order to enable mouse wheel zoom:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// zoom options enable/disable</span><br>mymap<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  mymap<span class="token punctuation">.</span>scrollWheelZoom<span class="token punctuation">.</span><span class="token function">enable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>mymap<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'mouseout'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  mymap<span class="token punctuation">.</span>scrollWheelZoom<span class="token punctuation">.</span><span class="token function">disable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Also note the <code>resetMap()</code> function packed into the bottom right attribution area of the map. It’s a simple function that resets the map and the store search, we’ll have a look at it further down below.</p>
<p>We’ll also add the “find me” button from <code>leaflet-locatecontrol</code> to the map:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// add GPS find me button</span><br><span class="token constant">L</span><span class="token punctuation">.</span>control<span class="token punctuation">.</span><span class="token function">locate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addTo</span><span class="token punctuation">(</span>mymap<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Originally, <code>leaflet-locatecontrol</code> uses Font Awesome which was not suitable for my client. I changed that to a normal Unicode “pin” character in CSS and updated the script accordingly.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.gps-marker::after</span> <span class="token punctuation">{</span><br>  <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">"📍"</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<h3 id="markers-and-clustering" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#markers-and-clustering"><span>Markers and Clustering</span></a></h3>
<p>Based on our list of stores, we’re going to create a marker for each one of them with a <code>for</code> loop. These markers then get added to a Leaflet layer group as required for <code>Leaflet.markercluster</code>.</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// create marker cluster layer</span><br><span class="token keyword">var</span> markers <span class="token operator">=</span> <span class="token constant">L</span><span class="token punctuation">.</span><span class="token function">markerClusterGroup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token comment">// iterate stores, add markers to map</span><br><span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> items<span class="token punctuation">.</span>length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> iLat <span class="token operator">=</span> items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-lat'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">let</span> iLon <span class="token operator">=</span> items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-lon'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">isEmpty</span><span class="token punctuation">(</span>iLat<span class="token punctuation">)</span> <span class="token operator">|</span> <span class="token operator">!</span><span class="token function">isEmpty</span><span class="token punctuation">(</span>iLon<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// get popup info</span><br>    <span class="token keyword">let</span> name <span class="token operator">=</span> items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-name'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">let</span> ad <span class="token operator">=</span> items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-add'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">let</span> plz <span class="token operator">=</span> items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-plz'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token comment">// create marker with associated popup</span><br>    markers<span class="token punctuation">.</span><span class="token function">addLayer</span><span class="token punctuation">(</span><span class="token constant">L</span><span class="token punctuation">.</span><span class="token function">marker</span><span class="token punctuation">(</span><span class="token punctuation">[</span>iLat<span class="token punctuation">,</span>iLon<span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token punctuation">{</span><span class="token literal-property property">key</span><span class="token operator">:</span>iLat<span class="token operator">+</span><span class="token string">'__'</span><span class="token operator">+</span>iLon<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">bindPopup</span><span class="token punctuation">(</span><span class="token string">"&lt;b>"</span> <span class="token operator">+</span> name <span class="token operator">+</span> <span class="token string">"&lt;/b>"</span> <span class="token operator">+</span> <span class="token string">"&lt;br>"</span> <span class="token operator">+</span> ad <span class="token operator">+</span> <span class="token string">", "</span> <span class="token operator">+</span> plz<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// marker added to cluster layer</span><br>    <span class="token comment">// we use an ID made up of iLat and iLon here, so we can find the marker again later</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// add clustered markers to map</span><br>mymap<span class="token punctuation">.</span><span class="token function">addLayer</span><span class="token punctuation">(</span>markers<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Just like the comment in the code above mentions, there’s an option <code>key</code>, essentially an ID made up of Latitude and Longitude. We’re going to need that for finding the correct marker when handling the clicks for the stores in the list.</p>
<h3 id="search-function" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#search-function"><span>Search Function</span></a></h3>
<p>As mentioned above, the list of stores should have a search function. We added the respective input above the list of stores in the template, the following <code>findStore()</code> is going to provide the necessary functionality.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">findStore</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> searchInput <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token string">'#storefinder'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> hidden <span class="token operator">=</span> <span class="token string">'sItem--hidden'</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token string">'#result'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">let</span> filter <span class="token operator">=</span> searchInput<span class="token punctuation">.</span><span class="token function">val</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">let</span> count <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> <span class="token comment">// reset on each function call</span><br><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> items<span class="token punctuation">.</span>length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">let</span> plz <span class="token operator">=</span> items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-plz'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">let</span> cty <span class="token operator">=</span> items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-cty'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>plz<span class="token punctuation">.</span><span class="token function">toUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span>filter<span class="token punctuation">)</span> <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// check PLZ</span><br>      items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span>hidden<span class="token punctuation">)</span><span class="token punctuation">;</span><br>      count <span class="token operator">=</span> count <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>cty<span class="token punctuation">.</span><span class="token function">toUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span>filter<span class="token punctuation">)</span> <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// PLZ not found, check country</span><br>      items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span>hidden<span class="token punctuation">)</span><span class="token punctuation">;</span><br>      count <span class="token operator">=</span> count <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token comment">// nothing found</span><br>      items<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span>hidden<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br>  result<span class="token punctuation">.</span><span class="token function">html</span><span class="token punctuation">(</span>count <span class="token operator">+</span> <span class="token string">' Shops - &lt;a href="javascript:clearSearch();">Reset&lt;/a>'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// print the seartch result</span><br><span class="token punctuation">}</span></code></pre>
<p>This function takes the search input, converts it with <code>toUpperCase()</code> and checks against the HTML <code>data-*</code> attributes of the stores in the list. Matches remain shown, everything else gets hidden.</p>
<h3 id="handle-store-clicks-for-the-map" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#handle-store-clicks-for-the-map"><span>Handle Store Clicks for the Map</span></a></h3>
<p>Once a search result is clicked, the map should navigate to the marker that belongs to the clicked store and open its popup.</p>
<p>In order to achieve that, we’re going to loop through all the markers currently on the map (and in the <code>markerClusterGroup</code>), trying to find a match based on the previously generated ID (made up of the store’s Lat and Lon):</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// handle item clicks</span><br>item<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br>  <span class="token keyword">let</span> ct <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">let</span> pt <span class="token operator">=</span> ct<span class="token punctuation">.</span><span class="token function">parent</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// the data-* attributes are with the parent &lt;div></span><br>  <span class="token keyword">let</span> pLat <span class="token operator">=</span> pt<span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">'data-lat'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">let</span> pLon <span class="token operator">=</span> pt<span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">'data-lon'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">let</span> id <span class="token operator">=</span> pLat <span class="token operator">+</span> <span class="token string">'__'</span> <span class="token operator">+</span> pLon<span class="token punctuation">;</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">isEmpty</span><span class="token punctuation">(</span>pLat<span class="token punctuation">)</span> <span class="token operator">|</span> <span class="token operator">!</span><span class="token function">isEmpty</span><span class="token punctuation">(</span>pLon<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// find the correct marker</span><br>    markers<span class="token punctuation">.</span><span class="token function">eachLayer</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">layer</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>layer<span class="token punctuation">.</span>options<span class="token punctuation">.</span>key <span class="token operator">===</span> id<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        mymap<span class="token punctuation">.</span><span class="token function">setView</span><span class="token punctuation">(</span><span class="token punctuation">[</span>pLat<span class="token punctuation">,</span>pLon<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">19</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// move to the selected item and zoom in</span><br>        layer<span class="token punctuation">.</span><span class="token function">openPopup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Each marker we previously added to <code>markerClusterGroup</code> (Leaflet <code>layer group</code>) is considered a separate <code>layer</code> in terms of Leaflet. That’s why <code>eachLayer()</code> is used here, checking each layer for its <code>key</code> and trying to find a match for the clicked store’s ID.</p>
<h3 id="reset" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#reset"><span>Reset</span></a></h3>
<p>We’ve got 2 reset functions - one for the search and another one for the map:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// reset map</span><br><span class="token keyword">function</span> <span class="token function">resetMap</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  mymap<span class="token punctuation">.</span><span class="token function">closePopup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  mymap<span class="token punctuation">.</span><span class="token function">setView</span><span class="token punctuation">(</span><span class="token punctuation">[</span>startLat<span class="token punctuation">,</span> startLon<span class="token punctuation">]</span><span class="token punctuation">,</span> startZoom<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// clear search</span><br><span class="token keyword">function</span> <span class="token function">clearSearch</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'storefinder'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>value <span class="token operator">=</span> <span class="token string">''</span><span class="token punctuation">;</span><br>  <span class="token function">findStore</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token function">resetMap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p><code>clearSearch()</code> first performs an “empty” search, thus clearing all the hidden items. Don’t know if that’s faster/more efficient than another loop through all items, but it’s certainly fewer lines.</p>
<p>Not much else to say here, basically just another convenience feature.</p>
<h2 id="demo" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#demo"><span>Demo</span></a></h2>
<blockquote>
<p>As mentioned above, there’s a live demo on Netlify. It can be found here: <a class="ext" href="https://leaflet-store-locator.netlify.app" rel="noopener">leaflet-store-locator</a></p>
</blockquote>
<p>The <a class="ext" href="https://github.com/ttntm/hugo-leaflet-store-locator" rel="noopener">GitHub Repository</a> requires Hugo to build/run the site; after cloning/downloading it, you can either <code>hugo server</code> or <code>npm run start</code> in order to view the site on <code>http://localhost:1313</code>.</p>
<p>All necessary information regarding Hugo installation can be found here: <a class="ext" href="https://gohugo.io/getting-started/installing/" rel="noopener">Hugo Docs - Install Hugo</a></p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>Building this store locator was fun and so was building a demo and writing this up.</p>
<p>Another article on the subject that was of some help can be found here: <a class="ext u-in-reply-to" href="https://www.getbounds.com/blog/leafet-store-locator/" rel="noopener">getbounds.com/blog/leaflet-store-locator/</a></p>
<p>I hope it helps someone, I spent quite some time reading Leaflet’s documentation and researching on the internet to build this. Just leave a comment below, feedback appreciated.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>&quot;Static&quot; Comments with Gulp, Hugo &amp; Netlify</title>
        
          
            <category term="guide" />
          
            <category term="hugo" />
          
            <category term="serverless" />
          
        
        <link href="https://ttntm.me/blog/static-blog-comments-hugo/"/>
        
        <updated>2023-06-14T19:30:00Z</updated>
        <id>https://ttntm.me/blog/static-blog-comments-hugo/</id>
        <content type="html"><![CDATA[
          
            
            <p>Working for friends and family is always quite a bit of an extra challenge, as “no” generally doesn’t count. That’s probably how I ended up looking for what I’ll call “‘static’ comments” for now. Sounds weird at first, but refers to comments (= dynamically added feedback/discussion) within the context and technical limitations of a static website.</p>
<p>There’s a fair amount of “out of the box” 3rd party services you can use, an overview can be found in the <a class="ext" href="https://gohugo.io/content-management/comments/" rel="noopener">Hugo Docs</a>. That’s not what I was after though - I wanted something lightweight and free that also conforms with the requirements the GDPR brought along in 2018.</p>
<p>After some extensive research and hours of studying various services’ documentations, I came across an article called <a class="ext" href="https://css-tricks.com/jamstack-comments/" rel="noopener">JAMstack Comments</a> on <a href="http://css-tricks.com" class="ext u-like-of" rel="noopener">css-tricks.com</a>. Lucky for me, that was exactly what I was looking for, even more complex than needed.</p>
<p>So, the following article will describe what I’d summarize as “probably the easiest way of adding comments to your Hugo site”. So, taking into consideration that this site also has a comment section now, please feel free to let me know your opinion. 😉</p>
<h2 id="overview" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#overview"><span>Overview</span></a></h2>
<p>As described in the linked article, this simple “comment engine” basically works based on 3 components:</p>
<ol>
<li>Netlify forms</li>
<li>Gulp accessing Netlify via API</li>
<li>a static site generator (Hugo in this case)</li>
</ol>
<p>Here’s a compact flowchart detailing the process:</p>
<img src="https://ttntm.me/static/img/blog/comment-flow.jpg" class="img-fluid img-center auto-invert" alt="Flowchart comment engine">
<h2 id="hugo-configuration" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#hugo-configuration"><span>Hugo Configuration</span></a></h2>
<p>First off, we’ll need to add a form to our website in order to collect our comment submissions.</p>
<p>Hugo supports so called <a class="ext" href="https://gohugo.io/templates/partials/" rel="noopener">Partial Templates</a> that we’re going to use here:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row mt-3 mb-5<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col-sm-12 mt-3<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span><span class="token punctuation">></span></span>Comments<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col-sm-12 col-lg-8 offset-lg-2<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>form</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>BlogComments<span class="token punctuation">"</span></span> <span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/comment-thanks<span class="token punctuation">"</span></span> <span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>POST<span class="token punctuation">"</span></span> <span class="token attr-name">netlify-honeypot</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bot-field<span class="token punctuation">"</span></span> <span class="token attr-name">netlify</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>path<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hidden<span class="token punctuation">"</span></span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ .RelPermalink }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bot-field<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hidden<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-group<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inputName<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h4</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mb-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Name<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h4</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Name<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>name<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-control<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inputName<span class="token punctuation">"</span></span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>John Doe<span class="token punctuation">"</span></span> <span class="token attr-name">required</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-group<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inputEmail<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h4</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mb-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Email address<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h4</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Email<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>email<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-control<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inputEmail<span class="token punctuation">"</span></span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>john.doe@example.com<span class="token punctuation">"</span></span> <span class="token attr-name">required</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>small<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Confidential, will not be shared with anyone or published here.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-group<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inputComment<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h4</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mb-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Comment<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h4</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>textarea</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Comment<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form-control<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inputComment<span class="token punctuation">"</span></span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Your comment...<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">height</span><span class="token punctuation">:</span> 150px<span class="token punctuation">;</span> <span class="token property">resize</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span> <span class="token attr-name">required</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>textarea</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>submit<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn btn-primary px-3<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>i</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>fas fa-comment<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>i</span><span class="token punctuation">></span></span><span class="token entity named-entity" title="&nbsp;">&amp;nbsp;</span><span class="token entity named-entity" title="&nbsp;">&amp;nbsp;</span>Submit Comment<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>form</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p><em>Excerpt of this site’s <code>comments.html</code></em></p>
<p>The form is based on the requirements for Netlify’s form processing found in their docs: <a class="ext" href="https://docs.netlify.com/forms/setup/" rel="noopener">Forms setup</a></p>
<blockquote>
<p>Netlify requires the <code>action</code> to be a string starting with a slash - in case of Hugo sites, that means that you need to have <code>canonifyUrls = &quot;false&quot;</code> in Hugo’s <em>config.toml</em>. Otherwise, Netlify won’t recognize your form and you’re stuck.</p>
</blockquote>
<p>(Don’t ask how much time went into finding this…)</p>
<p>Keep the <code>input</code> named <code>path</code> in mind, we’ll need that later on. Also, don’t forget to include your newly created partial in your respective page’s template:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>container<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row mt-3<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col pt-3 mkd<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>          {{ .Content }}<br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  {{ partial "blog/meta.html" . }}<br>  {{ partial "blog/nextprev.html" . }}<br>  {{ partial "blog/comments.html" . }}<br>  {{ partial "blog/related.html" . }}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>Once you’ve deployed your site including the form, Netlify should automatically recognize it and create an entry for it in your account at <code>app.netlify.com/sites/SiteName/forms</code>. It will be displayed there using the <code>&lt;form&gt;</code> element’s <code>name</code> property.</p>
<p>Feel free to test it now, we’ll need some submissions later on anyway.</p>
<p>There’s more to do with Hugo later - we’re still missing a section to display the comments. That’s easier though, if we have some comments first.</p>
<h2 id="netlify-configuration" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#netlify-configuration"><span>Netlify Configuration</span></a></h2>
<p>Your form should now be showing up in Netlify, submissions should also end up there.</p>
<p>Next, head over to Settings -&gt; Build &amp; deploy (<a href="http://app.netlify.com/sites/SiteName/settings/deploys" class="ext u-like-of" rel="noopener">app.netlify.com/sites/SiteName/settings/deploys</a>) and create a <em>Build hook</em>. You’ll have to give it a name and select the branch for it to deploy your site from. Once created, it will provide a unique URL that you can send a POST request to in order to trigger a build.</p>
<p>In <a href="http://app.netlify.com/sites/SiteName/settings/forms" class="ext u-like-of" rel="noopener">app.netlify.com/sites/SiteName/settings/forms</a> you’ll find a section called <em>Form notifications</em> that you can use to have Netlify notify you via Email when a new submission arrives. You should also create an <em>Outgoing webhook</em> here, which will send a POST request to your previously created <em>Build hook</em>. That means every comment will now lead to rebuild of your site, making sure it shows up where it was posted.</p>
<p>In the original css-tricks article, they also made use of Slack notifications approving/rejecting comments collected by Netlify. I decided not to go down that route and won’t be covering that part here. If you’d like to make use of that, just add the respective bits of code where necessary.</p>
<p>By now, you should receive an email notification for new submissions and you should also be able to see your site being rebuilt after each submission.</p>
<p>We need two more things though:</p>
<ul>
<li>an API key</li>
<li>the form ID</li>
</ul>
<p>To get an API key, head over to <a href="http://app.netlify.com/user/applications" class="ext u-like-of" rel="noopener">app.netlify.com/user/applications</a> and create a new <em>Personal access token</em>. Make sure to save that token properly, as it can’t ever be displayed again afterwards.</p>
<p>The form ID can be found at <a href="http://app.netlify.com/sites/SiteName/forms" class="ext u-like-of" rel="noopener">app.netlify.com/sites/SiteName/forms</a>. A click on the respective form’s name will show its ID in your URL bar. You’ll need that for the gulp configuration, so make sure you write it down.</p>
<h2 id="gulp-configuration" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#gulp-configuration"><span>Gulp Configuration</span></a></h2>
<p>I’m going to assume that your Hugo site is already working with gulp, as I won’t be covering gulp’s setup here.</p>
<p>Specific packages you’ll need are:</p>
<ul>
<li>request</li>
<li>dotenv</li>
</ul>
<p>Based on that, <code>gulpfile.js</code> starts like this now:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">var</span> gulp <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'gulp'</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token operator">...</span><br>  request <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'request'</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  fs <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'fs'</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  config <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'dotenv'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>For <code>dotenv</code> to work, we’ll need to work with the API key and the form ID from Netlify. Make sure you’re in your project’s root directory and create a file called <code>.env</code>. Don’t forget to add it to your <code>.gitignore</code> file, as this is merely for local builds/testing and shouldn’t be pushed to your (public) repository.</p>
<p>In <code>.env</code>, create a line each for your <em>Personal access token</em> and your Netlify form ID.</p>
<p>Mine looks like this:</p>
<pre class="language-bash"><code class="language-bash"><span class="token assign-left variable">API_AUTH</span><span class="token operator">=</span>a1b2c3<span class="token punctuation">..</span>.<br><span class="token assign-left variable">COMMENT_FORM_ID</span><span class="token operator">=</span>0123456789</code></pre>
<p>Now, on to <code>gulpfile.js</code> - we’ll first add <code>var buildSrc = &quot;./&quot;;</code>, then proceed to a new gulp task called <em>get-comments</em>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">var</span> buildSrc <span class="token operator">=</span> <span class="token string">"./"</span><span class="token punctuation">;</span><br><br>gulp<span class="token punctuation">.</span><span class="token function">task</span><span class="token punctuation">(</span><span class="token string">"get-comments"</span><span class="token punctuation">,</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">done</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// set up the request with appropriate auth token and Form ID</span><br>  <span class="token keyword">var</span> url <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://api.netlify.com/api/v1/forms/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">COMMENT_FORM_ID</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/submissions/?access_token=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">API_AUTH</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>  <span class="token comment">// get the data from Netlify's submissions API</span><br>  <span class="token function">request</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">err<span class="token punctuation">,</span> response<span class="token punctuation">,</span> body</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br>    <span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>err <span class="token operator">&amp;&amp;</span> response<span class="token punctuation">.</span>statusCode <span class="token operator">===</span> <span class="token number">200</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br>      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Submissions found"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token keyword">var</span> body <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>body<span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token keyword">var</span> comments <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br>      <span class="token comment">// shape the data</span><br>      <span class="token keyword">for</span><span class="token punctuation">(</span><span class="token keyword">var</span> item <span class="token keyword">in</span> body<span class="token punctuation">)</span><span class="token punctuation">{</span><br>        <span class="token keyword">var</span> data <span class="token operator">=</span> body<span class="token punctuation">[</span>item<span class="token punctuation">]</span><span class="token punctuation">.</span>data<span class="token punctuation">;</span><br>        <span class="token keyword">var</span> comment <span class="token operator">=</span> <span class="token punctuation">{</span><br>          <span class="token literal-property property">name</span><span class="token operator">:</span> data<span class="token punctuation">.</span>Name<span class="token punctuation">,</span><br>          <span class="token literal-property property">comment</span><span class="token operator">:</span> data<span class="token punctuation">.</span>Comment<span class="token punctuation">,</span><br>          <span class="token literal-property property">path</span><span class="token operator">:</span> data<span class="token punctuation">.</span>path<span class="token punctuation">,</span><br>          <span class="token literal-property property">date</span><span class="token operator">:</span> body<span class="token punctuation">[</span>item<span class="token punctuation">]</span><span class="token punctuation">.</span>created_at<br>        <span class="token punctuation">}</span><span class="token punctuation">;</span><br>        <span class="token comment">// Add it to an existing array or create a new one</span><br>        <span class="token keyword">if</span><span class="token punctuation">(</span>comments<span class="token punctuation">[</span>data<span class="token punctuation">.</span>path<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br>          comments<span class="token punctuation">[</span>data<span class="token punctuation">.</span>path<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>comment<span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>          comments<span class="token punctuation">[</span>data<span class="token punctuation">.</span>path<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span>comment<span class="token punctuation">]</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>      <span class="token comment">// write our data to a file where Hugo can get it.</span><br>      fs<span class="token punctuation">.</span><span class="token function">writeFile</span><span class="token punctuation">(</span>buildSrc <span class="token operator">+</span> <span class="token string">"data/comments.json"</span><span class="token punctuation">,</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>comments<span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">err</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">if</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><span class="token punctuation">;</span><br>          <span class="token function">done</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>          console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Comments data saved."</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>          <span class="token function">done</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>        console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Couldn't get comments from Netlify"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token function">done</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p><code>var url</code> is a template literal making use of our <code>.env</code> file and its respective content. Make sure to change that accordingly in case your files entries are named differently.</p>
<p><code>fs.writeFile(buildSrc + &quot;data/comments.json&quot;,</code> creates a new JSON file in Hugo’s <code>data</code> directory that we’ll pull the comments from. If you want your JSON data file to be called differently, change it here. In terms of the GDPR, this file should probably <em>not</em> be available in your (public) repository. It’s sufficient to have this as a build-time resource only, i.e. not having a local and/or committed copy of it at all.</p>
<p>You can test this gulp task now - <code>gulp get-comments</code> should connect to Netlify successfully and then proceed to create a <code>comments.json</code> file in you <code>data</code> directory.</p>
<h2 id="displaying-the-comments" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#displaying-the-comments"><span>Displaying the Comments</span></a></h2>
<p>As mentioned above, the display section for our comments is still missing.</p>
<p>In order to get that done, we’re going to make Hugo access our JSON data file, pulling the comments for the respective post out of it and rendering them below the comment form.</p>
<pre class="language-html"><code class="language-html">{{ $thisPost := .RelPermalink }}<br>{{ $comments := .Site.Data.comments }}<br>{{ $.Scratch.Set "counter" 0 }}<br>{{ range $comments }}<br>    {{ range . }}{{ if eq .path $thisPost }}{{ $.Scratch.Set "counter" (add ($.Scratch.Get "counter") 1) }}{{ end }}{{ end }}<br>{{ end }}<br>{{ if gt ($.Scratch.Get "counter") 0 }}{{/*  only show comment section if there are comments  */}}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row mb-5<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  {{ range $comments }}<br>    {{ range . }}<br>      {{ if eq .path $thisPost }}<br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col-sm-12 py-3<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>shadow-sm px-4 py-4<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>i</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>small<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ .name }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>small<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> on {{ .date | dateFormat "Mon, 02 Jan. 2006, 15:04 MST" }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>i</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>px-3 mt-3 mb-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ .comment }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>      {{ end }}<br>    {{ end }}<br>  {{ end }}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>{{ end }}</code></pre>
<p>Note that we need <code>$thisPost</code> in order to compare the post’s permalink to the <code>path</code> we stored when the comment was submitted.</p>
<p>Regardless of that, we’re going to <code>range</code> through the comments twice, first to figure out if there are any and a second time to render them if there are.</p>
<p>The <code>range</code> within a <code>range</code> construction is necessary because of the array-within-array structure of the JSON file - i.e. an array of the posts and a second array of comments therein.</p>
<p>By now, you should have your posts displayed with a comment form and the respective comments pulled from Netlify based on the <code>comments.json</code> file created by gulp.</p>
<h2 id="configuring-netlify-to-include-gulp" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#configuring-netlify-to-include-gulp"><span>Configuring Netlify to Include Gulp</span></a></h2>
<p>I haven’t mentioned it earlier, but we’ll have to do one last thing in order to let Netlify know that we’d like to include our gulp task in the build process.</p>
<p>First, head over to your <code>gulpfile.js</code> again and create another task:</p>
<pre class="language-js"><code class="language-js">gulp<span class="token punctuation">.</span><span class="token function">task</span><span class="token punctuation">(</span><span class="token string">'build'</span><span class="token punctuation">,</span> gulp<span class="token punctuation">.</span><span class="token function">series</span><span class="token punctuation">(</span><span class="token string">'your-other-stuff-if-any'</span><span class="token punctuation">,</span><span class="token string">'get-comments'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Then, open your <code>package.json</code> and make sure to include this task in the <code>scripts</code> section:</p>
<pre class="language-js"><code class="language-js"><span class="token string-property property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token operator">...</span><br>    <span class="token string-property property">"deploy"</span><span class="token operator">:</span> <span class="token string">"gulp build &amp;&amp; hugo --minify"</span><br><span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token operator">...</span></code></pre>
<p>Finally, update (or create) your <code>netlify.toml</code>:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token punctuation">[</span>build<span class="token punctuation">]</span><br>  publish = "public"<br>  command = "npm run deploy"</code></pre>
<p>Once committed and pushed to the branch your site deploys from, Netlify should process the comments based on your gulp configuration.</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>As stated in the introduction, this implementation of a comment engine for a Hugo site is about as lightweight as it can possibly be. However, features like comment moderation etc. that are most likely required for larger sites are missing, as I chose that that’s not required for the time being. As described in the css-tricks article though, this can easily be accomplished using Slack and Netlify functions together with the core functionality described here.</p>
<p>Overall, I’d say this is good enough for now and I’m happy to have found a “minimum viable solution”.</p>
<p>PS: there’s a <a class="ext" href="https://github.com/ttntm/hugo-comments" rel="noopener">GitHub Repo</a> that you can clone and deploy to try all of that yourself.</p>
<h2 id="addendum" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#addendum"><span>Addendum</span></a></h2>
<p>06/2023: Eric Stemmler built a <a class="ext" href="https://rcst.netlify.app/post/2023/04/14/static-comments-with-hugo-netlify-and-nothing-really-more/" rel="noopener">Zero-Dependency Commenting System</a> based on this article.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Update - February 2019</title>
        
          
            <category term="news" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/update-february-2019/"/>
        
        <updated>2019-02-10T10:30:00Z</updated>
        <id>https://ttntm.me/blog/update-february-2019/</id>
        <content type="html"><![CDATA[
          
            
            <p>Judging by the silence here, I must have been busy…</p>
<p>The christmas holiday brought along Rockstar’s <em>Red Dead Redemption 2</em>, some well deserved relaxation and good company of family and friends.</p>
<p>I took on a project for a friend some weeks before that though, she needed a portfolio website. The site’s done from a technical perspective, currently being filled with content as it’s supposed to go online soon.</p>
<p>Aside from that, I also managed to get another website project for a startup which will start within the next weeks or as soon as the design is finished.</p>
<h2 id="updates" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#updates"><span>Updates!</span></a></h2>
<p>As far as this site’s concerned, here’s what’s changed:</p>
<ul>
<li>Moved from Github pages and their <code>username.github.io</code> domain to <a href="https://ttntm.me" class="int">ttntm.me</a> which was on sale at Porkbun</li>
<li>Moved the deployment over to Netlify</li>
<li>Enabled an RSS feed for the blog</li>
</ul>
<h2 id="coming-soon" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#coming-soon"><span>Coming Soon</span></a></h2>
<ul>
<li>Planning to ditch gulp, replacing it with Hugo’s native asset processing functionality</li>
<li><s>Going to build a projects/portfolio section into this site</s> - <em>Update 03/2019: done!</em></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>jQuery Multiselect Filter</title>
        
          
            <category term="guide" />
          
            <category term="hugo" />
          
        
        <link href="https://ttntm.me/blog/jquery-multiselect-filter/"/>
        
        <updated>2018-12-12T10:30:00Z</updated>
        <id>https://ttntm.me/blog/jquery-multiselect-filter/</id>
        <content type="html"><![CDATA[
          
            
            <p>I spent quite some time working on a friend’s portfolio website recently. She wanted to get rid of some sort of hosted service, moving to her own website.</p>
<p>Showcasing her work was clearly the focus of the site, so it also needed to provide some decent filtering of the portfolio items.</p>
<p>The site itself was supposed to be built with Hugo that already provides a ton of functionality in terms of taxonomy and content metadata. That toolset could have worked for someone else, but in this case, multiselect was another requirement for the filters and all those extra list pages for the respective metadata items were unwanted.</p>
<p>So, I ended up building them myself, based on content metadata (Hugo: front matter) and jQuery.</p>
<p><em>All CSS is going to be missing here, but it’s included in the CodePen example.</em></p>
<h2 id="content-setup" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#content-setup"><span>Content Setup</span></a></h2>
<p>In order to make the portfolio manageable, the site was set up using Hugo’s so-called “Page Bundles” (<a class="ext" href="https://gohugo.io/content-management/page-bundles/" rel="noopener">Hugo Docs</a>), lots of shortcodes and customized metadata in each portfolio item’s front matter:</p>
<pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">title</span><span class="token punctuation">:</span> Example Case<br><span class="token key atrule">client</span><span class="token punctuation">:</span> Example Corp.<br><span class="token key atrule">team</span><span class="token punctuation">:</span> Homer Simpson (art director)<span class="token punctuation">,</span> Peter Griffin (designer)<span class="token punctuation">,</span> Burt Reynolds (badass)<br><span class="token key atrule">works</span><span class="token punctuation">:</span><br><span class="token punctuation">-</span> <span class="token key atrule">work</span><span class="token punctuation">:</span> branding<br><span class="token punctuation">-</span> <span class="token key atrule">work</span><span class="token punctuation">:</span> identity<br><span class="token key atrule">resources</span><span class="token punctuation">:</span><br><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> hero<br>    <span class="token key atrule">src</span><span class="token punctuation">:</span> hero.png<br><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> overview<br>    <span class="token key atrule">src</span><span class="token punctuation">:</span> overview.png<br><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> detail<br>    <span class="token key atrule">src</span><span class="token punctuation">:</span> detail.png<br><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> intro<br>    <span class="token key atrule">src</span><span class="token punctuation">:</span> intro.md<br><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> description1<br>    <span class="token key atrule">src</span><span class="token punctuation">:</span> description1.md<br><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> description2<br>    <span class="token key atrule">src</span><span class="token punctuation">:</span> description2.md</code></pre>
<p>As shown above, each portfolio item gets categorized with <code>work</code> elements. This categorization will serve as the backbone for the filter. Due to the requirement of the filter providing multiselect functionality, the front matter section “works” also has to provide an array that can hold multiple values (= portfolio item categories).</p>
<p>Due to the portfolio items being page bundles, the front matter above also shows a section <code>resources</code>. This list basically contains all the relevant bits of content the page shall have access to. For our filter, the image resource called <code>hero</code> will also be necessary as the list page of all portfolio items will be displayed as an image grid.</p>
<h2 id="portfolio-layout" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#portfolio-layout"><span>Portfolio Layout</span></a></h2>
<p>In order to display our portfolio items, we’re going to let Hugo create a list page of all the relevant posts, displaying them as an image grid with the filters on top.</p>
<p>The page template starts with the filters below the standard page header:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>filters<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>d-flex flex-row flex-wrap justify-content-between mb-5<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>works-filter flex-grow-1 w-sm-100 my-2<span class="token punctuation">"</span></span> <span class="token attr-name">data-filter</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>catA<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Category A<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>works-filter flex-grow-1 w-sm-100 my-2<span class="token punctuation">"</span></span> <span class="token attr-name">data-filter</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>catB<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Category B<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>works-filter flex-grow-1 w-sm-100 my-2<span class="token punctuation">"</span></span> <span class="token attr-name">data-filter</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>catC<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Category C<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-right works-filter flex-grow-1 w-sm-100 my-2 showAll<span class="token punctuation">"</span></span> <span class="token attr-name">data-filter</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>all<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>All Projects<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>All filters are equipped with an HTML data-* attribute, <code>data-filter</code>, that will provide the filtering criteria to our jQuery functions.</p>
<p>The actual portfolio items are displayed below the filters as a 2-column grid:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  {{ range .Pages }}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col-sm-12 col-md-6 gap-y workItem show-workItem {{range .Params.works }}{{ .work | urlize }} {{ end }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      {{ partial "img+overlay.html" . }}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  {{ end }}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>Before having a look at the process of obtaining the <code>hero</code> resource mentioned above, it’s important to mention that <code>range .Params.works</code> pulls all <code>work</code> elements, essentially providing their value as an additional class of each portfolio item’s parent <code>div</code>. Our filter is going to access this class list to determine which elements to show.</p>
<p>The images, each portfolio item’s respective <code>hero</code> resource, are rendered by the following bit of code:</p>
<pre class="language-html"><code class="language-html">{{ $scImg := .Resources.GetMatch "hero" }}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>img-container m-0 p-0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/img/loading.svg<span class="token punctuation">"</span></span> <span class="token attr-name">data-src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ $scImg.RelPermalink }}<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>img-lazy img-center img-fluid img-showcase<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>overlay d-flex flex-column align-items-center justify-content-center<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>link-overlay<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ .Permalink }}<span class="token punctuation">"</span></span> <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ .Title }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ .Title }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>d-block small<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      {{range $index, $tag := .Params.works }}<br>        {{- if eq $index 0 -}}<br>          {{ .work }}<br>        {{- else -}}<br>          , {{ .work }}<br>        {{- end -}}<br>      {{ end }}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>It’s a rather simple procedure, <code>.Resources.GetMatch &quot;hero&quot;</code> obtains each item’s <code>hero</code> resource and <code>$scImg.RelPermalink</code> makes sure the <code>img</code> element’s <code>src</code> tag is filled with its path correctly.</p>
<p>In addition to that, the overlay displays each item’s <code>.Title</code> and ranges through <code>.Params.works</code> once again, displaying the respective categories.</p>
<p>This code has been put into a <code>partial</code> called <code>img+overlay.html</code> for convenience reasons, as the same component is used for other page templates as well.</p>
<p>If you’re following this example, then you should have a page displaying filters followed by all portfolio items in an image grid by now. Time to move on to some jQuery then.</p>
<h2 id="j-query" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#j-query"><span>jQuery</span></a></h2>
<p>A short remark before we get started: as shown in the CodePen example, this code will work regardless of how your portfolio page is built, as long as the portfolio items have classes assigned to them that correspond to what’s set as <code>data-filter</code>. There’s no Hugo functionality and/or dependencies in there.</p>
<p>We’re going to store our filter functionality in jQuery’s <code>$(document).ready(function(){</code>, defining the necessary variables first:</p>
<pre class="language-js"><code class="language-js"><span class="token function">$</span><span class="token punctuation">(</span>document<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ready</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br>  <span class="token keyword">var</span> $filters <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token string">'.works-filter'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// find the filters</span><br>  <span class="token keyword">var</span> $works <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token string">'.workItem'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// find the portfolio items</span><br>  <span class="token keyword">var</span> showAll <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token string">'.showAll'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// identify the "show all" button</span><br><br>  <span class="token keyword">var</span> cFilter<span class="token punctuation">,</span> cFilterData<span class="token punctuation">;</span> <span class="token comment">// declare a variable to store the filter and one for the data to filter by</span><br>  <span class="token keyword">var</span> filtersActive <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">// an array to store the active filters</span></code></pre>
<p>The array called <code>filtersActive</code> is the key piece for providing multiselect functionality, storing the active filters until they’re deselected again, <code>All Projects</code> gets clicked, or the page gets reloaded.</p>
<p>Next, we’re going to handle the <code>click</code> event for our filters:</p>
<pre class="language-js"><code class="language-js">$filters<span class="token punctuation">.</span><span class="token function">click</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token comment">// if filters are clicked</span><br>  cFilter <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  cFilterData <span class="token operator">=</span> cFilter<span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">'data-filter'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// read filter value</span><br><br>  <span class="token function">highlightFilter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token function">applyFilter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The <code>click</code> event checks for the value of our attribute <code>data-filter</code> and then proceeds to execute 2 functions, <code>highlightFilter();</code> and <code>applyFilter();</code>. These two handle the process of highlighting the active filter and displaying/hiding the portfolio items (via the class called <code>show-workItem</code>) respectively.</p>
<p><code>highlightFilter();</code> looks like this:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">highlightFilter</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">var</span> filterClass <span class="token operator">=</span> <span class="token string">'works-filter-active'</span><span class="token punctuation">;</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>cFilter<span class="token punctuation">.</span><span class="token function">hasClass</span><span class="token punctuation">(</span>filterClass<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    cFilter<span class="token punctuation">.</span><span class="token function">removeClass</span><span class="token punctuation">(</span>filterClass<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token function">removeActiveFilter</span><span class="token punctuation">(</span>cFilterData<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>cFilter<span class="token punctuation">.</span><span class="token function">hasClass</span><span class="token punctuation">(</span><span class="token string">'showAll'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    $filters<span class="token punctuation">.</span><span class="token function">removeClass</span><span class="token punctuation">(</span>filterClass<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    filtersActive <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token comment">// clear the array</span><br>    cFilter<span class="token punctuation">.</span><span class="token function">addClass</span><span class="token punctuation">(</span>filterClass<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    showAll<span class="token punctuation">.</span><span class="token function">removeClass</span><span class="token punctuation">(</span>filterClass<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    cFilter<span class="token punctuation">.</span><span class="token function">addClass</span><span class="token punctuation">(</span>filterClass<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    filtersActive<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>cFilterData<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>This function controls/toggles the <code>works-filter-active</code> class and does the following:</p>
<ol>
<li>check if the item that triggered the <code>click</code> event is an active filter: if yes, remove its active state both visually and via <code>removeActiveFilter();</code></li>
<li>check if <code>All Projects</code> was clicked: if yes, clear the array of active filters and mark <code>All Projects</code> as active filter</li>
<li>if the item that triggered the <code>click</code> event is a new/additional filter, it will be marked as active and added to the <code>filtersActive</code> array</li>
</ol>
<p>Now, having determined the state of our filters and most importantly the <code>filtersActive</code> array, our code proceeds to the function called <code>applyFilter();</code> that takes care of showing the right items based on the active filters:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">applyFilter</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// go through all portfolio items and hide/show as necessary</span><br>  $works<span class="token punctuation">.</span><span class="token function">each</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br>    <span class="token keyword">var</span> i<span class="token punctuation">;</span><br>    <span class="token keyword">var</span> classes <span class="token operator">=</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">attr</span><span class="token punctuation">(</span><span class="token string">'class'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>cFilter<span class="token punctuation">.</span><span class="token function">hasClass</span><span class="token punctuation">(</span><span class="token string">'showAll'</span><span class="token punctuation">)</span> <span class="token operator">||</span> filtersActive<span class="token punctuation">.</span>length <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// makes sure we catch the array when its empty and revert to the default of showing all items</span><br>      $works<span class="token punctuation">.</span><span class="token function">addClass</span><span class="token punctuation">(</span><span class="token string">'show-workItem'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//show them all</span><br>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>      <span class="token function">$</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">removeClass</span><span class="token punctuation">(</span><span class="token string">'show-workItem'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token keyword">for</span> <span class="token punctuation">(</span>i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> classes<span class="token punctuation">.</span>length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>filtersActive<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span>classes<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          <span class="token function">$</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addClass</span><span class="token punctuation">(</span><span class="token string">'show-workItem'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p><code>applyFilter();</code> goes through <code>$works</code>, all elements that have the class <code>.workItem</code> assigned to them. It then loops through <code>classes</code> (all classes of each respective <code>workItem</code>), comparing them to what’s currently stored inside the <code>filtersActive</code> array. This procedure is basically the “heart” of the filter.</p>
<p>When the array is empty or <code>All Projects</code> becomes the active filter, the function shows all portfolio items.</p>
<p>There’s still another function that was mentioned above and that’s called <code>removeActiveFilter();</code>. This one takes care of removing filters from the <code>filtersActive</code> array when they’re deselected:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">removeActiveFilter</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">var</span> index <span class="token operator">=</span> filtersActive<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>index <span class="token operator">></span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    filtersActive<span class="token punctuation">.</span><span class="token function">splice</span><span class="token punctuation">(</span>index<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>Aside from ticking the box in front of a technical requirement on a list and making a friend happy, this filter was also an interesting challenge to myself. I couldn’t find any useful instructions or out of the box solutions before starting doing it on my own which might have been due to wrong Google keywords or just bad luck.</p>
<p>Anyway, I hope this can prove useful to someone looking for something similar. I created a CodePen Demo that can be found here: <a class="ext" href="https://codepen.io/ttntm/full/zyxGXe" rel="noopener">CodePen</a></p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Site Redesign</title>
        
          
            <category term="news" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/site-redesign/"/>
        
        <updated>2018-11-19T10:30:00Z</updated>
        <id>https://ttntm.me/blog/site-redesign/</id>
        <content type="html"><![CDATA[
          
            
            <p>When this site was put together initially, it was basically a prototype for some kind of blog/portfolio kind of thing.</p>
<p>Now, about 6 months later, there’s been a slight redesign - the first of many I’m afraid…</p>
<p>Anyway, this post will probably just serve me as a personal tracker of what’s changed, so here we go:</p>
<h2 id="behind-the-scenes" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#behind-the-scenes"><span>Behind the Scenes</span></a></h2>
<ol>
<li>Reduced Bootstrap 4 to the bare minimum of required components, thereby reducing the size of the CSS significantly. Currently using:
<ul>
<li>BS4 reboot</li>
<li>BS4 grid</li>
<li>BS4 navbar</li>
<li>BS4 utilities</li>
<li>250 lines of additional CSS for the buttons etc.</li>
</ul>
</li>
<li>Implemented CSS processing via <code>gulp</code> in conjunction with <code>postcss</code>, <code>concat-css</code> and <code>cssnano</code></li>
<li>Started using a Hugo Base Template, which is supposed to be making future changes easier to apply globally</li>
</ol>
<h2 id="visible-changes" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#visible-changes"><span>Visible Changes</span></a></h2>
<ol>
<li>Switched to <s>Nunito</s> “Inter” as the site’s font face</li>
<li>Found a nice colour palette and changed the site’s color scheme based on that</li>
<li>Reworked the navbar, adding some fancy CSS to the main navigation items</li>
<li>Reworked the buttons, some more fancy CSS</li>
<li>Added a real “Contact” page, displaying contact options as a centered <code>flexbox</code> element</li>
<li>Applied some CSS to the scrollbar, making it look a little more individual</li>
</ol>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Building a Responsive Menu with Tailwind CSS</title>
        
          
            <category term="css" />
          
            <category term="guide" />
          
            <category term="tailwind" />
          
        
        <link href="https://ttntm.me/blog/tailwind-responsive-menu/"/>
        
        <updated>2020-03-15T10:30:00Z</updated>
        <id>https://ttntm.me/blog/tailwind-responsive-menu/</id>
        <content type="html"><![CDATA[
          
            
            <p>According to their docs, “<strong>Tailwind is a utility-first CSS framework for rapidly building custom user interfaces.</strong>”</p>
<p>Being used to Bootstrap and/or other component based frameworks, this is quite a different approach.</p>
<p>Using Tailwind, basically everything you know from these frameworks is possible, but the way to get there is a little different. There’s no <code>navbar</code>, <code>card</code> or <code>modal</code> - instead, all of Tailwind’s classes can be used to actually build those components. Basically, that’s lots of freedom (<em>which is great!</em>), but it also requires some more knowledge of HTML and especially CSS in order to achieve the same result as using finished components would.</p>
<p>Oh, one more thing: Tailwind is a pure CSS framework and ships without any front-end JavaScript - that means that if you’d like to have a sticky header once the page scrolls or a collapsed navbar that expands when clicking a button, you need to build it.</p>
<h2 id="lets-build-a-menu" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#lets-build-a-menu"><span>Let’s Build a Menu</span></a></h2>
<p>Navigation is certainly one of the essentials when it comes to building a website, so let’s get right into that.</p>
<ol>
<li>
<p>We’re going to build a classic old-fashioned page header with a larger logo placed on top and a horizontal menu below that.</p>
</li>
<li>
<p>When the page scrolls down, the menu should become sticky so that the navigation options persist for increased accessibility on long pages like blog posts.</p>
</li>
<li>
<p>When browsing our example page on mobile devices, the whole page header should collapse, turning into a fixed navbar on top of the page that expands upward when a button is clicked.</p>
</li>
</ol>
<p>All of that should be achieved with the same header/menu and some JavaScript to check the scroll position and to provide expand/collapse functionality.</p>
<h2 id="the-header-and-the-menu" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-header-and-the-menu"><span>The Header and the Menu</span></a></h2>
<p>We’re going to keep the custom CSS at a bare minimum, using Tailwind’s classes as far as that’s possible.</p>
<p>That’s what our header is going to look like:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>header</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>top<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>w-full flex flex-col fixed sm:relative bg-white pin-t pin-r pin-l<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>nav</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>site-menu<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flex flex-col sm:flex-row w-full justify-between items-center px-4 sm:px-6 py-1 bg-white shadow sm:shadow-none border-t-4 border-red-900<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>w-full sm:w-auto self-start sm:self-center flex flex-row sm:flex-none flex-no-wrap justify-between items-center<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>no-underline py-1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>font-bold text-lg tracking-widest<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>LOGO<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>menuBtn<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hamburger block sm:hidden focus:outline-none<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>button<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value javascript language-javascript"><span class="token function">navToggle</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hamburger__top-bun<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hamburger__bottom-bun<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>menu<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>w-full sm:w-auto self-end sm:self-center sm:flex flex-col sm:flex-row items-center h-full py-1 pb-4 sm:py-0 sm:pb-0 hidden<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-dark font-bold hover:text-red text-lg w-full no-underline sm:w-auto sm:pr-4 py-2 sm:py-1 sm:pt-2<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://ttntm.me/blog/tailwind-responsive-menu/<span class="token punctuation">"</span></span> <span class="token attr-name">target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>_blank<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>About<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text-dark font-bold hover:text-red text-lg w-full no-underline sm:w-auto sm:px-4 py-2 sm:py-1 sm:pt-2<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#bottom<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Features<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>nav</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>header</span><span class="token punctuation">></span></span></code></pre>
<p>There are 2 <code>div</code> elements here, first the navigation bar and then the menu with <code>id=&quot;menu&quot;</code>. The menu button <code>menuBtn</code> has an <code>onclick</code> attribute that runs a function called <code>navToggle()</code>. This function will make sure that the menu can expand/collapse when the button is pressed.</p>
<p>Tailwind is “mobile first”, so its responsive utilities like <code>sm:hidden</code> work in a way that can be described as “valid <em>from</em> the specified breakpoint”. So, <code>sm:hidden</code> translates to “hidden on screens larger than the defined <code>sm</code>-breakpoint” (default: 576px).</p>
<h2 id="the-java-script" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-java-script"><span>The JavaScript</span></a></h2>
<p>We need JavaScript to enable 2 functions of our menu, the first one is the expand/collapse functionality for mobile devices, the second one’s the sticky navigation when scrolling down.</p>
<h3 id="expand-and-collapse-the-menu" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#expand-and-collapse-the-menu"><span>Expand and Collapse the Menu</span></a></h3>
<p>In order to achieve the proper functionality for our mobile menu, we’ve already placed a call to the <code>navToggle()</code> function in our menu.</p>
<p>The function itself is rather simple and looks like this:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">navToggle</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">var</span> btn <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'menuBtn'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">var</span> nav <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'menu'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  btn<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">toggle</span><span class="token punctuation">(</span><span class="token string">'open'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  nav<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">toggle</span><span class="token punctuation">(</span><span class="token string">'flex'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  nav<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">toggle</span><span class="token punctuation">(</span><span class="token string">'hidden'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>As shown in the larger code snippet above, the <code>nav</code> HTML-element is set to <code>hidden</code> by default. <code>navToggle()</code> adds/removes the <code>flex</code> class, thus expanding/collapsing the menu.</p>
<h3 id="sticky-menu" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#sticky-menu"><span>Sticky Menu</span></a></h3>
<p>Scrolling down far enough should make our <code>nav</code> stick to the top of the screen on any screen larger than 576px, providing persistent navigation to the users of the site.</p>
<p>To implement this functionality, we need to grab the <code>scroll</code> event and use it to add a class to the <code>nav</code> HTML-element to make it stick. This could be done as shown here:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">var</span> nav <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'site-menu'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">var</span> header <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'top'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'scroll'</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>window<span class="token punctuation">.</span>scrollY <span class="token operator">>=</span><span class="token number">400</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// adjust this value based on site structure and header image height</span><br>    nav<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">'nav-sticky'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    header<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">'pt-scroll'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    nav<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token string">'nav-sticky'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    header<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token string">'pt-scroll'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>We don’t just add <code>nav-sticky</code> though, there’s a second class called <code>bod-pt-scroll</code> which adds <code>padding-top</code> to the <code>&lt;body&gt;</code>, thus making sure that the page content doesn’t reflow when the <code>nav</code> gets stuck to the top of the page and taken out of the regular flow of the DOM elements.</p>
<h2 id="custom-css" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#custom-css"><span>Custom CSS</span></a></h2>
<p>In order to provide the necessary padding and the sticky-ness of the <code>nav</code>-element, we’re using some custom, non-Tailwind CSS:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">max-width</span><span class="token punctuation">:</span> 576px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>  <span class="token selector">.content</span> <span class="token punctuation">{</span><br>    <span class="token property">padding-top</span><span class="token punctuation">:</span> 51px<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 577px<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>  <span class="token selector">.pt-scroll</span> <span class="token punctuation">{</span><br>    <span class="token property">padding-top</span><span class="token punctuation">:</span> 51px<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token selector">.nav-sticky</span> <span class="token punctuation">{</span><br>    <span class="token property">position</span><span class="token punctuation">:</span> fixed<span class="token important">!important</span><span class="token punctuation">;</span><br>    <span class="token property">min-width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br>    <span class="token property">top</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br>    <span class="token property">box-shadow</span><span class="token punctuation">:</span> 0 2px 4px 0 <span class="token function">rgba</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> .1<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">transition</span><span class="token punctuation">:</span> all .25s ease-in<span class="token punctuation">;</span><br>    <span class="token property">z-index</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>The padding for the <code>&lt;body&gt;</code> could of course have been done with Tailwind classes, but this example came out of a project where Tailwind’s padding and margin utilities were not needed in their responsive variation, thus making the overall CSS bundle much smaller using just the 5 lines of custom CSS shown here.</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>Tailwind CSS is a very versatile, powerful and extremely customizable framework. It’s also quite fast once you’ve gotten into it, making it a joy to work with.</p>
<p>I hope this article is beneficial to someone out there. Here’s a pen and a link to an excellent Tailwind cheatsheet for your convenience:</p>
<ul>
<li><a class="ext" href="https://codepen.io/ttntm/full/dqaNPp" rel="noopener">CodePen</a></li>
<li><a class="ext" href="https://nerdcave.com/tailwind-cheat-sheet" rel="noopener">Cheatsheet</a></li>
</ul>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Using CSS Variables in Internet Explorer</title>
        
          
            <category term="css" />
          
            <category term="guide" />
          
        
        <link href="https://ttntm.me/blog/using-css-variables-internet-explorer/"/>
        
        <updated>2018-09-10T10:30:00Z</updated>
        <id>https://ttntm.me/blog/using-css-variables-internet-explorer/</id>
        <content type="html"><![CDATA[
          
            
            <p>CSS variables make working with stylesheets easier, no doubt about that. Defining a color, a breakpoint or even a font-family globally, makes changes easier and almost completely eliminates the copy/paste and the find/replace cycle when it comes to updates in your CSS.</p>
<p>Now, having to support Internet Explorer 11 and below with your CSS makes this a little less exciting, as it simply doesn’t support CSS variables at all.</p>
<p>But: not supporting IE 11 and below is not really an option yet. Even more so, if many of your site’s potential visitors are likely to be corporate users who may not even have another choice and who haven’t upgraded to Windows 10/Edge yet.</p>
<h2 id="supporting-internet-explorer" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#supporting-internet-explorer"><span>Supporting Internet Explorer</span></a></h2>
<p>In order to support IE, one could use CSS variables in development but provide production CSS without them in it, running it through Sass/CSS processing at build time.</p>
<p>If you’re working on a smaller project or if you don’t want to use any Sass/CSS processing in your site’s build process, there’s still another option that will be explained here: leaving the variables in your CSS and making sure IE will understand them.</p>
<p>In order to achieve that, we’re going to do the following:</p>
<ol>
<li>check whether or not IE is being used to view our site</li>
<li>add a ponyfill to process the CSS variables in case that’s true</li>
</ol>
<p>That way, we’re not generating another HTTP request for the rest of our potential visitors using other browsers.</p>
<h3 id="check-for-ie" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#check-for-ie"><span>Check for IE</span></a></h3>
<p>In order to check for IE, we’re going to add the following JavaScript to our site’s <code>&lt;head&gt;</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">checkForIE</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">var</span> ua <span class="token operator">=</span> window<span class="token punctuation">.</span>navigator<span class="token punctuation">.</span>userAgent<span class="token punctuation">;</span><br>    <span class="token keyword">var</span> msie <span class="token operator">=</span> ua<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'MSIE '</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>msie <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token comment">// IE 10 or older => return version number</span><br>        <span class="token keyword">return</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>ua<span class="token punctuation">.</span><span class="token function">substring</span><span class="token punctuation">(</span>msie <span class="token operator">+</span> <span class="token number">5</span><span class="token punctuation">,</span> ua<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">,</span> msie<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>    <span class="token keyword">var</span> trident <span class="token operator">=</span> ua<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'Trident/'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>trident <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token comment">// IE 11 => return version number</span><br>        <span class="token keyword">var</span> rv <span class="token operator">=</span> ua<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'rv:'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">return</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>ua<span class="token punctuation">.</span><span class="token function">substring</span><span class="token punctuation">(</span>rv <span class="token operator">+</span> <span class="token number">3</span><span class="token punctuation">,</span> ua<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">,</span> rv<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>    <span class="token comment">// all the other browsers</span><br>    <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">var</span> MSLegacy <span class="token operator">=</span> <span class="token function">checkForIE</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">if</span> <span class="token punctuation">(</span>MSLegacy <span class="token operator">!==</span> <span class="token boolean">false</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">var</span> insert <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementsByTagName</span><span class="token punctuation">(</span><span class="token string">'head'</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>    <span class="token keyword">var</span> script <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'script'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    script<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'src'</span><span class="token punctuation">,</span> <span class="token string">'/js/css-vars-ponyfill.min.js'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    insert<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>script<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'IE found, adding ponyfill.'</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'This is not the IE you are looking for...'</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>The function <code>checkForIE()</code> shown above, makes sure we don’t miss any Internet Explorer 11 and below.</p>
<p>Depending on the outcome of that function, a ponyfill that provides CSS variable support gets added to the DOM.</p>
<p>Most of the code above is based on this helpful resource found on CodePen: <a class="ext" href="https://ttntm.me/codepen.io/gapcode/pen/vEJNZN" rel="noopener">codepen.io/gapcode/pen/vEJNZN</a></p>
<h3 id="the-ponyfill" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-ponyfill"><span>The Ponyfill</span></a></h3>
<p>A so called “Ponyfill” “doesn’t monkey patch anything, but instead exports the functionality as a normal module, so you can use it locally without affecting other code”. The one used here is called “css-vars-ponyfill” and comes from this repository on GitHub: <a class="ext" href="https://github.com/jhildenbiddle/css-vars-ponyfill" rel="noopener">jhildenbiddle/css-vars-ponyfill</a></p>
<p>Simply adding the ponyfill to a page won’t do the trick, it also has to be executed via <code>cssVars()</code>. The site this example came from is also using jQuery, so this call has been placed in <code>document.ready()</code>, which looks like that:</p>
<pre class="language-js"><code class="language-js"><span class="token function">$</span><span class="token punctuation">(</span>document<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ready</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span><br>    <span class="token comment">//some code...</span><br><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>MSLegacy <span class="token operator">!==</span> <span class="token boolean">false</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token function">cssVars</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br>    <span class="token comment">//some more code...</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>Overall, this seems to be a rather convenient solution when it’s necessary to make CSS variables work in legacy IE browsers.</p>
<p>In the end though, I don’t think any of the above is relevant if you’re using Sass and/or CSS processing in your build setup. I just haven’t gotten around to digging into that yet, so it’s a definitive advantage over the copy/paste and the find/replace cycle respectively.</p>
<h2 id="update-10-09-2018" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#update-10-09-2018"><span>Update 10.09.2018</span></a></h2>
<p>For some reason, <code>$(document).ready()</code> didn’t fire consistently enough - it could happen, that <code>cssVars()</code> never got executed and the site rendered without resolving the variable values.</p>
<p>Putting <code>cssVars()</code> into an event listener instead helped here:</p>
<pre class="language-js"><code class="language-js">window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'load'</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>MSLegacy <span class="token operator">!==</span> <span class="token boolean">false</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token function">cssVars</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Random Content from Data in Hugo</title>
        
          
            <category term="guide" />
          
            <category term="hugo" />
          
        
        <link href="https://ttntm.me/blog/hugo-random-content-from-data/"/>
        
        <updated>2018-08-21T10:30:00Z</updated>
        <id>https://ttntm.me/blog/hugo-random-content-from-data/</id>
        <content type="html"><![CDATA[
          
            
            <h2 id="data-in-hugo" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#data-in-hugo"><span>Data in Hugo</span></a></h2>
<p>Hugo comes with the possibility of storing structured content in a data file where it can be pulled from by the template placing it on the site. I’ve seen this used for things like pricing tables for example. There’s quite a bunch of use cases for random content on a website though, from customer references on a company website to quotes of the day for a literary blog. This use case is going to be what the following article is about.</p>
<h2 id="the-setup" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-setup"><span>The Setup</span></a></h2>
<p>Let’s assume we’re creating a company or product marketing website. Wouldn’t it be nice to display an assortment of customer testimonies, success stories or customer logos there? At best, that content should change over time, so that curious readers and repeat) visitors of your site will be encouraged to check out the hypothetical <code>/references</code> section of the site.</p>
<p>For this example, we’ll stick to the customer logos, displaying them in a row across the whole page including a link to the <code>/references</code> page where we’ll display the complete list as a grid.</p>
<h2 id="the-data" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-data"><span>The Data</span></a></h2>
<p>Our customers are going to be stored in a file called <code>references.json</code> which we’re going to place in Hugo’s <code>Data</code> folder.</p>
<p>The file’s structure is going to look like that:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">[</span><br>  <span class="token punctuation">{</span><br>    <span class="token property">"custName"</span><span class="token operator">:</span> <span class="token string">"Contoso AG"</span><span class="token punctuation">,</span><br>    <span class="token property">"custDesc"</span><span class="token operator">:</span> <span class="token string">"Absolutely generic products and services."</span><span class="token punctuation">,</span><br>    <span class="token property">"custLogo"</span><span class="token operator">:</span> <span class="token string">"contosoag"</span><span class="token punctuation">,</span><br>    <span class="token property">"custLink"</span><span class="token operator">:</span> <span class="token string">"https://contoso.com"</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  ...<br><span class="token punctuation">]</span></code></pre>
<p>The customer logos are going to be stored in <code>static/img/cust/</code>, their file extension should be the same across the board (i.e. all PNG images) and their name should correspond to what’s stored as <code>custLogo</code> in the JSON file.</p>
<p>If you are going to have many different file extensions, it may be best to store the file name including its extension in your JSON file.</p>
<h2 id="the-templates" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-templates"><span>The Templates</span></a></h2>
<p>We’re going to use 2 kinds of templates, one <em>partial</em> to display the random selection from our data file wherever we see fit and one full template for the <code>/references</code> page including all the customer details.</p>
<h3 id="the-partial" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-partial"><span>The Partial</span></a></h3>
<p>Our partial’s purpose shall be obtaining 6 random customer logos and displaying them in a row next to each other.</p>
<p>The code for this procedure could look like that:</p>
<pre class="language-html"><code class="language-html">{{ range $i, $content := $.Site.Data.references | shuffle | first 6 }}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>customer-img<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/img/cust/{{ $content.custLogo }}.png<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ $content.custName }}<span class="token punctuation">"</span></span> <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ $content.custName }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>{{ end }}</code></pre>
<p>The selection of the random items is going to be triggered by the website build and each new build will create another 6 logos.</p>
<p>A possible implementation of the whole selection, opening with a heading and followed by some button leading to the <code>/references</code> page could look like this then:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col text-center<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span><span class="token punctuation">></span></span>Customer References<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>These are only some of our customers...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/references<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Browse all Customer References<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  {{- range $i, $content := $.Site.Data.references | shuffle | first 6 -}}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>customer-img<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/img/cust/{{ $content.custLogo }}.png<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ $content.custName }}<span class="token punctuation">"</span></span> <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ $content.custName }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  {{ end }}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>In order to use this newly created partial, we’ll store it as <code>rnd-customers.html</code> in <code>/themes/[theme-name]/layouts/partials/</code> of our Hugo site. After that, it’s available wherever we would like to display it using the syntax <code>{{ partial &quot;rnd-customers.html&quot; . }}</code>.</p>
<h3 id="the-template" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#the-template"><span>The Template</span></a></h3>
<p>Now that we have got our random content, let’s see how to build a page from the JSON file as well.</p>
<p>In order to do that, we’ll have to make sure that the <code>/references</code> page exists. We’ll also have to specify a specific template for it in front matter. The markdown file could look like this:</p>
<pre class="language-bash"><code class="language-bash">+++<br>title <span class="token operator">=</span> <span class="token string">"References"</span><br>layout <span class="token operator">=</span> <span class="token string">"reference"</span><br>description <span class="token operator">=</span> <span class="token string">"Information regarding our references"</span><br>+++<br><br><span class="token comment">## Reference Customers</span><br><span class="token punctuation">..</span>.</code></pre>
<p>The respective template <code>references.html</code> is stored in the <code>/layouts/</code> folder and looks like that:</p>
<pre class="language-html"><code class="language-html">{{ partial "head.html" . }}<br>{{ partial "header.html" . }}<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>header</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>header<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token comment">&lt;!-- Page Header Content --></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>header</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>container<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token comment">&lt;!-- Main Content --></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      {{- range $i, $content := $.Site.Data.references -}}<br>        {{- $counter :=add $i 1 -}}<br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>col-md-4<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>refcard text-center<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>img-center<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/img/klg/{{ $content.custLogo }}.png<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ $content.custName }}<span class="token punctuation">"</span></span> <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ $content.custName }}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h5</span><span class="token punctuation">></span></span>{{ $content.custName }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h5</span><span class="token punctuation">></span></span><br>            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>{{ $content.custDesc }}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>            <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>reflink<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{{ $content.custLink }}<span class="token punctuation">"</span></span> <span class="token attr-name">target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>_blank<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Customer Website<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>          <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>        {{- if modBool $counter 3 -}}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>clearfix<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>{{ end }}<br>      {{ end }}<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>{{ partial "contact.html" . }}<br>{{ partial "footer.html" . }}</code></pre>
<p>In order to have rows of 3 items in a 12 column grid that are equally spaced, I have added the <code>$counter</code> that will make sure to add a <code>&lt;div class=&quot;clearfix&quot;&gt;</code> after every 3 items. Depending on the CSS you’re using, this may not be necessary - I used Bootstrap 3 in my example above and it was necessary to have each row of 3 customers line up straight.</p>
<p>As you may have noticed, I removed most classes that were not essential for a basic understanding of the concept from the code examples above. So, if you’re going to use them, please make sure to check carefully and add the necessary class definitions.</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#conclusion"><span>Conclusion</span></a></h2>
<p>Working with data in Hugo is a very convenient thing to do, as the examples above may have been able to demonstrate. I’m glad I was able to discover this approach just at the right time - the thought of doing the same thing with jQuery at runtime doesn’t seem very appealing to me anymore honestly.</p>

          
        ]]></content>
      </entry>
    
      
      <entry>
        <title>Hello World</title>
        
          
            <category term="personal" />
          
            <category term="website" />
          
        
        <link href="https://ttntm.me/blog/hello-world/"/>
        
        <updated>2018-08-20T10:30:00Z</updated>
        <id>https://ttntm.me/blog/hello-world/</id>
        <content type="html"><![CDATA[
          
            
            <p>I’ve had a lot of websites over the years, ranging from more or less official business endeavors like a small record label to more private things like a website for my wedding a while ago. I’ve never had a website <em>just for me</em> though, so this is something new.</p>
<p>Also, I’ve never been involved much with giving back to the crowd - meaning all the people posting content like reviews or code snippets online. However, I undeniably profited from all of that helpful content in one way or another.</p>
<p>So, I guess aside from tracking my thoughts and logging both achievements and failures, another aim for this site shall be giving back something to the online world.</p>
<h2 id="how-and-why" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#how-and-why"><span>How and Why</span></a></h2>
<p>As mentioned on the <a href="https://ttntm.me/about" class="int">/about page</a>, I’m working in IT for many years, and I’ve always been very interested in web design. As such, I strongly supported the notion that a software development company like my current employer should be taking care of its own website instead of giving it to some overpaid agency using the same CMS template for each single one of their clients. I ended up with full responsibility for that project myself in the end.</p>
<p>After some rather quick reminder of why I wasn’t a huge WordPress fan, I ended up stumbling over Hugo that somehow stuck out of the vast amount of static site generators. It seemed perfectly suited for what we had in mind for our company website, essentially a rather compact presentation of our products and services along with the integration of our mailchimp newsletter signup and a secure contact form. These requirements together with the performance and security benefits of a static site made it all seem like a perfect fit.</p>
<p>The process of setting up a website with Hugo was pretty straightforward to be honest, mostly thanks to their excellent documentation and the logical structure behind the whole thing. After about 6 weeks of part-time work during hotel nights and airport waiting times, a completely reworked website was ready, essentially based on a custom theme I ended up creating for our purposes.</p>
<p>Another key element helping in the successful re-design of our website was the discovery of Netlify. Compared to a task like setting up, securing and maintaining your own web server to host an instance of WordPress, going live with our website there was basically a walk in the park. If you haven’t heard of their service before, you should definitely go and check it out or even better, try it yourself, it’s free.</p>
<blockquote>
<p>Based on this very positive experience, the idea of a personal website grew and eventually turned into what’s on the screen right now.</p>
</blockquote>
<h2 id="under-the-hood" tabindex="-1"><a class="header-anchor int" href="https://ttntm.me/everything.xml#under-the-hood"><span>Under the Hood</span></a></h2>
<p>To provide some insight, here’s how this site is configured at the moment.</p>
<p>It started out as a trial for Bootstrap 4 with Hugo and quickly turned into a useful yet minimal blog/portfolio kind of site. BS4 is much more utility-based than BS3 was, so that’s a really nice change of pace once you get into their thought pattern. The site started out with the default colors, but I changed them along with some other details to make it look a little less ‘common’ and a little more individual. All the unused parts of BS4 have been removed for the time being, as it would be way too heavy otherwise.</p>
<p>Initially, I had thought of giant hero-sized images for every blog post, but I ditched that idea in favour of load times and time saved when it comes to image search (even though I could spend hours upon hours on Unsplash…). Instead, there’s a header based on <code>particles.js</code> in place on some sites which makes for a nice atmosphere and also provides some interactivity. I remember seeing that on Netlify or somewhere else, and it looked really nice.</p>
<p>In terms of the Hugo setup, there are 2 content sections here, the blog in <code>/blog</code>, and a pre-built but still unused <code>/projects</code> section. The <code>/about</code> section is basically just a single markdown file to make editing it a little easier.</p>
<p>That’s about it for now, a simple, functional yet minimal site for the time being. If you like what you see here, feel free to have a look at the site’s repository over <a class="ext" href="https://github.com/ttntm/ttntm" rel="noopener">on GitHub</a>.</p>

          
        ]]></content>
      </entry>
    </feed>