<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Pascal Thormeier</title>
    <description>The latest articles on Forem by Pascal Thormeier (@thormeier).</description>
    <link>https://forem.com/thormeier</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F436259%2F00cde9af-e704-48b7-a00a-32cdc4997190.jpeg</url>
      <title>Forem: Pascal Thormeier</title>
      <link>https://forem.com/thormeier</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/thormeier"/>
    <language>en</language>
    <item>
      <title>How to use Timberborn 🦫 (yes, the beaver city-building game) as a database 💾</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Sat, 28 Mar 2026 19:39:32 +0000</pubDate>
      <link>https://forem.com/thormeier/how-to-use-timberborn-yes-the-beaver-city-building-game-as-a-database-489c</link>
      <guid>https://forem.com/thormeier/how-to-use-timberborn-yes-the-beaver-city-building-game-as-a-database-489c</guid>
      <description>&lt;p&gt;So, one of my favourite games lately, &lt;a href="https://store.steampowered.com/app/1062090/Timberborn/" rel="noopener noreferrer"&gt;Timberborn&lt;/a&gt;, has released its 1.0 version and finally left early access.&lt;/p&gt;

&lt;p&gt;For those who don't know it, Timberborn is a city-building strategy game. In this game, the player takes over the role of a ruler of a beaver colony - after humans (or hoomans, as they're called in-game) vanished from the face of the planet, the beavers took over. There's resource gathering, taking care of your beavers' different needs, building complex water-based power networks, factories, transportation, etc., etc.&lt;/p&gt;

&lt;p&gt;Or, a database, in my case.&lt;/p&gt;

&lt;p&gt;There are 3D water physics, different seasons such as droughts, where you need to retain water and bad tides, where new water entering the map is polluted and harmful to beavers.&lt;/p&gt;

&lt;p&gt;Since the initial release in 2021, many features have been added.&lt;/p&gt;

&lt;p&gt;And the latest update broke the game for me a bit. Because they added automation features. These automation features include various sensors for detecting bad tides, water flow, etc. They even added logic gates and HTTP levers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Oh no
&lt;/h2&gt;

&lt;p&gt;Oh no indeed. I &lt;em&gt;had&lt;/em&gt; to play around with these. This is what they look like in-game:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhillo8z3sz4yf8nuypk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhillo8z3sz4yf8nuypk5.png" alt="An HTTP Lever in Timberborn" width="800" height="726"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, they have a name (in this case “HTTP Lever 1”), a “Switch-on URL” and a “Switch-off URL”.&lt;/p&gt;

&lt;p&gt;The idea behind these is that streamers can hook these up to other services, for example, Twitch webhooks. If someone leaves a like, fireworks go off, stuff like that.&lt;/p&gt;

&lt;p&gt;But: Who says you can only use one of them?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj92v7gae5gayl2pl4sya.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj92v7gae5gayl2pl4sya.png" alt="Around 1000 HTTP Levers in Timberborn" width="800" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you see where I'm going with this?&lt;/p&gt;

&lt;h2&gt;
  
  
  Oh &lt;em&gt;no&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Oh yes. The game can work with around 1000 of them for some time. Windows said it "had some issues with the system" twice while I was experimenting, so from the very start, this isn't very practical. It doesn't have to be, though.&lt;/p&gt;

&lt;p&gt;For every lever, there is one endpoint that turns it on and one that turns it off. No batch processing, sadly, but perhaps there's a mod. Another endpoint returns the state of each lever on the current map. The data looks kind of like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTTP Lever 829"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"springReturn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTTP Lever 154"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"springReturn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTTP Lever 839"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"springReturn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTTP Lever 164"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"springReturn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With two states, such an HTTP Lever can be thought of as a single bit.&lt;/p&gt;

&lt;p&gt;All that is needed is a way to read from and write to them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Timberborn web interface: How it works
&lt;/h3&gt;

&lt;p&gt;The idea is that we’re going to convert some user input to JSON, convert that to binary using ASCII encoding and flip each bit one by one. When reading, we read all the lever states at once, rearrange them into a bit sequence, split it into 8-bit chunks, decode them back into characters, and parse the resulting JSON. And voila: A read/write data storage.&lt;/p&gt;

&lt;p&gt;Let’s start with a little HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Store in Timberborn&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"load"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Load data from Timberborn
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now comes the fun part. We start with a bit of scaffolding JS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="c1"&gt;// We need this to later split the bits&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;numberOfLevers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="c1"&gt;// This needs to be the exact number of levers in-game, otherwise it won't work reliably.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we listen to a form submit and build a JSON string from the two fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we have this, we can transform this into a series of binary strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asciiEncoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;asciiEncoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;num&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;num&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us an array of 8-bit long strings with 0s and 1s:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttnhxyu9k5bhezag709d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttnhxyu9k5bhezag709d.png" alt="A browser window showing the HTML form and the console.log of the binary data" width="606" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We next need to join these up and build the large final bit string. We translate those bits into booleans and then API URLs, which we can then call with &lt;code&gt;fetch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// So there's no leftover data in the registry &lt;/span&gt;
    &lt;span class="c1"&gt;// at the end of the current data&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;numberOfLevers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allUrls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="s2"&gt;`http://localhost:8080/api/switch-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bit&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;off&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/HTTP Lever &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When saving, this will trigger a total of 1000 HTTP requests to the game's endpoint. No wonder it doesn't like it.&lt;/p&gt;

&lt;p&gt;But: It works!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgasmyulkaf1vxfc0awj0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgasmyulkaf1vxfc0awj0.gif" alt="A GIF showing how the Timberborn HTTP levers update when saving text" width="200" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(There's a gif, it may take a few seconds to load...)&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading data from Timberborn
&lt;/h2&gt;

&lt;p&gt;Next, we need to read data. As we've seen in the example, the lever states can be fetched all at once, but their order is all scrambled up. We can fix that by first loading everything and then sorting. We then translate everything back into a bit string, chunk that up and decode that into ASCII again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;load&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8080/api/levers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP Lever &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP Lever &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;aNumber&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;bNumber&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bitString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;bitString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;// We filter out everything that's only 0s, because that's likely garbage-data&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;letters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;
  &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it!&lt;/p&gt;

&lt;p&gt;Technically, this can be considered a cloud storage now. Since Steam uploads save games to the cloud and Timberborn treats HTTP levers as stateful (i.e. it saves their state when saving the game), everything's persistent.&lt;/p&gt;

&lt;p&gt;Whoever manages to get more than a measly kilobit of data into Timberborn, please hit me up, I'm pretty certain we can make a Drupal database adapter for this &lt;em&gt;somehow&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;(PS: Sorry, not sorry for the AI image. I had to try it eventually!)&lt;/p&gt;

&lt;p&gt;(PPS: This post is not sponsored. But Timberborn devs, if you read this, I'm up for way more shenanigans using the automation features :D)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt;&lt;em&gt;! You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt; Or follow me on &lt;a href="https://bsky.app/profile/thormeier.dev" rel="noopener noreferrer"&gt;&lt;em&gt;Bluesky&lt;/em&gt; 🦋&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>donttrythisathome</category>
    </item>
    <item>
      <title>⚠️ Don't try this at home: Add Brainf**k support to your JS app today!</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Sat, 17 Jan 2026 19:00:24 +0000</pubDate>
      <link>https://forem.com/thormeier/dont-try-this-at-home-add-brainfk-support-to-your-js-app-today-o0h</link>
      <guid>https://forem.com/thormeier/dont-try-this-at-home-add-brainfk-support-to-your-js-app-today-o0h</guid>
      <description>&lt;p&gt;Dear Mods of dev.to, please bear with me for this one. This post will have a few profanities, as you might guess from the title. However, these profanities are names of packages and actual technical terms. None of these is meant to offend anyone. I’ll try to censor most of them where sensible.&lt;/p&gt;

&lt;p&gt;And here we go again. Stuff no one should probably do in production, because it’s either completely and utterly useless, will confuse the living heck out of everyone having to maintain it, or make everyone scream “No” and “Please don’t”.&lt;/p&gt;

&lt;p&gt;Esoteric programming languages it is this time. Specifically, &lt;a href="https://en.wikipedia.org/wiki/Brainfuck" rel="noopener noreferrer"&gt;Brainf**k&lt;/a&gt;, a dialect of &lt;a href="https://esolangs.org/wiki/Ook!" rel="noopener noreferrer"&gt;Ook!&lt;/a&gt; that sadly can't be understood by Orang Utans. And no, I refuse to believe that it's actually the other way around.&lt;/p&gt;

&lt;p&gt;Brainf**k is notoriously hard to read. Mostly because its commands are single characters that move the stack pointer by 1, increase or decrease the value it points at by 1, and loop as long as the current stack element the pointer is pointing to is greater than 0.&lt;/p&gt;

&lt;p&gt;I've recently written a post about &lt;a href="https://dev.to/thormeier/the-mythical-one-fits-all-build-tool-plugin-it-actually-exists-ke2"&gt;using and creating unplugins&lt;/a&gt; where I mentioned adding Brainf**k support and thought to myself:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F606n6pz8lgym97zxqja4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F606n6pz8lgym97zxqja4.png" alt="Meme of Bilbo Baggins asking " width="800" height="796"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So here we are. If you ever wanted to build half your app in a cumbersome, convoluted, unreadable way, the time is now! And you can even open-source it and let others do the same, too!&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the Unplugin boilerplate
&lt;/h2&gt;

&lt;p&gt;Straightforward. We also need another dependency called &lt;a href="https://www.npmjs.com/package/hirnfick" rel="noopener noreferrer"&gt;"hirnf**k"&lt;/a&gt;, a library that transpiles out Brainf*&lt;em&gt;k code to JS. For the non-German-speaking folks, the name of the package translates to "brainf&lt;/em&gt;*k", so there's that.&lt;/p&gt;

&lt;p&gt;So, let's open a terminal and get going:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;px degit unplugin/unplugin-starter unplugin-bf
bash: px: &lt;span class="nb"&gt;command &lt;/span&gt;not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whoops. Typo. Yes, this already started badly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npx degit unplugin/unplugin-starter unplugin-bf 
$ cd unplugin-bf/ &amp;amp;&amp;amp; npm i -s hirnfick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better.&lt;/p&gt;

&lt;p&gt;This creates an unplugin and installs our source-to-source compiler. We'll implement the compilation in the unplugin's &lt;code&gt;src/index.ts&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;For that, let's first look at hirnf**k's documentation. It includes an ESM example we can slightly modify. Here's what the example looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;hirnfick&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://jspm.dev/hirnfick&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;helloWorldBF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;++++++++[&amp;gt;++++[&amp;gt;++&amp;gt;+++&amp;gt;+++&amp;gt;+&amp;lt;&amp;lt;&amp;lt;&amp;lt;-]&amp;gt;+&amp;gt;+&amp;gt;-&amp;gt;&amp;gt;+[&amp;lt;]&amp;lt;-]&amp;gt;&amp;gt;.&amp;gt;---.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+++++++..+++.&amp;gt;&amp;gt;.&amp;lt;-.&amp;lt;.+++.------.--------.&amp;gt;&amp;gt;+.&amp;gt;++.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;helloJs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hirnfick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compileToJsWeb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;helloWorldBF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runHello&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;helloJs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; return main().output.trim();`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;runHello&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect, right? It creates an executable function and even contains a Hello World in Brainf**k already!&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing everything
&lt;/h2&gt;

&lt;p&gt;It's surprisingly straightforward. Luckily, the unplugin starter already gives us most of the boilerplate, we only need to fill in the gaps. Let's look at what the starter brought:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UnpluginFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Options&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createUnplugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unpluginFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UnpluginFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Options&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin-starter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;transformInclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__UNPLUGIN__&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Hello Unplugin! &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unplugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* #__PURE__ */&lt;/span&gt; &lt;span class="nf"&gt;createUnplugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unpluginFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;unplugin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we change the &lt;code&gt;transformInclude&lt;/code&gt; function. We want it to check for &lt;code&gt;.bf&lt;/code&gt; files instead of the &lt;code&gt;main.ts&lt;/code&gt;. While we're at it, we also rename the unplugin and empty the &lt;code&gt;transform&lt;/code&gt; function. The options are also not necessary, so let's remove them, too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unpluginFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UnpluginFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Options&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin-bf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;transformInclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.bf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're actually almost there already. Next, we actually copy/paste hirnf**k's example unto the &lt;code&gt;transform&lt;/code&gt; function and alter it a bit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UnpluginFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Options&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;hirnfick&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hirnfick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createUnplugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unpluginFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UnpluginFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Options&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin-bf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;transformInclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.bf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transformed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hirnfick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compileToJsWeb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`export default new Function(&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transformed&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; return main().output.trim();&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;wrapped&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unplugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* #__PURE__ */&lt;/span&gt; &lt;span class="nf"&gt;createUnplugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unpluginFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;unplugin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And believe it or not, we're done. We now have Brain**k support in our JS apps via our lovely unplugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Now, how does this work? First, it reads the file's content and passes it as a string (the &lt;code&gt;code&lt;/code&gt; parameter) to the &lt;code&gt;transform&lt;/code&gt; function. We then tell hirnf*&lt;em&gt;k to compile it to JS. That's, again, a string. That string is then wrapped into another string that contains the standard &lt;code&gt;export default&lt;/code&gt; stuff with an inline function. That function returns whatever the compiled Brainf&lt;/em&gt;*k code would try to print via the &lt;code&gt;.&lt;/code&gt; command, and trims all the whitespace from it.&lt;/p&gt;

&lt;p&gt;Let's simplify the Brainf**k code to this bit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiled JS would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`let position = 0;
const cells = [0];
let output = '';

function putchar() {
  output += String.fromCharCode(cells[position]);
}

function main() {
  if (cells[position] &amp;lt; 255) {
    cells[position] += 1;
  }
  if (cells[position] &amp;gt; 0) {
    cells[position] -= 1;
  }
  putchar(String.fromCharCode(cells[position]));

  return { cells, output };
}
 return main().output.trim();`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can clearly see where the Brainf**k code lives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;+&lt;/code&gt; becomes &lt;code&gt;cells[position] += 1;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt; becomes &lt;code&gt;cells[position] -= 1;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.&lt;/code&gt; becomes &lt;code&gt;putchar(String.fromCharCode(cells[position]));&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trying it out
&lt;/h2&gt;

&lt;p&gt;Let's try it out! Luckily, the unplugin starter contains a playground that already sets everything up for us.&lt;/p&gt;

&lt;p&gt;We create a new file called &lt;code&gt;hello-world.bf&lt;/code&gt; and add the Hello World there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;++++++++[&amp;gt;++++[&amp;gt;++&amp;gt;+++&amp;gt;+++&amp;gt;+&amp;lt;&amp;lt;&amp;lt;&amp;lt;-]&amp;gt;+&amp;gt;+&amp;gt;-&amp;gt;&amp;gt;+[&amp;lt;]&amp;lt;-]&amp;gt;&amp;gt;.&amp;gt;---.+++++++..+++.&amp;gt;&amp;gt;.&amp;lt;-.&amp;lt;.+++.------.--------.&amp;gt;&amp;gt;+.&amp;gt;++.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(I tried syntax highlighting, but... well... you know...)&lt;/p&gt;

&lt;p&gt;Next, in the example's main.ts, we import the file and execute the function it returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;HelloWorld&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./hello-world.bf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HelloWorld&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VSCode doesn’t like this in the &lt;em&gt;slightest&lt;/em&gt;. Perhaps it knows that we want to run Brainf**k. What it &lt;em&gt;doesn’t&lt;/em&gt; know is that .bf files return JS code, it doesn’t know that the unplugin allows that, and it will complain. We don’t care, though. Perhaps someone will eventually write a VSCode plugin to go with our unplugin. You never know.&lt;/p&gt;

&lt;p&gt;We then go to the playground, install dependencies and run this thing_&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;playground
&lt;span class="nv"&gt;$ &lt;/span&gt;npm i &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That'll start the dev server on localhost, which we open in the browser. And lo and behold:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Femwkn1nwco63chfb8lya.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Femwkn1nwco63chfb8lya.png" alt="" width="322" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It actually works!&lt;/p&gt;

&lt;p&gt;However, please, &lt;em&gt;please&lt;/em&gt; do not use Brainf**k in your production app. But if you're brave enough, you may add a few extra things yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add input support via &lt;code&gt;,&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add sourcemaps (good luck)&lt;/li&gt;
&lt;li&gt;Support Ook!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why does no one think of the Orang Utans.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt;&lt;em&gt;! You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt; Or follow me on &lt;a href="https://bsky.app/profile/thormeier.dev" rel="noopener noreferrer"&gt;&lt;em&gt;Bluesky&lt;/em&gt; 🦋&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>vite</category>
      <category>donttrythisathome</category>
    </item>
    <item>
      <title>The Mythical One-Fits-All Build Tool Plugin 🦄 (It Actually Exists)</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Sun, 11 Jan 2026 13:28:58 +0000</pubDate>
      <link>https://forem.com/thormeier/the-mythical-one-fits-all-build-tool-plugin-it-actually-exists-ke2</link>
      <guid>https://forem.com/thormeier/the-mythical-one-fits-all-build-tool-plugin-it-actually-exists-ke2</guid>
      <description>&lt;p&gt;Do you know that feeling when you're building a complex web app and you need some functionality that actually exists, but not for the framework &lt;em&gt;you're&lt;/em&gt; using? Or, let's say you're building a library that needs to hook into the build process of your project, you'd like to open source it, and you just so happen to use Vite, but some poor soul out there would need this exact library, but for Webpack instead? Or they use Snowpack? Or Brunch? Or... Gulp?&lt;/p&gt;




&lt;p&gt;Ok, perhaps it's not &lt;em&gt;that&lt;/em&gt; bad anymore. The wildest times of the JS world are definitely over. You know, the times when build tools and bundlers and frameworks and component libraries sprouted like mushrooms. A classic XKCD comic about competing standards fits pretty well:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xkcd.com/927/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxtymqcirbyiz3riq652.png" alt="Situation: There are 14 competing standards. " width="500" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can even read about my own adventures with the niche build tools &lt;a href="https://dev.to/thormeier/something-new-every-now-and-then-trying-brunch-109"&gt;Brunch&lt;/a&gt; and &lt;a href="https://dev.to/thormeier/i-m-going-to-give-snowpack-a-try-now-3ohm"&gt;Snowpack&lt;/a&gt; in some previous articles I wrote. Both of these tools haven’t received a commit in 4 to 5 years now, so support is minimal at best.&lt;/p&gt;

&lt;p&gt;Nowadays, there are still about half a dozen, give or take a few, build tools/bundlers left that are still highly maintained, broadly used, and that are generally accepted as "standard": Webpack, esbuild, Vite, rspack, Rollup, Rolldown, Bun, and some others based on these.&lt;/p&gt;

&lt;p&gt;The problem I described initially persists, though: Most of these work in wildly different ways. A Webpack plugin usually doesn't "just work" in Vite and vice versa. And let's not forget esbuild and all the others!&lt;/p&gt;

&lt;p&gt;Luckily, there's movement. Not only are people using things resembling "standard tools" by now, but ever more of these are emerging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing the UnJS ecosystem
&lt;/h2&gt;

&lt;p&gt;One particular group is building off-the-shelf, framework-agnostic packages that work on their own with few to no dependencies: &lt;a href="https://unjs.io" rel="noopener noreferrer"&gt;UnJS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhr0g1e22orjkn0nmlfm9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhr0g1e22orjkn0nmlfm9.png" alt="The UnJS website" width="800" height="354"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;(The UnJS website)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’ve built anything with Nuxt, Vue, Vite or thelike, you’ve likely already used some of their tools without even realising. There are some instant classics like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://unjs.io/packages/h3" rel="noopener noreferrer"&gt;h3&lt;/a&gt; - a portable and lightweight http server) &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://unjs.io/packages/citty" rel="noopener noreferrer"&gt;citty&lt;/a&gt; - a CLI builder&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://unjs.io/packages/changelogen" rel="noopener noreferrer"&gt;changelogen&lt;/a&gt; - a tool for generating changelogs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://unjs.io/packages/ofetch" rel="noopener noreferrer"&gt;ofetch&lt;/a&gt; - a highly portable &lt;code&gt;fetch&lt;/code&gt; replacement&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://unjs.io/packages/nitro" rel="noopener noreferrer"&gt;nitro&lt;/a&gt; - the very thing that powers most of Nuxt's server-side capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tools are &lt;em&gt;everywhere&lt;/em&gt;. Staying with Nuxt here for a second, it sometimes feels like Nuxt is simply Vue plus a bunch of UnJS packages and some glue code. Excellent work by these people, if you ask me.&lt;/p&gt;

&lt;p&gt;These libraries work agnostic of your build tool/bundler, but they don't directly &lt;em&gt;integrate&lt;/em&gt; with them. That's where the, at least in my humble opinion, magnum opus of the UnJS team comes in: &lt;a href="https://unjs.io/packages/unplugin" rel="noopener noreferrer"&gt;unplugins&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  I know what a plugin is - but what's an unplugin?
&lt;/h2&gt;

&lt;p&gt;Great question!&lt;/p&gt;

&lt;p&gt;I could imagine that the UnJS people had a look at some popular build tools and thought, "Most, if not all, of them use some hook system for plugins. Often, these hooks are named similarly. Why not unify them into a single plugin system?"&lt;/p&gt;

&lt;p&gt;And that's precisely what unplugin is: A unified system to hook into build tools. Authors of any unplugin only need to define the actual business logic (i.e., what does the plugin actually &lt;em&gt;do&lt;/em&gt;), and the unplugin system takes over the rest. It essentially defines a plugin for each supported build system, all of which contain the same logic the author has implemented. Let's compare the "legagcy plugin architecture" to the unplugin approach:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Without unplugin&lt;/th&gt;
&lt;th&gt;With unplugin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One codebase per plugin&lt;/td&gt;
&lt;td&gt;One logic factory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-specific APIs, lots of reading up on them&lt;/td&gt;
&lt;td&gt;Unified hooks, all behaving the same way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Higher maintenance&lt;/td&gt;
&lt;td&gt;Lower maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  An example using a starter template
&lt;/h2&gt;

&lt;p&gt;So, let's say we want to create a small plugin that replaces one word with another in the user's &lt;code&gt;main.ts&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;To get started, it's advised to use a template. Luckily, the folks over at UnJS have created a &lt;a href="https://github.com/unplugin/unplugin-starter" rel="noopener noreferrer"&gt;starter template&lt;/a&gt; for us that we can use by executing these commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit unplugin/unplugin-starter my-unplugin
&lt;span class="nb"&gt;cd &lt;/span&gt;my-unplugin
npm i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will clone the starter repository into a folder called &lt;code&gt;my-unplugin&lt;/code&gt;. It creates everything we need for a working unplugin.&lt;/p&gt;

&lt;p&gt;And lo and behold, it even includes our basic unplugin already! When we open &lt;code&gt;src/index.ts&lt;/code&gt;, we see the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UnpluginFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Options&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createUnplugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unpluginFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UnpluginFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Options&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin-starter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;transformInclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__UNPLUGIN__&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Hello Unplugin! &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unplugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* #__PURE__ */&lt;/span&gt; &lt;span class="nf"&gt;createUnplugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unpluginFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;unplugin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, there's a ton to unpack here. Unplugins are written by creating a factory function that takes a bunch of options. The function returns the unplugin's definition. Using some generic hooks (in this case, &lt;code&gt;transformInclude&lt;/code&gt; and &lt;code&gt;transform&lt;/code&gt;, we can do all sorts of things. In these hooks, we specify what will be executed when the user's build tool runs them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;transformInclude&lt;/code&gt; checks if a given file name (that's what &lt;code&gt;id&lt;/code&gt; is) should be transformed in the first place. If true, the &lt;code&gt;transform&lt;/code&gt; function then receives the contents of that file and returns a transformed version. In our case, we replace &lt;code&gt;__UNPLUGIN__&lt;/code&gt; with &lt;code&gt;Hello Unplugin!&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So if the user's project's &lt;code&gt;main.ts&lt;/code&gt; would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__UNPLUGIN__&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The built &lt;code&gt;main.js&lt;/code&gt; would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello Unplugin!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  And how does it now create the build tool specific stuff?
&lt;/h2&gt;

&lt;p&gt;Again, great question! We notice a bunch of other files in the &lt;code&gt;src/&lt;/code&gt; directory. They're named after the build tool they're for, for example, &lt;code&gt;vite.ts&lt;/code&gt;, &lt;code&gt;astro.ts&lt;/code&gt;, &lt;code&gt;webpack.ts&lt;/code&gt; and so on.&lt;/p&gt;

&lt;p&gt;Let's have a look at &lt;code&gt;vite.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createVitePlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unpluginFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;createVitePlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unpluginFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is it really &lt;em&gt;that&lt;/em&gt; simple? Let's look at &lt;code&gt;webpack.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createWebpackPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unplugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unpluginFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;createWebpackPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unpluginFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yup, seems like!&lt;/p&gt;

&lt;p&gt;What the unplugin library does here is take the factory and create a plugin from it, with specialised functions. Ideally, we don’t even need to touch these files, ever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkycc71glrip36gjf15g9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkycc71glrip36gjf15g9.png" alt="A diagram showing the workflow/architecture of unplugin" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sounds good - but what can we actually &lt;em&gt;do&lt;/em&gt; with this?
&lt;/h2&gt;

&lt;p&gt;Well, the possibilities are &lt;em&gt;endless&lt;/em&gt;. Here's a list of all supported hooks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;Rollup&lt;/th&gt;
&lt;th&gt;Vite&lt;/th&gt;
&lt;th&gt;webpack&lt;/th&gt;
&lt;th&gt;esbuild&lt;/th&gt;
&lt;th&gt;Rspack&lt;/th&gt;
&lt;th&gt;Farm&lt;/th&gt;
&lt;th&gt;Rolldown&lt;/th&gt;
&lt;th&gt;Bun&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;enforce&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;buildStart&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resolveId&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loadInclude&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;load&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transformInclude&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transform&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;watchChange&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;buildEnd&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;writeBundle&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;(Source: official unplugin guide)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I want to especially point out three of these hooks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;resolveId&lt;/code&gt; - This one's used for resolving file names, i.e. path rewriting, directory aliasing and similar&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;load&lt;/code&gt; - Change how specific files (determined via &lt;code&gt;loadInclude&lt;/code&gt;) are loaded. You could potentially even fetch things from a CDN here&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;transform&lt;/code&gt; - Change code directly as a string. Replace, add, remove, compile, whatever you can think of&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, though, not all build tools support all hooks. But that's ok. You usually can find a way to circumvent this or create logic specific to these build tools. I strongly recommend reading the &lt;a href="https://unplugin.unjs.io/guide/" rel="noopener noreferrer"&gt;official guide&lt;/a&gt; for this.&lt;/p&gt;

&lt;p&gt;Here's some ideas from the top of my head:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A plugin that offers compiler macros like &lt;code&gt;CURRENT_YEAR&lt;/code&gt; that get replaced at build time&lt;/li&gt;
&lt;li&gt;Count all unique &lt;code&gt;padding&lt;/code&gt;s and &lt;code&gt;margin&lt;/code&gt;s in the code base to give the user an overview&lt;/li&gt;
&lt;li&gt;Automagic &lt;a href="https://en.wikipedia.org/wiki/Brainfuck" rel="noopener noreferrer"&gt;Brainf**k&lt;/a&gt; support!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How would a project now use this unplugin
&lt;/h2&gt;

&lt;p&gt;Like any other plugin, mostly. In Vite, for example, a user could do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MyUnplugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@my-company/my-unplugin/vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;MyUnplugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sounds good - are there any real life use cases?
&lt;/h2&gt;

&lt;p&gt;Indeed, there are! The most popular is &lt;a href="https://unplugin.unjs.io/showcase/unplugin-icons.html" rel="noopener noreferrer"&gt;unplugin-icons&lt;/a&gt;, which lets us install almost any icon in any project by installing, configuring, and using it.&lt;/p&gt;

&lt;p&gt;Another real-life example is what we’ve built during my time at Liip for the Swiss canton of Basel-Stadt: a design system with an installable plugin that lets other agencies create websites that automatically align with the canton's CI/CD. It does that by providing Tailwind, installing all necessary PostCSS plugins and delivering a ton of prebuilt utilities and CSS components. You can read up on it on &lt;a href="https://www.liip.ch/en/blog/using-unplugin-tailwind-storybook-and-figma-to-create-a-design-system-that-works" rel="noopener noreferrer"&gt;Liip's blog&lt;/a&gt;!&lt;/p&gt;




&lt;h2&gt;
  
  
  Sooo, should everyone be writing unplugins now?
&lt;/h2&gt;

&lt;p&gt;As we German-speaking people say: "Jein" (yes-and-no).&lt;/p&gt;

&lt;p&gt;My recommendation, based on experience, is that it's sensible for things expected to be used by many different projects, as it gives you the maximum amount of freedom with little to no downsides, aside from being forced to write agnostic code.&lt;/p&gt;

&lt;p&gt;Generally, the business logic could even live in its own package. Why not build a library that exports the functionality and use that as a dependency for an unplugin? That way, the library itself is encapsulated, testable and could be used for other purposes and in different contexts, too, even without the need for a build tool.&lt;/p&gt;

&lt;p&gt;If you're writing a Vite package for your own project that you're never going to open-source or that doesn't make any sense at all when used without Vite, though, an unplugin seems like overkill or even a hindrance at times.&lt;/p&gt;

&lt;p&gt;Nevertheless, what the people at UnJS built here is a fantastic piece of technology! The logical next step is to standardise build tool interfaces, much like Vite and most UnJS packages already do.&lt;/p&gt;

&lt;p&gt;Which package do you think would be worth building an unplugin for? &lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt;&lt;em&gt;! You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt; Or follow me on &lt;a href="https://bsky.app/profile/thormeier.dev" rel="noopener noreferrer"&gt;&lt;em&gt;Bluesky&lt;/em&gt; 🦋&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Old School Tech: How to Animate The Classic DVD Logo Bouncing 📀📐</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Thu, 08 Jan 2026 18:29:44 +0000</pubDate>
      <link>https://forem.com/thormeier/old-school-tech-how-to-animate-the-classic-dvd-logo-bouncing-11d9</link>
      <guid>https://forem.com/thormeier/old-school-tech-how-to-animate-the-classic-dvd-logo-bouncing-11d9</guid>
      <description>&lt;p&gt;It's been quite a while since my last post here - 2 years to be precise. And it's been a wild two years, I tell ya. I got married, switched jobs, had some personal issues, a &lt;em&gt;ton&lt;/em&gt; of projects, and way too many ups and downs to put them into a tech post intro.&lt;/p&gt;

&lt;p&gt;One of my goals for 2026 was to get back into writing and to continue the style of posts and content that I've been producing for many years before my 2-year writing hiatus. I'm working on quite a few posts, actually, but today, we'll start with a relatively slow one. Or something rather old, I should say.&lt;/p&gt;

&lt;h2&gt;
  
  
  Old? So, no AI?
&lt;/h2&gt;

&lt;p&gt;No. No AI. Quite the contrary. If you've read the title, and I'm sure you did, otherwise you wouldn't have clicked, we're going to build a little something called the "DVD logo animation".&lt;/p&gt;

&lt;h2&gt;
  
  
  The what?
&lt;/h2&gt;

&lt;p&gt;... please don't tell me I'm &lt;em&gt;that&lt;/em&gt; old by now.&lt;/p&gt;

&lt;p&gt;There used to be a time when we would rent or buy movies not by clicking on them, but physically acquiring a round shiny object with a hole in the middle: A DVD. DVD used to stand for "Digital Video Disc", but since many companies (and private people) started putting non-movie data on them, the acronym today stands for "Digital Versatile Disc".&lt;/p&gt;

&lt;p&gt;To read data off them, we used special devices called "DVD players". Most of them, when you plugged them into your TV and turned them on, would show the DVD logo bouncing around the screen at perfect 45-degree angles. Some people may even remember sitting in front of this thing, waiting for it to bounce right into the corner of the screen.&lt;/p&gt;

&lt;p&gt;The inspiration for this came from a way-too-long YouTube session (ironic, isn't it?) where I saw someone build this animation out of Lego.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's get coding already!
&lt;/h2&gt;

&lt;p&gt;Alright, alright, enough nostalgia. We start by acquiring the DVD logo as an SVG. Luckily, Wikimedia is just the place to look for that.&lt;/p&gt;

&lt;p&gt;We then put it into a small little HTML file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"screen"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"logo"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 1058.4 465.84"&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#fff"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(I spare you the &lt;code&gt;d&lt;/code&gt; value of that path, it's really not the most interesting thing.)&lt;/p&gt;

&lt;p&gt;We then add a bit of styling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.screen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80vw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.logo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12vw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will add a white rectangle centered on the screen, with the logo placed in the top-right corner. The &lt;code&gt;12vw&lt;/code&gt; just felt right; there's no science or any sort of measurement behind that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Animating the whole thingie
&lt;/h2&gt;

&lt;p&gt;We're going to do that with plain ol' Vanilla JS. Yes, everything in this post is old school.&lt;/p&gt;

&lt;p&gt;Here's the principle: We change the top and left coordinates by 1 pixel per animation frame. CSS will ensure the animation is as smooth as it can be. By adding 1 pixel to the top and left value, the logo will move to the bottom-right at a 45-degree angle.&lt;/p&gt;

&lt;p&gt;Once the logo reaches the bottom of the screen, it should bounce back up while continuing to move to the right. This means the value of change for the top coordinate is now -1, not 1.&lt;/p&gt;

&lt;p&gt;Once the logo hits the right edge, the left value turns to -1 (since it should bounce). Once it hits the top edge, the top change value is set to 1 again, etc.&lt;/p&gt;

&lt;p&gt;We call these values a &lt;em&gt;delta&lt;/em&gt; - so, a "rate of change". Initially, since the logo starts in the top left corner of the screen, these deltas are &lt;code&gt;1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;topDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;leftDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we need to define the time between frames. 5ms gives a pretty smooth transition that isn't too fast. We'll use an interval to update the position repeatedly. In there, we first need to get the current top, bottom, left, and right values of the logo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentRight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentLeft&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentBottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentTop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientHeight&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now define the actual animation logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentBottom&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;topDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTop&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;topDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentRight&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;leftDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;leftDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, we need to update the position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentTop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;topDelta&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentLeft&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;leftDelta&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And done! That was a quick one! Let's see it in action:&lt;/p&gt;

&lt;p&gt;

&lt;iframe height="600" src="https://codepen.io/thormeier/embed/PwzNrgO?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;Now, the nice thing is: This already is fully responsive. You can change your screen size, rotate the screen in CSS, or even resize it. The animation will always work just the same way.&lt;/p&gt;

&lt;p&gt;Let's take a moment and appreciate when the DVD logo finally hits that corner!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt;&lt;em&gt;! You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Beating annoying minigames with Java☕ - Or: How to create a smart auto-clicker 🤖🎮</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Mon, 08 Jan 2024 11:49:36 +0000</pubDate>
      <link>https://forem.com/thormeier/beating-annoying-minigames-with-java-or-how-to-create-a-smart-auto-clicker-2gog</link>
      <guid>https://forem.com/thormeier/beating-annoying-minigames-with-java-or-how-to-create-a-smart-auto-clicker-2gog</guid>
      <description>&lt;p&gt;Last week was slow. After a rather busy holiday season, I felt like I needed to relax a bit (pro tip: do that occasionally; it does help!) and booted up my PS4 for the first time in months. My game of choice was one that I already played &lt;em&gt;ad nauseam&lt;/em&gt; when I was a kid: &lt;a href="https://finalfantasy.fandom.com/wiki/Final_Fantasy_X" rel="noopener noreferrer"&gt;Final Fantasy 10&lt;/a&gt;; only I was playing the HD remaster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait, is this a story, or are you coding something today?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;There will be code, promise! But to understand what the code is supposed to do, we first need to understand the problem, which is a rather specific one. Bear with me!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As with most RPGs by Square Enix, and with most Final Fantasy parts in general, the end game, right before the final story boss, is where most of the gameplay is hidden. I got there some time ago and started collecting the ultimate gear for all party members, levelling them up, etc., to face a hidden final super boss called &lt;a href="https://finalfantasy.fandom.com/wiki/Penance_(Final_Fantasy_X)" rel="noopener noreferrer"&gt;"Penance"&lt;/a&gt;. This can easily take up dozens of hours and includes a lot of repetitive gameplay, but alas. I like numbers going up.&lt;/p&gt;

&lt;p&gt;The player must beat a series of minigames to acquire the ultimate gear for all party members. Some of them can be done in half an hour, and others require days of work and perfect button input to beat them, a single wrong input resetting the progress of the minigame so far.&lt;/p&gt;

&lt;p&gt;One of these is called "Dodge the Lightning." In a specific region of the game world, called the "Thunder Plains", there are occasional lightning strikes to the player character that stun them for a couple of seconds. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa8qzk4eu2eaxd34kech.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa8qzk4eu2eaxd34kech.jpg" alt="Image of the Thunder Plains in-game, property of " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;This image of the Thunder Plains is taken from the &lt;a href="https://finalfantasy.fandom.com/wiki/Thunder_Plains_(Final_Fantasy_X)" rel="noopener noreferrer"&gt;Final Fantasy Wiki&lt;/a&gt;, all rights to their respective owners.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lightning is announced by a screen flash of around 400ms (believe it or not, I measured), followed by a roughly 1-second period. The character dodges the lightning if the player hits the cross button within that timeframe.&lt;/p&gt;

&lt;p&gt;Now, to beat that minigame, the player needs to dodge 200 (two hundred) lightning strikes. In succession. Without a single mistake. In one go.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ugh.
&lt;/h2&gt;

&lt;p&gt;Right? The most I've managed was 47. The frustration was real. I was already spending hours waiting for a flash of light blue on the screen and pressing the button at the right moment. If a single car outside drew my attention a little too much, I missed the time window and had to start over.&lt;/p&gt;

&lt;p&gt;This wasn't fun anymore. Or relaxing. At all.&lt;/p&gt;

&lt;p&gt;I got up to get a coffee (haha, in a Java post, get it? Just kidding, I actually did get a coffee...) and started thinking: If this challenge is so dull and not fun, how could I alter it to make it more fun and fit my skill set better? I'm not used to precision work for hours on end.&lt;/p&gt;

&lt;p&gt;So, if I can't do it, perhaps a machine can?&lt;/p&gt;
&lt;h2&gt;
  
  
  Opening up the borders
&lt;/h2&gt;

&lt;p&gt;First challenge: How was I supposed to execute &lt;em&gt;anything&lt;/em&gt; on the PS4? Running custom software on a console is tricky, and I did not want to tamper with my beloved PS4 too much. I didn't want to lose any game progress or, worse, brick it entirely. So, running software on the console was out.&lt;/p&gt;

&lt;p&gt;Luckily, Sony has this nifty thing called "Remote Play" that lets users connect to their console using a computer (as if the console &lt;em&gt;isn't&lt;/em&gt; a computer). The screen output is then visible in a window on the computer and can be interacted with. &lt;/p&gt;

&lt;p&gt;For example, streamers on Twitch and YouTube do that a lot. Installing streaming software on a PS is more tedious than using an existing setup, grabbing the screen and streaming that.&lt;/p&gt;

&lt;p&gt;However, there is a little issue: Remote Play no longer allows cross-button input in the latest build.&lt;/p&gt;

&lt;p&gt;So I used &lt;a href="https://sr.ht/~thestr4ng3r/chiaki/" rel="noopener noreferrer"&gt;Chiaki&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;Chiaki is a Remote Play protocol client and allows &lt;em&gt;any&lt;/em&gt; button input. Even with custom key mapping. Perfect!&lt;/p&gt;
&lt;h2&gt;
  
  
  Letting the machine do the job
&lt;/h2&gt;

&lt;p&gt;Once I played PS on my PC (this sentence alone was already an adventure), I got to coding. I needed to detect the screen flash and get the timing just right to press the cross button, which I mapped to the "Enter" key.&lt;/p&gt;

&lt;p&gt;I decided on Java since Java's AWT has a built-in class called "Robot" that does exactly what I wanted.&lt;/p&gt;

&lt;p&gt;Step one was to figure out where to take a sample from. I decided to use the centre of the screen since I would put the PS4 Chiaki window roughly there anyway:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: I'm leaving out any import statements since this is bog-standard Java stuff that doesn't require any additional dependency.)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;Dimension&lt;/span&gt; &lt;span class="n"&gt;screenSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Toolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultToolkit&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getScreenSize&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screenSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWidth&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screenSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHeight&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sampleX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
      &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sampleY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;//More code later&lt;/span&gt;

    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Oh noes: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, I needed the Robot to sample the screen and repeatedly wait for a specific brightness. Through trial and error (quite literally), I figured that a brightness value of 75% was ideal. The screen would get bright quickly for about a frame or two, every other frame is either build-up (brightness increasing) or fade-out (brightness decreasing).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;Dimension&lt;/span&gt; &lt;span class="n"&gt;screenSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Toolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultToolkit&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getScreenSize&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screenSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWidth&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screenSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHeight&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sampleX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
      &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sampleY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

      &lt;span class="nc"&gt;Robot&lt;/span&gt; &lt;span class="n"&gt;robot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Robot&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPixelColor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sampleX&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sampleY&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;hsbColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RGBtoHSB&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRed&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getGreen&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBlue&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hsbColor&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Screen flash detected, time to spam-click!&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Oh noes: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect. This was already working quite reliably. &lt;/p&gt;

&lt;p&gt;We're using the HSB colour space (which stands for "Hue, Saturation, Brightness") to determine the brightness value.&lt;/p&gt;

&lt;p&gt;I've explained the HSB (or HLS in some cases) in my &lt;a href="https://dev.to/thormeier/let-s-build-a-rainbow-on-a-canvas-from-scratch-40l5"&gt;post on how to create a rainbow from scratch 🌈📐&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In Java, a &lt;code&gt;Color&lt;/code&gt; object is always in the RGB (Red, Green, Blue) colour space, whereas the static function &lt;code&gt;RGBtoHSB&lt;/code&gt; returns a float array that contains the hue of the colour at index &lt;code&gt;0&lt;/code&gt;, the saturation at index &lt;code&gt;1&lt;/code&gt; and the brightness at index &lt;code&gt;2&lt;/code&gt;. The brightness value is between 0 and 1, indicating a percentage.&lt;/p&gt;

&lt;p&gt;I also added a sleep of 16ms to the loop to prevent wasting CPU cycles on checking frames that didn't change. Chiaki allows setting the frames per second (FPS) of the mirrored screen, which was either 30 or 60. To get the timing right, I needed 60 FPS. A second has 1000 milliseconds. At 60 FPS, every frame is up for roughly 16-17ms.&lt;/p&gt;

&lt;p&gt;This code detected all of the screen flashes, and through the magic of debugging, I was confident that I wouldn't miss any of the lightning strikes.&lt;/p&gt;

&lt;p&gt;Last but not least, I needed to add the actual button press. Luckily, the &lt;code&gt;Robot class&lt;/code&gt; can do that too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;Dimension&lt;/span&gt; &lt;span class="n"&gt;screenSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Toolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultToolkit&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getScreenSize&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screenSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWidth&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screenSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHeight&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sampleX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
      &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sampleY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

      &lt;span class="nc"&gt;Robot&lt;/span&gt; &lt;span class="n"&gt;robot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Robot&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

      &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;dodged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPixelColor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sampleX&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sampleY&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;hsbColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RGBtoHSB&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRed&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getGreen&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBlue&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hsbColor&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Simulates reaction time of a really _really_ fast human.&lt;/span&gt;
          &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keyPress&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KeyEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;VK_ENTER&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// More or less the lower bound of the average length of a key press.&lt;/span&gt;
            &lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keyRelease&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KeyEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;VK_ENTER&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// A little waiting time between releasing the button and pressing it again.&lt;/span&gt;
          &lt;span class="o"&gt;}&lt;/span&gt;

          &lt;span class="n"&gt;dodged&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
          &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dodged: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;dodged&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Oh noes: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, what happens here precisely? Once a lightning strike is detected, we wait for around 120ms, which simulates a ridiculously fast human, and then we spam-click the "Enter" button. Since the &lt;code&gt;Robot&lt;/code&gt; class method &lt;code&gt;keyPress&lt;/code&gt; is actually a &lt;code&gt;keyDown&lt;/code&gt;, we also need to call &lt;code&gt;keyRelease&lt;/code&gt; because it otherwise registers as a continuous button press. &lt;/p&gt;

&lt;p&gt;This, however, lets us steer the duration of the key press. Some sources suggest that the average key press is between 50 and 300ms for humans, so I figured if the Robot is already reacting at twice the average human reaction speed, why not give it super-human button-pressing abilities as well?&lt;/p&gt;

&lt;p&gt;Last but not least, the spam-clicking is essential here. If the network connection or Chiaki has a bit of delay, the spam-clicking guarantees that at least one button press gets through to the game.&lt;/p&gt;

&lt;h2&gt;
  
  
  A word of caution: Beware of auto-clickers
&lt;/h2&gt;

&lt;p&gt;During development, I once made the mistake of opening a different window with a white background. White means a brightness of 100%, which is larger than 75%, which means spam-clicking Enter.&lt;/p&gt;

&lt;p&gt;Needless to say, this almost wreaked havoc by &lt;em&gt;constantly&lt;/em&gt; pressing Enter. It started all sorts of other apps, basically behaving like a fork bomb after mere seconds. My CPU almost melted. And worst of all, I missed a lightning strike and had to start over.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, did the auto-clicker work in the end?
&lt;/h2&gt;

&lt;p&gt;Yes. And just for the flex, I dodged 222 lightning bolts.&lt;/p&gt;

&lt;p&gt;Take that, game!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt;&lt;em&gt;! You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>programming</category>
      <category>automation</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Coding with crustaceans?🦐 - CodeLobster IDE🦞 review</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Tue, 19 Dec 2023 10:36:14 +0000</pubDate>
      <link>https://forem.com/thormeier/coding-with-crustaceans-codelobster-ide-review-la5</link>
      <guid>https://forem.com/thormeier/coding-with-crustaceans-codelobster-ide-review-la5</guid>
      <description>&lt;p&gt;You probably know how much I like to try new things. I've published several posts about how I tried new tools, such as &lt;a href="https://dev.to/thormeier/i-m-going-to-give-snowpack-a-try-now-3ohm"&gt;Snowpack&lt;/a&gt;, and have built stuff that probably shouldn't exist, such as &lt;a href="https://dev.to/thormeier/let-s-do-machine-learning-in-scratch-h1c"&gt;Linear Regression in Scratch&lt;/a&gt;, or everyone's favourite monstrosity, &lt;a href="https://dev.to/thormeier/dont-try-this-at-home-css-as-the-backend-what-3oih"&gt;Cascading Server Sheets&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, rarely have I touched my toolchain. Especially my IDE. So, when I was approached by the team over at CodeLobster Software, I figured I should give this tool a try! So here's a review of CodeLobster IDE!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer: This post is sponsored by CodeLobster IDE, but I'll give it an honest review nevertheless. To me, delivering value to you is important, so if you think this tool looks interesting, give it a shot!&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The best way to figure out if a tool works is to actually use it. CodeLobster IDE has built-in support for a ton of frameworks, including, but not limited to, Symfony, Drupal, Angular, React, Vue, Magento and Laravel, so the web devs out there should feel at home.&lt;/p&gt;

&lt;p&gt;For this review, I've built a small Symfony app that would require the user to enter their location and get a weather forecast from an API. I won't go much into the details of this process, but I'll talk about the things I encountered along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the IDE
&lt;/h2&gt;

&lt;p&gt;First things first: Let's acquire the tool. We can download CodeLobster IDE on &lt;a href="http://www.codelobsteride.com" rel="noopener noreferrer"&gt;their website, http://www.codelobsteride.com&lt;/a&gt;. They offer versions for Windows, Linux (both .rpm and .deb packages), and MacOS. There is a free tier, but they're currently (as of November 2023) offering discounts on their Pro versions as well.&lt;/p&gt;

&lt;p&gt;In this post, we'll work with the Pro version on Arch Linux (&lt;a href="https://github.com/overmighty/i-use-arch-btw" rel="noopener noreferrer"&gt;I use Arch, btw&lt;/a&gt;) to cover as many of the features as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first glance
&lt;/h2&gt;

&lt;p&gt;The IDE itself is relatively simple. It resembles most other IDEs, which I don't find surprising. After all, the concept has worked so far, so why change it? It doesn't have any extra bells and whistles so far. Although the icons could use a bit of a brush-up, the overall look is tidy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vvrhg7paa1lobba65hk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2vvrhg7paa1lobba65hk.png" alt="A screenshot of the freshly booted up CodeLobster ID. It shows the window without open files; most icons are disabled." width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When we look at the second row of icons, we see things like a paragraph, an image, a table, the letter J, a colour palette, a line feed, and much more.&lt;/p&gt;

&lt;p&gt;Turns out, when we create a fresh new HTML file, these icons are shortcuts for often-used elements! &lt;/p&gt;

&lt;p&gt;Clicking on them directly adds the code to the HTML file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frrawq2qtrhbncue8j3nt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frrawq2qtrhbncue8j3nt.gif" alt="A GIF showing how clicking the buttons adds code to the HTML file." width="600" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now I wonder two things: a) Can I customize these, and b) are there keyboard shortcuts? But for the moment, I think I digress.&lt;/p&gt;

&lt;p&gt;However, my eyes feel a bit sore. CodeLobster IDE has a bunch of presets for the entire visual style of the app. Some look better than others, but that's a preference. For the nostalgic devs among us, there's even a theme called "Eclipse". And boy, oh boy, do I not like &lt;em&gt;that&lt;/em&gt; theme. Brings back some of the not-so-desired memories.&lt;/p&gt;

&lt;p&gt;However, I'll stick to the "AtomOne Dark" theme for the rest of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a project
&lt;/h2&gt;

&lt;p&gt;Now that we've eased my random-button-pressing urges let's start with the project. When Clicking on "File", hovering "New", and clicking on "Project", CodeLobster IDE greets me with a simple form. It lets me select the framework, if any, the location, and a name. It even shows me a "Project URL for debugging (F5)", which I'm intrigued to check out. After all, F5 is also only just a button.&lt;/p&gt;

&lt;p&gt;Selecting the "Symfony" framework continues the wizard. I can either create a project based on version 5.x or 4.4 of the framework. Clicking on "Next" asks for some sensible defaults, such as the installation type, which project template to use, if I want a database, and, if so, how to connect to it. Once done, I click on "Finish", and the IDE starts rolling the setup.&lt;/p&gt;

&lt;p&gt;It installs Symfony and opens up the project folder. Nice! This is a feature I lack from most IDEs I've used so far: A sensible default set of frameworks that just work. Some IDEs offer some boilerplates, but to specifically create a Symfony, Laravel or Drupal project, you'd need to do most of the setup yourself or install extra plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Language support
&lt;/h2&gt;

&lt;p&gt;First things first: I want to look at something. Ideally, something served by Symfony. So I create a PHP file in &lt;code&gt;src/Controller&lt;/code&gt; called &lt;code&gt;HomeController.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The PHP template itself is minimal, offering an opening and a closing tag. Given, the templates can be edited, but what I'm missing a bit here are custom templates or more sophisticated templates, such as class files that could automatically determine the namespace and such.&lt;/p&gt;

&lt;p&gt;Once done importing the necessary dependencies, such as an entity, the &lt;code&gt;AbstractController&lt;/code&gt; class, and the class &lt;code&gt;TextType&lt;/code&gt; to build the form, I start to create the form.&lt;/p&gt;

&lt;p&gt;The auto-complete feature is rather decent, offering IntelliSense and dedicated Symfony support, although only in the Pro version.&lt;/p&gt;

&lt;p&gt;Once done with the controller, I create a Twig file to render it. Again, the Twig support is decent! The syntax highlighting works very well (even a little more detailed than with some editors and IDEs), and I finally get to use the shortcut buttons mentioned earlier!&lt;/p&gt;

&lt;p&gt;YAML support works well, too, although I'd wish for a bit more of the &lt;code&gt;Ctrl + click&lt;/code&gt; support that I'm so used to from any JetBrains IDE. For example, when defining routes, I'd really like to be able to Ctrl+click on the fully qualified class name and jump to the respective file.&lt;/p&gt;

&lt;h2&gt;
  
  
  After building the thing
&lt;/h2&gt;

&lt;p&gt;To be honest, I need to get used to CodeLobster IDE. Coming from another editor makes things a little difficult, but that's to be expected.&lt;/p&gt;

&lt;p&gt;In general, I had a great time building with this IDE, and CodeLobster has supported me well along the way. I think though, some features are either missing or I haven't found them. For example, I'm missing standard auto-save a bit. But what I think I miss most is the ability to &lt;code&gt;Ctrl+click&lt;/code&gt; absolutely everything. From file paths to classes and variables. Sometimes, it allows &lt;code&gt;Ctrl+click&lt;/code&gt; on things that I don't think are supposed to be clickable, such as a &lt;code&gt;(bool)&lt;/code&gt; typecast that leads to an environment configurator from Symfony itself.&lt;/p&gt;

&lt;p&gt;What surprises me the most is the balance of features the developers of CodeLobster IDE have found. This is very subjective, but to me, it feels less cluttered and overloaded than, say, PHPStorm or Eclipse (if anyone out there actually still uses Eclipse, that is) but more complete than Visual Studio Code with a bare installation.&lt;/p&gt;

&lt;p&gt;Using the built-in Symfony support of the Pro version had another advantage: There's an extra GUI for commands with a tree-like browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F28r8pquoqoqoh186ogww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F28r8pquoqoqoh186ogww.png" alt="The Symfony command GUI" width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This nifty little tool helps to gain an overview of which commands actually exist and what they do. Instead of going through them one by one on the CLI, a simple click shows you the expected arguments and what the command does.&lt;/p&gt;

&lt;p&gt;Drupal, for example, also offers a "Create module" functionality - something that's usually a bit tedious. I'd wish they would add more of these functionalities for other frameworks, such as Vue or Nuxt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ecosystem
&lt;/h2&gt;

&lt;p&gt;During my journey to building the app, I found myself looking for advice on A Popular Search Engine™ pretty often. The one thing that kept popping up was the CodeLobster IDE forums. The community is pretty active, with a total of around 26 thousand registered members, roughly 2800 topics and 10 thousand posts.&lt;/p&gt;

&lt;p&gt;There are dedicated subforums for support, advice and desires, and news about CodeLobster IDE and the company behind it. The CodeLobster IDE team is active in the forum and answers questions. The community itself seems welcoming and very much on the topic.&lt;/p&gt;

&lt;p&gt;And let's be real here: An oldschool phpBB board? That brings back good old memories, doesn't it?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpw25m21nvfzi7gsl4apy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpw25m21nvfzi7gsl4apy.png" alt="CodeLobster IDE forums - nostalgic and helpful!" width="800" height="748"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The IDE itself works. It works really, really well; it's just the developers' decisions on some of the details that may cause some frustration if you're used to some other editor. Unfortunately, the IDE doesn't offer very much customization for those things, but if you're going to rebuild another IDE entirely with customization, why switch in the first place?&lt;/p&gt;

&lt;p&gt;I found a pitfall or two (I needed to explicitly set where it could find the PHP executable, for example), but those aren't game-breakers at all.&lt;/p&gt;

&lt;p&gt;So, if you're looking for a new IDE, give CodeLobster a try. There's a free version, and once you're used to it, try out the Pro version!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt; &lt;em&gt;or&lt;/em&gt; &lt;a href="https://twitter.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;follow me on Twitter&lt;/em&gt; 🐦&lt;/a&gt;&lt;em&gt;!&lt;/em&gt; &lt;em&gt;You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>review</category>
      <category>tooling</category>
    </item>
    <item>
      <title>⚠️ Don't try this at home: A CMS written in Bash ONLY??</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Sun, 03 Sep 2023 17:02:26 +0000</pubDate>
      <link>https://forem.com/thormeier/dont-try-this-at-home-a-cms-written-in-bash-only-4j6i</link>
      <guid>https://forem.com/thormeier/dont-try-this-at-home-a-cms-written-in-bash-only-4j6i</guid>
      <description>&lt;p&gt;Here we go again.&lt;/p&gt;

&lt;p&gt;After building an image modal with CSS only (and completely neglecting accessibility (sorry, &lt;a class="mentioned-user" href="https://dev.to/grahamthedev"&gt;@grahamthedev&lt;/a&gt;)) and an attempt to establish CSS as a backend language (although it worked, you people didn't like it very much for some reason. I wonder why.), we're finally back with stuff you and I probably shouldn't ever do. &lt;/p&gt;

&lt;p&gt;Today: &lt;strong&gt;Let's use Bash (yes, Bash) to create a CMS!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pascal no
&lt;/h2&gt;

&lt;p&gt;Pascal yes! Pascal always yes!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To give credit where credit is due&lt;/strong&gt;: This idea came from a funny discussion with a co-worker about how to overcomplicate stuff. He came up with a "bash server", and we started to exaggerate it more and more until I said, "Challenge accepted," and here we are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; We'll neglect security, apart from some password encryption and most best practices. This tool will be something I'll never ever ever use in production, and you, dear reader, should not do it either. Please. There. You've been warned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining what it does
&lt;/h2&gt;

&lt;p&gt;We'll create a very basic CMS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user can log in and log out (so we need users and session handling)&lt;/li&gt;
&lt;li&gt;A logged-in user can create, update and delete pages&lt;/li&gt;
&lt;li&gt;An anonymous user can read all pages&lt;/li&gt;
&lt;li&gt;A page consists of a navigation title, a route path, some markup and a flag if it should show up in the main navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, to use Bash as a backend language, we first need to get it to handle HTTP requests. Luckily, there are Bash utilities we can use to listen to TCP requests and send responses, most notably &lt;code&gt;netcat&lt;/code&gt;. We "only" need to parse a request and generate a response.&lt;/p&gt;

&lt;p&gt;Once that's working, we'll use SQLite to load the requested page and render its markup.&lt;/p&gt;

&lt;p&gt;Let's move on to the first bits of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database schema
&lt;/h2&gt;

&lt;p&gt;The boring bits first. We'll use the following database schema for our SQLite database and add a few default records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;routePath&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;navTitle&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;isInMainNavigation&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;markup&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Home'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h2&amp;gt;Hello, Bash CMS!&amp;lt;/h2&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/about'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'About'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h2&amp;gt;About&amp;lt;/h2&amp;gt;&amp;lt;p&amp;gt;This page was created entirely with BashCMS!&amp;lt;/p&amp;gt; &amp;lt;p&amp;gt;&amp;lt;a href="/about/bash-cms"&amp;gt;Learn more&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/about/bash-cms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;h2&amp;gt;About BashCMS&amp;lt;/h2&amp;gt;&amp;lt;p&amp;gt;BashCMS is a CMS entirely written in Bash!&amp;lt;/p&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="cm"&gt;/* Hash generated with sha256sum */&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fc8252c8dc55839967c58b9ad755a59b61b67c13227ddae4bd3f78a38bf394f7'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cm"&gt;/* pw: admin */&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;sessId&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;userRowId&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can access the database via the &lt;code&gt;sqlite3&lt;/code&gt; CLI tool, which we can feed with data from our Bash script.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual server
&lt;/h2&gt;

&lt;p&gt;Let's start with the Bash part now. To listen to HTTP requests, we use Netcat. We'll be using the OpenBSD Netcat version.&lt;/p&gt;

&lt;p&gt;In the listen mode, Netcat is interactive. It prints out the request details (i.e. the headers, the body, the HTTP method and all) on STDOUT and expects the user to write the response in the STDIN. &lt;/p&gt;

&lt;p&gt;For those unfamiliar with Linux/Bash, STDIN and STDOUT are the default ways to communicate with a program. What we see in the terminal is usually STDOUT, and the keyboard input is STDIN. A program is allowed to read from STDIN and write to STDOUT.&lt;/p&gt;

&lt;p&gt;Once we send something to Netcat, it sends that over the wire and terminates. This means that we can only handle a single request at a time. For the server to run continuously, we need to start Netcat again and let it listen after it has terminated.&lt;/p&gt;

&lt;p&gt;To read and write from Netcat, we also need a way to programmatically read STDOUT and write to STDIN. We can do this using a utility called &lt;code&gt;coproc&lt;/code&gt;, which executes a given command asynchronously in a subshell. We do nothing as long as Netcat is waiting for some incoming requests. Only once Netcat starts to write to the STDOUT do we start reading and save it to a variable. &lt;/p&gt;

&lt;p&gt;There is, however, one small problem: Netcat does not tell us if and when it's finished writing to STDOUT. We need to determine that ourselves. The most straightforward approach is to wait for an empty new line and stop there.&lt;/p&gt;

&lt;p&gt;We basically end up with a structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="c"&gt;# You know your script will be fun when it starts with an endless loop.&lt;/span&gt;
&lt;span class="k"&gt;do
  &lt;/span&gt;coproc nc &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 1440 &lt;span class="nt"&gt;-q1&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="c"&gt;# Spawns netcat such that we can read from and write to it&lt;/span&gt;

  &lt;span class="nv"&gt;REQ_RAW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="c"&gt;# This will contain the entire request&lt;/span&gt;
  &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="c"&gt;# Delimiter for `read`&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; TMP&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="c"&gt;# Read every line from STDOUT&lt;/span&gt;
    REQ_RAW+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="c"&gt;# Append the line to the REQ variable&lt;/span&gt;

    &lt;span class="c"&gt;# If the length of TMP is equal to one byte&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;TMP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 1 &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;break
    &lt;/span&gt;&lt;span class="k"&gt;fi
  done&lt;/span&gt; &amp;lt;&amp;amp;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COPROC&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Reads from the coproc STDOUT, line by line&lt;/span&gt;

  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$REQ_RAW&lt;/span&gt; &lt;span class="c"&gt;# Output the request for testing purposes&lt;/span&gt;

  &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COPROC_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Kill the process for the subsequent request&lt;/span&gt;
  &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COPROC_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Wait until it's actually gone&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're really unfamiliar with Bash, this looks intimidating. It might even do so for people who &lt;em&gt;know&lt;/em&gt; Bash. I learned a lot during this experiment, and I am deeply convinced that Bash works very much like quantum physics: One does not understand Bash; one gets used to it.&lt;/p&gt;

&lt;p&gt;Back to business... The "empty line" approach breaks down as soon as we want to read the HTTP body in case of a POST request. Luckily, HTTP knows a header called &lt;code&gt;Content-Length&lt;/code&gt; that tells us the exact number of bytes.&lt;/p&gt;

&lt;p&gt;This blows up the code for our server tremendously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;do
  &lt;/span&gt;coproc nc &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 1337 &lt;span class="nt"&gt;-q1&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="c"&gt;# Spawns netcat such that we can read from and write to it. Listens on port 1337.&lt;/span&gt;

  &lt;span class="nv"&gt;REQ_RAW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="c"&gt;# This will contain the entire request&lt;/span&gt;
  &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="c"&gt;# Delimiter for `read`&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; TMP&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;REQ_RAW+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="c"&gt;# Append the line to the REQ variable&lt;/span&gt;

    &lt;span class="nv"&gt;TMPLEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TMP&lt;/span&gt;|wc &lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="c"&gt;# Figure out the length of $TMP in bytes&lt;/span&gt;

    &lt;span class="c"&gt;# Deduct the length of the read bytes from the rest of the body length&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$BODYLENGTH&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="c"&gt;# Still some body left to read&lt;/span&gt;
      &lt;span class="nv"&gt;BODYLENGTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;BODYLENGTH &lt;span class="o"&gt;-&lt;/span&gt; TMPLEN&lt;span class="k"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;

    &lt;span class="c"&gt;# If the request has a body (determined by the header, which is usually the last one)&lt;/span&gt;
    &lt;span class="c"&gt;# We continue reading the exact number of bytes&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^&lt;span class="s2"&gt;"Content-Length: "&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;BODYLENGTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;|grep &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'[[:digit:]]\+'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;HAS_BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
    &lt;span class="k"&gt;fi&lt;/span&gt;

    &lt;span class="c"&gt;# Read the entire body; abort reading&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$HAS_BODY&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 1 &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$BODYLENGTH&lt;/span&gt; &lt;span class="nt"&gt;-le&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;break
    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

    &lt;span class="c"&gt;# No body but empty line encountered, abort reading&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$HAS_BODY&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$TMPLEN&lt;/span&gt; &lt;span class="nt"&gt;-le&lt;/span&gt; 2 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;break
    &lt;/span&gt;&lt;span class="k"&gt;fi
  done&lt;/span&gt; &amp;lt;&amp;amp;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COPROC&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Reads from the coproc STDOUT, line by line&lt;/span&gt;

  &lt;span class="c"&gt;# Display the entire request for debugging&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$REQ_RAW&lt;/span&gt;

  &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COPROC_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Kill the process for the subsequent request&lt;/span&gt;
  &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COPROC_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Wait until it's actually buried&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well already. We basically have a request logger now. Progress!&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy of a HTTP request
&lt;/h2&gt;

&lt;p&gt;We first need to parse the request to determine what the server should execute. Let's look at what we're dealing with.&lt;/p&gt;

&lt;p&gt;A typical HTTP request is structured like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Method] [Path + Query String] HTTP/[HTTP Version]
[Headers]

[Body]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I perform a GET request on the server, it outputs something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET / HTTP/1.1
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Cache-Control: no-cache
Host: localhost:1440
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A POST request, on the other hand, could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST / HTTP/1.1
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Cache-Control: no-cache
Host: localhost:1440
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------328683080620751780512479
Content-Length: 169

----------------------------328683080620751780512479
Content-Disposition: form-data; name="hello"

world
----------------------------328683080620751780512479--
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can work with this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding some logic
&lt;/h2&gt;

&lt;p&gt;The request is stored as a single string in a variable called &lt;code&gt;REQ_RAW&lt;/code&gt;, so we can parse it using several other Bash utilities.&lt;/p&gt;

&lt;p&gt;We create a function called &lt;code&gt;parse_request&lt;/code&gt; and put that into a separate file to keep things organized. We then call this function after the reading loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;source&lt;/span&gt; ./server/parse_request.sh

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="c"&gt;# Continue doing this, kind of like an event loop.&lt;/span&gt;
&lt;span class="k"&gt;do
  &lt;/span&gt;coproc nc &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 1440 &lt;span class="nt"&gt;-q1&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="c"&gt;# Spawns netcat such that we can read from and write to it&lt;/span&gt;

  &lt;span class="c"&gt;## ...&lt;/span&gt;

  &lt;span class="c"&gt;# Declare an associative array called `REQUEST`&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;

  parse_request &lt;span class="nv"&gt;$REQ_RAW&lt;/span&gt; REQUEST

  &lt;span class="c"&gt;# Print the contents of the associative array&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; REQUEST

  &lt;span class="c"&gt;# Add more magic here&lt;/span&gt;

  &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COPROC_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Kill the process for the subsequent request&lt;/span&gt;
  &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COPROC_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Wait until it's actually gone&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function needs to do a few things at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Determine the HTTP method&lt;/li&gt;
&lt;li&gt;Determine the route the user has requested&lt;/li&gt;
&lt;li&gt;Parse out any GET variables (i.e. &lt;code&gt;?foo=bar&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Parse out the body&lt;/li&gt;
&lt;li&gt;Parse out cookies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can parse the very first line of the request to get the HTTP method and route path. Afterwards, we parse the cookies and check if we need to parse a body, which only happens on POST and PUT requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Parses the entire request&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;parse_request&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;RAW_REQ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;

  &lt;span class="c"&gt;# This makes the REQUEST associative array available to write to&lt;/span&gt;
  &lt;span class="c"&gt;# We need to make sure to not call it REQUEST, though, because&lt;/span&gt;
  &lt;span class="c"&gt;# that name is already reserved in the outer scope&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;INNER_REQ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;# Extract the request line: method, path (+ query string) and version&lt;/span&gt;
  &lt;span class="nv"&gt;REQUESTLINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RAW_REQ&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1p&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-ra&lt;/span&gt; PARTS &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REQUESTLINE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;METHOD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PARTS&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;REQUEST_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PARTS&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;# Split query string from the actual route&lt;/span&gt;
  &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'?'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-ra&lt;/span&gt; REQUEST_PATH_PARTS &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REQUEST_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;REQUEST_ROUTE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST_PATH_PARTS&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;QUERY_STRING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST_PATH_PARTS&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUERY_STRING&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;parse_query_string &lt;span class="nv"&gt;$QUERY_STRING&lt;/span&gt; INNER_REQ
  &lt;span class="k"&gt;fi

  &lt;/span&gt;parse_cookies &lt;span class="nv"&gt;$RAW_REQ&lt;/span&gt; INNER_REQ

  &lt;span class="c"&gt;# If we're dealing with either a POST or a PUT request, chances are there's a form body.&lt;/span&gt;
  &lt;span class="c"&gt;# We extract that with the previously found $FORMDATA_BOUNDARY.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$METHOD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$METHOD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"PUT"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;parse_body &lt;span class="nv"&gt;$RAW_REQ&lt;/span&gt; INNER_REQ
  &lt;span class="k"&gt;fi

  &lt;/span&gt;INNER_REQ[&lt;span class="s2"&gt;"METHOD"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$METHOD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  INNER_REQ[&lt;span class="s2"&gt;"ROUTE"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REQUEST_ROUTE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query string parsing is pretty straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Parses the query string and assigns it to the request object&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;parse_query_string&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;RAW_QUERY_STRING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;REQ_ARR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;# Split the query parameters into a hashmap&lt;/span&gt;
  &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'&amp;amp;'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-ra&lt;/span&gt; QUERYPARTS &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUERY_STRING&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;PART &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;QUERYPARTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-ra&lt;/span&gt; KEYVALUE &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PART&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEYVALUE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;VALUE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEYVALUE&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    REQ_ARR[&lt;span class="s2"&gt;"QUERY"&lt;/span&gt;,&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VALUE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And so is the cookie parsing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Parses cookies out of the request headers&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;parse_cookies&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;RAW_REQ_BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;REQ_ARR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="nv"&gt;COOKIE_LINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$RAW_REQ_BODY&lt;/span&gt;|grep &lt;span class="s1"&gt;'Cookie:'&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nv"&gt;COOKIE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COOKIE_LINE&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="s2"&gt;"Cookie:"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COOKIE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; COOKIEPARTS &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COOKIE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;PART &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COOKIEPARTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
      if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PART&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-ra&lt;/span&gt; KEYVALUE &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PART&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEYVALUE&lt;/span&gt;&lt;span class="p"&gt;[0]//&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="c"&gt;# Remove all spaces, so we don't have leading spaces&lt;/span&gt;
        &lt;span class="nv"&gt;VALUE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEYVALUE&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
        REQ_ARR[&lt;span class="s2"&gt;"COOKIE"&lt;/span&gt;,&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VALUE&lt;/span&gt;::-1&lt;span class="k"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;fi
    done
  fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In both functions, we carefully rip the necessary parts out of the entire request and split it by some characters, namely &lt;code&gt;?&lt;/code&gt; and &lt;code&gt;=&lt;/code&gt; for the query string and &lt;code&gt;;&lt;/code&gt; and &lt;code&gt;=&lt;/code&gt; for the cookies. We then remove some unnecessary spaces and write it to the &lt;code&gt;REQUEST&lt;/code&gt; associative array.&lt;/p&gt;

&lt;p&gt;Parsing the body is more complex. We're dealing with the &lt;code&gt;multipart/form-data&lt;/code&gt; format to allow for multi-line strings and, potentially, file uploads. I found it actually more straightforward to work with than any URL encoding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Parses the POST body and assigns it to the request object&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;parse_body&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;RAW_REQ_BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;REQ_ARR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="nv"&gt;FORM_BOUNDARY_LINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$RAW_REQ_BODY&lt;/span&gt;|grep &lt;span class="s1"&gt;'Content-Type: multipart/form-data; boundary='&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nv"&gt;FORM_BOUNDARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FORM_BOUNDARY_LINE&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="s2"&gt;"Content-Type: multipart/form-data; boundary="&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;# Replace the $FORMDATA_BOUNDARY with a single character so we can split with that.&lt;/span&gt;
  &lt;span class="nv"&gt;TMP_BODY_PARTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RAW_REQ_BODY&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FORM_BOUNDARY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;$'§'&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-2&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="c"&gt;# We need to use _some_ character to use `read` here.&lt;/span&gt;

  &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'§'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; BODYPARTS &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP_BODY_PARTS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;PART &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BODYPARTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PART&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="s1"&gt;'(?&amp;lt;=name=").*?(?=")'&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;VALUE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PART&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; +4&lt;span class="sb"&gt;`&lt;/span&gt;
      REQ_ARR[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;,&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VALUE&lt;/span&gt;::-1&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;fi
  done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we run the code now with our GET request from before, we get the following output from our Bash server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;declare -A REQUEST=([ROUTE]="/" [METHOD]="GET" )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Yes, &lt;code&gt;declare -p&lt;/code&gt; creates a &lt;code&gt;declare -A&lt;/code&gt; statement, so one could execute that again to have the same associative array.)&lt;/p&gt;

&lt;p&gt;The mentioned POST request would output this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;declare -A REQUEST=([BODY,hello]="world" [ROUTE]="/" [METHOD]="POST" )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neat!&lt;/p&gt;

&lt;h2&gt;
  
  
  Reacting to the request
&lt;/h2&gt;

&lt;p&gt;Similar to the &lt;code&gt;REQUEST&lt;/code&gt; array, we declare a &lt;code&gt;RESPONSE&lt;/code&gt; array. This array will contain the DOM we deliver, the status code, and some headers, like &lt;code&gt;Set-Cookie&lt;/code&gt; or &lt;code&gt;Location&lt;/code&gt; for redirects.&lt;/p&gt;

&lt;p&gt;Since we need to be able to tell users apart (some are logged in and some are not), we implement a function called &lt;code&gt;set_session&lt;/code&gt;. This generates a session ID, writes it to the SQLite database, and sets a session cookie. Any following request from the same client will send that same session ID cookie.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;set_session&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;REQ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQ&lt;/span&gt;&lt;span class="p"&gt;[COOKIE,SESSID]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="c"&gt;# SESSID cookie was already set once; reset it&lt;/span&gt;
    RES[&lt;span class="s2"&gt;"COOKIES,SESSID"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQ&lt;/span&gt;&lt;span class="p"&gt;[COOKIE,SESSID]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="c"&gt;# No SESSID cookie, so let's generate one&lt;/span&gt;
    &lt;span class="nv"&gt;SESSID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$RANDOM&lt;/span&gt; | &lt;span class="nb"&gt;md5sum&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 20&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="c"&gt;# Taken from SO.&lt;/span&gt;

    &lt;span class="c"&gt;# Save cookie into database&lt;/span&gt;
    sqlite3 db.sqlite &lt;span class="s2"&gt;"insert into sessions values ('&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SESSID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', NULL);"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;
    RES[&lt;span class="s2"&gt;"COOKIES,SESSID"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SESSID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how we need both the &lt;code&gt;REQ&lt;/code&gt; and the &lt;code&gt;RES&lt;/code&gt; array: We already write to the &lt;code&gt;RESPONSE&lt;/code&gt; array by setting a &lt;code&gt;COOKIES&lt;/code&gt; key with a sub-key called &lt;code&gt;SESSID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We call this function after we call &lt;code&gt;parse_request&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;

  &lt;span class="c"&gt;# Request reading shenanigans&lt;/span&gt;

  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nv"&gt;RESPONSE&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
  parse_request &lt;span class="nv"&gt;$REQ_RAW&lt;/span&gt; REQUEST

  set_session REQUEST RESPONSE

  &lt;span class="c"&gt;# More stuff later, don't worry&lt;/span&gt;

  &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COPROC_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Kill the process for the subsequent request&lt;/span&gt;
  &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COPROC_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="c"&gt;# Wait until it's actually gone&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we can implement a function to react to the actual request. We call it &lt;code&gt;render_cms_page&lt;/code&gt;. In there, we look in the database for any entry with a route that matches the route from the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;render_cms_page&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="nv"&gt;DOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"select markup from pages where routePath='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[ROUTE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="s2"&gt;"Not found."&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
    RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"404 Not found"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="nv"&gt;$DOM&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
    RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"200 OK"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might notice the &lt;code&gt;render_response&lt;/code&gt; function in there, too. We use that to generate all of the surrounding HTML, such as a page header and navigation and some CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;render_response&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;DOC_START&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;doc_start &lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nv"&gt;PAGE_HEADER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;page_header&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nv"&gt;DOC_END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;doc_end&lt;span class="sb"&gt;`&lt;/span&gt;

  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$DOC_START&lt;/span&gt;&lt;span class="sh"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$PAGE_HEADER&lt;/span&gt;&lt;span class="sh"&gt;

    &amp;lt;main&amp;gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="sh"&gt;
    &amp;lt;/main&amp;gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$DOC_END&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;em&gt;there&lt;/em&gt;, however, we have the functions &lt;code&gt;doc_start&lt;/code&gt;, &lt;code&gt;page_header&lt;/code&gt; and &lt;code&gt;doc_end&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;doc_start&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
&amp;lt;!doctype html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset="utf-8"&amp;gt;
    &amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;/title&amp;gt;
    &amp;lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jgthms/minireset.css@master/minireset.min.css"&amp;gt;
    &amp;lt;style&amp;gt;
      * { font-family: monospace; font-size: 16px; }
      h1 { font-size: 1.5rem; font-weight: bold; }
      h2 { font-size: 2rem; font-weight: bold; margin-bottom: 0.5rem; }
      p { margin-bottom: 0.5rem; }
      header { padding: 1rem; border-bottom: 1px solid #000; }
      main { padding: 1rem; }
      nav ul { margin-top: 1rem; display: flex; gap: 1rem; }
      label { display: block; }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function &lt;/span&gt;doc_end&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function &lt;/span&gt;page_header&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;# Fetch header-relevant pages&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
  &amp;lt;header&amp;gt;
    &amp;lt;h1&amp;gt;Bash CMS&amp;lt;/h1&amp;gt;
    &amp;lt;nav&amp;gt;
      &amp;lt;ul&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; PAGE_ROW&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; PAGE_PARTS &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PAGE_ROW&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;NAV_TITLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PAGE_PARTS&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;ROUTE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PAGE_PARTS&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
        &amp;lt;li&amp;gt;&amp;lt;a href="&lt;/span&gt;&lt;span class="nv"&gt;$ROUTE_PATH&lt;/span&gt;&lt;span class="sh"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;$NAV_TITLE&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;  &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"select navTitle, routePath from pages where isInMainNavigation=1;"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;

  &lt;span class="nv"&gt;LOGGEDIN_USERID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"select userRowId from sessions where sessId = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[COOKIE,SESSID]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGGEDIN_USERID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
      &amp;lt;li&amp;gt;&amp;lt;a href="/login" class="login"&amp;gt;Login&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
        &amp;lt;li&amp;gt;&amp;lt;a href="/add-new-page" class="login"&amp;gt;Add new page&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;&amp;lt;a href="/logout" class="login"&amp;gt;Logout&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/header&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, we're &lt;em&gt;almost&lt;/em&gt; done.&lt;/p&gt;

&lt;p&gt;The last step to an actual response is to render a response string. Much like an HTTP request, a response is a single multi-line string with different parts. We only need to assemble it correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;generate_response_string&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;# Transform cookie entries into Set-Cookie headers&lt;/span&gt;
  &lt;span class="nv"&gt;COOKIES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;RESKEY &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!RES[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESKEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^&lt;span class="s2"&gt;"COOKIES,"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;COOKIE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESKEY&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="s2"&gt;"COOKIES,"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
      COOKIES+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Set-Cookie: &lt;/span&gt;&lt;span class="nv"&gt;$COOKIE_NAME&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$RESKEY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
"&lt;/span&gt; &lt;span class="c"&gt;# Adds a newline after this Set-Cookie header.&lt;/span&gt;
    &lt;span class="k"&gt;fi
  done

  &lt;/span&gt;RES[&lt;span class="s2"&gt;"CONTENT_TYPE"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"text/html"&lt;/span&gt;
  RES[&lt;span class="s2"&gt;"HEADERS"&lt;/span&gt;,&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="p"&gt;[CONTENT_TYPE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; charset=UTF-8"&lt;/span&gt;
  RES[&lt;span class="s2"&gt;"HEADERS"&lt;/span&gt;,&lt;span class="s2"&gt;"Server"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"Bash. Please don't send cat /etc/passwd as a cookie because hacking is bad :("&lt;/span&gt;

  &lt;span class="nv"&gt;HEADERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;RESKEY &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!RES[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESKEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^&lt;span class="s2"&gt;"HEADERS,"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;HEADER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESKEY&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="s2"&gt;"HEADERS,"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
      HEADERS+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HEADER_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$RESKEY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
"&lt;/span&gt; &lt;span class="c"&gt;# Adds a newline after this Set-Cookie header.&lt;/span&gt;
    &lt;span class="k"&gt;fi
  done&lt;/span&gt;

  &lt;span class="c"&gt;# declare -p RES&lt;/span&gt;

  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
HTTP/1.1 &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="p"&gt;[STATUS]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COOKIES&lt;/span&gt;::-1&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HEADERS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;

&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="p"&gt;[BODY]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we're good to go. Let's see what this does:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijz47xzor2f5rpayfzyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijz47xzor2f5rpayfzyk.png" alt="A header, navigation and the content from the  raw `/` endraw  page!" width="413" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(On a side note: Using backticks &lt;em&gt;anywhere&lt;/em&gt; is making me nervous now. Who knows what it'll execute...)&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the routes
&lt;/h2&gt;

&lt;p&gt;Now that we've implemented the dynamic routes let's take care of the static ones, such as &lt;code&gt;/login&lt;/code&gt;, &lt;code&gt;/edit&lt;/code&gt;, &lt;code&gt;/add-new-page&lt;/code&gt;, &lt;code&gt;/logout&lt;/code&gt; and &lt;code&gt;/delete&lt;/code&gt;. For that, we add two more functions: One for the login form and one for the edit form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;render_login_page&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
  &amp;lt;form method="POST" action="/login" enctype="multipart/form-data"&amp;gt;
    &amp;lt;label for="username"&amp;gt;User name&amp;lt;/label&amp;gt;
    &amp;lt;input type="text" name="username" id="username" value="&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;"&amp;gt;

    &amp;lt;label for="password"&amp;gt;Password&amp;lt;/label&amp;gt;
    &amp;lt;input type="password" name="password" id="password"&amp;gt;

    &amp;lt;button type="submit"&amp;gt;Login&amp;lt;/button&amp;gt;
  &amp;lt;/form&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function &lt;/span&gt;render_edit_form&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;NAVTITLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nv"&gt;IS_IN_MAIN_NAVIGATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="nv"&gt;ROUTEPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;
  &lt;span class="nv"&gt;DOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$4&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;IS_IN_MAIN_NAVIGATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;" checked"&lt;/span&gt;
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
  &amp;lt;form method="POST" enctype="multipart/form-data"&amp;gt;
    &amp;lt;label for="navtitle"&amp;gt;Navigation title&amp;lt;/label&amp;gt;
    &amp;lt;input type="text" name="navtitle" id="navtitle" value="&lt;/span&gt;&lt;span class="nv"&gt;$NAVTITLE&lt;/span&gt;&lt;span class="sh"&gt;"&amp;gt;

    &amp;lt;label for="routepath"&amp;gt;Route path&amp;lt;/label&amp;gt;
    &amp;lt;input type="text" name="routepath" id="routepath" value="&lt;/span&gt;&lt;span class="nv"&gt;$ROUTEPATH&lt;/span&gt;&lt;span class="sh"&gt;"&amp;gt;

    &amp;lt;label for="is_in_main_navigation"&amp;gt;
      Is it in navigation?
      &amp;lt;input type="checkbox" value="1" name="is_in_navigation"&lt;/span&gt;&lt;span class="nv"&gt;$IS_IN_MAIN_NAVIGATION&lt;/span&gt;&lt;span class="sh"&gt;&amp;gt;
    &amp;lt;/label&amp;gt;

    &amp;lt;div&amp;gt;
      &amp;lt;label for="dom"&amp;gt;Content&amp;lt;/label&amp;gt;
      &amp;lt;textarea name="dom" id="dom" style="width: 100%; height: 300px;"&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;$DOM&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;/textarea&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;button type="submit"&amp;gt;Save&amp;lt;/button&amp;gt;
  &amp;lt;/form&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And lastly, we expand the &lt;code&gt;render_cms_page&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;render_cms_page&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[ROUTE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"/login"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[METHOD]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,username]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
      &lt;span class="nv"&gt;PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,password]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; | &lt;span class="nb"&gt;sha256sum&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
      &lt;span class="nv"&gt;USERID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"select rowid from users where username='&lt;/span&gt;&lt;span class="nv"&gt;$USERNAME&lt;/span&gt;&lt;span class="s2"&gt;' and password='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PASSWORD&lt;/span&gt;::-3&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;DOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_login_page &lt;span class="nv"&gt;$USERNAME&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
        DOM+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;p&amp;gt;Username or password incorrect&amp;lt;/p&amp;gt;"&lt;/span&gt;
        RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="s2"&gt;"Login"&lt;/span&gt; &lt;span class="nv"&gt;$DOM&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
        RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"200 OK"&lt;/span&gt;
      &lt;span class="k"&gt;else
        &lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"update sessions set userRowId = &lt;/span&gt;&lt;span class="nv"&gt;$USERID&lt;/span&gt;&lt;span class="s2"&gt; where sessId = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[COOKIE,SESSID]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;
        RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"307 Temporary Redirect"&lt;/span&gt;
        RES[&lt;span class="s2"&gt;"HEADERS"&lt;/span&gt;,&lt;span class="s2"&gt;"Location"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;
      &lt;span class="k"&gt;fi
    else
      &lt;/span&gt;&lt;span class="nv"&gt;DOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_login_page&lt;span class="sb"&gt;`&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="s2"&gt;"Login"&lt;/span&gt; &lt;span class="nv"&gt;$DOM&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"200 OK"&lt;/span&gt;
    &lt;span class="k"&gt;fi

    &lt;/span&gt;&lt;span class="nv"&gt;DOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_login_page &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,username]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[ROUTE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"/logout"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"update sessions set userRowId = NULL where sessId = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[COOKIE,SESSID]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;
    RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"307 Temporary Redirect"&lt;/span&gt;
    RES[&lt;span class="s2"&gt;"HEADERS"&lt;/span&gt;,&lt;span class="s2"&gt;"Location"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[ROUTE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"/add-new-page"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[METHOD]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;IS_IN_MAIN_NAVIGATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,is_in_navigation]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;IS_IN_MAIN_NAVIGATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;
      &lt;span class="k"&gt;fi
      &lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"insert into pages values ('&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,routepath]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,navtitle]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IS_IN_MAIN_NAVIGATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,dom]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;');"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"307 Temporary Redirect"&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"HEADERS"&lt;/span&gt;,&lt;span class="s2"&gt;"Location"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,routepath]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else
      &lt;/span&gt;&lt;span class="nv"&gt;DOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_edit_form&lt;span class="sb"&gt;`&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="s2"&gt;"New page"&lt;/span&gt; &lt;span class="nv"&gt;$DOM&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"200 OK"&lt;/span&gt;
    &lt;span class="k"&gt;fi
  elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[ROUTE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"/edit"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;LOGGEDIN_USERID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"select userRowId from sessions where sessId = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[COOKIE,SESSID]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGGEDIN_USERID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"403 Forbidden"&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="s2"&gt;"Nope"&lt;/span&gt; &lt;span class="s2"&gt;"Not allowed to do that"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
    &lt;span class="k"&gt;else
      if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[METHOD]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;IS_IN_MAIN_NAVIGATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,is_in_navigation]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
          &lt;/span&gt;&lt;span class="nv"&gt;IS_IN_MAIN_NAVIGATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;
        &lt;span class="k"&gt;fi
        &lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"update pages set routePath='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,routepath]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', navTitle='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,navtitle]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', isInMainNavigation=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IS_IN_MAIN_NAVIGATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, markup='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,dom]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' where routePath='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[QUERY,route]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;
        RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"307 Temporary Redirect"&lt;/span&gt;
        RES[&lt;span class="s2"&gt;"HEADERS"&lt;/span&gt;,&lt;span class="s2"&gt;"Location"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[BODY,routepath]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;else
        &lt;/span&gt;&lt;span class="nv"&gt;PAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"select navTitle, isInMainNavigation, routePath, markup from pages where routePath='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[QUERY,route]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
        &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; PAGEPARTS &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nv"&gt;DOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_edit_form &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PAGEPARTS&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PAGEPARTS&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PAGEPARTS&lt;/span&gt;&lt;span class="p"&gt;[2]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PAGEPARTS&lt;/span&gt;&lt;span class="p"&gt;[3]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
        RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="s2"&gt;"Edit"&lt;/span&gt; &lt;span class="nv"&gt;$DOM&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
        RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"200 OK"&lt;/span&gt;
      &lt;span class="k"&gt;fi
    fi
  elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[ROUTE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"/delete"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;LOGGEDIN_USERID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"select userRowId from sessions where sessId = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[COOKIE,SESSID]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGGEDIN_USERID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"403 Forbidden"&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="s2"&gt;"Nope"&lt;/span&gt; &lt;span class="s2"&gt;"Not allowed to do that"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
    &lt;span class="k"&gt;else
      &lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"delete from pages where routePath='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[QUERY,route]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"delete from pages where routePath='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[QUERY,route]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';"&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"307 Temporary Redirect"&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"HEADERS"&lt;/span&gt;,&lt;span class="s2"&gt;"Location"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;
    &lt;span class="k"&gt;fi
  else
    &lt;/span&gt;&lt;span class="nv"&gt;DOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"select markup from pages where routePath='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[ROUTE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;';"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
    &lt;span class="nv"&gt;LOGGEDIN_USERID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;sqlite3 db.sqlite &lt;span class="s2"&gt;"select userRowId from sessions where sessId = '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[COOKIE,SESSID]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; &lt;span class="s2"&gt;".exit"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOGGEDIN_USERID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;DOM+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;div style='margin-top: 20px;'&amp;gt;&amp;lt;a href='/edit?route=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[ROUTE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'&amp;gt;Edit&amp;lt;/a&amp;gt; | &amp;lt;a href='/delete?route=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;[ROUTE]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'&amp;gt;Delete&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi

    if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="s2"&gt;"Not found"&lt;/span&gt; &lt;span class="s2"&gt;"Not found."&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"404 Not found"&lt;/span&gt;
    &lt;span class="k"&gt;else
      &lt;/span&gt;RES[&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;render_response &lt;span class="s2"&gt;"Bash CMS!"&lt;/span&gt; &lt;span class="nv"&gt;$DOM&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
      RES[&lt;span class="s2"&gt;"STATUS"&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;"200 OK"&lt;/span&gt;
    &lt;span class="k"&gt;fi
  fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we're good. With 443 lines of code, we've written a basic CMS from scratch in Bash only!&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo time!
&lt;/h2&gt;

&lt;p&gt;(The gif might take a few seconds to load...)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fla1drhph72d2kk442jbv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fla1drhph72d2kk442jbv.gif" alt="The BashCMS in action!" width="600" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A time!
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Does it perform well?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: No. Not at all. This script can handle a single request &lt;em&gt;at a time&lt;/em&gt;. Even Apache can handle several hundred connections at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I use this...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: No. Please, for the love of everything, don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does the font &lt;em&gt;need&lt;/em&gt; to be monospaced? That's so 1990s&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Yes. We're using Bash, so why shouldn't it be monospaced?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Anything else?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: I use Arch, by the way.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt; &lt;em&gt;or&lt;/em&gt; &lt;a href="https://twitter.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;follow me on Twitter&lt;/em&gt; 🐦&lt;/a&gt;&lt;em&gt;!&lt;/em&gt; &lt;em&gt;You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bash</category>
      <category>webdev</category>
      <category>programming</category>
      <category>donttrythisathome</category>
    </item>
    <item>
      <title>I wrote a book on CSS Grid - Here's how! 📖💡</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Sat, 20 May 2023 16:10:47 +0000</pubDate>
      <link>https://forem.com/thormeier/i-wrote-a-book-on-css-grid-heres-how-1ak8</link>
      <guid>https://forem.com/thormeier/i-wrote-a-book-on-css-grid-heres-how-1ak8</guid>
      <description>&lt;p&gt;It's been, again, a while since my last post here on DEV. In between posts, I've been rather busy with all kinds of projects. By far the largest of these is a &lt;a href="https://packt.link/4zyA0" rel="noopener noreferrer"&gt;book on CSS Grid&lt;/a&gt;. It'll be released soon and you can get a paperback or ebook copy over on Amazon.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Of course, this post is also meant to promote the book a bit, otherwise I wouldn't have posted it in #showdev, but I'd like to focus on the process of how this behemoth of a project came to be in the first place and what it took to get it out there. I also want to inspire people with this post. I want to show you how I worked on this and the necessary steps to complete it. A look behind the curtain, so to speak.&lt;/p&gt;

&lt;p&gt;As a little disclaimer: This story is written from memory, but it shows the process nevertheless.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  About the book itself
&lt;/h2&gt;

&lt;p&gt;So, what did I actually write about? The title "Mastering CSS Grid" pretty much explains it: CSS Grid. I wrote about the basics and advanced CSS Grid rules, such as grid templates, responsiveness and nested grids.&lt;/p&gt;

&lt;p&gt;The book also contains parts about design best practices, grid alternatives, and how CSS Grid and Flexbox play together. A chapter about different frameworks, such as Bootstrap, Tailwind, Tachyons and many more, shows how different popular frameworks understand grids and how they can be used to achieve default use cases, such as the "header-sidebar-content-footer" layout. The last chapter is a comprehensive cheat sheet and quick reference for all of CSS Grid and all related topics discussed in the book.&lt;/p&gt;

&lt;p&gt;If you want to order the book as either an ebook or a paperback, if you want to keep it on your desk, you can find it &lt;a href="https://packt.link/4zyA0" rel="noopener noreferrer"&gt;here&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  How it started
&lt;/h2&gt;

&lt;p&gt;My writing career started a few years ago on DEV. I first wanted to find a bit of a nieche to write about, but didn't get the traction I originally expected. I then settled on writing about all kinds of things tech, mostly what I'm good at, sometimes learning a lot myself while writing. I wanted to have fun writing, and boy oh boy, do I still have fun doing it. And, in my opinion, that's what it's all about: Having fun, growing and helping others grow, too.&lt;/p&gt;

&lt;p&gt;At some point in time, precisely in August 2022, I got lucky. Like, &lt;em&gt;very&lt;/em&gt; lucky. I received an email of someone at Packt. They said that they've read my articles and found them "insightful and impressive" and that they were currently looking for authors, specifically for a book about CSS Grid. I would've been foolish to deny that offer.&lt;/p&gt;

&lt;p&gt;And so I accepted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My personal first lesson:&lt;/strong&gt; Don't try, just do it. I never had the goal to "attract a publisher" and start working on a book. Of course, writing a book was a long-term goal of mine, but my writing on DEV wasn't meant to lead to that. I did what I liked, the rest came around eventually. Of course, a great bit of luck is involved, but writing is a skill one can train, learning in the open is a great thing to do anyways, and once a sufficient skill level and audience is achieved, publishers &lt;em&gt;will&lt;/em&gt; notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baby steps
&lt;/h2&gt;

&lt;p&gt;The first step wasn't even the contract. It was an outline. The folks at Packt provided me with a template, describing the target group, a list of chapters, what topics each chapter would cover, what the reader will learn and what benefits they would gave once they've read the chapter. They had a very specific idea of topics in mind and wanted to make sure that the outline would cover all of these at least somehow.&lt;/p&gt;

&lt;p&gt;The chapter names and rough section titles for each chapter, of course, were drafts. We wanted to make sure to have at least some idea of what this book will look like in the end.&lt;/p&gt;

&lt;p&gt;Afterwards came the organisational part. They proposed a contract, which we both signed. I got introduced to the project manager, the editor, and many more people of the team. These people then introduced me to the tools we would use.&lt;/p&gt;

&lt;p&gt;After that, I started writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My second learning:&lt;/strong&gt; Do not underestimate the organisational effort of such a project. There is a reason that book publishing companies are successful and exist since basically the invention of the printing press. These people are &lt;em&gt;experts&lt;/em&gt;. They know about steps in the process we non-publishers don't even know we don't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting sober
&lt;/h2&gt;

&lt;p&gt;And I immediately started writing on the wrong part of the book. I started with the foreword, including it in chapter 1. I thought that, logically, this would be the first part of the book, so it should be written first, right? &lt;/p&gt;

&lt;p&gt;Wrong.&lt;/p&gt;

&lt;p&gt;The reader should "hit the ground running" with CSS Grid. However, the people at Packt were understanding. They told me to keep the draft for a later stage of the project. So, after the first bit of frustration, I understood their intention: The reader may have already read the foreword and would want to finally learn something.&lt;/p&gt;

&lt;p&gt;After I completed the first draft of chapter 1, I eagerly awaited the feedback, expecting a few comments at maximum. But then I opened word and saw dozens of comments. They were devastating at first. Some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Please elaborate here." &lt;/li&gt;
&lt;li&gt;"What do you mean with this?" &lt;/li&gt;
&lt;li&gt;"Incomplete sentence, did you mean [...]?"&lt;/li&gt;
&lt;li&gt;"Please introduce this figure first, then include the figure, &lt;em&gt;then&lt;/em&gt; explain the figure."&lt;/li&gt;
&lt;li&gt;"Don't use words such as &lt;em&gt;above&lt;/em&gt; or &lt;em&gt;below&lt;/em&gt;, as locations may very well be off after editing in the print version."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I sobered up. And I was frustrated. The editor added that they first needed to accustom to my style of writing, but that it was otherwise a very solid start. I can only imagine what a non-solid start would've looked like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My third learning:&lt;/strong&gt; Even if there is a lot of critique, it only shows the potential. I started writing the book with a certain mindset and many assumptions about the process. I've never written a book before and deliberately wanted to dive into it head-first. Of course I would first run into a wall. But the publsiher is there to help, dunk on the author. After all, they are as interested in the quality and success of this book as I am, so why should they mean any harm? Of course, the critique is frustrating and demoralizing, but that's the ego talking. It should be seen as an opportunity to grow. Thinking back to my studies, the people proofreading my thesis told me that dozens and dozens of comments are the absolute &lt;em&gt;norm&lt;/em&gt;, not a sign that the work is of bad quality. And that's true.&lt;/p&gt;

&lt;h2&gt;
  
  
  A lot of progress
&lt;/h2&gt;

&lt;p&gt;After sobering up, discussing some concepts (such as how to deal with figures), having a call or two with the team and working further, I got into a flow.&lt;/p&gt;

&lt;p&gt;I handed in each chapter individually, as a MS Word document. Word, even though I personally prefer markdown, has the advantage of allowing for comments and very clear visual formatting. &lt;/p&gt;

&lt;p&gt;The people at Packt gave me some very sophisticated templates to work with. Bold text would not only be bold, but also magenta, code blocks would be monospaced and have a bright blue as their background colour, callouts and editor notes were in bright signal colours. Even though these formats irritaed me at first, because why would anyone print it this way, I quickly noticed that these tools also help me to gain an overview and that they also helped the layout team to understand how they should format the content.&lt;/p&gt;

&lt;p&gt;I had a deadline for each chapter. Once I received the chapter, I had a deadline to incoporate feedback. Once I received the tech reviews, I had fixed deadlines to incoporate their feedback. Once I worked on the final drafts, I had deadlines for incoporate their feedbacks.&lt;/p&gt;

&lt;p&gt;Deadlines help. Especially when working on a creative project. I encountered writer's block more than once, but I &lt;em&gt;had&lt;/em&gt; to find ways to counteract it, otherwise I would not meet the deadline. The feedback cycles got shorter, the comments got less and less and at some point, we all got into a flow. And progress was made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My fourth learning:&lt;/strong&gt; Find ways to get into a flow, embrace the flow and work on maintaining it. You'll get better at writing. Dividing the project into smaller sub-projects helps a lot and deadlines make you keep yourself from overengineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final steps
&lt;/h2&gt;

&lt;p&gt;The tech review was a huge milestone. The folks at Packt found two amazing experts to work on the entire book and give their feedback. They found details that I would've never seen, simply because they were not nearly as deep in this project as I was. The additional pairs of eyes were basically an asynchronous version of duck debugging.&lt;/p&gt;

&lt;p&gt;At this point, I want to shout out to &lt;br&gt;
&lt;em&gt;Giuseppe Caruso&lt;/em&gt; and &lt;em&gt;Michelle Manemann&lt;/em&gt; (ordered alphabetically) for their hard work. Without them, we wouldn't have reached the quality level we have reached in the end.&lt;/p&gt;

&lt;p&gt;Given, incorportaing their feedback was hard. I had to restructure many sections, replace many figures and add a ton of stuff, but in the end, it was worth the effort.&lt;/p&gt;

&lt;p&gt;The final drafts of all chapters had a total of a dozen comments in the end, we had to replace a few images because the platform refused them (something about illegible text on some screenshots), but the polishing phase did not take much from my end.&lt;/p&gt;

&lt;p&gt;And with this, we were done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My fifth learning:&lt;/strong&gt; Do not underestimate the help of people that don't know what you've been writing about. Get in a fresh pair of eyes every now and again to help with details. They will find things you would've never noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The aftermath
&lt;/h2&gt;

&lt;p&gt;The book is done. What more is there? Well, as it turns out, a lot: Posts like these, for example. &lt;/p&gt;

&lt;p&gt;I asked in my company if people would like a free ebook in exchange for an Amazon review. I still want to polish a thing or two in the official GitHub repository (mainly add some comments, but alas). I want to promote the work myself, get it out to people. There may be a second edition, a third one, fourth or fifth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My sixth learning:&lt;/strong&gt; Just because the bulk of the work is done doesn't mean that you'll never hear back from it. I'm constantly checking on my social media posts, tracking how many impressions they got. I check the Amazon best seller ranks to see how it's doing in pre-sales. I can't wait to hold it in my hands. Even though I'm finished, the best is yet to come.&lt;/p&gt;

&lt;h2&gt;
  
  
  In conclusion
&lt;/h2&gt;

&lt;p&gt;Writing a book is a lot of work. It took me many of my weekends, many of my free afternoons and, sometimes, frustrated me to the point that I just wanted to give up. I invested a lot, and so did the publisher team at Packt. We've all contributed to this. &lt;/p&gt;

&lt;p&gt;And this is my seventh learning: Writing a book is not something a single person does. Even though their name is written on the cover. It is a group effort. And I'm forever thankful for having this opportunity. It fulfills a live-long dream.&lt;/p&gt;

&lt;p&gt;And please excuse the occasional typo, wrong comma or whatever you may find. I've done my share of editing for a few weeks. :D&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt; &lt;em&gt;or&lt;/em&gt; &lt;a href="https://twitter.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;follow me on Twitter&lt;/em&gt; 🐦&lt;/a&gt;&lt;em&gt;!&lt;/em&gt; &lt;em&gt;You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>css</category>
      <category>watercooler</category>
      <category>design</category>
    </item>
    <item>
      <title>Use ALL the Features: How To Create a Fancy Password Input With Vue3 🔑✅</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Sat, 01 Apr 2023 10:50:39 +0000</pubDate>
      <link>https://forem.com/thormeier/use-all-the-features-how-to-create-a-fancy-password-input-with-vue3-ggi</link>
      <guid>https://forem.com/thormeier/use-all-the-features-how-to-create-a-fancy-password-input-with-vue3-ggi</guid>
      <description>&lt;p&gt;Vue 3 has brought us a &lt;em&gt;ton&lt;/em&gt; of fancy new features. The composition API lets us create components in a more intuitive, JS-y way, there is now support for multiple &lt;code&gt;v-model&lt;/code&gt; directives, full TypesSript support out of the box, and the reactivity improved a lot when compared to Vue2.&lt;/p&gt;

&lt;p&gt;Today, we'll put these things to use to create a fancy password input component: One that lets us define password requirements and informs the outer component and the user about the password validity and the password.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basis
&lt;/h2&gt;

&lt;p&gt;First, we create a new Vue component called &lt;code&gt;PasswordInput.vue&lt;/code&gt;. In there, we add some template, an amepty script tag and some styling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"password-input"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Enter password
    &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- We'll add stuff to this input later --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
      &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
      &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;
      Show password
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"password-repeat"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Repeat password
    &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- We'll add stuff to this input later --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
      &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
      &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password-repeat"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"requirements"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!-- Add requirements here --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nc"&gt;.password-input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.requirements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.is-success&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#96CA2D&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.is-error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#BA3637&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We added two input fields for password and password repeat, a button that will show and hide the password as well as an unordered list for the password requirements that we'll add later.&lt;/p&gt;

&lt;p&gt;Notice how we're using &lt;code&gt;&amp;lt;script setup lang="ts"&amp;gt;&lt;/code&gt; to tell Vue that we want to use the composition API for the entire component.&lt;/p&gt;

&lt;p&gt;The styling already includes classes for success and failure. The rest is rather rudimentary. When we use the component, we should see something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp10t5ou27lglwgz09u4q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp10t5ou27lglwgz09u4q.png" alt="Two input fields with labels and a button." width="573" height="187"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooking up the fields
&lt;/h2&gt;

&lt;p&gt;Let's add two refs for the password and password repeat. We also add a flag to determine if the password is currently shown or not. While we're at it, we also import &lt;code&gt;computed&lt;/code&gt; and &lt;code&gt;watch&lt;/code&gt;, because we'll use that later. For that, we add the following to the script tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passwordRepeat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;showPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now hook these up with the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Enter password
    &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
      &lt;span class="na"&gt;:type=&lt;/span&gt;&lt;span class="s"&gt;"showPassword ? 'text' : 'password'"&lt;/span&gt;
      &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
      &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;change=&lt;/span&gt;&lt;span class="s"&gt;"$emit('update:password', $event.target.value)"&lt;/span&gt;
      &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"showPassword = !showPassword"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      {{ showPassword ? 'Hide password' : 'Show password' }}
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"password-repeat"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Repeat password
    &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
      &lt;span class="na"&gt;:type=&lt;/span&gt;&lt;span class="s"&gt;"showPassword ? 'text' : 'password'"&lt;/span&gt;
      &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"passwordRepeat"&lt;/span&gt;
      &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password-repeat"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding the requirements
&lt;/h2&gt;

&lt;p&gt;Now comes the fun part. We want to validate the password with the following requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It should be at least 8 characters long&lt;/li&gt;
&lt;li&gt;It should contain at least one uppercase letter&lt;/li&gt;
&lt;li&gt;It should contain at least one lowercase letter&lt;/li&gt;
&lt;li&gt;It should contain at least one symbol&lt;/li&gt;
&lt;li&gt;It should contain at least one number&lt;/li&gt;
&lt;li&gt;It must match the "Repeat password" field&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get Vue's reactivity in, we can create a computed value that checks the &lt;code&gt;password&lt;/code&gt; ref for all fo these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passwordRequirements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Must contain uppercase letters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Must contain lowercase letters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Must contain numbers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Must contain symbols&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\W&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Must be at least 8 characters long&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Must match&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;passwordRepeat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using an array of requirements has several advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We can extend it whenever we like&lt;/li&gt;
&lt;li&gt;Rendering it is trivial with a &lt;code&gt;v-for&lt;/code&gt; loop&lt;/li&gt;
&lt;li&gt;We can reuse the predicates and move them to a utils file or use them in a state management system&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's render the requirements in the list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"requirements"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt;
        &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"(requirement, key) in passwordRequirements"&lt;/span&gt;
        &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"key"&lt;/span&gt;
        &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"requirement.predicate ? 'is-success' : 'is-error'"&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        {{ requirement.name }}
      &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, whenever the user changes either one of the values, the requirements are recalculated and updated in the template, giving feedback to the user directly. We should now see something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2xoxj0d168456b7c26pp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2xoxj0d168456b7c26pp.png" alt="The same password input as above, but now with a list of fulfilled and unfulfilled requirements" width="569" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So far so good!&lt;/p&gt;

&lt;h2&gt;
  
  
  Emitting changes and usage
&lt;/h2&gt;

&lt;p&gt;We now want to make the component usable. For that, we need to emit changes. Since Vue now allows to have multiple &lt;code&gt;v-model&lt;/code&gt; directives on a component, we can emit different events in case of changes to either password field. For that, we add &lt;code&gt;@change&lt;/code&gt; event listeners to the password input fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
      &lt;span class="na"&gt;:type=&lt;/span&gt;&lt;span class="s"&gt;"showPassword ? 'text' : 'password'"&lt;/span&gt;
      &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
      &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;change=&lt;/span&gt;&lt;span class="s"&gt;"$emit('update:password', $event.target.value)"&lt;/span&gt;
      &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
      &lt;span class="na"&gt;:type=&lt;/span&gt;&lt;span class="s"&gt;"showPassword ? 'text' : 'password'"&lt;/span&gt;
      &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"passwordRepeat"&lt;/span&gt;
      &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;change=&lt;/span&gt;&lt;span class="s"&gt;"$emit('update:passwordRepeat', $event.target.value)"&lt;/span&gt;
      &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password-repeat"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then also want to emit if the password is valid or not, so a single boolean value. We use a watch for that, that will watch the &lt;code&gt;passwordRequirements&lt;/code&gt; compured value and emit a single boolean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineEmits&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update:password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update:passwordRepeat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update:validity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passwordRequirements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update:validity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;passwordRequirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And done! The component now emits changes for the password, the password-repeat field, and the password validity. It also shows the user direct visual feedback about the password's validity and let's them show or hide the password to see if they perhaps mistyped.&lt;/p&gt;

&lt;p&gt;We can now use the component as follows in any template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PasswordInput&lt;/span&gt;
    &lt;span class="na"&gt;v-model:password=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
    &lt;span class="na"&gt;v-model:password-repeat=&lt;/span&gt;&lt;span class="s"&gt;"passwordRepeat"&lt;/span&gt;
    &lt;span class="na"&gt;v-model:validity=&lt;/span&gt;&lt;span class="s"&gt;"isPasswordValid"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awesome!&lt;/p&gt;




&lt;p&gt;💡 Want to learn more about Vue and its advanced features? I recently published a course on Educative.io called "&lt;a href="https://www.educative.io/courses/advanced-vuejs-build-better-applications-efficiently" rel="noopener noreferrer"&gt;Advanced VueJS: Build Better Applications Efficiently&lt;/a&gt;"! &lt;/p&gt;

&lt;p&gt;In 51 lessons, you can learn about all kinds of features, such as custom events, advanced templating, how to use APIs, I18n or slots and hooks.&lt;/p&gt;

&lt;p&gt;You can find the course over on &lt;a href="https://www.educative.io/courses/advanced-vuejs-build-better-applications-efficiently" rel="noopener noreferrer"&gt;Educative.io&lt;/a&gt;!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt; &lt;em&gt;or&lt;/em&gt; &lt;a href="https://twitter.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;follow me on Twitter&lt;/em&gt; 🐦&lt;/a&gt;&lt;em&gt;!&lt;/em&gt; &lt;em&gt;You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vue</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I asked ChatGPT to draft a four-hour agile retrospective - here's how it went 🦾💬</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Wed, 01 Feb 2023 21:00:00 +0000</pubDate>
      <link>https://forem.com/thormeier/i-asked-chatgpt-to-draft-a-four-hour-agile-restrospective-heres-how-it-went-5cai</link>
      <guid>https://forem.com/thormeier/i-asked-chatgpt-to-draft-a-four-hour-agile-restrospective-heres-how-it-went-5cai</guid>
      <description>&lt;p&gt;Last weekend, I had the task to craft and hold a four-hour workshop for a group of people who have been working together for years. The group faced a few problems lately: Since they started working on their shared project in their spare time, motivation has decreased, and so has productivity. At some point, there wasn't any progress in &lt;em&gt;months&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;They wanted to identify the main problems and find solutions for them. They were very well aware that they needed to discuss a lot and take their time, hence the four-hour timeframe.&lt;/p&gt;

&lt;p&gt;I hadn't held such a workshop in ages and was running out of time to prepare it, so I asked ChatGPT for help. We've seen ChatGPT code a lot now. Let's see if it can also act as an agile coach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt
&lt;/h2&gt;

&lt;p&gt;On LinkedIn, I've seen many guides on using ChatGPT. Most of them focus on how to pose questions, but one interesting detail that keeps popping up is how people tell the AI to "pretend" to be someone else that would be very knowledgeable on the requested topic.&lt;/p&gt;

&lt;p&gt;Grant "3blue1brown" Sanderson has shown us on Twitter what could be done with that technique:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1600565043602477056-742" src="https://platform.twitter.com/embed/Tweet.html?id=1600565043602477056"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1600565043602477056-742');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1600565043602477056&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;So I tried the same. I asked ChatGPT to pretend to be a business consultant with focused experience in process optimization and agile methods. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pretend you're an expert business consultant specializing in processes and Agile development methods.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I described the problem as detailed as I could and asked for a concept of a four-hour workshop:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The company you're consulting has an issue. It's a team of five people that were very motivated initially and had many ideas for bringing the company forward. They had worked hard on all their tasks and achieved a lot initially. However, since the company is a side project for all of them, they all lost motivation and started to work less and less on the actual tasks, always finding excuses. People seem demotivated now and are thinking about closing the company, even though they don't want to. They try to have regular stand-up meetings and update each other on the tasks, but often they don't have any updates. They used some agile process, which resembles Kanban. Please generate the concept for a four-hour workshop aiming at finding the reasons for the issues mentioned and finding solutions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I used the term "company", although it's a loose group of friends working on the same project. I thought that ChatGPT could, in this particular setting, work best with a company since it pretended to be a business consultant.&lt;/p&gt;

&lt;p&gt;Surprisingly, the concept was coherent and aimed at finding problems and generating solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workshop concept
&lt;/h2&gt;

&lt;p&gt;So, ChatGPT described a workshop in six steps, essentially describing a SCRUM retrospective.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is a concept for a four-hour workshop that aims to address the issues you described:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Introduction: Begin the workshop by setting the context and explaining the purpose of the session. Let the team know that the goal is to identify the reasons for their demotivation and find solutions to get back on track.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identifying the issues: In this step, the team will work together to identify the specific problems causing demotivation and decreased productivity. This can be done through a group discussion, individual brainstorming, or a combination of both. Encourage open and honest communication during this step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Root cause analysis: Once the issues have been identified, the next step is to understand why these issues are occurring. This can be done through a technique such as "5 Whys" where the team asks "why" a specific problem is occurring, and then asks "why" again for each answer until the root cause is identified.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Brainstorming solutions: Once the root causes have been identified, the team can brainstorm potential solutions to address these issues. Encourage the team to think outside the box and consider all options.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prioritizing and action planning: After the solutions have been identified, the team can prioritize which solutions to implement first. Then, they can create an action plan to put these solutions into practice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Closing: Close the workshop by summarizing what was discussed, identifying next steps, and setting up a follow-up meeting to track progress and make adjustments as needed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Throughout the workshop, it is important to focus on creating a positive and supportive environment, where everyone feels heard and respected. Encourage open communication, active participation, and a willingness to take ownership and make changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Although ChatGPT apparently does have some "expertise" with these topics, it does help to know agile setups and perhaps even have experience with moderating workshops in general.&lt;/p&gt;

&lt;p&gt;I dug deeper.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That's already a strong basis. How can they set the context better in the beginning? Please generate a light and fun starting exercise for the first step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And ChatGPT delivered.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here's a similar exercise that can work well for small groups of 4 participants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"Motivation Web": Create a large web on a whiteboard or flipchart, with a central circle labeled "Motivation" and lines radiating out to different circles labeled with various factors that can impact motivation such as "Workload", "Communication", "Recognition", "Purpose", "Autonomy", "Support", etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Give each participant a set of sticky notes, and ask them to write down their current thoughts and feelings on their motivation levels and how they relate to the different factors on the web.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once they have written their notes, ask them to place their notes on the appropriate circles of the web.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After everyone has placed their notes, ask the group to come back together and have a discussion about the web. This will help to create a sense of connection and understanding among the team, and also give you an idea of the team's current motivation levels and which factors are impacting it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This can be a great way to start the workshop, as it helps to set the context by identifying the team's current motivation levels and the factors that are impacting it, and provides a platform for open and honest communication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can use this exercise as an introduction to the workshop, and move on to the next steps of identifying the issues, root cause analysis, brainstorming solutions, and action planning.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This exercise is also light and fun, and should not take more than 15 minutes, it's meant to be a warm up that will help the team to relax and get ready for the rest of the workshop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And this &lt;em&gt;really&lt;/em&gt; surprised me. I didn't find any other resources on the "Motivation web" activity. So, either it found some obscure unknown method, or it came up with the activity itself, essentially showing creative skills.&lt;/p&gt;

&lt;p&gt;The former seems more likely, though.&lt;/p&gt;

&lt;p&gt;I asked a few more questions to elaborate on some of the activities, especially on the issue identification and the solution finding.&lt;/p&gt;

&lt;p&gt;One of the main issues, though, was that it kept suggesting activities for large groups and had to constantly be reminded that there would only be four to five people present.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjkzsbg1afqtwxzo81go4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjkzsbg1afqtwxzo81go4.png" alt="Me asking ChatGPT for an activity for smaller groups." width="737" height="794"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Holding the workshop
&lt;/h2&gt;

&lt;p&gt;I mostly stuck to the script: Starting with an exercise I came up with myself, continuing with the motivation web, and then going through the issue hunt and the "five whys" activity.&lt;/p&gt;

&lt;p&gt;It went surprisingly well. Although I had to improvise here and there, the timeframes it suggested were quite precise, and we ended up having identified some core issues and solutions for them.&lt;/p&gt;

&lt;p&gt;However, from here on, it's our job to stick to these solutions and implement them.&lt;/p&gt;

&lt;p&gt;I did tell the participants that I had a helping hand preparing the workshop and only told them afterwards that it was ChatGPT. Their surprised faces were priceless. Although, some felt that I was, in a way, cheating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Can ChatGPT replace an agile coach? Certainly not. It lacks the emotional intelligence that is necessary to react to conflicts.&lt;/p&gt;

&lt;p&gt;Since the chatbot is a passive entity, it can only react unless we constantly feed it with data from the daily standups and other SCRUM rituals.&lt;/p&gt;

&lt;p&gt;Can ChatGPT support junior agile coaches, though? It darn well can! Asking for workshop concepts that are tailored to situations is a feat ChatGPT can do very well.&lt;/p&gt;

&lt;p&gt;Generally speaking, the AI can cut the time necessary for workshop preparation a lot, giving agile coaches more opportunity to focus on other tasks.&lt;/p&gt;

&lt;p&gt;In conclusion, I would ask it again. I would, however, take the suggestions with a grain of salt and improvise where necessary. ChatGPT can replace the initial thinking steps, but it cannot replace knowledge and experience.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt; &lt;em&gt;or&lt;/em&gt; &lt;a href="https://twitter.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;follow me on Twitter&lt;/em&gt; 🐦&lt;/a&gt;&lt;em&gt;!&lt;/em&gt; &lt;em&gt;You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>welcome</category>
    </item>
    <item>
      <title>Throwing Around Text – Kinetic Typography Part 4: Rotating Around the World 🎡🌍</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Wed, 25 Jan 2023 15:00:00 +0000</pubDate>
      <link>https://forem.com/thormeier/throwing-around-text-kinetic-typography-part-4-rotating-around-the-world-5h7b</link>
      <guid>https://forem.com/thormeier/throwing-around-text-kinetic-typography-part-4-rotating-around-the-world-5h7b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 4 of my series about kinetic typography! Let's move some text around with HTML, CSS and JS! If you missed how I came about throwing around text and deforming it with only web stuff, be sure &lt;a href="https://dev.to/thormeier/lets-build-some-kinetic-typography-part-1-a-chilly-warm-up-36g8"&gt;to check out part 1&lt;/a&gt;, &lt;a href="https://dev.to/thormeier/throwing-around-text-kinetic-typography-part-2-it-defies-gravity-itself-thanks-to-matterjs-239e"&gt;part 2&lt;/a&gt; and &lt;a href="https://dev.to/thormeier/throwing-around-text-kinetic-typography-part-3-the-walking-text-thet-follows-you-17mb"&gt;part 3&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally, I've gotten around to writing again. It has been a busy year for me (both 2022 and even 2023 so far), and you can expect some news about what exactly I was busy with not too far in the future!&lt;/p&gt;

&lt;p&gt;Today, though, we're going to throw around some more text. Specifically, we're going to rotate text and make it look like a wheel.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;☕ But first, I'd like to shout out to &lt;em&gt;&lt;a class="mentioned-user" href="https://dev.to/jamjuniper"&gt;@jamjuniper&lt;/a&gt;&lt;/em&gt; for supporting me on &lt;em&gt;buymeacoffee&lt;/em&gt;! Not only were they really generous (they not only gifted me a coffee, but basically a sandwich with that, too!), but they also became my first-ever member on &lt;em&gt;buymeacoffee&lt;/em&gt;! Thank you very much for the support!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The inspiration
&lt;/h2&gt;

&lt;p&gt;I found this specific example in a post about kinetic typography on &lt;a href="https://www.webdew.com/blog/kinetic-typography" rel="noopener noreferrer"&gt;webdew.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1nxsupgyfeoer9o3vucg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1nxsupgyfeoer9o3vucg.gif" alt="The text " width="760" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What struck me about this is the illusion of a wheel rolling towards the user but built with words. The word "workflow" itself even gives this some philosophical aspect. But we're not here to reflect on our lives; we're here to code.&lt;/p&gt;

&lt;p&gt;Let's rebuild this!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the DOM ready
&lt;/h2&gt;

&lt;p&gt;So, to show the same text over and over, we need - well - the same text over and over. To get a good effect, around 21 copies should do.&lt;/p&gt;

&lt;p&gt;Coincidentally, the example we're about to build doubles as a visual representation of the lyrics of a famous song by a French electro duo. You can probably guess which one I mean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;/* CSS goes here */&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wheel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"line"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Around the world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it for the DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling the basics
&lt;/h2&gt;

&lt;p&gt;First of all, we want the entire thing to be centred. For that, we size the body to fill the viewport, make it a flexbox and justify its content to the centre.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we take care of the text styling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.line&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;monospace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;uppercase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;letter-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nowrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then give the &lt;code&gt;.wheel&lt;/code&gt; element a height and make it fill the entire width, so the text is centred, too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.wheel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30rem&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we get is this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fezn4sfxsihgw3nngnvhf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fezn4sfxsihgw3nngnvhf.png" alt="The text " width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well, this looks like some rendering error, but bear with me!&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the animation
&lt;/h2&gt;

&lt;p&gt;Let's analyze how we can animate the text to look like it's attached to a rotating wheel.&lt;/p&gt;

&lt;p&gt;First, there is the actual rotation on the X-axis. The text is tilted back 90 degrees when on the top of the wheel and rotates to be tilted forwards by 90 degrees once it arrives at the bottom. The rotation is linear since the "wheel" has a constant speed.&lt;/p&gt;

&lt;p&gt;Second, we have the movement. We need to move the text from the top to the bottom of the wheel, hence the height of the &lt;code&gt;.wheel&lt;/code&gt; div. Ideally, this would follow some bezier curve (the &lt;code&gt;ease-in-out&lt;/code&gt;, to be precise) because of perspective.&lt;/p&gt;

&lt;p&gt;Third, we need scaling. In the example, the text at the top and bottom is &lt;em&gt;slightly&lt;/em&gt; narrower than the text in the centre. To get the wheel effect, the sizes should describe some curve to give the illusion of a cylinder. So, this animation, too, should follow the same bezier curve.&lt;/p&gt;

&lt;p&gt;To animate all of these, we use keyframes. We create three definitions, so we can apply them individually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;90deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="err"&gt;50&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rotateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-90deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;scaleToCenter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="err"&gt;50&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;moveDown&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far, so good. Now, we add the keyframes to the elements. Next, we move and scale the &lt;code&gt;.line&lt;/code&gt; div and rotate the &lt;code&gt;p&lt;/code&gt; element inside it. This way, we can have two values for &lt;code&gt;transform&lt;/code&gt; animated with different transition timing functions. We also position the wheel relatively and the lines absolutely so we can use &lt;code&gt;top&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.wheel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.line&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="m"&gt;3s&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt; &lt;span class="n"&gt;ease-in-out&lt;/span&gt; &lt;span class="n"&gt;scaleToCenter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;3s&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt; &lt;span class="n"&gt;ease-in-out&lt;/span&gt; &lt;span class="n"&gt;moveDown&lt;/span&gt;
  &lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.line&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;backface-visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3s&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="n"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, of course, all the lines will animate simultaneously. We need to add some animation delay to each of them. Although a bit tedious, we can now really fine-tune the timing to get a nice wheel-shaped wall of text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;2&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.15s&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;4&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;4&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.45s&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;5&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.6s&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;6&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;6&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75s&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;7&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;.line&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;7&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.9s&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* You get the idea... */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also use the CSS &lt;code&gt;perspective&lt;/code&gt; property to &lt;em&gt;really&lt;/em&gt; get the wheel effect. This property lets us specify how far away the user is from the Z plane of the web application. When we transform an element in 3D space, CSS will calculate what this would look like, depending on the angle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.line&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;perspective&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;150rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;perspective-origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we're done already! Awesome!&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/thormeier/embed/GRByQvp?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You spin me right round, baby, right round!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a&lt;/em&gt; ❤️&lt;em&gt;! I write tech articles in my free time and like to drink coffee every once in a while.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to support my efforts,&lt;/em&gt; &lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;you can offer me a coffee&lt;/em&gt; ☕&lt;/a&gt; &lt;em&gt;or&lt;/em&gt; &lt;a href="https://twitter.com/pthormeier" rel="noopener noreferrer"&gt;&lt;em&gt;follow me on Twitter&lt;/em&gt; 🐦&lt;/a&gt;&lt;em&gt;!&lt;/em&gt; &lt;em&gt;You can also support me directly via &lt;a href="https://www.paypal.me/pthormeier" rel="noopener noreferrer"&gt;Paypal&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://buymeacoffee.com/pthormeier" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx55hp6jopwyy161d8e2u.png" alt="Buy me a coffee button" width="262" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>tutorial</category>
      <category>design</category>
    </item>
    <item>
      <title>dev.to codes! Collaborative coding experiment: The most liked comment picks the next line of code, day 5</title>
      <dc:creator>Pascal Thormeier</dc:creator>
      <pubDate>Sat, 12 Nov 2022 20:52:21 +0000</pubDate>
      <link>https://forem.com/thormeier/devto-codes-collaborative-coding-experiment-the-most-liked-comment-picks-the-next-line-of-code-day-5-156i</link>
      <guid>https://forem.com/thormeier/devto-codes-collaborative-coding-experiment-the-most-liked-comment-picks-the-next-line-of-code-day-5-156i</guid>
      <description>&lt;p&gt;Welcome to day 5! I think we're passt the next "line" of code, so we'll use the most liked "piece" of code from here on. Not an issue, though. Congrats to &lt;a class="mentioned-user" href="https://dev.to/rafaelhashimoto"&gt;@rafaelhashimoto&lt;/a&gt; for picking this large snippet. I had to adjust it a bit to fit the previous code.&lt;/p&gt;

&lt;p&gt;his experiment is inspired by a &lt;a href="https://www.reddit.com/r/ProgrammerHumor/comments/yot55f/the_most_upvoted_comment_picks_the_next_line_of/" rel="noopener noreferrer"&gt;post on the ProgrammerHumor subreddit&lt;/a&gt;, where the original author did this exact thing: The most liked comment after 24 hours picks the next line of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nothing that's against any applicable law&lt;/li&gt;
&lt;li&gt;Nothing that's against the community code of conduct, terms of use, or privacy policy&lt;/li&gt;
&lt;li&gt;No leaking of personal information of anyone&lt;/li&gt;
&lt;li&gt;No malware/ransomware/viruses/etc.&lt;/li&gt;
&lt;li&gt;Keep it civil&lt;/li&gt;
&lt;li&gt;(To be expanded, depending on the case)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our code so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;emotions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"🥲"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"🥰"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"🥺"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"😫"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"🤬"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"😞"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"😅"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"😊"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"😰"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;how_i_feel_right_now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emotions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;how_i_feel_right_now&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"😊"&lt;/span&gt;
    &lt;span class="n"&gt;letsDoThis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;letsDoThis&lt;/span&gt;
  &lt;span class="c1"&gt;# things a person can do that will change his humor&lt;/span&gt;
  &lt;span class="n"&gt;what_have_i_done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"🥲"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;big_mistakes&lt;/span&gt;
  &lt;span class="n"&gt;how_i_feel_right_now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="n"&gt;what_have_i_done&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;big_mistakes&lt;/span&gt;
  &lt;span class="n"&gt;deploy_on_friday&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;buy_twitter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;buy_twitter&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;AquisitionError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"I won't buy it"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user_base_has_bots?&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;AquisitionError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
   &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;
   &lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# because you can't step back&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user_base_has_bots?&lt;/span&gt;
  &lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# well... yes&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(I had to adjust the code a bit for it to be valid Ruby.)&lt;/p&gt;

&lt;p&gt;See you in 24 hours!&lt;/p&gt;

</description>
      <category>community</category>
      <category>opensource</category>
      <category>programming</category>
      <category>ruby</category>
    </item>
  </channel>
</rss>
