<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>OddPen</title>
    <link>https://blog.ungra.dev/</link>
    <description>Welcome to my blog where I write about software engineering and how to lead a team!</description>
    <pubDate>Mon, 20 Apr 2026 00:20:22 +0000</pubDate>
    <image>
      <url>https://i.snap.as/lymFxSts.png</url>
      <title>OddPen</title>
      <link>https://blog.ungra.dev/</link>
    </image>
    <item>
      <title>Including Schema.org Data in A PostHTML Component with Parcel.js</title>
      <link>https://blog.ungra.dev/including-schema-org-data-in-a-posthtml-component-with-parcel-js?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[I love Parcel.js to build static websites. It&#39;s really neat. There is one common use-case, I recently had to wrap my head around. Utilizing reusable code.&#xA;I used to have only a one-pager website (really, just an index.html) up to this point. However, with the rewrite of my personal page, I wanted to have more pages. Hence, I needed to figure how to reuse a header.&#xA;!--more--&#xA;Luckily, generally spoken, that is super simple with Parcel.js and PostHTML. Really, all you have to do is create an HTML file and put it in via include. You can even pass parameters with a special syntax. Read more about that here: https://parceljs.org/languages/html/#posthtml. &#xA;There was one issue with that. I didn&#39;t know how to pass Schema.org data. The problem was, that I used it with JSON-LD. That works through a script tag. Usually, you have something like: &#xA;&#xA;    script type=&#34;application/ld+json&#34;&#xA;    {&#xA;      &#34;@context&#34;:  &#34;https://schema.org/&#34;,&#xA;      &#34;@id&#34;: &#34;#record&#34;,&#xA;      &#34;@type&#34;: &#34;Book&#34;,&#xA;      &#34;additionalType&#34;: &#34;Product&#34;,&#xA;      &#34;name&#34;: &#34;Le concerto&#34;,&#xA;      &#34;author&#34;: &#34;Ferchault, Guy&#34;,&#xA;      &#34;offers&#34;:{&#xA;          &#34;@type&#34;: &#34;Offer&#34;,&#xA;          &#34;availability&#34;: &#34;https://schema.org/InStock&#34;,&#xA;          &#34;serialNumber&#34;: &#34;CONC91000937&#34;,&#xA;          &#34;sku&#34;: &#34;780 R2&#34;,&#xA;          &#34;offeredBy&#34;: {&#xA;              &#34;@type&#34;: &#34;Library&#34;,&#xA;              &#34;@id&#34;: &#34;http://library.anytown.gov.uk&#34;,&#xA;              &#34;name&#34;: &#34;Anytown City Library&#34;&#xA;          },&#xA;          &#34;businessFunction&#34;: &#34;http://purl.org/goodrelations/v1#LeaseOut&#34;,&#xA;          &#34;itemOffered&#34;: &#34;#record&#34;&#xA;        }&#xA;    }&#xA;    /script&#xA;&#xA;That format is what Google (or, more specifically, the Chrome team) recommends when it comes to Schema data. &#xA;Now we have a slight problem. You cannot pass JSON that easily, nor can you pass a whole HTML tag. At least not to my knowledge. However, at the same time, I didn&#39;t want to rewrite my whole Schema data, so I fiddled a bit with it. &#xA;At the end of the day, my solution was to create another reusable component as a file to be included with the original component (a header in my case). This include tag accepts a src attribute, that can be filled by the PostHTML parameter syntax! Hurray.&#xA;Okay, step by step. You have your HTML page. You want to have your header on there, without needing to maintain it for each page separately. So you create an HTML file for the header, that can be included via PostHTML. You parametrize all the dynamic attributes, like title, keywords and thereof.&#xA;&#xA;PAGE.html ──includes──► HEADER.html ──includes──► SCHEMA-FILE.html             &#xA;                                                                  &#xA;                             │                                                       &#xA;                             │                                                       &#xA;       passes parameters including a src (path) to the schema file&#xA;One of those parameters is the path to the schema file that you can pass to the src attribute of the include tag within the header.&#xA;Thus, when everything is glued together, the page requests PostHTML to include the header, which requests to include the schema file as configured by a parameter.&#xA;&#xA;The Page Includes the Header&#xA;&#xA;include src=&#34;./src/components/head.html&#34;&#xA;  {&#xA;  &#34;title&#34;: &#34;Foobar&#34;,&#xA;  &#34;schemaFile&#34;: &#34;./src/schemas/index-schema.html&#34;&#xA;  }&#xA;/include&#xA;&#xA;The Header Includes the Schema&#xA;&#xA;head&#xA;  meta charset=&#34;UTF-8&#34; /&#xA;  meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34; /&#xA;  title{{title}}/title&#xA;  include src=&#34;{{schemaFile}}&#34; /&#xA;/head&#xA;&#xA;The Schema File Contains the Data&#xA;&#xA;script type=&#34;application/ld+json&#34;&#xA;{&#xA;  &#34;@context&#34;: &#34;https://schema.org&#34;,&#xA;  &#34;@type&#34;: &#34;Organization&#34;,&#xA;  &#34;name&#34;: &#34;Foobar&#34;&#xA;}&#xA;/script&#xA;`]]&gt;</description>
      <content:encoded><![CDATA[<p>I love Parcel.js to build static websites. It&#39;s really neat. There is one common use-case, I recently had to wrap my head around. Utilizing reusable code.
I used to have only a one-pager website (really, just an index.html) up to this point. However, with the rewrite of my personal page, I wanted to have more pages. Hence, I needed to figure how to reuse a header.

Luckily, generally spoken, that is super simple with Parcel.js and PostHTML. Really, all you have to do is create an HTML file and put it in via <code>include</code>. You can even pass parameters with a special syntax. Read more about that here: <a href="https://parceljs.org/languages/html/#posthtml">https://parceljs.org/languages/html/#posthtml</a>.
There was one issue with that. I didn&#39;t know how to pass Schema.org data. The problem was, that I used it with JSON-LD. That works through a <code>script</code> tag. Usually, you have something like:</p>

<pre><code class="language-html">    &lt;script type=&#34;application/ld+json&#34;&gt;
    {
      &#34;@context&#34;:  &#34;https://schema.org/&#34;,
      &#34;@id&#34;: &#34;#record&#34;,
      &#34;@type&#34;: &#34;Book&#34;,
      &#34;additionalType&#34;: &#34;Product&#34;,
      &#34;name&#34;: &#34;Le concerto&#34;,
      &#34;author&#34;: &#34;Ferchault, Guy&#34;,
      &#34;offers&#34;:{
          &#34;@type&#34;: &#34;Offer&#34;,
          &#34;availability&#34;: &#34;https://schema.org/InStock&#34;,
          &#34;serialNumber&#34;: &#34;CONC91000937&#34;,
          &#34;sku&#34;: &#34;780 R2&#34;,
          &#34;offeredBy&#34;: {
              &#34;@type&#34;: &#34;Library&#34;,
              &#34;@id&#34;: &#34;http://library.anytown.gov.uk&#34;,
              &#34;name&#34;: &#34;Anytown City Library&#34;
          },
          &#34;businessFunction&#34;: &#34;http://purl.org/goodrelations/v1#LeaseOut&#34;,
          &#34;itemOffered&#34;: &#34;#record&#34;
        }
    }
    &lt;/script&gt;
</code></pre>

<p>That format is what Google (or, more specifically, the Chrome team) <a href="https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data#supported-formats">recommends</a> when it comes to Schema data.
Now we have a slight problem. You cannot pass JSON that easily, nor can you pass a whole HTML tag. At least not to my knowledge. However, at the same time, I didn&#39;t want to rewrite my whole Schema data, so I fiddled a bit with it.
At the end of the day, my solution was to create another reusable component as a file to be included with the original component (a header in my case). This <code>include</code> tag accepts a <code>src</code> attribute, that can be filled by the PostHTML parameter syntax! Hurray.
Okay, step by step. You have your HTML page. You want to have your header on there, without needing to maintain it for each page separately. So you create an HTML file for the header, that can be included via PostHTML. You parametrize all the dynamic attributes, like <code>title</code>, <code>keywords</code> and thereof.</p>

<pre><code>PAGE.html ──includes──► HEADER.html ──includes──► SCHEMA-FILE.html             
                                                                  
                             │                                                       
                             │                                                       
       passes parameters including a src (path) to the schema file
</code></pre>

<p>One of those parameters is the path to the schema file that you can pass to the <code>src</code> attribute of the <code>include</code> tag within the header.
Thus, when everything is glued together, the page requests PostHTML to include the header, which requests to include the schema file as configured by a parameter.</p>

<h2 id="the-page-includes-the-header" id="the-page-includes-the-header">The Page Includes the Header</h2>

<pre><code class="language-html">&lt;include src=&#34;./src/components/head.html&#34;&gt;
  {
  &#34;title&#34;: &#34;Foobar&#34;,
  &#34;schemaFile&#34;: &#34;./src/schemas/index-schema.html&#34;
  }
&lt;/include&gt;
</code></pre>

<h2 id="the-header-includes-the-schema" id="the-header-includes-the-schema">The Header Includes the Schema</h2>

<pre><code class="language-html">&lt;head&gt;
  &lt;meta charset=&#34;UTF-8&#34; /&gt;
  &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34; /&gt;
  &lt;title&gt;{{title}}&lt;/title&gt;
  &lt;include src=&#34;{{schemaFile}}&#34; /&gt;
&lt;/head&gt;
</code></pre>

<h2 id="the-schema-file-contains-the-data" id="the-schema-file-contains-the-data">The Schema File Contains the Data</h2>

<pre><code class="language-html">&lt;script type=&#34;application/ld+json&#34;&gt;
{
  &#34;@context&#34;: &#34;https://schema.org&#34;,
  &#34;@type&#34;: &#34;Organization&#34;,
  &#34;name&#34;: &#34;Foobar&#34;
}
&lt;/script&gt;
</code></pre>
]]></content:encoded>
      <guid>https://blog.ungra.dev/including-schema-org-data-in-a-posthtml-component-with-parcel-js</guid>
      <pubDate>Sun, 16 Feb 2025 17:21:42 +0000</pubDate>
    </item>
    <item>
      <title>Games to Play with a Distributed Team</title>
      <link>https://blog.ungra.dev/games-to-play-with-a-distributed-team?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Believe me or not, but games are super important with distributed teams. We do not meet in person that often. Hence, it&#39;s crucial to provide opportunity to socialize. And nothing better for that than playing games together!&#xA;Here is my subjective list of proven (by me 😜) games that work well through a video conference meeting and are SFW.&#xA;!--more--&#xA;&#xA;Codenames&#xA;This is easily the best, I guess. It is usually well received by everyone, does not require any special hand-eye-coordination, and has the perfect pace. It only really makes sense with at least four people, though.&#xA;Check it out here: https://codenames.game/&#xA;&#xA;Gartic Phone&#xA;That&#39;s always a good ice-breaker. From my experience, people are confused about it in the first round. The second round is hilarious and from then on, it wears off kind of quickly. Hence, I usually suggest it at a very early stage in the &#34;bonding journey&#34; of a group. Also, because there is no competition. There is very little opportunity to affront someone.&#xA;Find it here: https://garticphone.com/&#xA;&#xA;Make it Meme&#xA;Falls in the same category as Gartic Phone, but is very flexible in terms of how much time it consumes. It can only be a quick 10 minutes round and still provide good laughs. What I also like about this (same goes for Gartic Phone, btw!) is, that you create digital assets that you can archive in your Slack channel etc. There were often times when peeps created emojis out of these.&#xA;See who is the spiciest meme lord: https://makeitmeme.com/&#xA;&#xA;Letterjam&#xA;A bit harder to learn I would say, but once understood very lightweight. It is a viable alternative to Codenames, but can as well played with two people. Once people got the concept, it&#39;s loved. My advice would be to provide rules upfront.&#xA;Jam here: https://letterjam.game/&#xA;&#xA;Vote&#39;s Out&#xA;That&#39;s a very cool game if you want to introduce people. I heavily recommend it in groups where peers are new to each other. It can reveal a lot about a person in a fun and non-intrusive way. Other than that, I would prefer Codenames etc., but hey, try it out.&#xA;Start voting here: https://votesout.com/&#xA;&#xA;Squabble&#xA;That&#39;s one of my personal favorites, but not everyone&#39;s cup of tea, unfortunately. I would describe it as competitive Wordle. In my experience, it&#39;s definitely worth trying out, but do not get disappointed if people do not like it too much. It&#39;s one of those &#34;love it or hate it&#34; games.&#xA;Wordle with damage: https://squabble.me/&#xA;&#xA;Boring&#xA;A tedious game! No, that&#39;s, of course, just the ironic name. It&#39;s actually super cool and unique. You have to find words of a certain category and the other players vote if it&#39;s a fit. The clue is, that you do not get any points, if you have the same answer as someone else. This game is usually perceived well, albeit usually not the group&#39;s top favorite.&#xA;Get bored: https://really.boring.website/&#xA;&#xA;Spyfall&#xA;Finally, an absolute classic. As far as I&#39;m concerned, the original was taken down, but there are a gazillion of clones that are often even better. Check it out. Everyone loves it. Falls in the same category as Codenames: everybody&#39;s darling.&#xA;Find the spy here: https://spyfall.adrianocola.com/&#xA;&#xA;That&#39;s my curated list of games. We try to have a game session every three weeks or so in my teams, but whatever works for you, of course. I really think they are an essential tool (as ridiculous as this might sound) in distributed teams. Getting people together on a social and emotional level is super hard in a remote environment. Do yourself a favor and make it, at least, fun.]]&gt;</description>
      <content:encoded><![CDATA[<p>Believe me or not, but games are super important with distributed teams. We do not meet in person that often. Hence, it&#39;s crucial to provide opportunity to socialize. And nothing better for that than playing games together!
Here is my subjective list of proven (by me 😜) games that work well through a video conference meeting and are SFW.
</p>

<h2 id="codenames" id="codenames">Codenames</h2>

<p>This is easily the best, I guess. It is usually well received by everyone, does not require any special hand-eye-coordination, and has the perfect pace. It only really makes sense with at least four people, though.
Check it out here: <a href="https://codenames.game/">https://codenames.game/</a></p>

<h2 id="gartic-phone" id="gartic-phone">Gartic Phone</h2>

<p>That&#39;s always a good ice-breaker. From my experience, people are confused about it in the first round. The second round is hilarious and from then on, it wears off kind of quickly. Hence, I usually suggest it at a very early stage in the “bonding journey” of a group. Also, because there is no competition. There is very little opportunity to affront someone.
Find it here: <a href="https://garticphone.com/">https://garticphone.com/</a></p>

<h2 id="make-it-meme" id="make-it-meme">Make it Meme</h2>

<p>Falls in the same category as Gartic Phone, but is very flexible in terms of how much time it consumes. It can only be a quick 10 minutes round and still provide good laughs. What I also like about this (same goes for Gartic Phone, btw!) is, that you create digital assets that you can archive in your Slack channel etc. There were often times when peeps created emojis out of these.
See who is the spiciest meme lord: <a href="https://makeitmeme.com/">https://makeitmeme.com/</a></p>

<h2 id="letterjam" id="letterjam">Letterjam</h2>

<p>A bit harder to learn I would say, but once understood very lightweight. It is a viable alternative to Codenames, but can as well played with two people. Once people got the concept, it&#39;s loved. My advice would be to provide rules upfront.
Jam here: <a href="https://letterjam.game/">https://letterjam.game/</a></p>

<h2 id="vote-s-out" id="vote-s-out">Vote&#39;s Out</h2>

<p>That&#39;s a very cool game if you want to introduce people. I heavily recommend it in groups where peers are new to each other. It can reveal a lot about a person in a fun and non-intrusive way. Other than that, I would prefer Codenames etc., but hey, try it out.
Start voting here: <a href="https://votesout.com/">https://votesout.com/</a></p>

<h2 id="squabble" id="squabble">Squabble</h2>

<p>That&#39;s one of my personal favorites, but not everyone&#39;s cup of tea, unfortunately. I would describe it as competitive Wordle. In my experience, it&#39;s definitely worth trying out, but do not get disappointed if people do not like it too much. It&#39;s one of those “love it or hate it” games.
Wordle with damage: <a href="https://squabble.me/">https://squabble.me/</a></p>

<h2 id="boring" id="boring">Boring</h2>

<p>A tedious game! No, that&#39;s, of course, just the ironic name. It&#39;s actually super cool and unique. You have to find words of a certain category and the other players vote if it&#39;s a fit. The clue is, that you do not get any points, if you have the same answer as someone else. This game is usually perceived well, albeit usually not the group&#39;s top favorite.
Get bored: <a href="https://really.boring.website/">https://really.boring.website/</a></p>

<h2 id="spyfall" id="spyfall">Spyfall</h2>

<p>Finally, an absolute classic. As far as I&#39;m concerned, the original was taken down, but there are a gazillion of clones that are often even better. Check it out. Everyone loves it. Falls in the same category as Codenames: everybody&#39;s darling.
Find the spy here: <a href="https://spyfall.adrianocola.com/">https://spyfall.adrianocola.com/</a></p>

<p>That&#39;s my curated list of games. We try to have a game session every three weeks or so in my teams, but whatever works for you, of course. I really think they are an essential tool (as ridiculous as this might sound) in distributed teams. Getting people together on a social and emotional level is super hard in a remote environment. Do yourself a favor and make it, at least, fun.</p>
]]></content:encoded>
      <guid>https://blog.ungra.dev/games-to-play-with-a-distributed-team</guid>
      <pubDate>Mon, 03 Feb 2025 20:02:20 +0000</pubDate>
    </item>
    <item>
      <title>Please Start from Textbook</title>
      <link>https://blog.ungra.dev/please-start-from-textbook?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Nowadays, for good or for worse, if you google something you end up on Reddit. Wait, wait, stay here. I do not want to talk about how this is bad. I want to discuss a phenomenon you can observe there. Or more precisely: on every platform, people ask for help! (I think it&#39;s even one of the issues that people find so annoying about starting out on Stackoverflow.)&#xA;!--more--&#xA;Answerers are often either in the state of unconscious competence, thus be able to reflect and adapt a given standard solution, or were coached by such folk, and just proxy a bias.&#xA;&#xA;  Unconscious competence: The individual has had so much practice with a skill that it has become &#34;second nature&#34; and can be performed easily. As a result, the skill can be performed while executing another task. The individual may be able to teach it to others, depending upon how and when it was learned.&#xA;  &amp;mdash; Four Stages of Competence&#xA;&#xA;What does that mean? Let&#39;s start with an example. Lately, I came across a thread where people discussed Pair Programming. And, as expected, it was super controversial. I immediately thought it&#39;s an excellent example of what I want to talk about here.&#xA;You could observe two groups that did not recommend it. The first one suggested a solution that was better (in their anecdotal experience). The second one said they tried it, and didn&#39;t like it. This group, more often than not, described a process that was, in fact, not Pair Programming.&#xA;Another collective that we can throw into the pit here, is people suffering from “Not Invented Here Syndrome” in companies. It basically describes a bias against ideas from outside. We unconsciously are more critical with things we did not create in our “tribe.” I&#39;m sure you have heard variations of “this will not work here”, haven&#39;t you? 🙂&#xA;&#xA;  [Not Invented Here Syndrome is] the tendency of a project group of stable composition to believe it possesses a monopoly of knowledge of its field, which leads it to reject new ideas from outsiders to the likely detriment of its performance.&#xA;  &amp;mdash; Learnosity&#xA;&#xA;All these groups have one thing in common: they refuse or are incapable of consulting for or adopting the textbook approach. By that, I mean the concept as given, as it is fully defined. And that can be an issue.&#xA;The problems with that reach from a harder onboarding process, to lack of knowledge depth, and everything in between.&#xA;One of the most obvious ones is a communication problem. It already manifests at the earliest point during hiring. Either you explain what your special process looks like, or you will deliberately run into a misunderstanding. And that issue exists throughout the whole journey. Coining a term differently for your domain, let&#39;s say your team, is a misalignment waiting to happen.&#xA;Now, that does not mean you can&#39;t adapt a given standard to fit your scenario perfectly. No, absolutely not. However, please be very diligent with it and create small feedback loops. Start to adapt only, when you are in a state of unconscious competence. Only then you have the profound knowledge it takes to communicate why your approach is different.&#xA;A good example of this is Scrum. Contrary to popular belief, it is black and white. You either do Scrum, or you don&#39;t. There is no in between. Still, it gives you the freedom to adapt, once you know what you are doing. However, it is not Scrum anymore. It is something that you have to give a name. And to be able to do that, you have to understand the textbook approach first. You need to be able to tell the differences, why they were introduced, and how you measure whether they have the desired effects.&#xA;&#xA;  The Scrum framework, as outlined herein, is immutable. While implementing only parts of Scrum is possible, the result is not Scrum. Scrum exists only in its entirety and functions well as a container for other techniques, methodologies, and practices.&#xA;  &amp;mdash; The Scrum Guide&#xA;&#xA;If you make changes, it&#39;s in my opinion, crucial to measure data. What did you want to achieve by introducing that concept? Does the adaption aid that better? Because there is one major pitfall I observe quite often: The adaptions actually benefit a different goal than what we have introduced the general idea for. And, sorry for being the killjoy here, often it&#39;s to sacrifice actual goal score for engineers&#39; comfort.&#xA;If you do not start from how it is supposed to be, you never really tried it. You never put it into practice. Hence, you also cannot understand it fully. Starting with an adapted version shields you from making valuable experiences, that are necessary, to understand why an adaption might make sense.&#xA;Now, what can you do about that? It depends a bit on your context. Usually, you find yourself in one of the groups as described at the beginning. We have a group being experienced enough to adapt, we have a group that just echoes what they have been told (often to appear competent), and one that simply think their work environment is too special for something to work.&#xA;In the first, you have worked your way through a concept and thoroughly understood it. So much, that you were able to finally make changes to fit it directly onto your individual scenario. That is good. You optimized things and - hopefully - ended up more effective. &#xA;However, be aware of a pitfall! You went through a huge learning curve to end up where you are. To end up in a state where you could question things. Others did not. This is vital. If you provide consultancy or advice, you have to respect that. Of course, you can explain, why you have adapted or what optimizations you have found, but recommend others to start from 0. Start from textbook. Their scenario is not yours. &#xA;If you believe you have found a better universal solution, you have to create another textbook. 😁&#xA;That brings us to the next category, kind of like the victims of the first, if you want so. You research a topic and ask more experienced people for their opinion. That&#39;s wonderful! Always reach out for different perspectives.&#xA;However, hold in mind that you may not hear the most thoughtful, objective, and unbiased advice. Usually, you end up listening to anecdotes of very specific cases. All of them have in common (or should have) that they tried out what worked for them. This is not your reality. Do not start to simply echo what they told you.&#xA;I understand why we do that. We want to sound as competent and smart as them, right? And it&#39;s, indeed, smart to keep their remarks in your notebook. It is smart to, later, use them for your own thoughts and ideas. It&#39;s important, thought, that this needs to take place when you have moved to a state of unconscious competence yourself. You have to start from scratch. You have to start from textbook. Your scenario is not theirs.&#xA;The last group in my book is related to a different dynamic. If you often diverge from the vanilla textbook approach or are generally eager to challenge new ideas, you might be a victim of the Not Invented Here bias: You think your scenario is too special for anything to work. However, most likely it&#39;s not.&#xA;Most of the time, when we discuss methodologies, frameworks, or industry-standards, there was put a lot of thought into what is almost universally applicable, and what is not. It has already been stripped down to the basic things that are highly likely to fit every work environment.&#xA;To battle Not Invented Here Syndrome, it helps a lot to know that it exists and that it&#39;s easy to fall victim. My first advice is to exercise honest self reflection. Does this apply to you? What behaviors can you observe, and how can you change them?&#xA;You will never know whether you are right, unless you try things out. And to try them out profoundly, you have to - you guessed it - start from the textbook! Do not try to utilize an adapted version upfront, to accommodate your alleged &#34;specialities.&#34; Try to do it as given.&#xA;Now I really need to come to an end. This already feels wordy, sorry for that. It&#39;s just a phenomenon I observed quite a lot throughout my career, and it always bugged me. Hence, I really wanted to bring this post out. I hope that I got the point across and could convince you to &#34;start from the textbook!&#34; 😁&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Nowadays, for good or for worse, if you google something you end up on Reddit. Wait, wait, stay here. I do not want to talk about how this is bad. I want to discuss a phenomenon you can observe there. Or more precisely: on every platform, people ask for help! (I think it&#39;s even one of the issues that people find so annoying about starting out on Stackoverflow.)

Answerers are often either in the state of unconscious competence, thus be able to reflect and adapt a given standard solution, or were coached by such folk, and just proxy a bias.</p>

<blockquote><p>Unconscious competence: The individual has had so much practice with a skill that it has become “second nature” and can be performed easily. As a result, the skill can be performed while executing another task. The individual may be able to teach it to others, depending upon how and when it was learned.
— <a href="https://en.wikipedia.org/wiki/Four_stages_of_competence?oldformat=true#Stages">Four Stages of Competence</a></p></blockquote>

<p>What does that mean? Let&#39;s start with an example. Lately, I came across a thread where people discussed Pair Programming. And, as expected, it was super controversial. I immediately thought it&#39;s an excellent example of what I want to talk about here.
You could observe two groups that did not recommend it. The first one suggested a solution that was better (in their anecdotal experience). The second one said they tried it, and didn&#39;t like it. This group, more often than not, described a process that was, in fact, <em>not</em> Pair Programming.
Another collective that we can throw into the pit here, is people suffering from “Not Invented Here Syndrome” in companies. It basically describes a bias against ideas from outside. We unconsciously are more critical with things we did not create in our “tribe.” I&#39;m sure you have heard variations of “this will not work here”, haven&#39;t you? 🙂</p>

<blockquote><p>[Not Invented Here Syndrome is] the tendency of a project group of stable composition to believe it possesses a monopoly of knowledge of its field, which leads it to reject new ideas from outsiders to the likely detriment of its performance.
— <a href="https://web.archive.org/web/20250106124512/https://learnosity.com/edtech-blog/not-invented-here-syndrome-explained/">Learnosity</a></p></blockquote>

<p>All these groups have one thing in common: they refuse or are incapable of consulting for or adopting the textbook approach. By that, I mean the concept as given, as it is fully defined. And that can be an issue.
The problems with that reach from a harder onboarding process, to lack of knowledge depth, and everything in between.
One of the most obvious ones is a communication problem. It already manifests at the earliest point during hiring. Either you explain what <em>your</em> special process looks like, or you will deliberately run into a misunderstanding. And that issue exists throughout the whole journey. Coining a term differently for your domain, let&#39;s say your team, is a misalignment waiting to happen.
Now, that does not mean you can&#39;t adapt a given standard to fit your scenario perfectly. No, absolutely not. However, please be very diligent with it and create small feedback loops. Start to adapt only, when you are in a state of unconscious competence. Only then you have the profound knowledge it takes to communicate why your approach is different.
A good example of this is Scrum. Contrary to popular belief, it is black and white. You either do Scrum, or you don&#39;t. There is no in between. Still, it gives you the freedom to adapt, once you know what you are doing. However, it is <em>not</em> Scrum anymore. It is something that you have to give a name. And to be able to do that, you have to understand the textbook approach first. You need to be able to tell the differences, why they were introduced, and how you measure whether they have the desired effects.</p>

<blockquote><p>The Scrum framework, as outlined herein, is immutable. While implementing only parts of Scrum is possible, the result is not Scrum. Scrum exists only in its entirety and functions well as a container for other techniques, methodologies, and practices.
— <a href="https://web.archive.org/web/20250105124627/https://scrumguides.org/scrum-guide.html#scrum-definition">The Scrum Guide</a></p></blockquote>

<p>If you make changes, it&#39;s in my opinion, crucial to measure data. What did you want to achieve by introducing that concept? Does the adaption aid that better? Because there is one major pitfall I observe quite often: The adaptions actually benefit a different goal than what we have introduced the general idea for. And, sorry for being the killjoy here, often it&#39;s to sacrifice actual goal score for engineers&#39; comfort.
If you do not start from how it is supposed to be, you never really tried it. You never put it into practice. Hence, you also cannot understand it fully. Starting with an adapted version shields you from making valuable experiences, that are necessary, to understand why an adaption might make sense.
Now, what can you do about that? It depends a bit on your context. Usually, you find yourself in one of the groups as described at the beginning. We have a group being experienced enough to adapt, we have a group that just echoes what they have been told (often to appear competent), and one that simply think their work environment is too special for something to work.
In the first, you have worked your way through a concept and thoroughly understood it. So much, that you were able to finally make changes to fit it directly onto your individual scenario. That is good. You optimized things and – hopefully – ended up more effective.
However, be aware of a pitfall! You went through a huge learning curve to end up where you are. To end up in a state where you could question things. Others did not. <em>This is vital.</em> If you provide consultancy or advice, you have to respect that. Of course, you can explain, why you have adapted or what optimizations you have found, but recommend others to start from 0. Start from textbook. Their scenario is not yours.
If you believe you have found a better <em>universal</em> solution, you have to create another textbook. 😁
That brings us to the next category, kind of like the victims of the first, if you want so. You research a topic and ask more experienced people for their opinion. That&#39;s wonderful! Always reach out for different perspectives.
However, hold in mind that you may not hear the most thoughtful, objective, and unbiased advice. Usually, you end up listening to anecdotes of very specific cases. All of them have in common (or should have) that they tried out what worked for <em>them</em>. This is not your reality. Do not start to simply echo what they told you.
I understand why we do that. We want to sound as competent and smart as them, right? And it&#39;s, indeed, smart to keep their remarks in your notebook. It is smart to, later, use them for your own thoughts and ideas. It&#39;s important, thought, that this needs to take place when you have moved to a state of unconscious competence yourself. You have to start from scratch. You have to start from textbook. Your scenario is not theirs.
The last group in my book is related to a different dynamic. If you often diverge from the vanilla textbook approach or are generally eager to challenge new ideas, you might be a victim of the Not Invented Here bias: You think your scenario is too special for anything to work. However, most likely it&#39;s not.
Most of the time, when we discuss methodologies, frameworks, or industry-standards, there was put a lot of thought into what is almost universally applicable, and what is not. It has already been stripped down to the basic things that are highly likely to fit every work environment.
To battle Not Invented Here Syndrome, it helps a lot to know that it exists and that it&#39;s easy to fall victim. My first advice is to exercise honest self reflection. Does this apply to you? What behaviors can you observe, and how can you change them?
You will never know whether you are right, unless you try things out. And to try them out profoundly, you have to – you guessed it – start from the textbook! Do not try to utilize an adapted version upfront, to accommodate your alleged “specialities.” Try to do it as given.
Now I really need to come to an end. This already feels wordy, sorry for that. It&#39;s just a phenomenon I observed quite a lot throughout my career, and it always bugged me. Hence, I really wanted to bring this post out. I hope that I got the point across and could convince you to “start from the textbook!” 😁</p>
]]></content:encoded>
      <guid>https://blog.ungra.dev/please-start-from-textbook</guid>
      <pubDate>Sat, 28 Dec 2024 10:07:53 +0000</pubDate>
    </item>
    <item>
      <title>How to Know Whether to Use Data URLs</title>
      <link>https://blog.ungra.dev/how-to-know-whether-to-use-data-urls?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[We often can provide assets with a URI or a so-called &#34;Data URL&#34; on the web. As they are so straightforward to use, they get pretty often copied (from a tutorial, snippet, or whatnot) without us being able to tell what they do, when to prefer them, or whether you should still be using them altogether. It&#39;s one of the lesser-known performance tweaks I will shed some light on in this blog post. &#xA;&#xA;!--more--&#xA;&#xA;This post was reviewed by Leon Brocard &amp; David Lorenz. Thank you! ❤️&#xA;&#xA;What are Data URLs Anyway&#xA;&#xA;First things first, what are we talking about here? Let&#39;s make a simple example of a background image:&#xA;&#xA;div/div&#xA;&#xA;div {&#xA;  height: 100px;&#xA;  width: 100px;&#xA;  background-image: url(&#39;https://placehold.co/600x400&#39;);&#xA;}&#xA;&#xA;That snippet is quite simple. We add a div and configure it to hold a background image. You might know that there is a second approach to this, however. This approach makes use of a so-called &#34;data URL&#34;:&#xA;&#xA;div {&#xA;  height: 100px;&#xA;  width: 100px;&#xA;  background-image: url(&#39;data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL(...)TEwLjRaIi8+PC9zdmc+&#39;);&#xA;}&#xA;&#xA;Now, that is different. The two snippets output the same result. However, in the latter, we provide the image data directly in the CSS instead of requesting it from a server.&#xA;Here, we provide the encoded image binary data directly in a given convention. There is no need to catch it from the server, as we provide the file in CSS.&#xA;&#xA;  A new URL scheme, &#34;data&#34;, is defined. It allows inclusion of small data items as &#34;immediate&#34; data, as if it had been included externally.&#xA;&#xA;Base64 Encoding&#xA;&#xA;How does this work? The image data is encoded with an algorithm called &#34;Base64&#34;. It sounds a bit complicated, but it&#39;s, in fact, not. The main goal of such encodings is to display binary data in Unicode. That&#39;s why such are called &#34;binary to text encodings.&#34;&#xA;A digit in Base64 can represent a maximum of 6 bits. &#xA;Why is that? Let&#39;s make an example. We want to display 101101 as text. But hold on a second. Did you recognize something? We already did it! Writing out 101101 made a bit-sequence apparent to you as a reader. We encoded binary to text.&#xA;Now, let&#39;s imagine instead of &#34;0&#34; and &#34;1&#34;, we decide for &#34;A&#34; and &#34;B&#34;: BABBAB. Guess what? We have just Base2 encoded data! Yay.&#xA;Do you see the issue here? We must utilize eight bytes of data to encode six bits of data, as A and B in ASCII take up one byte. We &#34;inflate&#34; or bloat this data by ~1067% (100 % / 6 bit  64 bit).&#xA;&#xA;| Binary Sequence | Base2 Encoded |&#xA;|-----------------|---------------|&#xA;| 0               | A             |&#xA;| 1               | B             |&#xA;| 101101          | BABBAB        |&#xA;&#xA;To tackle this problem, there is a little trick: we add more characters to encode a bigger binary sequence at once. Let&#39;s make another example here: Base4 encoding.&#xA;&#xA;| Binary Sequence | Base4 Encoded |&#xA;|-----------------|---------------|&#xA;| 00              | A             |&#xA;| 01              | B             |&#xA;| 10              | C             |&#xA;| 11              | D             |&#xA;| 101101          | CDB           |&#xA;&#xA;How would our sequence of 101101 look with that algorithm? It encodes to CDB. This results in a far smaller bloat of only 400%!&#xA;We continue in that manner until we have 64 distinct characters representing a specific sequence each. That&#39;s what Base64 encoding is.&#xA;&#xA;| Binary Sequence | Base64 Encoded |&#xA;|-----------------|----------------|&#xA;| 000000          | A              |&#xA;| 000001          | B              |&#xA;| ...             | ...            |&#xA;| 011001          | Z              |&#xA;| 011010          | a              |&#xA;| ...             | ...            |&#xA;| 101101          | t              |&#xA;| ...             | ...            |&#xA;| 111111          | /              |&#xA;&#xA;In the given table, we see that our example sequence of 101101 is encoded by only using the character t. That means we only add two more bits to display 6 bits of data!  That is merely an inflation of roughly 133%. Awesome!&#xA;Well, but why stop there? Why did we expressly agree on &#34;6 bits&#34; and not use, e.g., Base128 encoding? ASCII has enough characters, right?&#xA;However, 32 of them are control characters that computers may interpret. That would lead to severe bugs. Why this is exactly is out of the scope of this blog post (I mean, the Base64 algorithm already kinda is).&#xA;Just so much: There is a Base91 and Base85 encoding; however, as illustrated in our examples above, &#34;power of two&#34; is more readily encoded when it comes to byte data, and that&#39;s why we mostly settled on that.&#xA;&#xA;To summarize this a little, let&#39;s say we want to encode 10 bytes of data, which is 80 bits. That means we need 14 characters (80/6≈14) to represent that. &#xA;The binary sequence 000000 is represented as ASCII A in Base64. Read this: &#34;The binary data of my image has six consecutive zeros somewhere. The algorithm of Base64 encoding will transform this into &#39;A.&#39;&#34; &#xA;In ASCII, A is eight bits long. Represented in binary, it looks like this: 01000001. Thus, we made this sequence two bits larger.&#xA;&#xA;Preamble&#xA;&#xA;As always, when it comes to performance, it&#39;s complex. There are a lot of layers to the question of whether to use a data URL or not. It heavily depends on how your external resources are hosted. Are they hosted on the edge? What&#39;s the latency? The same goes for the hardware of the users. How long does it take for them to decode the given data? May this take even longer than requesting the already decoded resource?&#xA;You must always collect RUM data and compare approaches to know best. However, generally speaking, we could say that there is a sweet spot where the Base64 encoding overhead exceeds the overhead of an HTTP request, and thus, it&#39;s advisable not to use Data URLs. &#xA;&#xA;The Premise&#xA;&#xA;Let&#39;s repeat this: if the overhead of a request to an external resource exceeds the size of the Base64 extra bloat, you should use a data-url. We are not talking about the payload here. It&#39;s just what the request in isolation costs us. Do we have a bigger total size with Base64 encoding or an HTTP request?&#xA;Let&#39;s make an example here. We start with a white pixel. We decode this as PNG in, well, the resolution 1x1. We will add other background colors throughout this blog post. The following table already yields the most important values: uncompressed file size as PNG, the (bloated) size when Base64 is encoded, the total request size (headers and payload), and finally, the total size of the data-URL. &#xA;&#xA;| Resolution | Background Color | Size | Base64 Encoded Size | Request Size (total) | Data URL Size |&#xA;|------------|------------------|------|---------------------|----------------------|---------------|&#xA;| 1x1        | White            | 90   | 120                 | 864                  | 143           |&#xA;&#xA;In this first example, we would need to transfer 721 (864 - 143) more bytes if we did not utilize a data URL.&#xA;&#xA;Measure the Request&#xA;&#xA;Before we head deeper into data points and their analysis, I want to clarify how we measure the total size of a request. How do we get the value in the &#34;Request Size (total)&#34; column?&#xA;The most straightforward way is to head to the network tab in the Web Developer Tools. The first thing you do is create a snapshot of all the page&#39;s network requests. Then, find the one for the particular media in question. &#xA;&#xA;A screenshot of web developer tools. The network tab is active. A particular network request of an image is highlighted and selected. A filter &#34;images&#34; is set. Three numbers emphasize the user journey: number one is on the network tab, number two is on the &#34;images&#34; filter, and number three is on the highlighted network request of an image.&#xA;&#xA;{&#xA;&#x9;&#34;Status&#34;: &#34;200OK&#34;,&#xA;&#x9;&#34;Version&#34;: &#34;HTTP/2&#34;,&#xA;&#x9;&#34;Transferred&#34;: &#34;22.12 kB (21.63 kB size)&#34;,&#xA;&#x9;&#34;Referrer Policy&#34;: &#34;strict-origin-when-cross-origin&#34;,&#xA;&#x9;&#34;Request Priority&#34;: &#34;Low&#34;,&#xA;&#x9;&#34;DNS Resolution&#34;: &#34;System&#34;&#xA;}&#xA;Now, you might think it&#39;s pretty easy, right? Subtract the payload (21.63 kB) from the transferred data (21.63 kB) and done. However, you also have to take the request header into account! That adds a little something.&#xA;&#xA;A screenshot from a network request as captured by Firefox Developer Tools. It features two important tabs: &#34;Response Headers&#34; and &#34;Request Headers.&#34;&#xA;&#xA;Long story short: in the screenshot above, we see both headers, request and response. This data is the &#34;overhead&#34; we need to take into consideration. Everything else is payload - not bloated by Base64 encoding.&#xA;&#xA;To the Lab!&#xA;&#xA;Now, equipped with that knowledge and skills, we can create some data points. I did this with a Node.js script. It creates many images and writes their file sizes and Base64 encoded sizes in a CSV file:&#xA;&#xA;import sharp from &#34;sharp&#34;;&#xA;import fs from &#34;fs&#34;;&#xA;&#xA;// Create a new direcotry for that run&#xA;const nowMs = Date.now();&#xA;const outputDirectory = ./out/${nowMs};&#xA;fs.mkdirSync(outputDirectory);&#xA;&#xA;// Create a CSV file in that directory&#xA;const csvWriter = fs.createWriteStream(${outputDirectory}/data.csv, {&#xA;  flags: &#39;a&#39;&#xA;});&#xA;&#xA;// Write the table column labels&#xA;csvWriter.write(&#39;color,resolution,fileSize,base64size \n&#39;);&#xA;&#xA;// Create 700 images for each color in the given list&#xA;[&#34;white&#34;, &#34;red&#34;, &#34;blue&#34;, &#34;green&#34;, &#34;lavender&#34;].forEach(async (color) =  {&#xA;  for (let index = 1; index &lt;= 700; index++) {&#xA;    const image = await createImage(color, index, index, outputDirectory);&#xA;    // Write the data into CSV file&#xA;    csvWriter.write(${color},${index},${image.sizeByteLength},${image.sizeBase64} \n);&#xA;  }&#xA;});&#xA;&#xA;async function createImage(color, width, height, outputDirectory) {&#xA;  const imageBuffer = await sharp({&#xA;    create: {&#xA;      width,&#xA;      height,&#xA;      channels: 3,&#xA;      background: color&#xA;    }&#xA;  }).png().toBuffer();&#xA;  const fileName = ${width}x${height}.png;&#xA;  fs.writeFileSync(${outputDirectory}/${fileName}, imageBuffer);&#xA;&#xA;  return {&#xA;    outputDirectory,&#xA;    fileName,&#xA;    // This is where we unwrap the relevant sizes and return them&#xA;    sizeByteLength: imageBuffer.byteLength,&#xA;    sizeBase64: imageBuffer.toString(&#39;base64url&#39;).length&#xA;  }&#xA;}&#xA;&#xA;Okay, this might seem a bit excessive (and it might be), but it generates 700 images with different resolutions (from 1x1 to 700x700) for five different background colors. It then writes the file size and the Base64 encoded size into a file that can be imported into a spreadsheet.&#xA;&#xA;The Data&#xA;&#xA;Let us analyze the data a bit, starting with the raw results. Just so that we know what we are basically* looking at. &#xA;&#xA;| color    | resolution | fileSize | base64size  | requestSize | Data-url size |&#xA;|----------|------------|----------|-------------|-------------|---------------|&#xA;| blue     | 1          | 90       | 120         | 864         | 143           |&#xA;| blue     | 2          | 93       | 124         | 867         | 147           |&#xA;| ...      | ...        | ...      | ...         | ...         | ...           |&#xA;| blue     | 699        | 8592     | 11456       | 9366        | 11479         |&#xA;| blue     | 700        | 8606     | 11475       | 9380        | 11498         |&#xA;| green    | 1          | 90       | 120         | 864         | 143           |&#xA;| green    | 2          | 93       | 124         | 867         | 147           |&#xA;| ...      | ...        | ...      | ...         | ...         | ...           |&#xA;| lavender | 700        | 9218     | 12291       | 9992        | 12314         |&#xA;| red      | 1          | 90       | 120         | 864         | 143           |&#xA;| ...      | ...        | ...      | ...         | ...         | ...           |&#xA;| red      | 700        | 8606     | 11475       | 9380        | 11498         |&#xA;| white    | 1          | 90       | 120         | 864         | 143           |&#xA;| ...      | ...        | ...      | ...         | ...         | ...           |&#xA;| white    | 700        | 2992     | 3990        | 3766        | 4013          |&#xA;&#xA;We have data points for 700 PNG images for five colors: blue, green, red, lavender, and white. The essential part is within the columns &#34;fileSize&#34; and &#34;base64size&#34;. These are the (uncompressed) file size and the size when we Base64 encode the same file. &#xA;These data points are followed by columns referring to the total sizes.&#xA;Column &#34;requestSize&#34; stands for the size of the payload plus the headers (request and response). In reality, they will differ a bit from request to request. Here, I assumed a static value of 774 bytes, more or less the median in Firefox. That&#39;s good enough for the analysis.&#xA;The same goes for the data URL size. We want to add 23 bytes, as the encoded string is prefixed by data:image/png;base64,.&#xA;&#xA;Findings&#xA;&#xA;Interestingly, more than one data point may cross the line of 100% bloat. It&#39;s the line where the sizes would be exactly equal. That means that there may be cases where it would have been finally better not to use a data-URL, to be the other way around again right afterward. &#xA;&#xA;A diagram that shows how the &#39;bloat&#39; data points evolve. The bloat is the size of the Base64 encrypted image to the basic file size. It&#39;s a line diagram with a data series for each color: white, blue, green, red, and lavender. The 100% line is highlighted. It&#39;s noticeable how they cross this line in a stackering motion once. But white crosses it two times.&#xA;&#xA;Let&#39;s understand the basic chart first. What does this mean? Simply put, for lavender, red, green, and blue, it would be preferable to use data-url up to a resolution of about 340x340. For white, you must start asking questions from a resolution of 570x570. &#xA;Let&#39;s dig deeper into it. You can recognize a few things. First of all, the more complex (in our case, &#39;lavender&#39;) an image is, the more it gets bloated, thus reaching the point of &#34;request preference&#34; faster. That makes sense, as the PNG encoding needs to yield more uncompressable value.&#xA;The next thing to notice is how the three base colors (Red, Green, and Blue) behave more or less identically. This makes sense because their binary data looks more or less the same. &#xA;Of course, the most remarkable is the white data series. Not only is it the least complex image, thus the one that&#39;s longest in the &#34;use data-url sector,&#34; but it&#39;s also crossing the 100% bloat line twice!&#xA;Well, the most critical data points are the ones where we cross the line of 100% bloat, right? Let&#39;s have a look at this then:&#xA;&#xA;| resolution | blue   | green  | lavender | red    | white  |&#xA;|------------|--------|--------|----------|--------|--------|&#xA;| 337        |        |        | 0.9993   |        |        |&#xA;| 339        |        |        | 1.0013   |        |        |&#xA;| 341        |        |        | 0.9997   |        |        |&#xA;| 343        | 0.9997 | 0.9997 |          | 0.9997 |        |&#xA;| 573        |        |        |          |        | 0.9997 |&#xA;| 602        |        |        |          |        | 1.0287 |&#xA;| 608        |        |        |          |        | 0.9936 |&#xA;&#xA;A diagram illustrating where the different color data series cross the 100% bloat line. Lavender tips over at a resolution of 337 and goes above 100% at 339 again, to fall below the threshold again at 341. The point of falling beneath 100% is identical to 343 for blue, green, and red. White falls under 100% at 573, climbs above 100% again at 602, and is eventually lower than 100 at 608.&#xA;&#xA;Woah! Even the lavender-series does the &#34;crossing the line twice move&#34;! Outstanding. &#xA;Okay, what do these data points mean? &#xA;&#xA;For blue, red, and green, using a data URL for resolutions up to 343x343 would be preferable&#xA;Lavender has different sections:&#xA; 337 and 338: A request is preferable&#xA; 339 and 340: A data URL is preferable (again)&#xA; 341 and bigger: A request is preferable&#xA;As already said, white also has different sections that stretch over a greater delta:&#xA; 573 to 601: A request is preferable&#xA; 602 to 607: A data URL is preferable (again)&#xA; 608 and bigger: A request is preferable&#xA;&#xA;  ⚠️ Just a heads up again: this is merely a lab. It is there to showcase how to measure and compare your data. It is not 100% accurate for the field, as we assumed static sizes that may differ from request to request. This is especially relevant for the lavender series, where the conclusion might change from one probe to the next.&#xA;Also, it would help if you did not draw general conclusions from here. Depending on the complexity and file format, the results are entirely different. See how lavender is already far more different from white. That means that the edge cases are not to be generalized.&#xA;&#xA;To the Field!&#xA;&#xA;Now, let&#39;s examine a real example. There is a great website hosted at https://www.ungra.dev/. We load one optimized image there. It&#39;s as good as it gets for a JPG. The image has 21.63 kB, which adds up to 22.57 kB with the header overhead. &#xA;&#xA;A screenshot of the network information of an HTTP request. It transferred a picture. Three values are highlighted: &#34;Transferred 22.12 kB (21.63 kB size)&#34;, &#34;Response Header (492 B)&#34;, &#34;Request Header (452 B)&#34;.&#xA;&#xA;I refrain from posting the Base64 encoded version here. It&#39;s huge. It has a solid of 28,840 characters. Thus, the Data URL is 28,2 kB large. All in all, using a Data URL would transfer 5,751 bytes more.&#xA;&#xA;Now, that image is only 239 × 340 pixels large. You can see that the evaluation depends more on the complexity of the image than its sheer resolution.&#xA;&#xA;File Formats&#xA;&#xA;You may wonder about file formats. I tried to see how far I could go with the image in question from the previous chapter and converted it to AVIF. I compressed it - with reasonable losses - to 3,9 kB! That sounds promising to come to an even more apparent conclusion this time, right? However, the same picture also only has 5,2 kB Base64 encoded. &#xA;That means that, in general terms, the question of whether to use a Data URL is not significantly impacted by the file format. It just scales the numbers. &#xA;&#xA;Excurse: What&#39;s with non-binary data?&#xA;&#xA;Something that is probably interesting here is what happens with non-binary data. Let&#39;s take SVGs, for example. How are they Base64 encoded, and what does that mean for our decision to put them into data URLs or not? You can, of course, Base64 encode them just as any other data. However, it generally doesn&#39;t make sense, as the original file is already available in Unicode. Remember, Base64 is a &#34;binary to text encoding&#34; algorithm! &#xA;If you encode SVGs, you don&#39;t win anything but bloat the file. &#xA;&#xA;  ⚠️ There are some edge cases where this might come in handy! In general terms, however, refrain from doing this.&#xA;&#xA;However, you can still use Data URIs for SVGs and the like! It&#39;s just that you do not add charset=utf-8;base64 to it. It&#39;s merely data:image/svg+xml;utf8,SVG....&#xA;Remember that such a URL does not need to be Base64 encoded. We need this extra step to render binary data. &#xA;&#xA;Draw a Conclusion&#xA;&#xA;Now that you know how to get the relevant numbers, it&#39;s relatively easy to conclude. Again, you must compare the request overhead with the increased file size of the Base64 encoded media.&#xA;Is the request header size, response header size, and payload bigger than the Base64 data URL string? If yes, use the latter. If not, make a request.&#xA;&#xA;Other Considerations&#xA;&#xA;In reality, you have to take a few more considerations into account. However, as they are very individual for particular scenarios or concern other &#34;layers,&#34; I will only briefly mention them here. &#xA;&#xA;Media Processing&#xA;&#xA;First, if your media is not hosted on the edge, the latency increase may not be worth it. Also, the other way around: if your web app runs on really (!) poor hardware, it might not be worth using a data-url.&#xA;&#xA;Compare Zipped Data&#xA;&#xA;Your data should be compressed. If you haven&#39;t done so yet, do it now (there is a big chance you do this already, as it&#39;s pretty much the default for most hosters/servers/...). This also means that we need to compare the compressed data here. (What we didn&#39;t do throughout this post to keep things simple.)&#xA;The same goes for optimizing your resources first. For images, compress them as much as possible and use modern file formats like AVIF or WebP. SVGs should also be minimized with tools like OMGSVG. &#xA;&#xA;HTTP/2 and Server Push&#xA;&#xA;With HTTP/2, a concept called server push makes data URLs nearly obsolete! A server can provide the resources we have encoded inline in one go. Read more about that here: https://web.dev/performance-http2/#server-push.&#xA;&#xA;Unfortunately, server push is mainly abandoned and deprecated nowadays. Even to an extent where I can say ignore it. If you want to read more about the why, head to this article: https://developer.chrome.com/blog/removing-push/. &#xA;&#xA;Conclusion&#xA;&#xA;Data URIs are primarily used with binary data encoded with an algorithm called Base64. It is made to display binary data in a text-based way. This is excellent for Data URIs, for they are text-based! &#xA;However, Base64 makes the data larger. That means you must check whether it&#39;s better to have overhead with the request or to bloat your file.&#xA;&#xA;Further Reading&#xA;&#xA;https://www.smashingmagazine.com/2017/04/guide-http2-server-push/&#xA;https://www.davidbcalhoun.com/2011/when-to-base64-encode-images-and-when-not-to/&#xA;https://developer.mozilla.org/en-US/docs/web/http/basicsofhttp/dataurls&#xA;https://web.dev/performance-http2/&#xA;https://www.rfc-editor.org/rfc/rfc2397#section-2&#xA;https://developer.mozilla.org/en-US/docs/Glossary/Base64&#xA;https://css-tricks.com/probably-dont-base64-svg/&#xA;https://css-tricks.com/lodge/svg/09-svg-data-uris/&#xA;https://developer.chrome.com/blog/removing-push/]]&gt;</description>
      <content:encoded><![CDATA[<p>We often can provide assets with a URI or a so-called “Data URL” on the web. As they are so straightforward to use, they get pretty often copied (from a tutorial, snippet, or whatnot) without us being able to tell what they do, when to prefer them, or whether you should still be using them altogether. It&#39;s one of the lesser-known performance tweaks I will shed some light on in this blog post.</p>



<p>This post was reviewed by <a href="https://github.com/acme">Leon Brocard</a> &amp; <a href="https://activenode.de/">David Lorenz</a>. Thank you! ❤️</p>

<h2 id="what-are-data-urls-anyway" id="what-are-data-urls-anyway">What are Data URLs Anyway</h2>

<p>First things first, what are we talking about here? Let&#39;s make a simple example of a background image:</p>

<pre><code class="language-html">&lt;div&gt;&lt;/div&gt;
</code></pre>

<pre><code class="language-css">div {
  height: 100px;
  width: 100px;
  background-image: url(&#39;https://placehold.co/600x400&#39;);
}
</code></pre>

<p>That snippet is quite simple. We add a div and configure it to hold a background image. You might know that there is a second approach to this, however. This approach makes use of a so-called “data URL”:</p>

<pre><code class="language-css">div {
  height: 100px;
  width: 100px;
  background-image: url(&#39;data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL(...)TEwLjRaIi8+PC9zdmc+&#39;);
}
</code></pre>

<p>Now, that is different. The two snippets output the same result. However, in the latter, we provide the image data directly in the CSS instead of requesting it from a server.
Here, we provide the encoded image binary data directly in a given convention. There is no need to catch it from the server, as we provide the file in CSS.</p>

<blockquote><p>A new URL scheme, “data”, is defined. It allows inclusion of small data items as “immediate” data, as if it had been included externally.</p></blockquote>

<h3 id="base64-encoding" id="base64-encoding">Base64 Encoding</h3>

<p>How does this work? The image data is encoded with an algorithm called “Base64”. It sounds a bit complicated, but it&#39;s, in fact, not. The main goal of such encodings is to display binary data in Unicode. That&#39;s why such are called “binary to text encodings.”
A digit in Base64 can represent a maximum of 6 bits.
Why is that? Let&#39;s make an example. We want to display <code>101101</code> as text. But hold on a second. Did you recognize something? <em>We already did it!</em> Writing out <code>101101</code> made a bit-sequence apparent to you as a reader. <em>We encoded binary to text.</em>
Now, let&#39;s imagine instead of “0” and “1”, we decide for “A” and “B”: <code>BABBAB</code>. Guess what? We have just Base<strong>2</strong> encoded data! Yay.
Do you see the issue here? We must utilize eight <strong>bytes</strong> of data to encode six <strong>bits</strong> of data, as A and B in ASCII take up one byte. We “inflate” or bloat this data by ~1067% (100 % / 6 bit * 64 bit).</p>

<table>
<thead>
<tr>
<th>Binary Sequence</th>
<th>Base2 Encoded</th>
</tr>
</thead>

<tbody>
<tr>
<td>0</td>
<td>A</td>
</tr>

<tr>
<td>1</td>
<td>B</td>
</tr>

<tr>
<td>101101</td>
<td>BABBAB</td>
</tr>
</tbody>
</table>

<p>To tackle this problem, there is a little trick: we add more characters to encode a bigger binary sequence at once. Let&#39;s make another example here: Base4 encoding.</p>

<table>
<thead>
<tr>
<th>Binary Sequence</th>
<th>Base4 Encoded</th>
</tr>
</thead>

<tbody>
<tr>
<td>00</td>
<td>A</td>
</tr>

<tr>
<td>01</td>
<td>B</td>
</tr>

<tr>
<td>10</td>
<td>C</td>
</tr>

<tr>
<td>11</td>
<td>D</td>
</tr>

<tr>
<td>101101</td>
<td>CDB</td>
</tr>
</tbody>
</table>

<p>How would our sequence of <code>101101</code> look with that algorithm? It encodes to <code>CDB</code>. This results in a far smaller bloat of only 400%!
We continue in that manner until we have 64 distinct characters representing a specific sequence each. That&#39;s what Base64 encoding is.</p>

<table>
<thead>
<tr>
<th>Binary Sequence</th>
<th>Base64 Encoded</th>
</tr>
</thead>

<tbody>
<tr>
<td>000000</td>
<td>A</td>
</tr>

<tr>
<td>000001</td>
<td>B</td>
</tr>

<tr>
<td>...</td>
<td>...</td>
</tr>

<tr>
<td>011001</td>
<td>Z</td>
</tr>

<tr>
<td>011010</td>
<td>a</td>
</tr>

<tr>
<td>...</td>
<td>...</td>
</tr>

<tr>
<td>101101</td>
<td>t</td>
</tr>

<tr>
<td>...</td>
<td>...</td>
</tr>

<tr>
<td>111111</td>
<td>/</td>
</tr>
</tbody>
</table>

<p>In the given table, we see that our example sequence of <code>101101</code> is encoded by only using the character <code>t.</code> That means we only add two more bits to display 6 bits of data!  That is merely an inflation of roughly 133%. Awesome!
Well, but why stop there? Why did we expressly agree on “6 bits” and not use, e.g., Base128 encoding? ASCII has enough characters, right?
However, 32 of them are control characters that computers may interpret. That would lead to severe bugs. Why this is exactly is out of the scope of this blog post (I mean, the Base64 algorithm already kinda is).
Just so much: There is a Base91 and Base85 encoding; however, as illustrated in our examples above, “power of two” is more readily encoded when it comes to byte data, and that&#39;s why we mostly settled on that.</p>

<p>To summarize this a little, let&#39;s say we want to encode 10 bytes of data, which is 80 bits. That means we need 14 characters (80/6≈14) to represent that.
The binary sequence <code>000000</code> is represented as ASCII <code>A</code> in Base64. Read this: “The binary data of my image has six consecutive zeros somewhere. The algorithm of Base64 encoding will transform this into &#39;A.&#39;”
In ASCII, <code>A</code> is eight bits long. Represented in binary, it looks like this: 01000001. Thus, we made this sequence two bits larger.</p>

<h2 id="preamble" id="preamble">Preamble</h2>

<p>As always, when it comes to performance, it&#39;s complex. There are a lot of layers to the question of whether to use a data URL or not. It heavily depends on how your external resources are hosted. Are they hosted on the edge? What&#39;s the latency? The same goes for the hardware of the users. How long does it take for them to decode the given data? May this take even longer than requesting the already decoded resource?
You must always collect RUM data and compare approaches to know best. However, generally speaking, we could say that there is a sweet spot where the Base64 encoding overhead exceeds the overhead of an HTTP request, and thus, it&#39;s advisable not to use Data URLs.</p>

<h2 id="the-premise" id="the-premise">The Premise</h2>

<p>Let&#39;s repeat this: if the overhead of a request to an external resource exceeds the size of the Base64 extra bloat, you should use a data-url. We are not talking about the payload here. It&#39;s just what the request in isolation costs us. Do we have a bigger <strong>total size</strong> with Base64 encoding or an HTTP request?
Let&#39;s make an example here. We start with a white pixel. We decode this as PNG in, well, the resolution 1x1. We will add other background colors throughout this blog post. The following table already yields the most important values: uncompressed file size as PNG, the (bloated) size when Base64 is encoded, the total request size (headers and payload), and finally, the total size of the data-URL.</p>

<table>
<thead>
<tr>
<th>Resolution</th>
<th>Background Color</th>
<th>Size</th>
<th>Base64 Encoded Size</th>
<th>Request Size (total)</th>
<th>Data URL Size</th>
</tr>
</thead>

<tbody>
<tr>
<td>1x1</td>
<td>White</td>
<td>90</td>
<td>120</td>
<td>864</td>
<td>143</td>
</tr>
</tbody>
</table>

<p>In this first example, we would need to transfer 721 (864 – 143) more bytes if we did <strong>not</strong> utilize a data URL.</p>

<h2 id="measure-the-request" id="measure-the-request">Measure the Request</h2>

<p>Before we head deeper into data points and their analysis, I want to clarify how we measure the total size of a request. How do we get the value in the “Request Size (total)” column?
The most straightforward way is to head to the network tab in the Web Developer Tools. The first thing you do is create a snapshot of all the page&#39;s network requests. Then, find the one for the particular media in question.</p>

<p><img src="https://i.snap.as/VhL5ZPkI.png" alt="A screenshot of web developer tools. The network tab is active. A particular network request of an image is highlighted and selected. A filter &#34;images&#34; is set. Three numbers emphasize the user journey: number one is on the network tab, number two is on the &#34;images&#34; filter, and number three is on the highlighted network request of an image."/></p>

<pre><code>{
	&#34;Status&#34;: &#34;200OK&#34;,
	&#34;Version&#34;: &#34;HTTP/2&#34;,
	&#34;Transferred&#34;: &#34;22.12 kB (21.63 kB size)&#34;,
	&#34;Referrer Policy&#34;: &#34;strict-origin-when-cross-origin&#34;,
	&#34;Request Priority&#34;: &#34;Low&#34;,
	&#34;DNS Resolution&#34;: &#34;System&#34;
}
</code></pre>

<p>Now, you might think it&#39;s pretty easy, right? Subtract the payload (21.63 kB) from the transferred data (21.63 kB) and done. However, you also have to take the request header into account! That adds a little something.</p>

<p><img src="https://i.snap.as/qN2TccQH.png" alt="A screenshot from a network request as captured by Firefox Developer Tools. It features two important tabs: &#34;Response Headers&#34; and &#34;Request Headers.&#34;"/></p>

<p>Long story short: in the screenshot above, we see both headers, request and response. This data is the “overhead” we need to take into consideration. Everything else is payload – not bloated by Base64 encoding.</p>

<h2 id="to-the-lab" id="to-the-lab">To the Lab!</h2>

<p>Now, equipped with that knowledge and skills, we can create some data points. I did this with a Node.js script. It creates many images and writes their file sizes and Base64 encoded sizes in a CSV file:</p>

<pre><code class="language-JS">import sharp from &#34;sharp&#34;;
import fs from &#34;fs&#34;;

// Create a new direcotry for that run
const nowMs = Date.now();
const outputDirectory = `./out/${nowMs}`;
fs.mkdirSync(outputDirectory);

// Create a CSV file in that directory
const csvWriter = fs.createWriteStream(`${outputDirectory}/_data.csv`, {
  flags: &#39;a&#39;
});

// Write the table column labels
csvWriter.write(&#39;color,resolution,fileSize,base64size \n&#39;);

// Create 700 images for each color in the given list
[&#34;white&#34;, &#34;red&#34;, &#34;blue&#34;, &#34;green&#34;, &#34;lavender&#34;].forEach(async (color) =&gt; {
  for (let index = 1; index &lt;= 700; index++) {
    const image = await createImage(color, index, index, outputDirectory);
    // Write the data into CSV file
    csvWriter.write(`${color},${index},${image.sizeByteLength},${image.sizeBase64} \n`);
  }
});

async function createImage(color, width, height, outputDirectory) {
  const imageBuffer = await sharp({
    create: {
      width,
      height,
      channels: 3,
      background: color
    }
  }).png().toBuffer();
  const fileName = `${width}x${height}.png`;
  fs.writeFileSync(`${outputDirectory}/${fileName}`, imageBuffer);

  return {
    outputDirectory,
    fileName,
    // This is where we unwrap the relevant sizes and return them
    sizeByteLength: imageBuffer.byteLength,
    sizeBase64: imageBuffer.toString(&#39;base64url&#39;).length
  }
}
</code></pre>

<p>Okay, this might seem a bit excessive (and it might be), but it generates 700 images with different resolutions (from 1x1 to 700x700) for five different background colors. It then writes the file size and the Base64 encoded size into a file that can be imported into a spreadsheet.</p>

<h3 id="the-data" id="the-data">The Data</h3>

<p>Let us analyze the data a bit, starting with the raw results. Just so that we know what we are <em>basically</em> looking at.</p>

<table>
<thead>
<tr>
<th>color</th>
<th>resolution</th>
<th>fileSize</th>
<th>base64size</th>
<th>requestSize</th>
<th>Data-url size</th>
</tr>
</thead>

<tbody>
<tr>
<td>blue</td>
<td>1</td>
<td>90</td>
<td>120</td>
<td>864</td>
<td>143</td>
</tr>

<tr>
<td>blue</td>
<td>2</td>
<td>93</td>
<td>124</td>
<td>867</td>
<td>147</td>
</tr>

<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>

<tr>
<td>blue</td>
<td>699</td>
<td>8592</td>
<td>11456</td>
<td>9366</td>
<td>11479</td>
</tr>

<tr>
<td>blue</td>
<td>700</td>
<td>8606</td>
<td>11475</td>
<td>9380</td>
<td>11498</td>
</tr>

<tr>
<td>green</td>
<td>1</td>
<td>90</td>
<td>120</td>
<td>864</td>
<td>143</td>
</tr>

<tr>
<td>green</td>
<td>2</td>
<td>93</td>
<td>124</td>
<td>867</td>
<td>147</td>
</tr>

<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>

<tr>
<td>lavender</td>
<td>700</td>
<td>9218</td>
<td>12291</td>
<td>9992</td>
<td>12314</td>
</tr>

<tr>
<td>red</td>
<td>1</td>
<td>90</td>
<td>120</td>
<td>864</td>
<td>143</td>
</tr>

<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>

<tr>
<td>red</td>
<td>700</td>
<td>8606</td>
<td>11475</td>
<td>9380</td>
<td>11498</td>
</tr>

<tr>
<td>white</td>
<td>1</td>
<td>90</td>
<td>120</td>
<td>864</td>
<td>143</td>
</tr>

<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>

<tr>
<td>white</td>
<td>700</td>
<td>2992</td>
<td>3990</td>
<td>3766</td>
<td>4013</td>
</tr>
</tbody>
</table>

<p>We have data points for 700 PNG images for five colors: blue, green, red, lavender, and white. The essential part is within the columns “fileSize” and “base64size”. These are the (uncompressed) file size and the size when we Base64 encode the same file.
These data points are followed by columns referring to the total sizes.
Column “requestSize” stands for the size of the payload plus the headers (request and response). In reality, they will differ a bit from request to request. Here, I assumed a static value of 774 bytes, more or less the median in Firefox. That&#39;s good enough for the analysis.
The same goes for the data URL size. We want to add 23 bytes, as the encoded string is prefixed by <code>data:image/png;base64,</code>.</p>

<h3 id="findings" id="findings">Findings</h3>

<p>Interestingly, more than one data point may cross the line of 100% bloat. It&#39;s the line where the sizes would be exactly equal. That means that there may be cases where it would have been finally better not to use a data-URL, to be the other way around again right afterward.</p>

<p><img src="https://i.snap.as/BfWIcR2I.png" alt="A diagram that shows how the &#39;bloat&#39; data points evolve. The bloat is the size of the Base64 encrypted image to the basic file size. It&#39;s a line diagram with a data series for each color: white, blue, green, red, and lavender. The 100% line is highlighted. It&#39;s noticeable how they cross this line in a stackering motion once. But white crosses it two times."/></p>

<p>Let&#39;s understand the basic chart first. What does this mean? Simply put, for lavender, red, green, and blue, it would be preferable to use data-url up to a resolution of about 340x340. For white, you must start asking questions from a resolution of 570x570.
Let&#39;s dig deeper into it. You can recognize a few things. First of all, the more complex (in our case, &#39;lavender&#39;) an image is, the more it gets bloated, thus reaching the point of “request preference” faster. That makes sense, as the PNG encoding needs to yield more uncompressable value.
The next thing to notice is how the three base colors (Red, Green, and Blue) behave more or less identically. This makes sense because their binary data looks more or less the same.
Of course, the most remarkable is the white data series. Not only is it the least complex image, thus the one that&#39;s longest in the “use data-url sector,” but it&#39;s also crossing the 100% bloat line twice!
Well, the most critical data points are the ones where we cross the line of 100% bloat, right? Let&#39;s have a look at this then:</p>

<table>
<thead>
<tr>
<th>resolution</th>
<th>blue</th>
<th>green</th>
<th>lavender</th>
<th>red</th>
<th>white</th>
</tr>
</thead>

<tbody>
<tr>
<td>337</td>
<td></td>
<td></td>
<td>0.9993</td>
<td></td>
<td></td>
</tr>

<tr>
<td>339</td>
<td></td>
<td></td>
<td>1.0013</td>
<td></td>
<td></td>
</tr>

<tr>
<td>341</td>
<td></td>
<td></td>
<td>0.9997</td>
<td></td>
<td></td>
</tr>

<tr>
<td>343</td>
<td>0.9997</td>
<td>0.9997</td>
<td></td>
<td>0.9997</td>
<td></td>
</tr>

<tr>
<td>573</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>0.9997</td>
</tr>

<tr>
<td>602</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>1.0287</td>
</tr>

<tr>
<td>608</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>0.9936</td>
</tr>
</tbody>
</table>

<p><img src="https://i.snap.as/9ncE3Jr7.png" alt="A diagram illustrating where the different color data series cross the 100% bloat line. Lavender tips over at a resolution of 337 and goes above 100% at 339 again, to fall below the threshold again at 341. The point of falling beneath 100% is identical to 343 for blue, green, and red. White falls under 100% at 573, climbs above 100% again at 602, and is eventually lower than 100 at 608."/></p>

<p>Woah! Even the lavender-series does the “crossing the line twice move”! Outstanding.
Okay, what do these data points mean?</p>
<ul><li>For blue, red, and green, using a data URL for resolutions up to 343x343 would be preferable</li>
<li>Lavender has different sections:
<ul><li>337 and 338: A request is preferable</li>
<li>339 and 340: A data URL is preferable (again)</li>
<li>341 and bigger: A request is preferable</li></ul></li>
<li>As already said, white also has different sections that stretch over a greater delta:
<ul><li>573 to 601: A request is preferable</li>
<li>602 to 607: A data URL is preferable (again)</li>
<li>608 and bigger: A request is preferable</li></ul></li></ul>

<blockquote><p>⚠️ Just a heads up again: this is merely a lab. It is there to showcase how to measure and compare your data. It is not 100% accurate for the field, as we assumed static sizes that may differ from request to request. This is especially relevant for the lavender series, where the conclusion might change from one probe to the next.
Also, it would help if you did not draw general conclusions from here. Depending on the complexity and file format, the results are entirely different. See how lavender is already far more different from white. That means that the edge cases are not to be generalized.</p></blockquote>

<h2 id="to-the-field" id="to-the-field">To the Field!</h2>

<p>Now, let&#39;s examine a real example. There is a great website hosted at <a href="https://www.ungra.dev/">https://www.ungra.dev/</a>. We load one optimized image there. It&#39;s as good as it gets for a JPG. The image has 21.63 kB, which adds up to 22.57 kB with the header overhead.</p>

<p><img src="https://i.snap.as/C1mawWek.png" alt="A screenshot of the network information of an HTTP request. It transferred a picture. Three values are highlighted: &#34;Transferred 22.12 kB (21.63 kB size)&#34;, &#34;Response Header (492 B)&#34;, &#34;Request Header (452 B)&#34;."/></p>

<p>I refrain from posting the Base64 encoded version here. It&#39;s huge. It has a solid of 28,840 characters. Thus, the Data URL is 28,2 kB large. All in all, using <strong>a Data URL would transfer 5,751 bytes more.</strong></p>

<p>Now, that image is only 239 × 340 pixels large. You can see that the evaluation depends more on the complexity of the image than its sheer resolution.</p>

<h2 id="file-formats" id="file-formats">File Formats</h2>

<p>You may wonder about file formats. I tried to see how far I could go with the image in question from the previous chapter and converted it to AVIF. I compressed it – with reasonable losses - to 3,9 kB! That sounds promising to come to an even more apparent conclusion this time, right? However, the same picture also only has 5,2 kB Base64 encoded.
That means that, in general terms, the question of whether to use a Data URL is not significantly impacted by the file format. It just scales the numbers.</p>

<h2 id="excurse-what-s-with-non-binary-data" id="excurse-what-s-with-non-binary-data">Excurse: What&#39;s with non-binary data?</h2>

<p>Something that is probably interesting here is what happens with non-binary data. Let&#39;s take SVGs, for example. How are they Base64 encoded, and what does that mean for our decision to put them into data URLs or not? You can, of course, Base64 encode them just as any other data. However, it generally doesn&#39;t make sense, as the original file is already available in Unicode. Remember, Base64 is a “binary to text encoding” algorithm!
If you encode SVGs, you don&#39;t win anything but bloat the file.</p>

<blockquote><p>⚠️ There are some edge cases where this might come in handy! In general terms, however, refrain from doing this.</p></blockquote>

<p>However, you can still use Data URIs for SVGs and the like! It&#39;s just that you do not add <code>charset=utf-8;base64</code> to it. It&#39;s merely <code>data:image/svg+xml;utf8,&lt;SVG&gt;...</code>.
Remember that such a URL does not need to be Base64 encoded. We need this extra step to render binary data.</p>

<h2 id="draw-a-conclusion" id="draw-a-conclusion">Draw a Conclusion</h2>

<p>Now that you know how to get the relevant numbers, it&#39;s relatively easy to conclude. Again, you must compare the request overhead with the increased file size of the Base64 encoded media.
Is the request header size, response header size, and payload bigger than the Base64 data URL string? If yes, use the latter. If not, make a request.</p>

<h2 id="other-considerations" id="other-considerations">Other Considerations</h2>

<p>In reality, you have to take a few more considerations into account. However, as they are very individual for particular scenarios or concern other “layers,” I will only briefly mention them here.</p>

<h3 id="media-processing" id="media-processing">Media Processing</h3>

<p>First, if your media is not hosted on the edge, the latency increase may not be worth it. Also, the other way around: if your web app runs on really (!) poor hardware, it might not be worth using a data-url.</p>

<h3 id="compare-zipped-data" id="compare-zipped-data">Compare Zipped Data</h3>

<p>Your data should be compressed. If you haven&#39;t done so yet, do it now (there is a big chance you do this already, as it&#39;s pretty much the default for most hosters/servers/...). This also means that we need to compare the compressed data here. (What we didn&#39;t do throughout this post to keep things simple.)
The same goes for optimizing your resources first. For images, compress them as much as possible and use modern file formats like AVIF or WebP. SVGs should also be minimized with tools like OMGSVG.</p>

<h3 id="http-2-and-server-push" id="http-2-and-server-push">HTTP/2 and Server Push</h3>

<p>With HTTP/2, a concept called server push makes data URLs nearly obsolete! A server can provide the resources we have encoded inline in one go. Read more about that here: <a href="https://web.dev/performance-http2/#server-push">https://web.dev/performance-http2/#server-push</a>.</p>

<p>Unfortunately, <strong>server push is mainly abandoned and deprecated</strong> nowadays. Even to an extent where I can say ignore it. If you want to read more about the why, head to this article: <a href="https://developer.chrome.com/blog/removing-push/">https://developer.chrome.com/blog/removing-push/</a>.</p>

<h2 id="conclusion" id="conclusion">Conclusion</h2>

<p>Data URIs are primarily used with binary data encoded with an algorithm called Base64. It is made to display binary data in a text-based way. This is excellent for Data URIs, for they are text-based!
However, Base64 makes the data larger. That means you must check whether it&#39;s better to have overhead with the request or to bloat your file.</p>

<h2 id="further-reading" id="further-reading">Further Reading</h2>
<ul><li><a href="https://www.smashingmagazine.com/2017/04/guide-http2-server-push/">https://www.smashingmagazine.com/2017/04/guide-http2-server-push/</a></li>
<li><a href="https://www.davidbcalhoun.com/2011/when-to-base64-encode-images-and-when-not-to/">https://www.davidbcalhoun.com/2011/when-to-base64-encode-images-and-when-not-to/</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/web/http/basics_of_http/data_urls">https://developer.mozilla.org/en-US/docs/web/http/basics_of_http/data_urls</a></li>
<li><a href="https://web.dev/performance-http2/">https://web.dev/performance-http2/</a></li>
<li><a href="https://www.rfc-editor.org/rfc/rfc2397#section-2">https://www.rfc-editor.org/rfc/rfc2397#section-2</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Glossary/Base64">https://developer.mozilla.org/en-US/docs/Glossary/Base64</a></li>
<li><a href="https://css-tricks.com/probably-dont-base64-svg/">https://css-tricks.com/probably-dont-base64-svg/</a></li>
<li><a href="https://css-tricks.com/lodge/svg/09-svg-data-uris/">https://css-tricks.com/lodge/svg/09-svg-data-uris/</a></li>
<li><a href="https://developer.chrome.com/blog/removing-push/">https://developer.chrome.com/blog/removing-push/</a></li></ul>
]]></content:encoded>
      <guid>https://blog.ungra.dev/how-to-know-whether-to-use-data-urls</guid>
      <pubDate>Sat, 24 Feb 2024 14:49:43 +0000</pubDate>
    </item>
    <item>
      <title>Confluence - a List of Recurring Posts</title>
      <link>https://blog.ungra.dev/confluence-a-list-of-recurring-posts?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[One of my most controversial opinions is that I have a soft spot for Confluence. But wait, let me explain it a bit further. I understand all the pain people have with it. I get it. I&#39;m usually in the same boat. However, I do not think it&#39;s Atlassian&#39;s fault for providing a better UX. For many things, it is just administrated and used poorly! &#xA;Or, to put it in simpler terms, Confluence needs to be utilized correctly.&#xA;!--more--&#xA;I do not want to discuss this further in this post, but I want to provide an excellent example of what you can do with Confluence. This already implicitly covers my main (opinionated) arguments for my stance on the discussion.&#xA;&#xA;The Use-Case&#xA;&#xA;One of the most common things people want to do in Confluence is to have a list of recurring posts. Picture meeting notes, workshops, etc. Usually, they have these requirements:&#xA;&#xA; a structured template for a post&#xA; an overview of the posts&#xA; a complete/detailed post&#xA; having that data searchable&#xA; good UX to add another item&#xA;&#xA;What we want is a list of notes that share the same structure. For example, all of them should serve an agenda, a date, a list of participants, and so on. They should be organized in a structured overview to be easily scanned and searched.&#xA;&#xA;The Problems&#xA;&#xA;I discovered that many teams (or even organizations) struggle to have a good concept for this everyday use case. The problems are often the same:&#xA;&#xA;Bad UX of Adding a New Entry&#xA;&#xA;That&#39;s probably the most annoying issue. The concept of adding a new entry is quite often to copy a portion of the page. This comes with various problems: It&#39;s unclear what to copy, formatting issues are cloned, copying on a slow page (and such page is getting bloated, thus slow over time) is cumbersome, specific formats are cluttered and need to be manually fixed, etc.&#xA;Wouldn&#39;t having a &#34;Add Entry&#34; button be nice instead?&#xA;&#xA;No Overview&#xA;&#xA;It usually gets confusing when all the entries are pushed into one page. The information is all over the place, you can&#39;t keep track, it&#39;s hard to find and filter entries, the page gets slow, and the structure is not visible from the page tree. It&#39;s also hard to distinguish between general and detailed information. Latter is not necessarily needed prominently in the list, yet you have no choice.&#xA;&#xA;No Clear Blueprint&#xA;&#xA;Having a blueprint with mandatory information is relatively easy to maintain initially. People commonly add a table with respectively labeled columns. However, as the lists progress, columns might get removed, renamed, added, formatted differently, interpreted, copied with flaws, and so on. The needed information more and more get ambiguous. And the lower the quality of the entries, the more people feel demotivated to maintain the same in the upcoming items.&#xA;Do you also have a list with missing and low-quality data in the rows? Didn&#39;t that list start out pretty ambitious and well-thought-out, only to be neglected after a while?&#xA;&#xA;Making it Work&#xA;&#xA;Now comes the fun part. We solve these issues in an exquisite and convenient way. We use custom templates, the table of contents feature, the excerpt macro, task lists, and a button to add a new child page. &#xA;&#xA;Adding a Template&#xA;&#xA;The first thing we need is a template. That&#39;s basically a prefilled page representing our recurring document. Let&#39;s stick with meeting notes for our example.&#xA;&#xA;  ℹ️ Info                                                                               &#xA;Explanation on how to exactly add a template is beyond the scope of this post and varies heavily between the different Confluence versions. However, you can always head to the official Atlassian documentation and read about it there.&#xA;&#xA;First of all, you need to know these things:&#xA;&#xA; What information is needed on the blog post as a whole?&#xA; What information is known upfront or mandatory?&#xA; What information is needed in an overview? &#xA; The basic outline and layout of your post.&#xA;&#xA;Based on this, you can create your post template. &#xA;What we do now is create a blank template. First, we are going to add variables to the page. To do so, open the add macro dialog (macros are called slash commands in the newest versions) and choose &#34;New Variable.&#34; Do this for every mandatory information that can not be automatically (computational) evaluated, and the author knows upfront. Let&#39;s say, for example, the time and date the meeting will take place.&#xA;Don&#39;t overthink it at this point. Our template can be further optimized and adapted as time moves on. &#xA;All we have to do now is further build (or set up) the post template. Add the needed sections, fields, placeholders, headlines, and whatnot. Follow along with the official documentation in case you need help—attention to the &#34;Instructional Text&#34; and &#34;Template Variables&#34;  chapters.&#xA;There is one last step you need to do now: add an excerpt! To do so, choose the excerpt macro (or slash command) and add it. Now place everything into the box needed for a basic post overview. Commonly you&#39;d refer to this as &#34;header data.&#34; For meeting notes, it can be, for example, the date of the meeting, participants, agenda, etc. Only add a little. Be very cautious not to overload the excerpt. Further information and a detailed guide are here: https://confluence.atlassian.com/doc/excerpt-macro-148062.html.&#xA;The idea is that we, later on, show this exact excerpt in the overview. This can be easily done with a specific macro.&#xA;&#xA;A screenshot showing an example template. It showcases the usage of the excerpt, variable, and placeholder macros.&#xA;&#xA;  ⚠️ First Confluence Pitfall: Do not restrict users from adding templates! They are a crucial part of the app and are meant to be dynamically used.&#xA;&#xA;Adding a Parent Page&#xA;&#xA;Now we want to have a parent page. Think of this more as a &#34;folder&#34; than really a page. The difference to a directory as we know it from operating systems is that it&#39;s quite feature-rich. We can provide any content we want, including an overview of child pages!&#xA;That&#39;s precisely what we do now. First, add some meta information to your page. Let&#39;s say a title: &#34;Meeting Notes of Foobar!&#34;. You can do whatever you want and need. Add a header image, disclaimer, intro, or whatever.&#xA;Now comes the critical part: the child-page macro. Add it where you want to have the overview of past meetings and configure it like so:&#xA;&#xA; Excerpt Display: Rich Text&#xA; Heading Style: Something that suits your document, but the second level should do the trick, usually&#xA; Sort Children by: Creation&#xA; Optional - Number of Children: Normally, you want to restrict the child pages displayed&#xA;    For meeting notes, it&#39;s often a good idea to set this to three. It would show the currently open notes, the preceding meeting, and one additional for reference. &#xA;&#xA;More usage information here: https://confluence.atlassian.com/doc/children-display-macro-139501.html &#xA;&#xA;Mention the &#34;Exceprt Display&#34; option. This is where you tell the macro to show the page excerpts. Remember how we added it to the template? The option &#34;Rich Text&#34; shows the content precisely like on the page (otherwise, it would be transformed into a text-only version). &#xA;&#xA;  ⚠️ Second Confluence Pitfall: Pages are directories, not documents! Please do not use them as you would with a word processor (or whatever is used to create a content-complete PDF eventually). Generally, it would help if you strived to have many pages with little content. &#xA;The same goes for spaces. There is no one fits all solution, but often spaces are defined far too broadly. &#xA;&#xA;Okay, now you should have a list of child pages that, more or less, looks like you&#39;d have added their excerpt content to the just added page. You can try this out by adding a few example pages (they don&#39;t necessarily have to be created from the template from the first chapter).&#xA;Now we need one additional thing. Add the macro &#34;Create from Template.&#34; We use it to have a button to create a child page, so a meeting note. Configure it like so:&#xA;&#xA; Button Text: Set this to your needs, e.g., &#34;Add Meeting Note&#34;&#xA; Template Name: The Template you have created in the first chapter&#xA; Title of the Page to be Created: Set this to your needs (be aware that you can use variables here), e.g., &#34;Meeting - @spaceName on @currentDate&#34;&#xA; Space Key: the key of the current space (usually defaults to that already)&#xA;&#xA;More usage information here: https://support.atlassian.com/confluence-cloud/docs/insert-the-create-from-template-macro/&#xA;&#xA;A screenshot showing an example overview page. It showcases the usage of the child display and the create from template button.&#xA;&#xA;  ⚠️ Third Confluence Pitfall: Don&#39;t consider Confluence to be a document silo. It&#39;s not Google Drive. Try to utilize dynamic tools and macros as much as possible. Try to use it as a no-code front-end platform rather than a document management system.&#xA;&#xA;Bonus - Task List&#xA;&#xA;If you plan to keep track of the pages&#39; tasks, you may also consider adding the task report macro. It lists all the tasks on a particular page and its child pages. In our case, that would be our newly created parent page. &#xA;&#xA;Conclusion&#xA;&#xA;With this approach, you have an easy way for users to add a new post with a given format. You have a very easy-to-scan archive of posts and keep the overview performant. It mitigates the problems we addressed in this post. You may want to read through them again now.&#xA;Please also take this opportunity to rethink how you utilize the tools you have within Confluence! Get creative. 😊 The features are meant to be used!]]&gt;</description>
      <content:encoded><![CDATA[<p>One of my most controversial opinions is that I have a soft spot for Confluence. But wait, let me explain it a bit further. I understand all the pain people have with it. I get it. I&#39;m usually in the same boat. <em>However</em>, I do not think it&#39;s Atlassian&#39;s fault for providing a better UX. For many things, it is just administrated and used poorly!
Or, to put it in simpler terms, Confluence needs to be utilized correctly.

I do not want to discuss this further in this post, but I want to provide an excellent example of what you can do with Confluence. This already implicitly covers my main (opinionated) arguments for my stance on the discussion.</p>

<h2 id="the-use-case" id="the-use-case">The Use-Case</h2>

<p>One of the most common things people want to do in Confluence is to have a list of recurring posts. Picture meeting notes, workshops, etc. Usually, they have these requirements:</p>
<ul><li>a structured template for a post</li>
<li>an overview of the posts</li>
<li>a complete/detailed post</li>
<li>having that data searchable</li>
<li>good UX to add another item</li></ul>

<p>What we want is a list of notes that share the same structure. For example, all of them should serve an agenda, a date, a list of participants, and so on. They should be organized in a structured overview to be easily scanned and searched.</p>

<h2 id="the-problems" id="the-problems">The Problems</h2>

<p>I discovered that many teams (or even organizations) struggle to have a good concept for this everyday use case. The problems are often the same:</p>

<h3 id="bad-ux-of-adding-a-new-entry" id="bad-ux-of-adding-a-new-entry">Bad UX of Adding a New Entry</h3>

<p>That&#39;s probably the most annoying issue. The concept of adding a new entry is quite often to copy a portion of the page. This comes with various problems: It&#39;s unclear what to copy, formatting issues are cloned, copying on a slow page (and such page <em>is</em> getting bloated, thus slow over time) is cumbersome, specific formats are cluttered and need to be manually fixed, etc.
Wouldn&#39;t having a “Add Entry” button be nice instead?</p>

<h3 id="no-overview" id="no-overview">No Overview</h3>

<p>It usually gets confusing when all the entries are pushed into one page. The information is all over the place, you can&#39;t keep track, it&#39;s hard to find and filter entries, the page gets slow, and the structure is not visible from the page tree. It&#39;s also hard to distinguish between general and detailed information. Latter is not necessarily needed prominently in the list, yet you have no choice.</p>

<h3 id="no-clear-blueprint" id="no-clear-blueprint">No Clear Blueprint</h3>

<p>Having a blueprint with mandatory information is relatively easy to maintain initially. People commonly add a table with respectively labeled columns. However, as the lists progress, columns might get removed, renamed, added, formatted differently, interpreted, copied with flaws, and so on. The needed information more and more get ambiguous. And the lower the quality of the entries, the more people feel demotivated to maintain the same in the upcoming items.
Do you also have a list with missing and low-quality data in the rows? Didn&#39;t that list start out pretty ambitious and well-thought-out, only to be neglected after a while?</p>

<h2 id="making-it-work" id="making-it-work">Making it Work</h2>

<p>Now comes the fun part. We solve these issues in an exquisite and convenient way. We use custom templates, the table of contents feature, the excerpt macro, task lists, and a button to add a new child page.</p>

<h3 id="adding-a-template" id="adding-a-template">Adding a Template</h3>

<p>The first thing we need is a template. That&#39;s basically a prefilled page representing our recurring document. Let&#39;s stick with meeting notes for our example.</p>

<blockquote><p>ℹ️ Info<br/>
Explanation on how to exactly add a template is beyond the scope of this post and varies heavily between the different Confluence versions. However, you can always head to the <a href="https://confluence.atlassian.com/doc/create-a-template-296093779.html">official Atlassian documentation</a> and read about it there.</p></blockquote>

<p>First of all, you need to know these things:</p>
<ul><li>What information is needed on the blog post as a whole?</li>
<li>What information is known upfront or mandatory?</li>
<li>What information is needed in an overview?</li>
<li>The basic outline and layout of your post.</li></ul>

<p>Based on this, you can create your post template.
What we do now is create a blank template. First, we are going to add variables to the page. To do so, open the add macro dialog (macros are called slash commands in the newest versions) and choose “New Variable.” Do this for every mandatory information that can not be automatically (computational) evaluated, and the author knows upfront. Let&#39;s say, for example, the time and date the meeting will take place.
Don&#39;t overthink it at this point. Our template can be further optimized and adapted as time moves on.
All we have to do now is further build (or set up) the post template. Add the needed sections, fields, placeholders, headlines, and whatnot. Follow along with the <a href="https://confluence.atlassian.com/doc/create-a-template-296093779.html">official documentation</a> in case you need help—attention to the “Instructional Text” and “Template Variables”  chapters.
There is one last step you need to do now: add an excerpt! To do so, choose the excerpt macro (or slash command) and add it. Now place everything into the box needed for a basic post overview. Commonly you&#39;d refer to this as “header data.” For meeting notes, it can be, for example, the date of the meeting, participants, agenda, etc. Only add a little. Be very cautious not to overload the excerpt. Further information and a detailed guide are here: <a href="https://confluence.atlassian.com/doc/excerpt-macro-148062.html">https://confluence.atlassian.com/doc/excerpt-macro-148062.html</a>.
The idea is that we, later on, show this exact excerpt in the overview. This can be easily done with a specific macro.</p>

<p><img src="https://i.snap.as/xDcQpZoC.png" alt="A screenshot showing an example template. It showcases the usage of the excerpt, variable, and placeholder macros."/></p>

<blockquote><p><strong>⚠️ First Confluence Pitfall:</strong> Do not restrict users from adding templates! They are a crucial part of the app and are meant to be dynamically used.</p></blockquote>

<h3 id="adding-a-parent-page" id="adding-a-parent-page">Adding a Parent Page</h3>

<p>Now we want to have a parent page. Think of this more as a “folder” than really a page. The difference to a directory as we know it from operating systems is that it&#39;s quite feature-rich. We can provide any content we want, including an overview of child pages!
That&#39;s precisely what we do now. First, add some meta information to your page. Let&#39;s say a title: “Meeting Notes of Foobar!”. You can do whatever you want and need. Add a header image, disclaimer, intro, or whatever.
Now comes the critical part: the child-page macro. Add it where you want to have the overview of past meetings and configure it like so:</p>
<ul><li>Excerpt Display: Rich Text</li>
<li>Heading Style: Something that suits your document, but the second level should do the trick, usually</li>
<li>Sort Children by: Creation</li>
<li>Optional – Number of Children: Normally, you want to restrict the child pages displayed
For meeting notes, it&#39;s often a good idea to set this to three. It would show the currently open notes, the preceding meeting, and one additional for reference.</li></ul>

<p>More usage information here: <a href="https://confluence.atlassian.com/doc/children-display-macro-139501.html">https://confluence.atlassian.com/doc/children-display-macro-139501.html</a></p>

<p>Mention the “Exceprt Display” option. This is where you tell the macro to show the page excerpts. Remember how we added it to the template? The option “Rich Text” shows the content precisely like on the page (otherwise, it would be transformed into a text-only version).</p>

<blockquote><p><strong>⚠️ Second Confluence Pitfall:</strong> Pages are directories, not documents! Please do not use them as you would with a word processor (or whatever is used to create a content-complete PDF eventually). Generally, it would help if you strived to have many pages with little content.
The same goes for spaces. There is no one fits all solution, but often spaces are defined far too broadly.</p></blockquote>

<p>Okay, now you should have a list of child pages that, more or less, looks like you&#39;d have added their excerpt content to the just added page. You can try this out by adding a few example pages (they don&#39;t necessarily have to be created from the template from the first chapter).
Now we need one additional thing. Add the macro “Create from Template.” We use it to have a button to create a child page, so a meeting note. Configure it like so:</p>
<ul><li>Button Text: Set this to your needs, e.g., “Add Meeting Note”</li>
<li>Template Name: The Template you have created in the first chapter</li>
<li>Title of the Page to be Created: Set this to your needs (be aware that you can use variables here), e.g., “Meeting – @spaceName on @currentDate”</li>
<li>Space Key: the key of the current space (usually defaults to that already)</li></ul>

<p>More usage information here: <a href="https://support.atlassian.com/confluence-cloud/docs/insert-the-create-from-template-macro/">https://support.atlassian.com/confluence-cloud/docs/insert-the-create-from-template-macro/</a></p>

<p><img src="https://i.snap.as/eZVIEgRX.png" alt="A screenshot showing an example overview page. It showcases the usage of the child display and the create from template button."/></p>

<blockquote><p><strong>⚠️ Third Confluence Pitfall:</strong> Don&#39;t consider Confluence to be a document silo. It&#39;s not Google Drive. Try to utilize dynamic tools and macros as much as possible. Try to use it as a no-code front-end platform rather than a document management system.</p></blockquote>

<h4 id="bonus-task-list" id="bonus-task-list">Bonus – Task List</h4>

<p>If you plan to keep track of the pages&#39; tasks, you may also consider adding the <a href="https://support.atlassian.com/confluence-cloud/docs/insert-the-task-report-macro/">task report macro</a>. It lists all the tasks on a particular page and its child pages. In our case, that would be our newly created parent page.</p>

<h2 id="conclusion" id="conclusion">Conclusion</h2>

<p>With this approach, you have an easy way for users to add a new post with a given format. You have a very easy-to-scan archive of posts and keep the overview performant. It mitigates the problems we addressed in this post. You may want to read through them again now.
Please also take this opportunity to rethink how you utilize the tools you have within Confluence! Get creative. 😊 The features are meant to be used!</p>
]]></content:encoded>
      <guid>https://blog.ungra.dev/confluence-a-list-of-recurring-posts</guid>
      <pubDate>Wed, 24 May 2023 17:38:27 +0000</pubDate>
    </item>
    <item>
      <title>Why I don&#39;t Use Odd.af Anymore</title>
      <link>https://blog.ungra.dev/why-i-dont-use-odd-af-anymore?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[The last few days, I have been pretty busy migrating my domain from &#34;odd.af&#34; to &#34;ungra.dev&#34;. I posted a bit on Social Media about the story, but I wanted to wrap it up in detail in this blog post. &#xA;!--more--&#xA;&#xA;What Happened&#xA;&#xA;  ℹ️ The top-level domain &#34;AF&#34; is Afghanistan&#39;s country code top-level domain. A country that the Taliban took over in 2021).&#xA;&#xA;A few weeks ago, my domain provider, Vercel, wrote me that my domain will not be able to be renewed. It&#39;s due for auto-renewal on the 22nd of April. They stated they have issues with the authorities in Afghanistan. &#xA;&#xA;  Due to factors that are out of Vercel’s control, we are no longer able to accept or renew .AF registrations, similarly to most registries today. CcTLDs (country-code Top Level Domains), like .AF, are managed by bureaus from the country, sovereign states, or dependent territory. An attempt to register a ccTLD will open a series of requests to the managing entity. We have made this decision due to the uncertainty Afghanistan continues to have with an evolving political situation. The .AF registry has been unresponsive and payments to them have not been possible, disrupting process renewals and new registrations. &#xA;&#xA;So, long story short, the Tailban did not create a bureau being able to handle domain registry. I&#39;m, however, still not sure whether this means every single domain provider is in the same boat and all &#34;.AF&#34; domains will expire sooner or later. On one hand, it sounds like this. On the other hand, this would be huge. Like, imagine not holding a silly domain for private use but for a big company with its HQ in Afgahnistan. &#xA;&#xA;What I had to do&#xA;&#xA;Vercel recommended I get a new domain ready. &#xA;&#xA;  We recommend you prepare another domain and are here to help you with that process. &#xA;&#xA;To be honest, at first, I thought about talking to other providers in order to keep &#34;odd.af&#34;. I love that domain and didn&#39;t want to go through the pain of migration. &#xA;The longer I slept about it, the clearer it was that I had to move away. I had several good reasons. &#xA;&#xA;Reason One: The Expiry&#xA;&#xA;Well, that one is easy. Since my domain cannot be renewed, I have to do something. A possible solution to the problem is to ditch it and use another.&#xA;&#xA;Reason Two: Language Barrier&#xA;&#xA;A problem I always had with &#34;odd.af&#34; was that it was hardly understood in Germany. Not only was it a different language, but you also needed to know basic &#34;internet slang&#34;. Especially for older folks, it was quite confusing. &#xA;Admittedly it was rather short and very easy to spell out. &#xA;Also, not that I really cared, but many people are not keen on profanity in - at least somewhat - professional domains.&#xA;&#xA;  ℹ️ &#34;af&#34; is an internet acronym for &#34;as fuck&#34;. The domain meant &#34;odd as fuck&#34;. &#xA;&#xA;Reason Three: Uncertainty of CcTLDs&#xA;&#xA;The first reason I didn&#39;t like to use a ccTLD anymore was simply that it was semantically incorrect. I have nothing to do with Afgahnistan, especially because AF is not a ccTLD with a &#34;Commercial License&#34;. &#xA;Such domains (e.g., &#34;me&#34;, &#34;am&#34;, &#34;fm&#34;, &#34;sh&#34;) adopt policies to be not reliant on the infrastructure of a country. For example, &#34;.co&#34; is not managed by the Colombian Government but rather a Colombian and US company.&#xA;The second reason is, well, that it&#39;s a ccTLD, thus dependent on the particular country. 😊 What happened to my domain is seldom enough but can happen anytime. I don&#39;t want to have this unnecessary risk anymore.&#xA;&#xA;What I did&#xA;&#xA;At first, I gathered a list of potential domain names. &#xA;A screenshot of a note-taking app. The headline is Domains and it is followed by a list of potential domain names that are odd.dev, odd.am, oddest.me, odddev.codes, iodd.dev, iodd.de, odd.zone, ungera.de, odd.law, oddi.am, ungra.de&#xA;The problem was to find one that had not a ccTLD (see previous chapter), was affordable, and was not too boring. I pretty much liked the idea of translating &#34;odd&#34; to German. Among others, it means &#34;ungerade&#34; or &#34;ungrade&#34;.  Then &#34;ungra.de&#34; came to my mind. However, first of all, that domain was already taken, and second, it still relies on a ccTLD. (I mean, &#34;DE&#34; would&#39;ve been not too bad since I&#39;m a resident of Germany, but well.)&#xA;Finally, I went with &#34;ungra.dev&#34; it&#39;s a portmanteau of &#34;ungrade&#34; (German for &#34;odd&#34;) and &#34;dev. I pretty much like it, and it&#39;s only 15 $ per year. &#xA;&#xA;Now came the hard part: migrating. It was a bit easier than I initially thought to be honest, but still pretty much painful. At first, I went through all accounts using an &#34;odd.af&#34; mail address. I just filtered for &#34;odd.af&#34; in my password manager. Surprisingly, they were very few since I use masked e-mails for most of my credentials. &#xA;&#xA;After I changed all the addresses in these accounts (not to my new &#34;ungra.dev&#34;, but rather &#34;masked e-mails&#34;), I went on and checked my mail account. I informed everyone I had contact with that my mail address changed. &#xA;&#xA;Then I updated all the links on my social media profiles and my website. Nothing should point to &#34;odd.af&#34; anymore. &#xA;&#xA;The last step was to redirect the old domain to the new endpoints technically. In most cases, this was just a case of adding &#34;ungra.dev&#34; to the project and installing a 301 (&#34;moved permanently&#34;) redirect to the &#34;odd.af&#34; domain. One redirect was a bit more complicated. It was blog.odd.af. I changed the custom domain configuration in write.as, but I can only provide one, meaning I needed to create a redirect at Vercel. &#xA;This is also surprisingly inelegant, as you have to create a repository with a certain configuration file and then assign the &#34;to be redirected&#34; domain to it.&#xA;&#xA;{&#xA;    &#34;redirects&#34;: [&#xA;        { &#34;source&#34;: &#34;/&#34;, &#34;destination&#34;: &#34;https://blog.ungra.dev/&#34; }&#xA;    ]&#xA;}&#xA;&#xA;Or in other words, I needed to create a nearly empty project and assign &#34;blog.odd.af&#34; to it. I found this blog post explaining this approach. &#xA;&#xA;Done&#xA;&#xA;I&#39;m so glad that I&#39;m through with this truly unnecessary workload. It&#39;s super hard to find the time and energy to do this. It&#39;s quite a relief that I think I have it off the plate. Keep your fingers crossed that I didn&#39;t forget about anything. 🤞]]&gt;</description>
      <content:encoded><![CDATA[<p>The last few days, I have been pretty busy migrating my domain from “odd.af” to “ungra.dev”. I posted a bit on Social Media about the story, but I wanted to wrap it up in detail in this blog post.
</p>

<h2 id="what-happened" id="what-happened">What Happened</h2>

<blockquote><p>ℹ️ The top-level domain “AF” is Afghanistan&#39;s country code top-level domain. A country that the <a href="https://en.wikipedia.org/wiki/Afghanistan?oldformat=true#Second_Taliban_era_(2021%E2%80%93present)">Taliban took over in 2021</a>.</p></blockquote>

<p>A few weeks ago, my domain provider, Vercel, wrote me that my domain will not be able to be renewed. It&#39;s due for auto-renewal on the 22nd of April. They stated they have issues with the authorities in Afghanistan.</p>

<blockquote><p>Due to factors that are out of Vercel’s control, we are no longer able to accept or renew .AF registrations, similarly to most registries today. CcTLDs (country-code Top Level Domains), like .AF, are managed by bureaus from the country, sovereign states, or dependent territory. An attempt to register a ccTLD will open a series of requests to the managing entity. We have made this decision due to the uncertainty Afghanistan continues to have with an evolving political situation. The .AF registry has been unresponsive and payments to them have not been possible, disrupting process renewals and new registrations.</p></blockquote>

<p>So, long story short, the Tailban did not create a bureau being able to handle domain registry. I&#39;m, however, still not sure whether this means every single domain provider is in the same boat and all “.AF” domains will expire sooner or later. On one hand, it sounds like this. On the other hand, this would be huge. Like, imagine not holding a silly domain for private use but for a big company with its HQ in Afgahnistan.</p>

<h2 id="what-i-had-to-do" id="what-i-had-to-do">What I had to do</h2>

<p>Vercel recommended I get a new domain ready.</p>

<blockquote><p>We recommend you prepare another domain and are here to help you with that process.</p></blockquote>

<p>To be honest, at first, I thought about talking to other providers in order to keep “odd.af”. I love that domain and didn&#39;t want to go through the pain of migration.
The longer I slept about it, the clearer it was that I had to move away. I had several good reasons.</p>

<h3 id="reason-one-the-expiry" id="reason-one-the-expiry">Reason One: The Expiry</h3>

<p>Well, that one is easy. Since my domain cannot be renewed, I have to do something. A possible solution to the problem is to ditch it and use another.</p>

<h3 id="reason-two-language-barrier" id="reason-two-language-barrier">Reason Two: Language Barrier</h3>

<p>A problem I always had with “odd.af” was that it was hardly understood in Germany. Not only was it a different language, but you also needed to know basic “internet slang”. Especially for older folks, it was quite confusing.
Admittedly it was rather short and very easy to spell out.
Also, not that I really cared, but many people are not keen on profanity in – at least somewhat – professional domains.</p>

<blockquote><p>ℹ️ “af” is an internet acronym for “as fuck”. The domain meant “odd as fuck”.</p></blockquote>

<h3 id="reason-three-uncertainty-of-cctlds" id="reason-three-uncertainty-of-cctlds">Reason Three: Uncertainty of CcTLDs</h3>

<p>The first reason I didn&#39;t like to use a ccTLD anymore was simply that it was semantically incorrect. I have nothing to do with Afgahnistan, especially because AF is not a ccTLD with a “Commercial License”.
Such <a href="https://en.wikipedia.org/wiki/Country_code_top-level_domains_with_commercial_licenses?oldformat=true">domains</a> (e.g., “me”, “am”, “fm”, “sh”) adopt policies to be not reliant on the infrastructure of a country. For example, “.co” is not managed by the Colombian Government but rather a Colombian and US company.
The second reason is, well, that it&#39;s a ccTLD, thus dependent on the particular country. 😊 What happened to my domain is seldom enough but can happen anytime. I don&#39;t want to have this unnecessary risk anymore.</p>

<h2 id="what-i-did" id="what-i-did">What I did</h2>

<p>At first, I gathered a list of potential domain names.
<img src="https://i.snap.as/QHokptVd.png" alt="A screenshot of a note-taking app. The headline is Domains and it is followed by a list of potential domain names that are odd.dev, odd.am, oddest.me, odddev.codes, iodd.dev, iodd.de, odd.zone, ungera.de, odd.law, oddi.am, ungra.de"/>
The problem was to find one that had not a ccTLD (see previous chapter), was affordable, and was not too boring. I pretty much liked the idea of translating “odd” to German. Among others, it means “ungerade” or “ungrade”.  Then “ungra.de” came to my mind. However, first of all, that domain was already taken, and second, it <em>still</em> relies on a ccTLD. (I mean, “DE” would&#39;ve been not too bad since I&#39;m a resident of Germany, but well.)
Finally, I went with “ungra.dev” it&#39;s a portmanteau of “ungrade” (German for “odd”) and “dev. I pretty much like it, and it&#39;s only 15 $ per year.</p>

<p>Now came the hard part: migrating. It was a bit easier than I initially thought to be honest, but still pretty much painful. At first, I went through all accounts using an “odd.af” mail address. I just filtered for “odd.af” in my password manager. Surprisingly, they were very few since I use masked e-mails for most of my credentials.</p>

<p>After I changed all the addresses in these accounts (not to my new “ungra.dev”, but rather “masked e-mails”), I went on and checked my mail account. I informed everyone I had contact with that my mail address changed.</p>

<p>Then I updated all the links on my social media profiles and my website. Nothing should point to “odd.af” anymore.</p>

<p>The last step was to redirect the old domain to the new endpoints technically. In most cases, this was just a case of adding “ungra.dev” to the project and installing a 301 (“moved permanently”) redirect to the “odd.af” domain. One redirect was a bit more complicated. It was blog.odd.af. I changed the custom domain configuration in write.as, but I can only provide one, meaning I needed to create a redirect at Vercel.
This is also surprisingly inelegant, as you have to create a <a href="https://github.com/OddDev/blog.odd.af/blob/main/vercel.json">repository with a certain configuration file</a> and then assign the “to be redirected” domain to it.</p>

<pre><code class="language-json">{
    &#34;redirects&#34;: [
        { &#34;source&#34;: &#34;/&#34;, &#34;destination&#34;: &#34;https://blog.ungra.dev/&#34; }
    ]
}
</code></pre>

<p>Or in other words, I needed to create a nearly empty project and assign “blog.odd.af” to it. I found <a href="https://blog.ali.dev/posts/how-to-do-subdomain-url-redirects-with-vercel-hosting/">this blog post</a> explaining this approach.</p>

<h2 id="done" id="done">Done</h2>

<p>I&#39;m so glad that I&#39;m through with this truly unnecessary workload. It&#39;s super hard to find the time and energy to do this. It&#39;s quite a relief that I think I have it off the plate. Keep your fingers crossed that I didn&#39;t forget about anything. 🤞</p>
]]></content:encoded>
      <guid>https://blog.ungra.dev/why-i-dont-use-odd-af-anymore</guid>
      <pubDate>Sat, 25 Feb 2023 19:53:34 +0000</pubDate>
    </item>
    <item>
      <title>How to Fix Faulty Spotify Icon in KDE Plasma Application Switch</title>
      <link>https://blog.ungra.dev/how-to-fix-faulty-spotify-icon-in-kde-plasma-application-switch?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Whether I installed the app image or the Flatpack version of Spotify, I always had issues with the app icon in the window&#39;s title bar and the application switch. It fell back to a defaulting x11 icon.&#xA;!--more--&#xA;A screenshot showing an application switch with three open apps system settings, Spotify and Firefox. The icon of the Spotify entry is highlighted and wrong. Only the label Spotify gives away that it is the app. The icon is the X11 fallback icon.&#xA;A screenshot showing the title bar of Spotify. The title bar features the wrong icon. It&#39;s the icon of X11.&#xA;After investigating, it turned out that it&#39;s relatively easy to fix. All you need is a simple KWin rule. You don&#39;t even need to set any properties with it (at least, I have observed so far). It just needs to be there. For the sake of &#34;better safe than sorry&#34;, we add a meaningful property anyway.&#xA;Open up &#34;Window Rules&#34; in the &#34;System Settings&#34;. Add a new rule and give it a meaningful description. Something like &#34;Spotify&#34; should do the trick. Then add spotify (lowercase is essential!) as the Window Class. Afterward, click &#34;Add Property&#34; and choose &#34;Desktop file name&#34;. Add spotify-client as the value and select the mode Force.&#xA;A screenshot showing the Window Rules dialog with the settings as described in the paragraph above.&#xA;That&#39;s it already! Applying the changes should fix the icon.&#xA;A screenshot showing the title bar of Spotify and an application switch. Both icons are now correctly displaying the official Spotify app image.&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Whether I installed the app image or the Flatpack version of Spotify, I always had issues with the app icon in the window&#39;s title bar and the application switch. It fell back to a defaulting x11 icon.

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aj61jsbfusxwbyyxyx8t.png" alt="A screenshot showing an application switch with three open apps system settings, Spotify and Firefox. The icon of the Spotify entry is highlighted and wrong. Only the label Spotify gives away that it is the app. The icon is the X11 fallback icon."/>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ew8v93cfc3yqk7qkni5e.png" alt="A screenshot showing the title bar of Spotify. The title bar features the wrong icon. It&#39;s the icon of X11."/>
After investigating, it turned out that it&#39;s relatively easy to fix. All you need is a simple KWin rule. You don&#39;t even need to set any properties with it (at least, I have observed so far). It just needs to be there. For the sake of “better safe than sorry”, we add a meaningful property anyway.
Open up “Window Rules” in the “System Settings”. Add a new rule and give it a meaningful description. Something like “Spotify” should do the trick. Then add <code>spotify</code> (lowercase is essential!) as the Window Class. Afterward, click “Add Property” and choose “Desktop file name”. Add <code>spotify-client</code> as the value and select the mode <code>Force</code>.
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ie3o5w63jj3k26nu0zzk.png" alt="A screenshot showing the Window Rules dialog with the settings as described in the paragraph above."/>
That&#39;s it already! Applying the changes should fix the icon.
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mfcfubnmiigyxhwed2i9.png" alt="A screenshot showing the title bar of Spotify and an application switch. Both icons are now correctly displaying the official Spotify app image."/></p>
]]></content:encoded>
      <guid>https://blog.ungra.dev/how-to-fix-faulty-spotify-icon-in-kde-plasma-application-switch</guid>
      <pubDate>Mon, 13 Feb 2023 18:44:43 +0000</pubDate>
    </item>
    <item>
      <title>Destructuring Tweets - Episode 13 - Let&#39;s Construct</title>
      <link>https://blog.ungra.dev/destructuring-tweets-episode-13-lets-construct?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[I welcome you to the marvelous world of JavaScript quizzes on Social Media! This very article is part of a series where we deep dive and demystify them. This is a tough one, so this article is a little longer than usual. So let&#39;s jump right into the in-depth exploration of constructors in JavaScript!&#xA;&#xA;The Snippet&#xA;&#xA;const c = &#39;constructor&#39;;&#xA;cc(&#39;console.log(&#34;wth?&#34;)&#39;)();&#xA;!--more--&#xA;This time we have a complex one. However confusing as it might seem, it also makes a hell of a lot of fun to destruct.&#xA;We start by creating a string. The value of it is pretty essential here, for it&#39;s the name of the property we access in the second line two times iteratively. We are accessing the constructor property of the string. Then we (again) access the property constructor of the resulting value.&#xA;The resulting function gets called with an argument representing a call of a function, console.log(&#34;WTH?&#34;), itself. The return value of that call gets executed (()) anonymously right after.&#xA;So far, so confusing. But worry not; we clear things up right away.&#xA;&#xA;The Output&#xA;&#xA;The output here is probably something the least can guess on the fly. Presumably, the context of the riddle gave it away a bit, tho. It is a log to the console reading &#34;wth?&#34; Well, indeed: what the heck?!&#xA;&#xA;The Analysis&#xA;&#xA;Let us move through this step by step. First, we get the more accessible things out of the way: accessing object properties by the given string value constructor twice. Splitting this fraction up in an additional snippet helps to understand what&#39;s going on:&#xA;&#xA;const stringObject = new String(&#39;test&#39;);&#xA;stringObject.test = { test: &#39;foobar&#39;};&#xA;// Three times the same call:&#xA;console.log(stringObjectstringObject); // foobar&#xA;console.log(stringObject&#39;test&#39;); // foobar&#xA;console.log(stringObject.test.test); // foobar&#xA;&#xA;It shows how we can access specific object properties by a string, even if they are part of the string object reference itself. For our Twitter quiz meaning, we are accessing the constructor property of the string. Then yet again, the property constructor of the resulting value.&#xA;Now comes the crucial part of understanding all of this — the property constructor. What does it hold? Why is it there? Well, it stores the function used to construct the object. Let me explain this concept in another snippet (I&#39;m sorry):&#xA;&#xA;function FooBar() {&#xA;}&#xA;const fooBarInstance = new FooBar();&#xA;console.log(fooBarInstance.constructor); // function FooBar()&#xA;&#xA;const stringInstance = &#39;foobar&#39;; // OR new String(&#39;foobar&#39;)&#xA;console.log(stringInstance.constructor); // function String()&#xA;&#xA;Here we define a custom function used to construct an instance. This instance then holds a property constructor with the value of the specified function. That&#39;s simply what JavaScript automatically does on instantiation.&#xA;Further, you can see how this concept works with strings. You use syntactical sugar to avoid writing new String(&#39;&#39;), but the paradigm is the same: a function &#34;String&#34; exists. It accepts an argument, and when called to create an instance, the resulting object has a property constructor holding the used function. And that&#39;s the key secret here.&#xA;Returning to the original snippet, we create a string and access its property constructor. By now, we know that this property holds the function String. So what happens if we access the constructor property of a function object? Well, yet again, it holds a function. This time the one used to construct, well, functions themselves. Which is indeed function Function().&#xA;Let&#39;s examine this by another snippet:&#xA;&#xA;function foo() {}&#xA;console.log(foo.constructor); // function Function()&#xA;&#xA;const bar = new Function(&#39;console.log(&#34;something&#34;)&#39;);&#xA;bar(); // something&#xA;console.log(bar.constructor); // function Function()&#xA;&#xA;const stringInstance = &#39;foobar&#39;; // OR new String(&#39;foobar&#39;)&#xA;console.log(stringInstance.constructor); // function String()&#xA;console.log(stringInstance.constructor.constructor); // function Function()&#xA;&#xA;Mention how a function declaration is just syntactical sugar around new Function(). So, if the constructor property of a String object is a function, the constructor property of this function is the function used to construct functions, thus the function Function(). 🤯😂&#xA;Now that we have learned this, the rest is pretty straightforward. After accessing the constructor property the second time, we have a function constructing functions. So whatever we pass in as a string gets returned as a function instance. In our case, console.log(&#34;wth?&#34;). Since we do not store this return value, we call it anonymously immediately right after via (). And that&#39;s how after all of this, console.log(&#34;wth?&#34;) gets executed and logs the string wth? to the console.&#xA;Let&#39;s wrap it up in one last snippet:&#xA;&#xA;const c = &#39;constructor&#39;;&#xA;const stringConstructorFunction = c[c];&#xA;console.log(stringConstructorFunction[c]); // function Function()&#xA;&#xA;// As simple as:&#xA;Function(&#39;console.log(&#34;wth?&#34;)&#39;)(); // wth?&#xA;// Or in a more common syntax:&#xA;(() =  console.log(&#39;wth?&#39;))(); // wth?&#xA;&#xA;Further Reading&#xA;    Object Property Bracket Notation&#xA;    Constructor Property of Object Prototype&#xA;    Function Constructor]]&gt;</description>
      <content:encoded><![CDATA[<p>I welcome you to the marvelous world of JavaScript quizzes on Social Media! This very article is part of a series where we deep dive and demystify them. This is a tough one, so this article is a little longer than usual. So let&#39;s jump right into the in-depth exploration of constructors in JavaScript!</p>

<h2 id="the-snippet" id="the-snippet">The Snippet</h2>

<pre><code class="language-javascript">const c = &#39;constructor&#39;;
c[c][c](&#39;console.log(&#34;wth?&#34;)&#39;)();
</code></pre>



<p>This time we have a complex one. However confusing as it might seem, it also makes a hell of a lot of fun to destruct.
We start by creating a string. The value of it is pretty essential here, for it&#39;s the name of the property we access in the second line two times iteratively. We are accessing the <code>constructor</code> property of the string. Then we (again) access the property <code>constructor</code> of the resulting value.
The resulting function gets called with an argument representing a call of a function, <code>console.log(&#34;WTH?&#34;)</code>, itself. The return value of that call gets executed (<code>()</code>) anonymously right after.
So far, so confusing. But worry not; we clear things up right away.</p>

<h2 id="the-output" id="the-output">The Output</h2>

<p>The output here is probably something the least can guess on the fly. Presumably, the context of the riddle gave it away a bit, tho. It is a log to the console reading “wth?” Well, indeed: what the heck?!</p>

<h2 id="the-analysis" id="the-analysis">The Analysis</h2>

<p>Let us move through this step by step. First, we get the more accessible things out of the way: accessing object properties by the given string value <code>constructor</code> twice. Splitting this fraction up in an additional snippet helps to understand what&#39;s going on:</p>

<pre><code class="language-javascript">const stringObject = new String(&#39;test&#39;);
stringObject.test = { test: &#39;foobar&#39;};
// Three times the same call:
console.log(stringObject[stringObject][stringObject]); // foobar
console.log(stringObject[&#39;test&#39;][&#39;test&#39;]); // foobar
console.log(stringObject.test.test); // foobar
</code></pre>

<p>It shows how we can access specific object properties by a string, even if they are part of the string object reference itself. For our Twitter quiz meaning, we are accessing the <code>constructor</code> property of the string. Then yet again, the property <code>constructor</code> of the resulting value.
Now comes the crucial part of understanding all of this — the property constructor. What does it hold? Why is it there? Well, it stores the function used to construct the object. Let me explain this concept in another snippet (I&#39;m sorry):</p>

<pre><code class="language-javascript">function FooBar() {
}
const fooBarInstance = new FooBar();
console.log(fooBarInstance.constructor); // function FooBar()

const stringInstance = &#39;foobar&#39;; // OR new String(&#39;foobar&#39;)
console.log(stringInstance.constructor); // function String()
</code></pre>

<p>Here we define a custom function used to construct an instance. This instance then holds a property <code>constructor</code> with the value of the specified function. That&#39;s simply what JavaScript automatically does on instantiation.
Further, you can see how this concept works with strings. You use syntactical sugar to avoid writing <code>new String(&#39;&#39;)</code>, but the paradigm is the same: a function “String” exists. It accepts an argument, and when called to create an instance, the resulting object has a property <code>constructor</code> holding the used function. And that&#39;s the key secret here.
Returning to the original snippet, we create a string and access its property constructor. By now, we know that this property holds the function <code>String</code>. So what happens if we access the constructor property of a function object? Well, yet again, it holds a function. This time the one used to construct, well, functions themselves. Which is indeed <code>function Function()</code>.
Let&#39;s examine this by another snippet:</p>

<pre><code class="language-javascript">function foo() {}
console.log(foo.constructor); // function Function()

const bar = new Function(&#39;console.log(&#34;something&#34;)&#39;);
bar(); // something
console.log(bar.constructor); // function Function()

const stringInstance = &#39;foobar&#39;; // OR new String(&#39;foobar&#39;)
console.log(stringInstance.constructor); // function String()
console.log(stringInstance.constructor.constructor); // function Function()
</code></pre>

<p>Mention how a function declaration is just syntactical sugar around <code>new Function()</code>. So, if the constructor property of a String object is a function, the constructor property of this function is the function used to construct functions, thus the function <code>Function()</code>. 🤯😂
Now that we have learned this, the rest is pretty straightforward. After accessing the constructor property the second time, we have a function constructing functions. So whatever we pass in as a string gets returned as a function instance. In our case, <code>console.log(&#34;wth?&#34;)</code>. Since we do not store this return value, we call it anonymously immediately right after via <code>()</code>. And that&#39;s how after all of this, <code>console.log(&#34;wth?&#34;)</code> gets executed and logs the string <code>wth?</code> to the console.
Let&#39;s wrap it up in one last snippet:</p>

<pre><code class="language-javascript">const c = &#39;constructor&#39;;
const stringConstructorFunction = c[c];
console.log(stringConstructorFunction[c]); // function Function()

// As simple as:
Function(&#39;console.log(&#34;wth?&#34;)&#39;)(); // wth?
// Or in a more common syntax:
(() =&gt; console.log(&#39;wth?&#39;))(); // wth?
</code></pre>

<h2 id="further-reading" id="further-reading">Further Reading</h2>

<p>    – <a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Basics#bracket_notation">Object Property Bracket Notation</a>
    – <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor">Constructor Property of Object Prototype</a>
    – <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function">Function Constructor</a></p>
]]></content:encoded>
      <guid>https://blog.ungra.dev/destructuring-tweets-episode-13-lets-construct</guid>
      <pubDate>Mon, 13 Feb 2023 18:42:44 +0000</pubDate>
    </item>
    <item>
      <title>Destructuring Tweets - Episode 12 - Coalesced False</title>
      <link>https://blog.ungra.dev/destructuring-tweets-episode-12-coalesced-false?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Look who managed to head to my blog only to find himself reading an article about a tweeted Javascript quiz. In this series, I try to break such down, and this post features a lesser-known but often useful operator: Nullish Coalescing!&#xA;&#xA;The Snippet&#xA;&#xA;console.log(1 === null ?? 1)&#xA;!--more--&#xA;This time we have a super quick one. It logs a logical expression to the console. And that&#39;s pretty much already it!&#xA;The expression itself consists of two parts. The first one compares null to 1. The result is the left-hand side operand of a so-called &#34;Nullish Coalescing Operator&#34; typed as ??. &#xA;The right-hand side operand is, again, just 1.&#xA;&#xA;The Output&#xA;&#xA;The output is also pretty straightforward. At least if you know what a &#34;nullish coalescing operator&#34; does. This expression will log false to the console.&#xA;&#xA;The Analysis&#xA;&#xA;For the analysis, we start with the Strict Equal Operator (===) as it takes higher precedence (&#34;will be evaluated before&#34;) than the mysterious two question marks. And it&#39;s also reasonably easy. Comparing null to 1 will undoubtedly result in false, won&#39;t it? &#xA;What&#39;s left looks something like: false ?? 1.&#xA;So far, so good. Now comes the fun part. A Nullish Coalescing Operator works in its core like a Logical Or (||). However, instead of checking for falsy values, it only validates for nullish ones! That means either null or undefined. To get this straight: it will return the left-hand side value if it&#39;s not &#34;nullish&#34; or the right-hand side value if not.&#xA;And that is the reason false gets printed to the console. 🥧&#xA;&#xA;Further Reading&#xA;&#xA;    Nullish Coalescing Operator&#xA;    Operator Precedence]]&gt;</description>
      <content:encoded><![CDATA[<p>Look who managed to head to my blog only to find himself reading an article about a tweeted Javascript quiz. In this series, I try to break such down, and this post features a lesser-known but often useful operator: Nullish Coalescing!</p>

<h2 id="the-snippet" id="the-snippet">The Snippet</h2>

<pre><code class="language-javascript">console.log(1 === null ?? 1)
</code></pre>



<p>This time we have a super quick one. It logs a logical expression to the console. And that&#39;s pretty much already it!
The expression itself consists of two parts. The first one compares <code>null</code> to <code>1</code>. The result is the left-hand side operand of a so-called “Nullish Coalescing Operator” typed as <code>??</code>.
The right-hand side operand is, again, just <code>1</code>.</p>

<h2 id="the-output" id="the-output">The Output</h2>

<p>The output is also pretty straightforward. At least if you know what a “nullish coalescing operator” does. This expression will log <code>false</code> to the console.</p>

<h1 id="the-analysis" id="the-analysis">The Analysis</h1>

<p>For the analysis, we start with the Strict Equal Operator (<code>===</code>) as it takes higher precedence (“will be evaluated before”) than the mysterious two question marks. And it&#39;s also reasonably easy. Comparing <code>null</code> to <code>1</code> will undoubtedly result in <code>false</code>, won&#39;t it?
What&#39;s left looks something like: <code>false ?? 1</code>.
So far, so good. Now comes the fun part. A Nullish Coalescing Operator works in its core like a Logical Or (<code>||</code>). However, instead of checking for falsy values, it only validates for nullish ones! That means either <code>null</code> or <code>undefined</code>. To get this straight: it will return the left-hand side value if it&#39;s not “nullish” or the right-hand side value if not.
And that is the reason <code>false</code> gets printed to the console. 🥧</p>

<h2 id="further-reading" id="further-reading">Further Reading</h2>

<p>    – <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator">Nullish Coalescing Operator</a>
    – <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence">Operator Precedence</a></p>
]]></content:encoded>
      <guid>https://blog.ungra.dev/destructuring-tweets-episode-12-coalesced-false</guid>
      <pubDate>Mon, 13 Feb 2023 18:03:09 +0000</pubDate>
    </item>
    <item>
      <title>Destructuring Tweets - Episode 11 - Continue to Break</title>
      <link>https://blog.ungra.dev/destructuring-tweets-episode-11-continue-to-break?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Glad you found this blog series about JavaScript Social Media quizzes! This week is a funny snippet around the loop-manipulating keywords break and continue. Get ready for some serious trickery!&#xA;&#xA;The Snippet&#xA;&#xA;for (var i = 1; i &lt; 10; ++i) {&#xA;  if (i % 3) {&#xA;    continue;&#xA;  }&#xA;  if (i == 7) {&#xA;    break;&#xA;  }&#xA;  console.log(i);&#xA;}&#xA;!--more--&#xA;The snippet features a for-loop initializing the variable i with 1. Said gets incremented by 1 until it&#39;s 9 (&lt; 10). To put it simply: we count from 1 to 9.&#xA;In the loop, there are two checks before printing the current index to the console — the first tests whether modulo 3 returns a truthy value, which means everything except 0. In that case, continue gets executed.&#xA;If not, the snippet further validates whether the current index holds 7. In case it does, break is executed.&#xA;&#xA;The Output&#xA;&#xA;Okay, that one is a bit of a brain-fuck. On first thought, you might conclude it&#39;ll log 3 and 6. Since the loop breaks on 7, right? However, it will still also print 3, 6, and 9! All three values have no remainder when divided by 3 (% 3). If you are still or even more confused, worry not and jump right to the next chapter.&#xA;&#xA;The Analysis&#xA;&#xA;So, we already figured the loop is counting from 1 to 9. Let&#39;s focus on the first check, i % 3.  It will always be true if it does not result in 0. The expression is false if the index can be divided by three without having a remainder. It&#39;s true for 1, 2, 4, 5, 7, and 8 in this context, to be even more exact.&#xA;Let&#39;s examine what happens then: continue is called. This keyword tells the loop to skip the rest of its body and move to the next iteration. Meaning the console.log at the bottom is never reached. Or the other way around, only 3, 6, and 9 are logged.&#xA;Now let&#39;s focus on the second if-statement. If the index holds the value 7, the loop breaks. break is a keyword telling the loop to stop with everything. It skips the rest of its body and will not iterate further, continuing with whatever is next in the sequence.&#xA;That&#39;s why this snippet is so funny. It tricks you into believing 9 will not print since it stops after i reaches 7. Well, as we know, this is not the case. And by now, you probably already figured also why. The second statement is, just as console.log(i), only reached by 3, 6, and 9. These numbers are not 7 🤯, thus making the body of the second if-statement unreachable code! break is never executed.&#xA;&#xA;Further Reading&#xA;&#xA;    break&#xA;    continue&#xA;    Remainder Operator (%)]]&gt;</description>
      <content:encoded><![CDATA[<p>Glad you found this blog series about JavaScript Social Media quizzes! This week is a funny snippet around the loop-manipulating keywords <code>break</code> and <code>continue</code>. Get ready for some serious trickery!</p>

<h2 id="the-snippet" id="the-snippet">The Snippet</h2>

<pre><code class="language-javascript">for (var i = 1; i &lt; 10; ++i) {
  if (i % 3) {
    continue;
  }
  if (i == 7) {
    break;
  }
  console.log(i);
}
</code></pre>



<p>The snippet features a for-loop initializing the variable <code>i</code> with 1. Said gets incremented by 1 until it&#39;s 9 (<code>&lt; 10</code>). To put it simply: we count from 1 to 9.
In the loop, there are two checks before printing the current index to the console — the first tests whether modulo 3 returns a truthy value, which means everything except 0. In that case, <code>continue</code> gets executed.
If not, the snippet further validates whether the current index holds 7. In case it does, <code>break</code> is executed.</p>

<h2 id="the-output" id="the-output">The Output</h2>

<p>Okay, that one is a bit of a brain-fuck. On first thought, you might conclude it&#39;ll log 3 and 6. Since the loop breaks on 7, right? However, it will still also print 3, 6, and 9! All three values have no remainder when divided by 3 (<code>% 3</code>). If you are still or even more confused, worry not and jump right to the next chapter.</p>

<h2 id="the-analysis" id="the-analysis">The Analysis</h2>

<p>So, we already figured the loop is counting from 1 to 9. Let&#39;s focus on the first check, <code>i % 3</code>.  It will always be true if it does not result in <code>0</code>. The expression is false if the index can be divided by three without having a remainder. It&#39;s true for 1, 2, 4, 5, 7, and 8 in this context, to be even more exact.
Let&#39;s examine what happens then: <code>continue</code> is called. This keyword tells the loop to skip the rest of its body and move to the next iteration. Meaning the <code>console.log</code> at the bottom is never reached. Or the other way around, only 3, 6, and 9 are logged.
Now let&#39;s focus on the second if-statement. If the index holds the value 7, the loop breaks. <code>break</code> is a keyword telling the loop to stop with everything. It skips the rest of its body and will not iterate further, continuing with whatever is next in the sequence.
That&#39;s why this snippet is so funny. It tricks you into believing 9 will not print since it stops after <code>i</code> reaches 7. Well, as we know, this is not the case. And by now, you probably already figured also why. The second statement is, just as <code>console.log(i)</code>, only reached by 3, 6, and 9. These numbers are not 7 🤯, thus making the body of the second if-statement unreachable code! <code>break</code> is never executed.</p>

<h2 id="further-reading" id="further-reading">Further Reading</h2>

<p>    – <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break">break</a>
    – <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/continue">continue</a>
    – <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder">Remainder Operator (%)</a></p>
]]></content:encoded>
      <guid>https://blog.ungra.dev/destructuring-tweets-episode-11-continue-to-break</guid>
      <pubDate>Mon, 13 Feb 2023 17:24:09 +0000</pubDate>
    </item>
  </channel>
</rss>