<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/vendor/feed/atom.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
                        <id>https://tim-kleyersburg.de/feed.xml</id>
                                <link href="https://tim-kleyersburg.de/feed.xml" rel="self"></link>
                                <title><![CDATA[Tim Kleyersburg]]></title>
                    
                                <subtitle></subtitle>
                                                    <updated>2026-01-25T00:00:00+00:00</updated>
                        <entry>
            <title><![CDATA[Setup OpenClaw on a Hetzner VPS for 24/7 access]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/painless-openclaw-setup" />
            <id>https://tim-kleyersburg.de/painless-openclaw-setup</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<blockquote>
<p><strong>Note:</strong> This project was originally released as <strong>Clawdbot</strong>, then renamed to <strong>Moltbot</strong>, and is now called <strong>OpenClaw</strong>. If you followed this guide under one of the previous names, the steps are the same — only the package and command names have changed. This guide has been updated to reflect the current name.</p>
</blockquote>
<p>OpenClaw seems to be the biggest hype in January 2026. Many people talk about it, the amount of development poured into it and the speed it’s developing is crazy.</p>
<p>The documentation is thorough, but even with Claude Code I didn’t come to a working solution without manually intervening.</p>
<p>So what follows is a quick walkthrough on how to get OpenClaw working on a fresh Ubuntu 24.04 VPS from Hetzner.</p>
<h2><a id="content-1-create-vps" href="#content-1-create-vps" class="prezet-heading" title="Permalink">#</a>1. Create VPS</h2>
<p>Log into your cloud provider and create a simple VPS. 2 vCPUs and 4GB of RAM is more than enough to start. I used the cheapest Hetzner option:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./hetzner-vps-480w.png 480w, /articles/img/./hetzner-vps-640w.png 640w, /articles/img/./hetzner-vps-768w.png 768w, /articles/img/./hetzner-vps-960w.png 960w, /articles/img/./hetzner-vps-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/hetzner-vps.png" alt="Screenshot of the Hetzner server creation panel" />
<figcaption class="prezet-figcaption">Screenshot of the Hetzner server creation panel</figcaption>
</figure>
<h2><a id="content-2-basic-vps-setup" href="#content-2-basic-vps-setup" class="prezet-heading" title="Permalink">#</a>2. Basic VPS setup</h2>
<p>We’ll start with logging into the server, updating, and creating a dedicated user to use with OpenClaw.</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">ssh</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">root@your-server-ip</span><span style="color: #A6ACCD;"> </span><span style="color: #676E95;"># enter the password you received via email</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #FFCB6B;">apt</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">update</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;&amp;</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">apt</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">upgrade</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-y</span><span style="color: #A6ACCD;"> </span><span style="color: #676E95;"># update packages</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #676E95;"># if prompted to reboot, do it.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #FFCB6B;">adduser</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">openclaw</span><span style="color: #A6ACCD;"> </span><span style="color: #676E95;"># create the new user. You can leave all details empty, but don&#39;t forget to set a strong password</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #FFCB6B;">usermod</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-aG</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">openclaw</span><span style="color: #A6ACCD;"> </span><span style="color: #676E95;"># add the user to the sudoers group so we can use `sudo`</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">9</span><span style="color: #FFCB6B;">su</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">openclaw</span><span style="color: #A6ACCD;"> </span><span style="color: #676E95;"># change to the new user</span></div></code></pre>
<h2><a id="content-3-securing-your-vps" href="#content-3-securing-your-vps" class="prezet-heading" title="Permalink">#</a>3. Securing your VPS</h2>
<p>Before installing OpenClaw, we should secure the server with some basic security measures. These steps will protect your VPS from common attacks.</p>
<h3><a id="content-setup-ssh-key-authentication" href="#content-setup-ssh-key-authentication" class="prezet-heading" title="Permalink">#</a>Setup SSH key authentication</h3>
<p>First, let’s set up SSH key authentication so you can log in without a password. Run this on your <strong>local machine</strong>:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #676E95;"># Generate SSH key if you don&#39;t have one already</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">ssh-keygen</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-t</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ed25519</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-C</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">your_email@example.com</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #676E95;"># Copy your public key to the server</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #FFCB6B;">ssh-copy-id</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">openclaw@your-server-ip</span></div></code></pre>
<p>Test the SSH key login by opening a new terminal and connecting:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">ssh</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">openclaw@your-server-ip</span></div></code></pre>
<p>If you can log in without entering a password, the key authentication is working.</p>
<h3><a id="content-disable-password-authentication" href="#content-disable-password-authentication" class="prezet-heading" title="Permalink">#</a>Disable password authentication</h3>
<p>Now that key-based authentication is working, let’s disable password login. This prevents brute force attacks.</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #676E95;"># Edit SSH configuration</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">vim</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">/etc/ssh/sshd_config</span></div></code></pre>
<p>Find and update these lines (uncomment if needed):</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">PasswordAuthentication no</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">PubkeyAuthentication yes</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">PermitRootLogin no</span></div></code></pre>
<p>Save the file (<code>:wq</code>) and restart SSH:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">systemctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">restart</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ssh</span></div></code></pre>
<h3><a id="content-install-fail2ban" href="#content-install-fail2ban" class="prezet-heading" title="Permalink">#</a>Install fail2ban</h3>
<p>Fail2ban monitors login attempts and automatically blocks IP addresses that show malicious behavior.</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">apt</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">install</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">fail2ban</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-y</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #676E95;"># Create a local configuration file</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">cp</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">/etc/fail2ban/jail.conf</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">/etc/fail2ban/jail.local</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #676E95;"># Edit the configuration</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">vim</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">/etc/fail2ban/jail.local</span></div></code></pre>
<p>Find the <code>[sshd]</code> section and ensure it looks like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">[sshd]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">enabled = true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">port = ssh</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">logpath = /var/log/auth.log</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">maxretry = 5</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #A6ACCD;">bantime = 3600</span></div></code></pre>
<p>Start and enable fail2ban:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">systemctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">start</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">fail2ban</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">systemctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">enable</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">fail2ban</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #676E95;"># Check status</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">fail2ban-client</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">status</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">sshd</span></div></code></pre>
<h3><a id="content-setup-ufw-firewall" href="#content-setup-ufw-firewall" class="prezet-heading" title="Permalink">#</a>Setup UFW firewall</h3>
<p>Configure a basic firewall to only allow necessary connections:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #676E95;"># Install UFW</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">apt</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">install</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-y</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #676E95;"># Allow SSH (important - do this first!)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">allow</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ssh</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #676E95;"># Enable the firewall</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">enable</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #676E95;"># Check status</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">status</span></div></code></pre>
<p>If you plan to expose OpenClaw through a web interface later, you can allow HTTP/HTTPS:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">allow</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">http</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">allow</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https</span></div></code></pre>
<h2><a id="content-4-installing-prerequisites" href="#content-4-installing-prerequisites" class="prezet-heading" title="Permalink">#</a>4. Installing prerequisites</h2>
<p>We need some packages for OpenClaw to work properly. Verify that you are logged in as the <code>openclaw</code> user or you might run into permission problems.</p>
<h3><a id="content-node" href="#content-node" class="prezet-heading" title="Permalink">#</a>Node</h3>
<p>In my testing I found <code>npm</code> to be the most reliable package manager, but you can also try <code>pnpm</code>. You can find the up-to-date install instructions at <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://nodejs.org/en/download">nodejs.org/en/download</a>.</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #676E95;"># Install Node</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #676E95;"># Download and install nvm:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #FFCB6B;">curl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-o-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">|</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">bash</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #676E95;"># in lieu of restarting the shell</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">\. </span><span style="color: #89DDFF;">&quot;</span><span style="color: #A6ACCD;">$HOME</span><span style="color: #C3E88D;">/.nvm/nvm.sh</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #676E95;"># Download and install Node.js:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #FFCB6B;">nvm</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">install</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">24</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #676E95;"># Verify the Node.js version:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #FFCB6B;">node</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-v</span><span style="color: #A6ACCD;"> </span><span style="color: #676E95;"># Should print &quot;v24.13.0&quot;.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #676E95;"># Verify npm version:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #FFCB6B;">npm</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-v</span><span style="color: #A6ACCD;"> </span><span style="color: #676E95;"># Should print &quot;11.6.2&quot;.</span></div></code></pre>
<h3><a id="content-homebrew" href="#content-homebrew" class="prezet-heading" title="Permalink">#</a>Homebrew</h3>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://brew.sh/">Homebrew</a> is a package manager. It is needed by some skills (instructions used by OpenClaw to enhance its abilities).</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">/bin/bash</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-c</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;$(</span><span style="color: #FFCB6B;">curl</span><span style="color: #C3E88D;"> -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh</span><span style="color: #89DDFF;">)&quot;</span></div></code></pre>
<h3><a id="content-tailscale-optional" href="#content-tailscale-optional" class="prezet-heading" title="Permalink">#</a>Tailscale (optional)</h3>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="http://tailscale.com/">Tailscale</a> is a Zero Trust identity-based connectivity platform. It can be used to securely connect your local computer with your OpenClaw gateway making it possible for the gateway to do things on your local computer.
I currently don’t have a use case for this, so you might also skip it.</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">curl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-fsSL</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://tailscale.com/install.sh</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">|</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">sh</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">tailscale</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">up</span></div></code></pre>
<h2><a id="content-5-openclaw" href="#content-5-openclaw" class="prezet-heading" title="Permalink">#</a>5. OpenClaw</h2>
<p>Now the fun begins 🦞. Let’s install OpenClaw and onboard you.</p>
<h3><a id="content-installation" href="#content-installation" class="prezet-heading" title="Permalink">#</a>Installation</h3>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">npm</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">i</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-g</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">openclaw</span></div></code></pre>
<p>This might take a while, but that’s it. You’ve installed OpenClaw and are ready to onboard.</p>
<h3><a id="content-onboarding" href="#content-onboarding" class="prezet-heading" title="Permalink">#</a>Onboarding</h3>
<p>Run <code>openclaw onboard</code>. It will take you through its interactive onboarding guide. Below are the settings I used but be aware that OpenClaw is very actively developed, so settings might change quickly. I’ve left some comments.</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #A6ACCD;">◆  I understand this is powerful and inherently risky. Continue?</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">│  Yes</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">◆  Onboarding mode</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">│  ○ QuickStart</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">│  ● Manual (Configure port, network, Tailscale, and auth options.)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">|  # Use manual mode for more control</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">◆  What do you want to set up?</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">│  ● Local gateway (this machine) (Gateway reachable (ws://127.0.0.1:18789))</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">│  ○ Remote gateway (info-only)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">|  # It might seem counterintuitive at first, but you want the Gateway to run locally on the VPS</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">◆  Workspace directory</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">│  /home/openclaw/clawd</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">◆  Model/auth provider</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">│  ● OpenAI (Codex OAuth + API key)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">│  ○ Anthropic</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">│  ○ MiniMax</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">│  ○ Qwen</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">│  ○ Synthetic</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #A6ACCD;">│  ○ Google</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">│  ○ Copilot</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #A6ACCD;">|  ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span><span style="color: #A6ACCD;">|  # Choose what&#39;s right for you! I&#39;ve used my Claude Code subscription to authenticate. Refer to the documentation if needed.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span><span style="color: #A6ACCD;">◆  Gateway port</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">29</span><span style="color: #A6ACCD;">│  18789</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">30</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">31</span><span style="color: #A6ACCD;">◆  Gateway bind</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">32</span><span style="color: #A6ACCD;">│  ● Loopback (127.0.0.1)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">33</span><span style="color: #A6ACCD;">│  ○ LAN (0.0.0.0)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">34</span><span style="color: #A6ACCD;">│  ○ Tailnet (Tailscale IP)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">35</span><span style="color: #A6ACCD;">│  ○ Auto (Loopback → LAN)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">36</span><span style="color: #A6ACCD;">│  ○ Custom IP</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">37</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">38</span><span style="color: #A6ACCD;">◆  Gateway auth</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">39</span><span style="color: #A6ACCD;">│  ○ Off (loopback only)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">40</span><span style="color: #A6ACCD;">│  ● Token (Recommended default (local + remote))</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">41</span><span style="color: #A6ACCD;">│  ○ Password</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">42</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">43</span><span style="color: #A6ACCD;">|  # this will give a token you may use to access the dashboard</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">44</span><span style="color: #A6ACCD;">◆  Tailscale exposure</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">45</span><span style="color: #A6ACCD;">│  ○ Off</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">46</span><span style="color: #A6ACCD;">│  ● Serve (Private HTTPS for your tailnet (devices on Tailscale))</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">47</span><span style="color: #A6ACCD;">│  ○ Funnel</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">48</span><span style="color: #A6ACCD;">|  # choose &quot;off&quot; if you don&#39;t use Tailscale</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">49</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">50</span><span style="color: #A6ACCD;">◆  Reset Tailscale serve/funnel on exit?</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">51</span><span style="color: #A6ACCD;">│  ○ Yes / ● No</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">52</span><span style="color: #A6ACCD;">|  # choose &quot;no&quot; if you used &quot;Serve&quot; as Tailscale exposure</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">53</span><span style="color: #A6ACCD;">|</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">54</span><span style="color: #A6ACCD;">◆  Configure chat channels now?</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">55</span><span style="color: #A6ACCD;">│  ● Yes / ○ No</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">56</span><span style="color: #A6ACCD;">|  # You probably want to do this. Having access via chat makes the setup much more flexible.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">57</span><span style="color: #A6ACCD;">|  # Choose your favorite provider. When in doubt just use Telegram, they have the easiest setup for a chat bot.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">58</span><span style="color: #A6ACCD;">│</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">59</span><span style="color: #A6ACCD;">◇  Skills status ────────────╮</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">60</span><span style="color: #A6ACCD;">│                            │</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">61</span><span style="color: #A6ACCD;">│  Eligible: 13              │</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">62</span><span style="color: #A6ACCD;">│  Missing requirements: 38  │</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">63</span><span style="color: #A6ACCD;">│  Blocked by allowlist: 0   │</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">64</span><span style="color: #A6ACCD;">│                            │</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">65</span><span style="color: #A6ACCD;">├────────────────────────────╯</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">66</span><span style="color: #A6ACCD;">│</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">67</span><span style="color: #A6ACCD;">◆  Configure skills now? (recommended)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">68</span><span style="color: #A6ACCD;">│  ● Yes / ○ No</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">69</span><span style="color: #A6ACCD;">|  # Choose `Yes` and select all Skills you want to have installed with `Spacebar` and confirm with `Enter`</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">70</span><span style="color: #A6ACCD;">|  # If unsure just skip it for now, you can always configure this later</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">71</span><span style="color: #A6ACCD;">│</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">72</span><span style="color: #A6ACCD;">◆  Preferred node manager for skill installs</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">73</span><span style="color: #A6ACCD;">│  ● npm</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">74</span><span style="color: #A6ACCD;">│  ○ pnpm</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">75</span><span style="color: #A6ACCD;">│  ○ bun</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">76</span><span style="color: #A6ACCD;">│</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">77</span><span style="color: #A6ACCD;">◆  Set GOOGLE_PLACES_API_KEY for goplaces?</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">78</span><span style="color: #A6ACCD;">│  ○ Yes / ● No</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">79</span><span style="color: #A6ACCD;">|  # Unless you have API keys just answer no to all the API key questions</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">80</span><span style="color: #A6ACCD;">│</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">81</span><span style="color: #A6ACCD;">◆  Enable hooks?</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">82</span><span style="color: #A6ACCD;">│  ◼ Skip for now</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">83</span><span style="color: #A6ACCD;">│  ◻ 🚀 boot-md</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">84</span><span style="color: #A6ACCD;">│  ◻ 📝 command-logger</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">85</span><span style="color: #A6ACCD;">│  ◻ 💾 session-memory</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">86</span><span style="color: #A6ACCD;">|  # Skip, unless you know what you are doing (or read the manual)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">87</span><span style="color: #A6ACCD;">│</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">88</span><span style="color: #A6ACCD;">◆  Install Gateway service (recommended)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">89</span><span style="color: #A6ACCD;">│  ● Yes / ○ No</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">90</span><span style="color: #A6ACCD;">│</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">91</span><span style="color: #A6ACCD;">◆  Gateway service runtime</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">92</span><span style="color: #A6ACCD;">│  ● Node (recommended) (Required for WhatsApp + Telegram. Bun can corrupt memory</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">93</span><span style="color: #A6ACCD;">on reconnect.)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">94</span><span style="color: #A6ACCD;">│</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">95</span><span style="color: #A6ACCD;">◆  How do you want to hatch your bot?</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">96</span><span style="color: #A6ACCD;">│  ● Hatch in TUI (recommended)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">97</span><span style="color: #A6ACCD;">│  ○ Open the Web UI</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">98</span><span style="color: #A6ACCD;">│  ○ Do this later</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">99</span><span style="color: #A6ACCD;">|  # After confirming this you will be put into a TUI (Terminal User Interface) to &quot;hatch&quot; your new assistant. Have fun!</span></div></code></pre>
<h3><a id="content-telegram" href="#content-telegram" class="prezet-heading" title="Permalink">#</a>Telegram</h3>
<p>After the gateway has started (the onboarding setup should have shown you a success message) you can message your bot if you created one with <code>/start</code> to pair your Telegram chat sessions to your OpenClaw gateway. Everything is explained in the chat.</p>
<h2><a id="content-6-use-cases-and-configuration" href="#content-6-use-cases-and-configuration" class="prezet-heading" title="Permalink">#</a>6. Use cases and configuration</h2>
<p>You might be tempted to configure workflows, automations, or other features via the Dashboard UI. While this works for some things, I highly recommend using the chat interface instead.</p>
<p>Simply describe what you want to achieve. OpenClaw is remarkably good at understanding intent and will guide you through the setup process. For example, I’ve long wanted a summary of my bookmarked tweets. I tend to hit the bookmark icon and then forget to review them later. I asked OpenClaw to send me a daily digest, and it walked me through the entire process: setting up <code>bird</code> (the CLI for interacting with X), configuring the skill, and even sending a test message to confirm everything worked.</p>
<p>If there’s something you want to accomplish, just ask. It genuinely feels like having a personal assistant. Rather than responding with “I’m sorry, I can’t do that,” it proposes a plan to achieve your desired outcome.</p>
]]>
            </summary>
                                    <updated>2026-01-25T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Using Kindle and Home Assistant for my favorite automation]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/kindle-automation" />
            <id>https://tim-kleyersburg.de/kindle-automation</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>I love reading before bed, but I always forget the time and have to find the right button to turn off the bedside lamp. This is one of those tiny annoyances that’s perfect for home automation.</p>
<p>The beauty of this solution is its simplicity: the Kindle only connects to Wi-Fi when you turn it on, making it a perfect trigger for automation.</p>
<h2><a id="content-how-it-works" href="#content-how-it-works" class="prezet-heading" title="Permalink">#</a>How it works</h2>
<p>When you activate your Kindle, it connects to your Wi-Fi network. Home Assistant detects this through a device tracker and starts a 10-minute timer. After the timer expires, your bedroom lights gradually fade out.</p>
<p>I added a time condition to prevent the lights from turning off too early in the evening - nobody wants their lights to go out at 7 PM just because they picked up their Kindle.</p>
<h2><a id="content-setting-up-the-automation" href="#content-setting-up-the-automation" class="prezet-heading" title="Permalink">#</a>Setting up the automation</h2>
<p>Create an automation with the following configuration:</p>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #F07178;">alias</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">Bedroom: Turn off night lamp after 10 minutes of reading</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">description</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F07178;">mode</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">single</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #F07178;">triggers</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">  </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #F07178;">entity_id</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">device_tracker.kindle</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">to</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">home</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">trigger</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">state</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">conditions</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">  </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #F07178;">condition</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">time</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">before</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">23:59:58</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">after</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">21:00:01</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #F07178;">actions</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">  </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #F07178;">delay</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">hours</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">0</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">minutes</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">10</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">seconds</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">0</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">milliseconds</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">0</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">  </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #F07178;">data</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">transition</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">5</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">target</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">area_id</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">bedroom</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">action</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">light.turn_off</span></div></code></pre>
<p><strong>Important</strong>: Replace <code>bedroom</code> with your bedroom area ID, or use specific entity IDs if you haven’t configured areas in Home Assistant.</p>
<p>The <code>transition: 5</code> parameter gradually dims the lights over 5 seconds, preventing the jarring experience of sudden darkness when you’re likely already half asleep.</p>
<h2><a id="content-setting-up-device-tracking-for-your-kindle" href="#content-setting-up-device-tracking-for-your-kindle" class="prezet-heading" title="Permalink">#</a>Setting up device tracking for your Kindle</h2>
<p>If your Kindle doesn’t appear as a device tracker yet, you’ll need to add it:</p>
<ol>
<li>Open Settings → Devices &amp; Services</li>
<li>Check if your router integration is set up (most routers are supported)</li>
<li>Your Kindle should appear as a device once it connects to Wi-Fi</li>
<li>Note the entity ID (likely something like <code>device_tracker.kindle</code>)</li>
</ol>
<p>That’s it! A simple automation that makes bedtime just a little bit better.</p>
]]>
            </summary>
                                    <updated>2025-11-22T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building A Life OS - Part 2]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/building-a-life-os-part-2" />
            <id>https://tim-kleyersburg.de/building-a-life-os-part-2</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<h2><a id="content-quick-recap" href="#content-quick-recap" class="prezet-heading" title="Permalink">#</a>Quick recap</h2>
<p><a href="/articles/building-a-life-os">Last time</a> I’ve explained my motiviation behind building a project like this, what features I’ve built (like a notification system for the digital timetable of my kid) and what my plans were for the future.</p>
<p>Over half a year later and I still use many of the things I’ve built, discarded some, and removed things that didn’t work.</p>
<h2><a id="content-what-worked" href="#content-what-worked" class="prezet-heading" title="Permalink">#</a>What worked</h2>
<p>All features I’ve implemented initially (digital timetable with notifications, vocabulary tests, goal tracking, bookmarks) are still in place.<br />
Although recently there weren’t many vocabulary tests done (learning methods changed a bit, grammar coming in).</p>
<p>But: <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/tim-kleyersburg.de/commit/7365e75935e927353cdd0210312a327472b1c55b">I don’t use an LLM anymore</a> to create motivational messages for goal tracking. Initially, the thought of having a “personal assistant” sounded great, but after implementation it was just plain boring. That might be a skill issue on my part, but I’ve removed it completely for now.</p>
<p>The goal tracking itself still motivates me to stay on track. To make it more rewarding I’ve created an XP system which rewards streaks.<br />
Although I must say this isn’t motivating me as much as I’ve hoped, so I might remove it in the near future.</p>
<p>That’s one of the aspects of this project that I like most: it feels like a living organism adjusting to my needs. Maybe this also feeds a hidden god-complex, I don’t know.</p>
<h2><a id="content-what-didnt-work" href="#content-what-didnt-work" class="prezet-heading" title="Permalink">#</a>What didn’t work</h2>
<p>In the closing words of my <a href="/articles/building-a-life-os">last post</a> I had the plan to build a daily digest, which I did.<br />
But turns out, this is also not as useful as I hoped, so I quickly removed it again. The problem I’ve faced was that in regard to fulfil the requirement of the aforementioned personal assistant, the timing would need to be much better. I don’t want to see nearly identical messages each day at 9 am, but when I’m ready to consume it. Maybe I need a Home Assistant integration to make this better.</p>
<h2><a id="content-what-is-new" href="#content-what-is-new" class="prezet-heading" title="Permalink">#</a>What is new</h2>
<p>I’ve added <em>a bunch</em> of new stuff since February.</p>
<h3><a id="content-gym-tracker" href="#content-gym-tracker" class="prezet-heading" title="Permalink">#</a>Gym tracker</h3>
<p>Before, I was using the app my gym provided to track my sessions. I also used <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://alphaprogression.com/">Alpha Progression</a> for maybe a year. The gym provided system wasn’t very user friendly and I didn’t like that some simple functions in Alpha Progression needed a subscription.</p>
<p>So like any good programmer I underestimated the complexity massively and built my own gym session tracker.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./grind-tracking-480w.png 480w, /articles/img/./grind-tracking-640w.png 640w, /articles/img/./grind-tracking-768w.png 768w, /articles/img/./grind-tracking-960w.png 960w, /articles/img/./grind-tracking-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/grind-tracking.png" alt="Screenshot of the tracking of a session" />
<figcaption class="prezet-figcaption">Screenshot of the tracking of a session</figcaption>
</figure>
<p>I started pretty small. Just having training plans, exercises and a way to track each set. One thing important for me was to have a clear indicator when to progress. It’s a simple thing but helps me get better. Alpha Progress called this “smart progression hints” or something like that. Nothing about this has to be smart. If I’ve hit the upper bound of my rep range I put on more weight. Easy as that.</p>
<h3><a id="content-packlist-generator" href="#content-packlist-generator" class="prezet-heading" title="Permalink">#</a>Packlist Generator</h3>
<p>I grep up in a household where we had a 4 pages long Excel sheet to plan what to take with us on vacation. This causes irreversible brain damage to a little human being, causing them to write a program they can use to track what to pack for a journey.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./gear-journey-480w.png 480w, /articles/img/./gear-journey-640w.png 640w, /articles/img/./gear-journey-768w.png 768w, /articles/img/./gear-journey-960w.png 960w, /articles/img/./gear-journey-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/gear-journey.png" alt="Screenshot of a journey" />
<figcaption class="prezet-figcaption">Screenshot of a journey</figcaption>
</figure>
<p>The interesting part wasn’t the items itself but the business logic what should be packed. Each journey can have multiple properties (like <code>traveling-with-kid</code>, <code>business-trip</code>) which is be used to match to specific items.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./gear-item-properties-480w.png 480w, /articles/img/./gear-item-properties-640w.png 640w, /articles/img/./gear-item-properties-768w.png 768w, /articles/img/./gear-item-properties-960w.png 960w, /articles/img/./gear-item-properties-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/gear-item-properties.png" alt="Screenshot of the item properties" />
<figcaption class="prezet-figcaption">Screenshot of the item properties</figcaption>
</figure>
<p>Each property has a matching function defining if the item should be included in the journey. For properties like <code>warm-weather</code> or <code>rain-expected</code> I needed a Weather API implementation. But most APIs only give you a forecast of around 14 days. While this might sound like there are APIs with a more extensive forecast that’s unfortunately not the case. The most limiting factor is the weather models, getting less and less precise farther into the future.</p>
<p>But I didn’t want to plan journeys only 2 weeks ahead. While this would be sufficient in most cases, solving the problem of looking farther into the future was the problem I wanted to solve.</p>
<p>I’m using <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://open-meteo.com/">Open-Meteo</a> to get the weather. They have great APIs, good documentation and on top of that are free.</p>
<p>If the journey is planned more days ahead I’m using an average from the past five years to assume what the weather might me.</p>
<pre><code data-theme="material-theme-palenight" data-lang="php" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #676E95;">/**</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #676E95;"> * </span><span style="color: #F78C6C;">@param</span><span style="color: #676E95;">  </span><span style="color: #F78C6C;">array</span><span style="color: #676E95;">{</span><span style="color: #FFCB6B;">latitude</span><span style="color: #676E95;">:</span><span style="color: #F78C6C;">float</span><span style="color: #676E95;">,</span><span style="color: #FFCB6B;">longitude</span><span style="color: #676E95;">:</span><span style="color: #F78C6C;">float</span><span style="color: #676E95;">}  $coordinates</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #676E95;"> * </span><span style="color: #F78C6C;">@return</span><span style="color: #676E95;"> </span><span style="color: #FFCB6B;">Collection</span><span style="color: #676E95;">&lt;</span><span style="color: #F78C6C;">int</span><span style="color: #676E95;">, DayForecast&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #676E95;"> */</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #C792EA;">private</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">static</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">fetchHistoricalAverageForecast</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">array</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">coordinates</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">CarbonImmutable</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">start</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">CarbonImmutable</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">end</span><span style="color: #89DDFF;">):</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Collection</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">historicalStartDate </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">start</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">subYears</span><span style="color: #89DDFF;">(</span><span style="color: #C792EA;">self</span><span style="color: #89DDFF;">::</span><span style="color: #A6ACCD;">HISTORICAL_YEARS_TO_AVERAGE</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">historicalEndDate </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">end</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">subYears</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">1</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">params </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">latitude</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">coordinates</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">latitude</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">],</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">longitude</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">coordinates</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">longitude</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">],</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">daily</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">start_date</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">historicalStartDate</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">format</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Y-m-d</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">end_date</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">historicalEndDate</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">format</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Y-m-d</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">self</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">makeApiCall</span><span style="color: #89DDFF;">(</span><span style="color: #C792EA;">self</span><span style="color: #89DDFF;">::</span><span style="color: #A6ACCD;">HISTORICAL_API_URL</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">params</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">if</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(!</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response </span><span style="color: #89DDFF;">||</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">!</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">isset</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">daily</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">]))</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">collect</span><span style="color: #89DDFF;">();</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">self</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">processHistoricalData</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">daily</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">],</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">start</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">end</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>No idea yet how precise this will be, but for a rough estimate it should suffice. I always have the possibility to recreate the list if needed.</p>
<h3><a id="content-lastfm-importer" href="#content-lastfm-importer" class="prezet-heading" title="Permalink">#</a>LastFM importer</h3>
<p>Over the last 18 years I’ve accumulated <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.last.fm/user/Timmotheus">over 215k scrobbles</a> on Last.fm. Apart from maybe Google this is one of the services I use the longest and without much interruptions.</p>
<p>In the last months I more and more lean towards the idea of owning my own data, so I wrote an importer for Last.fm.</p>
<p>Because of the way the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.last.fm/api">Last.fm API</a> works there unfortunately is no easy way to export everything.<br />
Pages are counted from present to past and there is no way to reverse this logic. This could mean that a scrobble will change the currently first page the second.</p>
<p>But I think the solution I came up with is simple and elegant:</p>
<pre><code data-theme="material-theme-palenight" data-lang="php" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #C792EA;">public</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">handle</span><span style="color: #89DDFF;">(</span><span style="color: #FFCB6B;">LastFm</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">lastFm</span><span style="color: #89DDFF;">):</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">void</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">localScrobbles </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Scrobble</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">query</span><span style="color: #89DDFF;">()-&gt;</span><span style="color: #82AAFF;">count</span><span style="color: #89DDFF;">();</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">latestScrobble </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">lastFm</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">getRecentTracks</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">1</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// Get the latest scrobble to get the total scrobbles from the API response</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">totalScrobbles </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #C792EA;">int</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">data_get</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">latestScrobble</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">@attr.total</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// If we have the same amount of scrobbles locally, bail</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">if</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">totalScrobbles </span><span style="color: #89DDFF;">===</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">localScrobbles</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// calculate the total number of pages</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">totalPages </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #C792EA;">int</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">ceil</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">totalScrobbles </span><span style="color: #89DDFF;">/</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">self</span><span style="color: #89DDFF;">::</span><span style="color: #A6ACCD;">LIMIT</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// calculate the difference to know which page to get</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">pageToFetch </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">max</span><span style="color: #89DDFF;">($this-&gt;</span><span style="color: #A6ACCD;">page </span><span style="color: #89DDFF;">??</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #C792EA;">int</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">ceil</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">totalPages </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">localScrobbles </span><span style="color: #89DDFF;">/</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">self</span><span style="color: #89DDFF;">::</span><span style="color: #A6ACCD;">LIMIT</span><span style="color: #89DDFF;">)),</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">1</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// get the scrobbles and reject all with the exact same date</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">allScrobbles </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">collect</span><span style="color: #89DDFF;">(</span><span style="color: #82AAFF;">data_get</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">lastFm</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">getRecentTracks</span><span style="color: #89DDFF;">(</span><span style="color: #FFCB6B;">limit</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">self</span><span style="color: #89DDFF;">::</span><span style="color: #A6ACCD;">LIMIT</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">page</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">pageToFetch</span><span style="color: #89DDFF;">),</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">track</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">));</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">scrobbles </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">allScrobbles</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">reject</span><span style="color: #89DDFF;">(</span><span style="color: #C792EA;">fn</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">scrobble</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">!</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">data_get</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">scrobble</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">date.uts</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">));</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// format and save to db</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">data </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">scrobbles</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">map</span><span style="color: #89DDFF;">(</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">scrobble</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">artist</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">data_get</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">scrobble</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">artist.#text</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">29</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">album</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">data_get</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">scrobble</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">album.#text</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">30</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">track</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">data_get</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">scrobble</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">name</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">31</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">played_at</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Carbon</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">createFromTimestamp</span><span style="color: #89DDFF;">(</span><span style="color: #82AAFF;">data_get</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">scrobble</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">date.uts</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">)),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">32</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">payload</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">json_encode</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">scrobble</span><span style="color: #89DDFF;">),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">33</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">34</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">})-&gt;</span><span style="color: #82AAFF;">filter</span><span style="color: #89DDFF;">();</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">35</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">36</span><span style="color: #A6ACCD;">    </span><span style="color: #FFCB6B;">Scrobble</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">query</span><span style="color: #89DDFF;">()-&gt;</span><span style="color: #82AAFF;">upsert</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">data</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">toArray</span><span style="color: #89DDFF;">(),</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">artist</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">track</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">played_at</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">]);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">37</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">38</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// if we are on the first page, bail</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">39</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">if</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">pageToFetch </span><span style="color: #89DDFF;">===</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">1</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">40</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">41</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">42</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">43</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// recursive logic, if we had Scrobbles get the next page</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">44</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// if not, fetch the same page to make sure we now have all Scrobbles</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">45</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">self</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">dispatch</span><span style="color: #89DDFF;">(($</span><span style="color: #A6ACCD;">scrobbles</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">count</span><span style="color: #89DDFF;">()</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">?</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">pageToFetch </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">1</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">pageToFetch</span><span style="color: #89DDFF;">));</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">46</span><span style="color: #89DDFF;">}</span></div></code></pre>
<h3><a id="content-todo-management" href="#content-todo-management" class="prezet-heading" title="Permalink">#</a>Todo Management</h3>
<p>It started like a simple todo list tutorial. But I’ve had some ideas and concepts in mind that manifested over the years of usage of other tools.</p>
<ul>
<li><strong>There are no overdue tasks.</strong> A task is either due today, or not. I can’t do something yesterday so don’t punish me for not meeting a deadline but let it gracefully roll over.</li>
<li><strong>Infinite sub tasks.</strong> It’s known that big tasks fuel procrastination because you don’t know where to start. So I wanted a system that is able to handle a task no matter how big, by giving me the possibility to break it down into as small sub tasks as needed.</li>
<li><strong>Notes integration.</strong> The border between notes and tasks is sometimes very thin. Oftentimes a finished task becomes a note or some task spawns from a note. So every task in my system can be a note or a task and it can be switched around as needed. Apart from completing they have the same parts:
<ul>
<li>description</li>
<li>links</li>
<li>attachments</li>
<li>comments</li>
<li>subtasks / subnotes</li>
</ul>
</li>
</ul>
<p>The first <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/tim-kleyersburg.de/commit/163adf1fd8d6ad4afe62a03d9bbf57b6c0342b1c">version in April 2025</a> didn’t have that much functionality. It was a very basic todo list with only the notes added.<br />
But it evolved and evolved as I saw what worked and what didn’t work. A few months in I moved all my notes from Obsidian over and haven’t looked back since.</p>
<p>Some time in the future I want to tackle the personal assistant again, using all my notes and tasks as context.</p>
<h3><a id="content-printer-system-using-a-receipt-printer" href="#content-printer-system-using-a-receipt-printer" class="prezet-heading" title="Permalink">#</a>Printer System using a receipt printer</h3>
<p>This one is the most controversial project I’ve done - at least when talking about it in my social circle. While nobody ever really sees most of the things I’m programming: the receipt printer on my desk <em>will</em> rise questions.</p>
<p>I’ve read this great post by Laurie: <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.laurieherault.com/articles/a-thermal-receipt-printer-cured-my-procrastination">A receipt printer cured my procrastination</a>. It resonated so much with how I see task management, e.g. he also advocates for breaking big tasks down into smaller ones.<br />
So I’ve started researching receipt printers, how they can be connected and how I could print things on it.</p>
<p>What a fun rabbit hole! Building stuff on the web fascinates me since nearly 20 years. But building stuff that interacts with the <em>physical world</em> had some kind of magic feeling. I can now press a little print button in my task management and it prints the task nearly instantly. Regardless of where I am.</p>
<p>How I’ve built the necessary parts will be a post of its own. But let me tell you: having a physical representation of a task helped me very much staying on top of my todos. More like any other system before. I highly recommend it - if you are able to live with the snarky comments you most likely will get.</p>
]]>
            </summary>
                                    <updated>2025-10-31T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Building A Life OS]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/building-a-life-os" />
            <id>https://tim-kleyersburg.de/building-a-life-os</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<h2><a id="content-my-personal-definition-of-a-life-os" href="#content-my-personal-definition-of-a-life-os" class="prezet-heading" title="Permalink">#</a>My personal definition of a Life OS</h2>
<p>The term “Life OS” is often associated with a note taking system helping you manage your life. It does so by giving you structures to organize your thoughts, tasks, and goals.</p>
<p>For me, that’s not enough. An Operating System should be able to <em>manage</em> your life, not just <em>help you manage</em> it. It should be able to automate tasks, remind you of important things and be truly made for you.</p>
<p>In my opinion a note taking system falls short of that. It’s a great tool to help you manage your thoughts, but it’s not a Life OS.</p>
<h2><a id="content-motivation-behind-building-a-life-os" href="#content-motivation-behind-building-a-life-os" class="prezet-heading" title="Permalink">#</a>Motivation behind building a Life OS</h2>
<p>For multiple months I’ve juggled with the thought of creating a system that is able to combine all the different digital sources that are part of my life.</p>
<p>The tipping point for me was when my kid got into a new school and got a digital school schedule, namely <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://webuntis.com/">Webuntis</a>. It contains information about the classes, homework and upcoming tests. The concept is great but the UX is, sorry Untis-Team, terrible.</p>
<ul>
<li>There are no notifications</li>
<li>You don’t see all homeworks or tests at a glance but have to check <em>each day individually</em></li>
<li>abbreviations are used that are not explained anywhere</li>
</ul>
<p>But: they have an API*. So I thought: why not build a system that combines all the different sources of information that are part of my life? I love working with APIs or with data from different sources that do things.</p>
<p>So I started building a Life OS.</p>
<p><em>* The API sucks, also. But that’s a different story.</em></p>
<h2><a id="content-whats-inside" href="#content-whats-inside" class="prezet-heading" title="Permalink">#</a>What’s inside</h2>
<p>I’m using the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://tallstack.dev/">TALL stack</a>, which stands for Tailwind CSS, Alpine.js, Laravel and Livewire. I’ve been using the stack professionally for some time now, so I’m very comfortable with it.</p>
<p>For content management and blogging in markdown I’m using <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://prezet.com">Prezet</a>. It’s a package that allows for easy markdown blogging in Laravel. Since all the articles I’ve written before are in markdown it was a great fit and made the switch even easier.</p>
<h3><a id="content-features" href="#content-features" class="prezet-heading" title="Permalink">#</a>Features</h3>
<p>To this day I’ve implemented the following features:</p>
<h4>Webuntis Integration</h4>
<ul>
<li>it uses the API to fetch all the needed data like the classes, homeworks, tests and news</li>
<li>displays the data in a nice way</li>
<li>sends me a Discord notification if a class was cancelled or a new test was added or new news was published</li>
</ul>
<h4>Vocabulary Tests</h4>
<p>This is for my kid. And a little bit for me, too. To create a good vocabulary learning system takes a lot of time if you do it analog. So I created a system that allows me to create vocabulary tests and my kid to learn them.</p>
<ul>
<li>I can add all current words with their translations</li>
<li>I can create tests with a certain amount of words</li>
<li>The system keeps track of the score by subtracting wrong from right answers, therefore allowing us to focus on words that are harder to remember</li>
</ul>
<h4>Goal Tracking</h4>
<p>Although there are so many goal tracking apps out there I was never quite satisfied with some part of them. Either the notifications where bothering me, or the UX was not good enough, or the features were too much or too little.</p>
<p>So I created a goal tracking system that allows me to create goals, track them and get notifications if I haven’t worked on them for a certain amount of time.</p>
<p>I’m using LLMs for creating the messages, incorporating the weather so it feels more like a personal coach instead of a dumb notification.</p>
<p>Next steps are the implementation of streaks to motivate me even more.</p>
<h4>Bookmarks</h4>
<p>I’m not the most organized person when it comes to bookmarks. Most of the time I just keep some tabs open, never read them, close them and when I need them can’t remember where I’ve seen them.</p>
<p>I’ve created a simple module to put in the URL for something I don’t want to forget. I’m then using a scraper to extract all information from the page to give me a little more information what the page is about.</p>
<h3><a id="content-the-future" href="#content-the-future" class="prezet-heading" title="Permalink">#</a>The future</h3>
<p>I’m planning to add more features to the system. I’m thinking a daily digest which uses multiple sources like my iCloud reminders, calendar events, the weather, and the news to give me a daily overview of what’s happening and what I have to do. And also which goals I’ve missed yesterday and motivate me to achieve them today.</p>
]]>
            </summary>
                                    <updated>2025-02-07T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Setup Firecrawl with Docker and Traefik]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/setup-firecrawl-with-docker-and-traefik" />
            <id>https://tim-kleyersburg.de/setup-firecrawl-with-docker-and-traefik</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<h2><a id="content-what-is-firecrawl" href="#content-what-is-firecrawl" class="prezet-heading" title="Permalink">#</a>What is Firecrawl?</h2>
<blockquote>
<p>Turn websites into LLM-ready data<br />
Power your AI apps with clean data crawled from any website. It’s also open-source.</p>
</blockquote>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.firecrawl.dev/">Source</a></p>
<p>It’s a project that enables you to scrape and crawl websites and get well-formatted data. It also handles JavaScript rendering, which is a common issue when scraping websites. And the best part: it’s open source and you can host it yourself.</p>
<h2><a id="content-installing-firecrawl-with-docker" href="#content-installing-firecrawl-with-docker" class="prezet-heading" title="Permalink">#</a>Installing Firecrawl with Docker</h2>
<p>Firecrawl provides everything you need to get started quickly. You may use the <code>docker-compose.yaml</code> file provided in their <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/mendableai/firecrawl/blob/d316d52c963eeb2f05e7624204e969fb73f43e9b/docker-compose.yaml">GitHub repository</a>.</p>
<p>Make sure to clone the whole repository as the <code>docker-compose.yaml</code> file references other files in the repository.</p>
<p>Copy the <code>.env.example</code> file from the directory <code>apps/api</code> into the same directory where you have the <code>docker-compose.yaml</code> file and rename it to <code>.env</code>. If you don’t have reasons to change anything the default values work fine.</p>
<h2><a id="content-integrating-with-traefik" href="#content-integrating-with-traefik" class="prezet-heading" title="Permalink">#</a>Integrating with Traefik</h2>
<p>If, like me, you are using <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://traefik.io/">Traefik</a> as a reverse proxy, use the following example configuration to configure Traefik with labels.</p>
<p>This assumes you are using an external network called <code>web</code> to route all your traffic through Traefik.</p>
<p>I’ve used the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/omar-shrbajy-arive/headerauthentication">headerauthentication</a> plugin by <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/omar-shrbajy-arive">omar-shrbajy-arive</a>. This plugin allows you to define a specific header key and value that must be present in the request to access Firecrawl. You can also use it without authentication, but I prefer and recommend not to leave a web scraper open to the internet for anyone to use.</p>
<p>To use this plugin, add the following configuration to your Traefik static configuration file (<code>traefik.toml</code>).</p>
<pre><code data-theme="material-theme-palenight" data-lang="toml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">[</span><span style="color: #FFCB6B;">experimental</span><span style="color: #A6ACCD;">.</span><span style="color: #FFCB6B;">plugins</span><span style="color: #A6ACCD;">.</span><span style="color: #FFCB6B;">headerauthentication</span><span style="color: #89DDFF;">]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">  moduleName </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">github.com/omar-shrbajy-arive/headerauthentication</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">  version </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">v1.0.3</span><span style="color: #89DDFF;">&quot;</span></div></code></pre>
<p>Now you can use the following <code>docker-compose.yaml</code> file to set up Firecrawl with Traefik, it is the exact same as the one provided by Firecrawl, but with the Traefik labels added.</p>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">  1</span><span style="color: #F07178;">name</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">firecrawl</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">  2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">  3</span><span style="color: #F07178;">x-common-service</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;</span><span style="color: #FFCB6B;">common-service</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">  4</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">build</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">apps/api</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">  5</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">  6</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">backend</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">  7</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">extra_hosts</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">  8</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">host.docker.internal:host-gateway</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">  9</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 10</span><span style="color: #F07178;">services</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 11</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">playwright-service</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 12</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">build</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">apps/playwright-service</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 13</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">environment</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 14</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">PORT=3000</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 15</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">PROXY_SERVER=${PROXY_SERVER}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 16</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">PROXY_USERNAME=${PROXY_USERNAME}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 17</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">PROXY_PASSWORD=${PROXY_PASSWORD}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 18</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">BLOCK_MEDIA=${BLOCK_MEDIA}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 19</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 20</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">backend</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 21</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 22</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">api</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 23</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&lt;&lt;:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">*</span><span style="color: #A6ACCD;">common-service</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 24</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">labels</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 25</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.middlewares.firecrawl.plugin.headerauthentication.header.name=Authorization</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 26</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.middlewares.firecrawl.plugin.headerauthentication.header.key=Bearer ${BEARER_TOKEN}</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 27</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.firecrawl.rule=Host(`firecrawl.your-domain.com`)</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 28</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.firecrawl.tls=true</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 29</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.firecrawl.tls.certresolver=lets-encrypt</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 30</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.firecrawl.middlewares=firecrawl</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 31</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">environment</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 32</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">REDIS_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${REDIS_URL:-redis://redis:6379}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 33</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">REDIS_RATE_LIMIT_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${REDIS_URL:-redis://redis:6379}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 34</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">PLAYWRIGHT_MICROSERVICE_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${PLAYWRIGHT_MICROSERVICE_URL:-http://playwright-service:3000}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 35</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">USE_DB_AUTHENTICATION</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${USE_DB_AUTHENTICATION}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 36</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">PORT</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${PORT:-3002}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 37</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">NUM_WORKERS_PER_QUEUE</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${NUM_WORKERS_PER_QUEUE}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 38</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">OPENAI_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${OPENAI_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 39</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">OPENAI_BASE_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${OPENAI_BASE_URL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 40</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">MODEL_NAME</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${MODEL_NAME:-gpt-4o}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 41</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SLACK_WEBHOOK_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SLACK_WEBHOOK_URL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 42</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">LLAMAPARSE_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${LLAMAPARSE_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 43</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">LOGTAIL_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${LOGTAIL_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 44</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">BULL_AUTH_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${BULL_AUTH_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 45</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">TEST_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${TEST_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 46</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">POSTHOG_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${POSTHOG_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 47</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">POSTHOG_HOST</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${POSTHOG_HOST}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 48</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SUPABASE_ANON_TOKEN</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SUPABASE_ANON_TOKEN}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 49</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SUPABASE_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SUPABASE_URL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 50</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SUPABASE_SERVICE_TOKEN</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SUPABASE_SERVICE_TOKEN}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 51</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SCRAPING_BEE_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SCRAPING_BEE_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 52</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">HOST</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${HOST:-0.0.0.0}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 53</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SELF_HOSTED_WEBHOOK_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SELF_HOSTED_WEBHOOK_URL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 54</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">LOGGING_LEVEL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${LOGGING_LEVEL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 55</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">FLY_PROCESS_GROUP</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">app</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 56</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">depends_on</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 57</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">redis</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 58</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">playwright-service</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 59</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">ports</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 60</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">3002:3002</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 61</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">command</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">pnpm</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">run</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">start:production</span><span style="color: #89DDFF;">&quot;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 62</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 63</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">web</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 64</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 65</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">worker</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 66</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&lt;&lt;:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">*</span><span style="color: #A6ACCD;">common-service</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 67</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">environment</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 68</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">REDIS_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${REDIS_URL:-redis://redis:6379}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 69</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">REDIS_RATE_LIMIT_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${REDIS_URL:-redis://redis:6379}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 70</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">PLAYWRIGHT_MICROSERVICE_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${PLAYWRIGHT_MICROSERVICE_URL:-http://playwright-service:3000}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 71</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">USE_DB_AUTHENTICATION</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${USE_DB_AUTHENTICATION}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 72</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">PORT</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${PORT:-3002}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 73</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">NUM_WORKERS_PER_QUEUE</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${NUM_WORKERS_PER_QUEUE}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 74</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">OPENAI_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${OPENAI_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 75</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">OPENAI_BASE_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${OPENAI_BASE_URL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 76</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">MODEL_NAME</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${MODEL_NAME:-gpt-4o}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 77</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SLACK_WEBHOOK_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SLACK_WEBHOOK_URL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 78</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">LLAMAPARSE_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${LLAMAPARSE_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 79</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">LOGTAIL_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${LOGTAIL_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 80</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">BULL_AUTH_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${BULL_AUTH_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 81</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">TEST_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${TEST_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 82</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">POSTHOG_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${POSTHOG_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 83</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">POSTHOG_HOST</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${POSTHOG_HOST}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 84</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SUPABASE_ANON_TOKEN</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SUPABASE_ANON_TOKEN}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 85</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SUPABASE_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SUPABASE_URL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 86</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SUPABASE_SERVICE_TOKEN</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SUPABASE_SERVICE_TOKEN}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 87</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SCRAPING_BEE_API_KEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SCRAPING_BEE_API_KEY}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 88</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">HOST</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${HOST:-0.0.0.0}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 89</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">SELF_HOSTED_WEBHOOK_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${SELF_HOSTED_WEBHOOK_URL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 90</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">LOGGING_LEVEL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">${LOGGING_LEVEL}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 91</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">FLY_PROCESS_GROUP</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">worker</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 92</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">depends_on</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 93</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">redis</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 94</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">playwright-service</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 95</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">api</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 96</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">command</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">pnpm</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">run</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">workers</span><span style="color: #89DDFF;">&quot;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 97</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 98</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">redis</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 99</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">image</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">redis:alpine</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">100</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">101</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">backend</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">102</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">command</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">redis-server --bind 0.0.0.0</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">103</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">104</span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">105</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">backend</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">106</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">driver</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">bridge</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">107</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">web</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">108</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">true</span></div></code></pre>
<p>Please create an <code>.env</code> file which holds the value of the authorization bearer. For example:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">BEARER_TOKEN</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">your-token</span></div></code></pre>
<h2><a id="content-conclusion" href="#content-conclusion" class="prezet-heading" title="Permalink">#</a>Conclusion</h2>
<p>You should now have a fully functional Firecrawl instance running on your server. Make sure to point your domain to the server.</p>
]]>
            </summary>
                                    <updated>2024-12-08T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Keys to Efficiency Part 3: Documenting for the Future]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/keys-to-effiency/part-3-documenting-the-future" />
            <id>https://tim-kleyersburg.de/keys-to-effiency/part-3-documenting-the-future</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>You’ve <a href="/articles/keys-to-effiency/part-1-hard-skills">honed your skills</a> and <a href="/articles/keys-to-effiency/part-2-expanding-the-toolbelt">expanded your toolbelt</a> – now it’s time to ensure your knowledge and experience don’t go to waste.</p>
<p>In a fast-paced work environment, it’s all too common to leave things undocumented, thinking we’ll remember the details later. But then life happens – maybe just the weekend, a vacation, or another project pulls us away. When we finally return to that task or project, we waste precious time reconstructing what was left unfinished. Documenting as you work isn’t just a nice-to-have – it’s essential for efficiency.</p>
<p>Documentation is like a breadcrumb trail you leave for your future self, and it’s also a safety net for your team. Whether you’re learning new skills, tracking task progress, or working on a project that might be passed to someone else, having organized notes can make all the difference. By blending professional and personal note-taking, you create a quick reference system that captures experience you don’t need to recall every day but might need another time. It’s also a well studied fact that writing things down helps you remember them better.</p>
<h2><a id="content-why-documentation-is-essential-for-efficiency" href="#content-why-documentation-is-essential-for-efficiency" class="prezet-heading" title="Permalink">#</a>Why Documentation is Essential for Efficiency</h2>
<p>When we don’t document our work, we force ourselves to rely on memory – an inherently unreliable and time-consuming process. Here’s why documentation is so essential for maintaining an efficient workflow:</p>
<ol>
<li><strong>Continuity</strong> – When you step away from a project, even for a short break, your brain has to work to “reload” context when you return. Documenting lets you pick up right where you left off, eliminating the time it takes to remember what you were working on.</li>
<li><strong>Team Collaboration</strong> – Documentation is crucial when you’re part of a team. It allows others to pick up where you left off, reduces the need for extra meetings, and prevents redundant efforts.</li>
<li><strong>Skill Retention</strong> – Documentation doesn’t just apply to project tasks. When learning new skills or techniques, keeping a record ensures you have a quick reference to review instead of starting from scratch each time.</li>
</ol>
<p>The goal is simple: you shouldn’t waste time on tasks you’ve already done. Document once, and you’ll save yourself (and your colleagues) time in the future.</p>
<h2><a id="content-organized-purposeful-note-taking" href="#content-organized-purposeful-note-taking" class="prezet-heading" title="Permalink">#</a>Organized, Purposeful Note-Taking</h2>
<p>Documentation is most effective when it’s well-organized, purposeful, and accessible. That means using different systems for different types of information and maintaining a structured approach that makes it easy to know exactly where your information is stored. Here’s one way to approach it:</p>
<h3><a id="content-different-systems-for-different-purposes" href="#content-different-systems-for-different-purposes" class="prezet-heading" title="Permalink">#</a>Different Systems for Different Purposes</h3>
<p>Use multiple documentation systems that align with the specific purpose of each type of note:</p>
<ul>
<li><strong>Long-term documentation</strong> – Use a platform like Confluence or Google Drive for project documentation or information that needs to be accessible over time. This is where key project details, processes, and team-shared knowledge should live.</li>
<li><strong>Task-related documentation</strong> – Keep project and task-specific notes in a project management tool (like Asana, Trello, or Jira) so everyone involved has quick access to current task information. Make sure to always update these when new information is available.</li>
<li><strong>Personal notes</strong> – For skills you’re developing or quick, daily notes, use a tool that’s easily accessible to you, such as a note-taking app or physical notebook. This system should serve as a place for information that you’ll review, consolidate, or integrate over time.</li>
</ul>
<h3><a id="content-make-daily-notes-a-habit" href="#content-make-daily-notes-a-habit" class="prezet-heading" title="Permalink">#</a>Make Daily Notes a Habit</h3>
<p>Develop a habit of making daily notes to capture everything relevant that happens throughout your workday. If you prefer to use paper, consider digitizing these notes at the end of each day so they’re easier to search and reference later. These daily notes can act as a temporary “memory bank” until you review and integrate them into your long-term documentation system.</p>
<p>This also solves the following problems:</p>
<ul>
<li>You always have a place to jot down quick thoughts, ideas or meeting notes</li>
<li>You can easily find information later when you need it</li>
<li>You get into the habit of documenting as you go</li>
</ul>
<h3><a id="content-regularly-review-and-integrate-your-notes" href="#content-regularly-review-and-integrate-your-notes" class="prezet-heading" title="Permalink">#</a>Regularly Review and Integrate Your Notes</h3>
<p>To prevent information from getting stale or lost, review your notes periodically. During these reviews, integrate key points from your daily notes into your main documentation systems. For example:</p>
<ul>
<li>Summarize major insights or decisions from daily notes into project documentation.</li>
<li>Add key learning points from personal notes into a more comprehensive skill-building document.</li>
</ul>
<p>This process ensures your documentation is always up-to-date, clear, and relevant. It also reinforces your understanding and saves time later when you or others need to reference important information.</p>
<p>It’s also your touch point to create boilerplate templates for complex tasks or projects. This way you can start new projects faster and with less friction.</p>
<h3><a id="content-the-goal-know-where-to-find-important-information" href="#content-the-goal-know-where-to-find-important-information" class="prezet-heading" title="Permalink">#</a>The Goal: Know Where to Find Important Information</h3>
<p>Your documentation system should make it easy to find what you need, exactly where you expect it. Storing task-related information in a personal notebook, for example, may prevent team members from accessing it when needed. Instead, having a reliable system where each type of note is stored appropriately allows you and your team to access crucial information without delay or confusion.</p>
<h2><a id="content-characteristics-of-good-documentation" href="#content-characteristics-of-good-documentation" class="prezet-heading" title="Permalink">#</a>Characteristics of Good Documentation</h2>
<p>Good documentation isn’t just about capturing information; it’s about making it useful. Aim to create documentation that is:</p>
<ul>
<li>
<p><strong>Clear</strong> – Use concise language that anyone on the team can understand.</p>
</li>
<li>
<p><strong>Concise</strong> – Avoid excess details; include only what’s essential for understanding the work.</p>
</li>
<li>
<p><strong>Transparent</strong> – Document the reasoning behind key decisions so that others understand not just what was done but why.</p>
</li>
<li>
<p><strong>Easily accessible</strong> – Store notes in places where they can be found when needed, using tags, folders, or categories as needed to keep everything organized.</p>
</li>
<li>
<p>The specific meaning of these criteria will depend on the context – long-term documentation for a project, for instance, may require more detail and structure than a quick daily note. However, clarity and accessibility should be the guiding principles across all documentation efforts.</p>
</li>
</ul>
<h2><a id="content-conclusion" href="#content-conclusion" class="prezet-heading" title="Permalink">#</a>Conclusion</h2>
<p>Documenting as you go may seem like an extra step, but it’s a powerful investment in your future efficiency. By organizing your notes with purpose, maintaining consistency in your systems, and combining professional and personal insights, you create a reliable, searchable knowledge base. The time you save by not having to “relearn” or “rediscover” is time you can reinvest in moving forward, not retracing old steps.</p>
<p>As with the other skills: You need to develop a mindset for documenting and see it as a part of your workflow, not an extra task. The more you practice, the more natural it will become, and the more time you’ll save in the long run.</p>
]]>
            </summary>
                                    <updated>2024-11-25T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Keys to Efficiency Part 2: Expanding the Toolbelt]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/keys-to-effiency/part-2-expanding-the-toolbelt" />
            <id>https://tim-kleyersburg.de/keys-to-effiency/part-2-expanding-the-toolbelt</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In <a href="/articles/keys-to-effiency/part-1-hard-skills">my previous post</a>, we explored the power at your fingertips to speed up everyday tasks and bring efficiency to the forefront of your workflow. Let’s imagine you invested the time to know how to use your hammer. And now you’re hooked. You want more. That’s when you notice: Many tasks are repetitive, complex, or simply lack a clear, simple shortcut. This is where specialized tools come in – tools that empower you to automate, centralize, and amplify your work, all while keeping your keyboard as your primary driver.</p>
<p>Imagine your keyboard is not just a tool but a toolbox. Typing speed and shortcuts are the basic tools you’ve learned to wield expertly, but sometimes you need more advanced instruments to get the job done efficiently. In this post, we’ll look at how certain productivity tools, such as <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.raycast.com/">Raycast</a> (for Mac users) and other automation tools, can extend your tool belt and help you optimize your workflow.</p>
<blockquote>
<p><strong>Disclaimer:</strong> While Raycast is a Mac-specific tool, the principles we’ll discuss apply broadly. For Windows users, there are alternatives such as <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/microsoft/PowerToys">PowerToys</a> and <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/Wox-launcher/Wox">Wox</a>, though functionality and integrations may vary.</p>
</blockquote>
<h2><a id="content-the-role-of-workflow-enhancing-tools" href="#content-the-role-of-workflow-enhancing-tools" class="prezet-heading" title="Permalink">#</a>The Role of Workflow-Enhancing Tools</h2>
<p>Shortcuts are excellent for speeding up simple, repetitive actions, but as tasks become more complex, the time savings from shortcuts alone become limited. <strong>Workflow-enhancing tools</strong> provide a solution by consolidating multiple steps into a single action, automating routines, or adding functions that aren’t otherwise accessible through native system shortcuts.</p>
<h3><a id="content-why-workflow-tools-matter" href="#content-why-workflow-tools-matter" class="prezet-heading" title="Permalink">#</a>Why Workflow Tools Matter</h3>
<p>Tools like Raycast create an environment where:</p>
<ul>
<li><strong>Automation</strong> reduces the need to manually repeat tasks.</li>
<li><strong>Centralization</strong> enables quick access to files, apps, and commands without leaving your workspace.</li>
<li><strong>Customization</strong> lets you shape your workflow around your unique needs, not the other way around.</li>
</ul>
<p>These tools allow you to work faster, minimize distractions, and stay focused on the task at hand – all while keeping you in a “keyboard-first” mindset.</p>
<h2><a id="content-adding-functionality-to-your-tool-belt" href="#content-adding-functionality-to-your-tool-belt" class="prezet-heading" title="Permalink">#</a>Adding Functionality to Your Tool Belt</h2>
<ol>
<li><strong>Universal Search and Access</strong> – Imagine being able to instantly find any file, document, app, or setting from one command center. Tools like Raycast allow you to search across your system without interrupting your current task.</li>
<li><strong>Automating Repetitive Tasks</strong> – Many of us perform the same set of actions multiple times a day. A tool that allows you to automate these actions – like opening a set of apps for a particular project or executing specific commands – can save time and mental energy.</li>
<li><strong>Integrating Third-Party Tools</strong> – Project management apps, version control, task tracking: Workflow tools allow you to interact with various apps directly, without needing to switch contexts.</li>
</ol>
<h2><a id="content-building-an-automation-mindset" href="#content-building-an-automation-mindset" class="prezet-heading" title="Permalink">#</a>Building an Automation Mindset</h2>
<p>One of the biggest benefits of these workflow tools isn’t just the time they save directly but the way they encourage you to <strong>think</strong> about automation. Here are a few ways to start using workflow tools with an automation mindset.</p>
<h3><a id="content-consolidate-tools-with-integrations" href="#content-consolidate-tools-with-integrations" class="prezet-heading" title="Permalink">#</a>Consolidate Tools with Integrations</h3>
<p>Using tools like Raycast, you can interact with project management software, document storage, or even communication apps directly from the command center. This means you don’t have to switch between apps constantly, keeping your focus and minimizing digital clutter.</p>
<blockquote>
<p><strong>Tip:</strong> Try setting up a command or automation to retrieve project tasks or deadlines from a tool like Notion, Asana, or Trello. This keeps your workflow focused and reduces the need to open multiple tabs.</p>
</blockquote>
<h3><a id="content-for-the-devs-experiment-with-small-custom-scripts" href="#content-for-the-devs-experiment-with-small-custom-scripts" class="prezet-heading" title="Permalink">#</a>For the Devs: Experiment with Small Custom Scripts</h3>
<p>If you’re comfortable in a code editor, try writing simple scripts or commands to automate repetitive actions, like renaming files in bulk, running a quick search across documents, or organizing new files. Even basic scripting can bring a huge efficiency boost. Or go beyond and create your own custom Raycast extensions to solve your unique problems.</p>
<h2><a id="content-keeping-the-keyboard-as-your-anchor" href="#content-keeping-the-keyboard-as-your-anchor" class="prezet-heading" title="Permalink">#</a>Keeping the Keyboard as Your Anchor</h2>
<p>One reason why tools like Raycast work so well for efficiency enthusiasts is that they <strong>keep the keyboard as the primary interface</strong>. This is the perfect evolution from mastering shortcuts: your hands stay on the keyboard, your attention stays on the task, and your workflow remains uninterrupted.</p>
<h3><a id="content-ideas-for-keyboard-centric-workflow-enhancements" href="#content-ideas-for-keyboard-centric-workflow-enhancements" class="prezet-heading" title="Permalink">#</a>Ideas for Keyboard-Centric Workflow Enhancements</h3>
<ol>
<li><strong>Quick Calculations and Conversions</strong> – Instead of reaching for a calculator, a command center tool can handle basic calculations or unit conversions directly.</li>
<li><strong>Clipboard History</strong> – Workflow tools often include clipboard managers, allowing you to access previously copied items without switching apps.</li>
<li><strong>Access System Settings</strong> – Quickly adjust settings like brightness, volume, or Wi-Fi without leaving your keyboard.</li>
</ol>
<p>This keyboard-centric approach helps maintain flow, limits distractions, and optimizes for speed.</p>
<p>Expanding your tool belt beyond shortcuts with workflow-enhancing tools brings your efficiency to a new level. By centralizing commands, automating routines, and enabling integrations, tools like Raycast provide capabilities that make complex workflows manageable and keep you focused on the bigger picture.</p>
<p>If you’re a Mac user, give Raycast a try and explore the possibilities it unlocks. For Windows users, tools like PowerToys, Wox, and Power Automate offer similar ways to streamline your tasks.</p>
<p>Efficiency doesn’t just stop at shortcuts – it grows with every tool you add to your belt. Start small, build the habit, and discover how much smoother and more productive your day can become.</p>
]]>
            </summary>
                                    <updated>2024-11-18T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Keys to Efficiency Part 1: Hard Skills]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/keys-to-effiency/part-1-hard-skills" />
            <id>https://tim-kleyersburg.de/keys-to-effiency/part-1-hard-skills</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p><strong>Efficiency is a mindset</strong>. You don’t get efficient by learning just one or two skills alone. Instead, you want to master the basics and get into a mental state where you’re always looking for ways to improve. This series is about building a foundation of skills and hopefully inspiring you to keep questioning and improving your workflows.</p>
<p>Imagine a carpenter who needs 10 minutes just to hold his hammer correctly – unimaginable, right? Yet many office workers spend valuable time each day inefficiently using their basic tools like the keyboard and mouse. With just a bit of effort, you could work much faster and with less frustration. This first post is all about the basics for an efficient workflow and why I think it should be your top priority.</p>
<h2><a id="content-the-keyboard--your-most-important-tool" href="#content-the-keyboard--your-most-important-tool" class="prezet-heading" title="Permalink">#</a>The Keyboard – Your Most Important Tool</h2>
<p>The keyboard is the centerpiece of your daily work. Every word, line of code, and almost every interaction with your computer relies on it. If you’re still “hunting and pecking” for each letter as you type, you’re leaving tremendous potential on the table. There’s hardly any other skill in an office environment that can make such a big difference as being able to type quickly and accurately.</p>
<h3><a id="content-work-on-your-typing-speed" href="#content-work-on-your-typing-speed" class="prezet-heading" title="Permalink">#</a>Work on Your Typing Speed</h3>
<p>Whether you are a writer, a programmer, or an office worker, typing is a fundamental skill. The faster you can type, the more you can get done in a day. The average typing speed is around 40 words per minute, but with a bit of practice, you can <strong>easily double that</strong>. Just imagine how much time you could save if you could type twice as fast! Or imagine how much time you’ve wasted by not improving this skill earlier.</p>
<h4>How to Learn to Type Faster</h4>
<p>There are plenty of free tools to help you get faster and more accurate. I really liked <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.keybr.com">Keybr</a> for their simple and intuitive interface with a lot of information about your progress.</p>
<p>A few minutes of practice a day is all it takes to develop this skill over the course of a few weeks.</p>
<p>The hardest part is acknowledging that you <em>need</em> to improve. It’s easy to get stuck in your ways and think that you’re “good enough”. Just type a few words into Keybr and look at your speed. I’ll wait. The time you invest in improving your typing speed will pay off in the very near future. And since you’ve chosen to work in front of a computer: don’t you want to be good at it? Or do you want to be the carpenter who can’t handle his hammer?</p>
<blockquote>
<p><strong>Tip:</strong> Training doesn’t have to take long – 5 minutes a day is a great start. You’ll quickly see progress, and soon you’ll be typing without needing to look at the keyboard!</p>
</blockquote>
<h2><a id="content-shortcuts--small-helpers-with-a-big-impact" href="#content-shortcuts--small-helpers-with-a-big-impact" class="prezet-heading" title="Permalink">#</a>Shortcuts – Small Helpers with a Big Impact</h2>
<p>Clicking with the mouse may not seem like it takes much time, but over the course of a day, those seconds add up. If you perform the same action repeatedly – like copying and pasting text – it’s worth considering how much time a simple shortcut could save. A shortcut is like a secret “magic trick” that takes you straight to the action you need.</p>
<p>While you might and should be used to the most common shortcuts like <code>Ctrl + C</code> and <code>Ctrl + V</code>, there are many more shortcuts that can save you time and make your work more efficient.<br />
Take the time to learn the shortcuts for your most-used applications – it’s a small investment that pays off quickly. Just take a look in the menu of your application – most of the time, the shortcuts are listed right next to the action or when you hover over the button with your mouse.</p>
<blockquote>
<p><strong>Tip</strong>: Don’t overwhelm yourself. Pick one or two new shortcuts each week and practice using them consistently until they become second nature.</p>
</blockquote>
<p>Once you get used to shortcuts, your workflow will feel more natural and intuitive – less time spent clicking, more focus on what really matters. And you’ll look back at your old self, wondering how you ever got anything done without them.</p>
<h2><a id="content-mouse-vs-keyboard" href="#content-mouse-vs-keyboard" class="prezet-heading" title="Permalink">#</a>Mouse vs. Keyboard</h2>
<p>The mouse is certainly useful, but many tasks are quicker to complete with the keyboard. This has two advantages: You save time and avoid moving your hand constantly between the mouse and keyboard – also a lot of little time savers. And it also helps to prevent repetitive strain injuries, which can be caused by using the mouse too much.</p>
<blockquote>
<p>Take a closer look at how you’re using your keyboard and mouse this week – where are you losing time? Could a shortcut replace an action you do repeatedly with the mouse?</p>
</blockquote>
<h2><a id="content-conclusion" href="#content-conclusion" class="prezet-heading" title="Permalink">#</a>Conclusion</h2>
<p>Mastering your basic tools is like a carpenter mastering the hammer. The time you invest in developing these foundational skills will pay off in a smoother, more productive workday. Just a few minutes a day practicing the 10-finger method or learning new shortcuts can help you work faster and with less frustration. You’ll also stand out amongst your peers as someone who knows how to use their tools efficiently.</p>
<p>I also want to make clear again, that this is not about speed for speed’s sake. You owe it to yourself and your work to use your tools efficiently. It’s about working smarter, not harder. Ask yourself: would you trust a carpenter who can’t handle his hammer?</p>
]]>
            </summary>
                                    <updated>2024-11-11T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How to install Librechat AI with Docker and Traefik]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/installing-librechat-ai-with-traefik" />
            <id>https://tim-kleyersburg.de/installing-librechat-ai-with-traefik</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://librechat.ai/">Librechat.ai</a> is an open source AI chat platform that you can host yourself. In this tutorial, we will install Librechat AI using Docker and Traefik.</p>
<p>I’ve had some hoops to jump through to get everything working, so I thought I’d share the steps I took to get it up and running.</p>
<p>This guide assumes you are using an external network called <code>web</code> to route all your traffic through Traefik.</p>
<h2><a id="content-step-1-follow-the-official-installation-guide" href="#content-step-1-follow-the-official-installation-guide" class="prezet-heading" title="Permalink">#</a>Step 1: Follow the official installation guide</h2>
<p>The official installation guide can be found <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.librechat.ai/install/installation/docker_compose_install.html#quick-start-tldr">here</a>. Follow the guide to set up the required environment variables and start the Docker containers.</p>
<h3><a id="content-adjust-the-docker-composeoverrideyml-file" href="#content-adjust-the-docker-composeoverrideyml-file" class="prezet-heading" title="Permalink">#</a>Adjust the <code>docker-compose.override.yml</code> file</h3>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #F07178;">version</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">3.4</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F07178;">services</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">api</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">labels</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.enable=true</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.routers.librechat.rule=Host(`your.domain.example.com`)</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.routers.librechat.tls=true</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.routers.librechat.tls.certresolver=lets-encrypt</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.services.librechat.loadbalancer.server.port=3080</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">web</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">librechat_default</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">volumes</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">./librechat.yaml:/app/librechat.yaml</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">web</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">librechat_default</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">true</span></div></code></pre>
<h3><a id="content-adjust-the-env-file" href="#content-adjust-the-env-file" class="prezet-heading" title="Permalink">#</a>Adjust the <code>.env</code> file</h3>
<p>To get the keys you may use the following script, place it in a file and run the file with <code>node file.js</code>:</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> crypto </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">require</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">crypto</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #676E95;">// Generate a 32-byte key (64 characters in hex)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> key </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> crypto</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">randomBytes</span><span style="color: #A6ACCD;">(</span><span style="color: #F78C6C;">32</span><span style="color: #A6ACCD;">)</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">toString</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">hex</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #676E95;">// Generate a 16-byte IV (32 characters in hex)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> iv </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> crypto</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">randomBytes</span><span style="color: #A6ACCD;">(</span><span style="color: #F78C6C;">16</span><span style="color: #A6ACCD;">)</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">toString</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">hex</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #676E95;">// Generate a 32-byte key (64 characters in hex)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> jwt </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> crypto</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">randomBytes</span><span style="color: #A6ACCD;">(</span><span style="color: #F78C6C;">32</span><span style="color: #A6ACCD;">)</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">toString</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">hex</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #676E95;">// Generate a 32-byte key (64 characters in hex)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> jwt2 </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> crypto</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">randomBytes</span><span style="color: #A6ACCD;">(</span><span style="color: #F78C6C;">32</span><span style="color: #A6ACCD;">)</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">toString</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">hex</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">console</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">log</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">CREDS_KEY=</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">key</span><span style="color: #89DDFF;">}`</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">console</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">log</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">CREDS_IV=</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">iv</span><span style="color: #89DDFF;">}`</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">console</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">log</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">JWT_SECRET=</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">jwt</span><span style="color: #89DDFF;">}`</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">console</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">log</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">JWT_REFRESH_SECRET=</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">jwt2</span><span style="color: #89DDFF;">}`</span><span style="color: #A6ACCD;">)</span></div></code></pre>
<p>After that you can adjust the <code>.env</code> file:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #A6ACCD;">HOST</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">localhost</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">PORT</span><span style="color: #89DDFF;">=</span><span style="color: #F78C6C;">3080</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">DOMAIN_CLIENT</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">https://your.domain.example.com</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">DOMAIN_SERVER</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">https://your.domain.example.com</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">OPENAI_API_KEY</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">your-key</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">ASSISTANTS_API_KEY</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">your-key</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">APP_TITLE</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">a fancy title</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">CREDS_KEY</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">f151369e25852102edfa394fd034df5ac492c2a0028acaa51260402916488c65</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">CREDS_IV</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">13272260bd313fd2d032ddcd70b75769</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">JWT_SECRET</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">4d10e5b41de2d88a819b0e4b8600d1834c25356a44323cfb1bd3a8e839688b04</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">JWT_REFRESH_SECRET</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">4f73e40aadf694f829b9980d0224cd14783d791f5744627af9d94ed71dc34943</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">ALLOW_REGISTRATION</span><span style="color: #89DDFF;">=true</span></div></code></pre>
<h2><a id="content-starting-the-containers" href="#content-starting-the-containers" class="prezet-heading" title="Permalink">#</a>Starting the containers</h2>
<p>After adjusting the files, you can start the containers with the following command:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">docker-compose</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">up</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-d</span></div></code></pre>
<p>That should be it! You should now be able to access Librechat AI at <code>https://your.domain.example.com</code>.</p>
]]>
            </summary>
                                    <updated>2024-04-25T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Set up Typesense on Ubuntu 22.04 including SSL]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/setting-up-typesense-on-a-vps" />
            <id>https://tim-kleyersburg.de/setting-up-typesense-on-a-vps</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Typesense is a fast, typo-tolerant search engine that is easy to set up and use. In this tutorial, we will set up Typesense on a VPS and configure it to use SSL certificates. We will use a cheap VPS from Hetzner, but you can use any VPS provider that you prefer.</p>
<h2><a id="content-prerequisites" href="#content-prerequisites" class="prezet-heading" title="Permalink">#</a>Prerequisites</h2>
<p>Before we start, you will need the following:</p>
<ul>
<li>A VPS with Ubuntu 22.04 with at least 1GB of RAM and 1 CPU core</li>
<li>A domain name that you can point to your VPS</li>
<li>A basic understanding of Linux and the command line</li>
</ul>
<h2><a id="content-step-1-install-typesense" href="#content-step-1-install-typesense" class="prezet-heading" title="Permalink">#</a>Step 1: Install Typesense</h2>
<p>After your server is spun up, SSH into it and run the following commands:</p>
<div class="not-prose rounded-md border-l-4 border-l-orange-300 bg-yellow-50 px-3 py-2 leading-snug dark:border-l-yellow-900 dark:bg-yellow-700 dark:text-white/90">
    <p>Make sure to check the <a href="https://typesense.org/docs/guide/install-typesense.html#linux-binary">official documentation</a> for the latest version of Typesense.</p>

</div>

<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #676E95;"># Update the package list and the packages</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #FFCB6B;">apt</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">update</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;&amp;</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">apt</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">upgrade</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-y</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #676E95;"># Install Typesense</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #676E95;"># x64</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #FFCB6B;">curl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-O</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://dl.typesense.org/releases/26.0/typesense-server-26.0-linux-amd64.tar.gz</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #FFCB6B;">tar</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-xzf</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">typesense-server-26.0-linux-amd64.tar.gz</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #676E95;"># arm64</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #FFCB6B;">curl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-O</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://dl.typesense.org/releases/26.0/typesense-server-26.0-linux-arm64.tar.gz</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #FFCB6B;">tar</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-xzf</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">typesense-server-26.0-linux-arm64.tar.gz</span></div></code></pre>
<h2><a id="content-step-2-set-up-ssl" href="#content-step-2-set-up-ssl" class="prezet-heading" title="Permalink">#</a>Step 2: Set up SSL</h2>
<p>Before proceeding, make sure that your domain is pointing to your VPS.</p>
<p>To set up SSL, we will use Certbot to generate SSL certificates for our domain. Run the following commands:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">apt</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">install</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">snapd</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">snap</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">install</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">core</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;&amp;</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">snap</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">refresh</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">core</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #FFCB6B;">snap</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">install</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">--classic</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">certbot</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #FFCB6B;">certbot</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">certonly</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">--standalone</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-d</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">your.domain.example.com</span></div></code></pre>
<p>This will also set up auto renewal of your certificates. To make sure that Typesense will use the new certificates you will need to restart the service when the certificates have been renewed.</p>
<p>With your preferred text editor, open the Certbot renewal configuration file <code>/etc/letsencrypt/renewal/your.domain.example.com.conf</code>.</p>
<p>Add the following line to the file:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">renew_hook</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">systemctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">reload</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">typesense-server.service</span></div></code></pre>
<h2><a id="content-step-3-configure-typesense" href="#content-step-3-configure-typesense" class="prezet-heading" title="Permalink">#</a>Step 3: Configure Typesense</h2>
<p>Open the Typesense configuration file <code>/etc/typesense/typesense-server.ini</code> with your preferred text editor and change the lines with the comments. All other lines can be left as they are.</p>
<pre><code data-theme="material-theme-palenight" data-lang="ini" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">[server]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #F07178;">api-address</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> 0.0.0.0</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #F07178;">api-port</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> 443 </span><span style="color: #676E95;"># Adjust the port</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #F07178;">api-key</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> api-key </span><span style="color: #676E95;"># Set a secure API Key</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #F07178;">log-dir</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> /var/log/typesense</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #F07178;">ssl-certificate</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> /etc/letsencrypt/live/your.domain.example.com/fullchain.pem </span><span style="color: #676E95;"># Path to the SSL certificate</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span><span style="color: #F07178;">ssl-certificate-key</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> /etc/letsencrypt/live/your.domain.example.com/privkey.pem </span><span style="color: #676E95;"># Path to the SSL certificate key</span></div></code></pre>
<h2><a id="content-step-4-adjust-firewall" href="#content-step-4-adjust-firewall" class="prezet-heading" title="Permalink">#</a>Step 4: Adjust firewall</h2>
<p>As a security measure, you should adjust the firewall to only allow incoming traffic on the ports you need. Run the following commands:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">allow</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">443</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">allow</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">80</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #FFCB6B;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">allow</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">22</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #FFCB6B;">ufw</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">enable</span></div></code></pre>
<p>This ensures, that only traffic from these ports are allowed.</p>
<h2><a id="content-step-5-start-typesense" href="#content-step-5-start-typesense" class="prezet-heading" title="Permalink">#</a>Step 5: Start Typesense</h2>
<p>You are now ready to start Typesense!</p>
<p>Run the command below to start Typesense:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">systemctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">start</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">typesense-server.service</span></div></code></pre>
<p>You may check if everything worked by opening the health check URL in your browser: <code>https://your.domain.example.com/health</code>.</p>
<p>If everything worked, you should see a JSON response like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="json" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">ok</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #89DDFF;">}</span></div></code></pre>
]]>
            </summary>
                                    <updated>2024-04-25T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Mattermost plugin to send a webhook when a bot is messaged]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/mattermost-plugin-chatbot" />
            <id>https://tim-kleyersburg.de/mattermost-plugin-chatbot</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>I wanted to be able to message my bot in Mattermost and have him respond to me.</p>
<p>Since I’ve wanted to use OpenAI to generate text and already have a web service for this in place which is connected to a few other accounts I wanted to be able to generate the response in this service instead of having to write a plugin for Mattermost.</p>
<p>But for this to work I needed to be able to know when someone messaged my bot account.</p>
<p>Unfortunately the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developers.mattermost.com/integrate/webhooks/outgoing/">Outgoing webhooks integration</a> only works in public channels, not in direct messages.</p>
<p>So I decided to write a little plugin for Mattermost which sends a webhook to my service when someone messages the bot account.</p>
<h2><a id="content-the-plugin" href="#content-the-plugin" class="prezet-heading" title="Permalink">#</a>The plugin</h2>
<p>I’ve open sourced the plugin <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/mattermost-plugin-bot-webhook">on GitHub</a>. You can use this if you don’t want to write your own plugin.</p>
<p>For those interested I’ll explain the things which weren’t clear to me when creating the plugin because there was no clear documentation on this (or I couldn’t find it).</p>
<p>The plugin is based on the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/mattermost/mattermost-plugin-starter-template">mattermost-plugin-starter-template</a>, so I won’t explain the basics of how to create a plugin but only the things which are specific to this plugin.</p>
<p>Everything important happens in the file <code>server/plugin.go</code>.</p>
<h2><a id="content-adding-configuration" href="#content-adding-configuration" class="prezet-heading" title="Permalink">#</a>Adding configuration</h2>
<p>I wanted to be able to quickly change the webhook URL without having to recompile the plugin. For this I added a configuration option to the plugin.</p>
<p>You do this by adding a struct called <code>Configuration</code> to the plugin struct:</p>
<pre><code data-theme="material-theme-palenight" data-lang="go" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">type</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Configuration</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">struct</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    BotUserID  </span><span style="color: #C792EA;">string</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    WebhookURL </span><span style="color: #C792EA;">string</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>You can then add a new type for your plugin which embeds the Mattermost plugin and adds the configuration:</p>
<pre><code data-theme="material-theme-palenight" data-lang="go" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">type</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">BotWebhookPlugin</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">struct</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    plugin</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">MattermostPlugin</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    configuration </span><span style="color: #89DDFF;">*</span><span style="color: #A6ACCD;">Configuration</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>To react to messages we’ll implement the <code>MessageHasBeenPosted</code> function. For easier reading I’ve removed the error handling from the code snippets.</p>
<pre><code data-theme="material-theme-palenight" data-lang="go" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">func</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">p </span><span style="color: #89DDFF;">*</span><span style="color: #A6ACCD;">BotWebhookPlugin</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">MessageHasBeenPosted</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">post </span><span style="color: #89DDFF;">*</span><span style="color: #A6ACCD;">model</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">Post</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">  </span><span style="color: #676E95;">// get the channel from the post</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">  </span><span style="color: #676E95;">// we need this to check if the message was sent to the bot</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">    channel</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> err </span><span style="color: #89DDFF;">:=</span><span style="color: #A6ACCD;"> p</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">API</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">GetChannel</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">post</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">ChannelId</span><span style="color: #89DDFF;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">  </span><span style="color: #676E95;">// if the post was by the bot ignore it</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">if</span><span style="color: #A6ACCD;"> post</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">UserId </span><span style="color: #89DDFF;">==</span><span style="color: #A6ACCD;"> p</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">configuration</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">BotUserID </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">  </span><span style="color: #676E95;">// check if the message was sent to the bot</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">  </span><span style="color: #676E95;">// the channel name in a direct channel looks like this:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">  </span><span style="color: #676E95;">// &lt;bot username&gt;__&lt;user id&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">if</span><span style="color: #A6ACCD;"> strings</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">Contains</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">channel</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">Name</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> p</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">configuration</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">BotUserID</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">    </span><span style="color: #676E95;">// convert the post to JSON</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">        jsonPayload</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> err </span><span style="color: #89DDFF;">:=</span><span style="color: #A6ACCD;"> json</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">Marshal</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">post</span><span style="color: #89DDFF;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">    </span><span style="color: #676E95;">// send a POST request to the webhook URL with the post as the body</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">        req</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> err </span><span style="color: #89DDFF;">:=</span><span style="color: #A6ACCD;"> http</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">NewRequest</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">POST</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> p</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">configuration</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">WebhookURL</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> bytes</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">NewBuffer</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">jsonPayload</span><span style="color: #89DDFF;">))</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">        req</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">Header</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">Set</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">Content-Type</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">application/json</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">        client </span><span style="color: #89DDFF;">:=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;</span><span style="color: #A6ACCD;">http</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">Client</span><span style="color: #89DDFF;">{}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">        resp</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> err </span><span style="color: #89DDFF;">:=</span><span style="color: #A6ACCD;"> client</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">Do</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">req</span><span style="color: #89DDFF;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">defer</span><span style="color: #A6ACCD;"> resp</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">Body</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">Close</span><span style="color: #89DDFF;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>That’s all there is to it. Every time someone messages the bot the plugin will send a webhook to the configured URL.</p>
]]>
            </summary>
                                    <updated>2023-06-26T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[AI-Driven Documentation Search with GPT, Weaviate, and Laravel]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/ai-driven-doc-search" />
            <id>https://tim-kleyersburg.de/ai-driven-doc-search</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>As a developer, I often find myself digging through documentation to solve problems or learn about new tools. Sometimes, I encounter issues with finding the right information, or the search functionality is limited. That’s when I had an idea: What if we could create a <em>natural language search</em> for our agency’s documentation using cutting-edge AI technology?</p>
<p>In this article, I’ll walk you through my journey of using OpenAI’s GPT models, their embeddings, and a <a href="/articles/setup-weaviate-with-docker-and-traefik">vector database called Weaviate</a> to enhance our documentation search capabilities.</p>
<h2><a id="content-integrate-openais-api-with-laravel" href="#content-integrate-openais-api-with-laravel" class="prezet-heading" title="Permalink">#</a>Integrate OpenAI’s API with Laravel</h2>
<p>We’ll assume you already have a Laravel application set up, or you’re familiar with setting up a new Laravel project. In this section, we’ll focus on integrating the OpenAI API using the <code>openai-php/laravel</code> Composer package.</p>
<h3><a id="content-step-1-install-the-openai-phplaravel-package" href="#content-step-1-install-the-openai-phplaravel-package" class="prezet-heading" title="Permalink">#</a>Step 1: Install the openai-php/laravel Package</h3>
<p>To install the <code>openai-php/laravel</code> package, use the Composer command below:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">composer</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">require</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">openai-php/laravel</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">php</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">artisan</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">vendor:publish</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">--provider=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">OpenAI\Laravel\ServiceProvider</span><span style="color: #89DDFF;">&quot;</span></div></code></pre>
<h3><a id="content-step-2-configure-environment-variables" href="#content-step-2-configure-environment-variables" class="prezet-heading" title="Permalink">#</a>Step 2: Configure Environment Variables</h3>
<p>Navigate to the root of your Laravel project and locate the <code>.env</code> file. This file contains environment-specific settings. We’ll need to add our OpenAI API key to this file. You can obtain an API key by signing up for an OpenAI account.</p>
<p>Add the following line to your <code>.env</code> file:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">OPENAI_API_KEY</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">sk-...</span></div></code></pre>
<p>You are now ready to use the <code>OpenAI</code> facade in your Laravel application.</p>
<pre><code data-theme="material-theme-palenight" data-lang="php" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #F78C6C;">use</span><span style="color: #FFCB6B;"> </span><span style="color: #A6ACCD;">OpenAI</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Laravel</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Facades</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">OpenAI</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">result </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">OpenAI</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">completions</span><span style="color: #89DDFF;">()-&gt;</span><span style="color: #82AAFF;">create</span><span style="color: #89DDFF;">([</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">model</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">text-davinci-003</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">prompt</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">I want to </span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #89DDFF;">]);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span><span style="color: #82AAFF;">echo</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">result</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">choices</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">][</span><span style="color: #F78C6C;">0</span><span style="color: #89DDFF;">][</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">text</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">];</span></div></code></pre>
<h2><a id="content-retrieving-documentation-data-from-confluence-api" href="#content-retrieving-documentation-data-from-confluence-api" class="prezet-heading" title="Permalink">#</a>Retrieving Documentation Data from Confluence API</h2>
<p>In this section, we will go through the process of fetching data from the Confluence API, which stores your documentation. We will be using this data for our natural language search.</p>
<h3><a id="content-step-1-set-up-confluence-api-credentials" href="#content-step-1-set-up-confluence-api-credentials" class="prezet-heading" title="Permalink">#</a>Step 1: Set Up Confluence API Credentials</h3>
<p>To interact with the Confluence API, you will need an API token and your Confluence URL. You can create an API token by following the instructions in the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/">official documentation</a>.</p>
<p>Once you have your API token, add the following lines to your .env file:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">CONFLUENCE_API_USER</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">your_api_user_here</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">CONFLUENCE_API_KEY</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">your_api_key_here</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">CONFLUENCE_URL</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">https://your_domain.atlassian.net</span></div></code></pre>
<p>Replace the values with your own values and make sure, the user you are using has access to the documentation you want to search.</p>
<p>Now add these values to your config/services.php file:</p>
<pre><code data-theme="material-theme-palenight" data-lang="php" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">confluence</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">api_user</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">env</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">CONFLUENCE_API_USER</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">api_key</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">env</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">CONFLUENCE_API_KEY</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">api_url</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">env</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">CONFLUENCE_API_URL</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">),</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #89DDFF;">],</span></div></code></pre>
<h3><a id="content-step-2-create-a-confluenceservice-class" href="#content-step-2-create-a-confluenceservice-class" class="prezet-heading" title="Permalink">#</a>Step 2: Create a ConfluenceService Class</h3>
<p>Create a new service class called ConfluenceService in the <code>app/Services</code> directory to handle interactions with the Confluence API.</p>
<p>Below is a simple service class which handles the fetching of all pages from a specific parent page in Confluence.</p>
<pre><code data-theme="material-theme-palenight" data-lang="php" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">&lt;?php</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F78C6C;">namespace</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">App</span><span style="color: #89DDFF;">\</span><span style="color: #FFCB6B;">Services</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #F78C6C;">use</span><span style="color: #FFCB6B;"> </span><span style="color: #A6ACCD;">Illuminate</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Http</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Client</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Response</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F78C6C;">use</span><span style="color: #FFCB6B;"> </span><span style="color: #A6ACCD;">Illuminate</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Support</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Facades</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Http</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #C792EA;">class</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">ConfluenceService</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">private</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">string</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">apiUser</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">private</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">string</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">apiKey</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">private</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">string</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">apiUrl</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">public</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">__construct</span><span style="color: #89DDFF;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$this-&gt;</span><span style="color: #A6ACCD;">apiUser </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">config</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">services.confluence.api_user</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$this-&gt;</span><span style="color: #A6ACCD;">apiKey </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">config</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">services.confluence.api_key</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$this-&gt;</span><span style="color: #A6ACCD;">apiUrl </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">config</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">services.confluence.api_url</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">public</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">getPageDescendants</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">int</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">pageId</span><span style="color: #89DDFF;">):</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Response</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">entityUrl </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">sprintf</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">content/%s/descendant/page</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">pageId</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">data </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">expand</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">body.view</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">limit</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">1000</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">29</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">url </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">sprintf</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">%s/%s</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$this-&gt;</span><span style="color: #A6ACCD;">apiUrl</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">trim</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">entityUrl</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">/</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">));</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">30</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">31</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Http</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">withBasicAuth</span><span style="color: #89DDFF;">($this-&gt;</span><span style="color: #A6ACCD;">apiUser</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$this-&gt;</span><span style="color: #A6ACCD;">apiKey</span><span style="color: #89DDFF;">)-&gt;</span><span style="color: #82AAFF;">get</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">url</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">data</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">32</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">33</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>You may now use this service class to fetch data from the Confluence API.</p>
<pre><code data-theme="material-theme-palenight" data-lang="php" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">confluenceService </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">new</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">ConfluenceService</span><span style="color: #89DDFF;">();</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">confluenceService</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">getPageDescendants</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">123456</span><span style="color: #89DDFF;">);</span></div></code></pre>
<p>This response contains all subpages including their contents.</p>
<h2><a id="content-store-embeddings-of-documentation-in-weaviate" href="#content-store-embeddings-of-documentation-in-weaviate" class="prezet-heading" title="Permalink">#</a>Store embeddings of documentation in Weaviate</h2>
<p>Weaviate is a vector database that allows you to store and query data in a vector space. You can read my article on how to set up Weaviate with Docker and Traefik to learn more about Weaviate and how to set it up. <a href="/articles/setup-weaviate-with-docker-and-traefik">Read the article</a>. If you have no previous experience or knowledge of Weaviate, I recommend reading the article first.</p>
<p>Weaviate uses a schema to define its data structure. In this example, we will use the following schema:</p>
<pre><code data-theme="material-theme-palenight" data-lang="graphql" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">class&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">Chunk&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">description&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">Some chunk of knowledge&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">vectorizer&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">text2vec-openai&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">moduleConfig&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">text2vec-openai&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">model&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">ada&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">modelVersion&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">002&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">type&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">text&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">properties&quot;</span><span style="color: #A6ACCD;">: [</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">name&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">identifier&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">description&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">The identifier of the particular chunk of knowledge&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">dataType&quot;</span><span style="color: #A6ACCD;">: [</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">string&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">            ]</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">moduleConfig&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">text2vec-openai&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">                    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">skip&quot;</span><span style="color: #A6ACCD;">: true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">name&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">content&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">description&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">The contents&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">dataType&quot;</span><span style="color: #A6ACCD;">: [</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">29</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">text&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">30</span><span style="color: #A6ACCD;">            ]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">31</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">32</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">33</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">name&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">source&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">34</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">description&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">The source type&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">35</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">dataType&quot;</span><span style="color: #A6ACCD;">: [</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">36</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">string&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">37</span><span style="color: #A6ACCD;">            ]</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">38</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">moduleConfig&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">39</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">text2vec-openai&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">40</span><span style="color: #A6ACCD;">                    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">skip&quot;</span><span style="color: #A6ACCD;">: true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">41</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">42</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">43</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">44</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">45</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">name&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">sourceLink&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">46</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">description&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">URL to the article&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">47</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">dataType&quot;</span><span style="color: #A6ACCD;">: [</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">48</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">string&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">49</span><span style="color: #A6ACCD;">            ]</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">50</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">moduleConfig&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">51</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">text2vec-openai&quot;</span><span style="color: #A6ACCD;">: </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">52</span><span style="color: #A6ACCD;">                    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #676E95;">skip&quot;</span><span style="color: #A6ACCD;">: true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">53</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">54</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">55</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">56</span><span style="color: #A6ACCD;">    ]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">57</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>We aren’t storing whole pages content because this would mean that our prompts will get too big. Instead we will chunk the content and store each chunk in Weaviate. We will also store the source and sourceLink properties to be able to link back to the original source.</p>
<h3><a id="content-step-1-accessing-weaviate-from-php" href="#content-step-1-accessing-weaviate-from-php" class="prezet-heading" title="Permalink">#</a>Step 1: Accessing Weaviate from PHP</h3>
<p>To access Weaviate from PHP, we will use the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/weaviate-php">weaviate-php</a> package. Install the package using the Composer command below:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">composer</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">require</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">timkley/weaviate-php</span></div></code></pre>
<p>You can now use the Weaviate client in your PHP code.</p>
<pre><code data-theme="material-theme-palenight" data-lang="php" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">&lt;?php</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #F78C6C;">use</span><span style="color: #FFCB6B;"> </span><span style="color: #A6ACCD;">Weaviate</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Weaviate</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">weaviate </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">new</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Weaviate</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">http://localhost:8080</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">your-token</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">);</span></div></code></pre>
<h3><a id="content-step-2-chunk-and-store-content-in-weaviate" href="#content-step-2-chunk-and-store-content-in-weaviate" class="prezet-heading" title="Permalink">#</a>Step 2: Chunk and store content in Weaviate</h3>
<p>Looping over all our pages we’ll do the following things:</p>
<ol>
<li>Remove all HTML tags from the content</li>
<li>Split the content into chunks manageble chunks</li>
<li>Create a new Weaviate object for each chunk</li>
<li>Store the object in Weaviate</li>
</ol>
<pre><code data-theme="material-theme-palenight" data-lang="php" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #F78C6C;">use</span><span style="color: #FFCB6B;"> </span><span style="color: #A6ACCD;">App</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Services</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">ConfluenceService</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">confluenceService </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">new</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">ConfluenceService</span><span style="color: #89DDFF;">();</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">confluenceService</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">getPageDescendants</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">12345</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #89DDFF;">if</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">successful</span><span style="color: #89DDFF;">())</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">pages </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">json</span><span style="color: #89DDFF;">()[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">results</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">foreach</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">pages </span><span style="color: #89DDFF;">as</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">page</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">content </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">cleanUpContent</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">page</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">body</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">][</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">view</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">][</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">value</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">]);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunks </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">chunkContent</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">content</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #89DDFF;">        </span><span style="color: #676E95;">// Delete all old chunks before creating new ones</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">weaviate</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">batch</span><span style="color: #89DDFF;">()-&gt;</span><span style="color: #82AAFF;">delete</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Chunk</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">path</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">identifier</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">],</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">operator</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Equal</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">valueString</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunkyBoy</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #A6ACCD;">identifier</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">]);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">count </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">0</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">objects </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #89DDFF;">        </span><span style="color: #676E95;">// Loop over the chunks and create objects matching our Weaviate schema</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">foreach</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">chunks </span><span style="color: #89DDFF;">as</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunk</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">objects</span><span style="color: #89DDFF;">[]</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">class</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Chunk</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">properties</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span><span style="color: #A6ACCD;">                    </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">identifier</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunkyBoy</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #A6ACCD;">identifier</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">29</span><span style="color: #A6ACCD;">                    </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">content</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunk</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">value</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">],</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">30</span><span style="color: #A6ACCD;">                    </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">source</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunkyBoy</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #A6ACCD;">source</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">31</span><span style="color: #A6ACCD;">                    </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">sourceLink</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunkyBoy</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #A6ACCD;">sourceLink</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">32</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">],</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">33</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">34</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">35</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">if</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(++$</span><span style="color: #A6ACCD;">count </span><span style="color: #89DDFF;">%</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">batchSize </span><span style="color: #89DDFF;">===</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">100</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">36</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">weaviate</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">batch</span><span style="color: #89DDFF;">()-&gt;</span><span style="color: #82AAFF;">create</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">objects</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">37</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">objects </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">38</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">count </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">0</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">39</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">40</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">41</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">42</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">weaviate</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">batch</span><span style="color: #89DDFF;">()-&gt;</span><span style="color: #82AAFF;">create</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">objects</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">43</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">44</span><span style="color: #89DDFF;">}</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">else</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">45</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// Handle the error</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">46</span><span style="color: #A6ACCD;">    </span><span style="color: #82AAFF;">echo</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">Failed to fetch descendant pages: </span><span style="color: #89DDFF;">&quot;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">status</span><span style="color: #89DDFF;">();</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">47</span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">48</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">49</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">cleanUpContent</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">string</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">content</span><span style="color: #89DDFF;">):</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">string</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">50</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">51</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Str</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">of</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">content</span><span style="color: #89DDFF;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">52</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">replace</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">&lt;</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;"> &lt;</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">53</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">stripTags</span><span style="color: #89DDFF;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">54</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">replace</span><span style="color: #89DDFF;">([</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">\r</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">\n</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">],</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">55</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">replaceMatches</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;/</span><span style="color: #A6ACCD;">\s</span><span style="color: #89DDFF;">+/&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">56</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">trim</span><span style="color: #89DDFF;">();</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">57</span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">58</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">59</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">chunkContent</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">string</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">content</span><span style="color: #89DDFF;">):</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">array</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">60</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">61</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">tokensPerCharacter </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">0</span><span style="color: #89DDFF;">.</span><span style="color: #F78C6C;">4</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">62</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">tokenLimit </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">150</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">63</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunkCharacterLimit </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">tokenLimit </span><span style="color: #89DDFF;">/</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">tokensPerCharacter</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">64</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">65</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// Split the input string into an array of sentences</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">66</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">sentences </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">collect</span><span style="color: #89DDFF;">(</span><span style="color: #82AAFF;">preg_split</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;/</span><span style="color: #C3E88D;">(?&lt;=</span><span style="color: #89DDFF;">[</span><span style="color: #C3E88D;">.?!</span><span style="color: #89DDFF;">]</span><span style="color: #C3E88D;">)</span><span style="color: #A6ACCD;">\s</span><span style="color: #C3E88D;">?(?=</span><span style="color: #89DDFF;">[</span><span style="color: #C3E88D;">a-z</span><span style="color: #89DDFF;">]</span><span style="color: #C3E88D;">)</span><span style="color: #89DDFF;">/i&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">content</span><span style="color: #89DDFF;">));</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">67</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">68</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunks </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">sentences</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">chunkWhile</span><span style="color: #89DDFF;">(</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">69</span><span style="color: #A6ACCD;">        </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">string</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">sentence</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">int</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">key</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Collection</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunk</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">use</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">chunkCharacterLimit</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">70</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunk</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">sum</span><span style="color: #89DDFF;">(</span><span style="color: #C792EA;">fn</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">string</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">sentence</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">strlen</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">sentence</span><span style="color: #89DDFF;">))</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&lt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunkCharacterLimit</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">71</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">72</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">)-&gt;</span><span style="color: #82AAFF;">map</span><span style="color: #89DDFF;">(</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #FFCB6B;">Collection</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunk</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">73</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">value </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunk</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">implode</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">74</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">checksum </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">md5</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">value</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">75</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">76</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">77</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">checksum</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">checksum</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">78</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">value</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">value</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">79</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">80</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">});</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">81</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">82</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunks</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">all</span><span style="color: #89DDFF;">();</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">83</span><span style="color: #89DDFF;">}</span></div></code></pre>
<h2><a id="content-implementing-the-natural-language-search" href="#content-implementing-the-natural-language-search" class="prezet-heading" title="Permalink">#</a>Implementing the Natural Language Search</h2>
<p>In this section, we’ll implement the natural language search feature using OpenAI’s GPT models and the Weaviate vector database. Our goal is to allow users to search the documentation using natural language queries, and return the most relevant results. Here’s how we’ll do it:</p>
<p>Assume we’ll have an endpoint that accepts a <code>question</code> parameter, you could implement this in your own application using the code below:</p>
<pre><code data-theme="material-theme-palenight" data-lang="php" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">&lt;?php</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F78C6C;">namespace</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">App</span><span style="color: #89DDFF;">\</span><span style="color: #FFCB6B;">Http</span><span style="color: #89DDFF;">\</span><span style="color: #FFCB6B;">Controllers</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #F78C6C;">use</span><span style="color: #FFCB6B;"> </span><span style="color: #A6ACCD;">Illuminate</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Http</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Request</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F78C6C;">use</span><span style="color: #FFCB6B;"> </span><span style="color: #A6ACCD;">OpenAI</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Laravel</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Facades</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">OpenAI</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #F78C6C;">use</span><span style="color: #FFCB6B;"> </span><span style="color: #A6ACCD;">Weaviate</span><span style="color: #89DDFF;">\</span><span style="color: #A6ACCD;">Weaviate</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #C792EA;">class</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">DocSearchController</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">extends</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">Controller</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">public</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">search</span><span style="color: #89DDFF;">(</span><span style="color: #FFCB6B;">Request</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">request</span><span style="color: #89DDFF;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">question </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">request</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">input</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">question</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">if</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">question</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunks </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$this-&gt;</span><span style="color: #82AAFF;">getChunks</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">question</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">messages </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$this-&gt;</span><span style="color: #82AAFF;">getMessages</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">question</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunks</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">OpenAI</span><span style="color: #89DDFF;">::</span><span style="color: #82AAFF;">chat</span><span style="color: #89DDFF;">()-&gt;</span><span style="color: #82AAFF;">create</span><span style="color: #89DDFF;">([</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">model</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">gpt-3.5-turbo</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">messages</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">messages</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">]);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">answer </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">choices</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">][</span><span style="color: #F78C6C;">0</span><span style="color: #89DDFF;">][</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">message</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">][</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">content</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">view</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">docsearch</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">answer</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">answer </span><span style="color: #89DDFF;">??</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">29</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">]);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">30</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">31</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">32</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">protected</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">getChunks</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">string</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">text</span><span style="color: #89DDFF;">):</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">array</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">33</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">34</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">weaviate </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">app</span><span style="color: #89DDFF;">(</span><span style="color: #FFCB6B;">Weaviate</span><span style="color: #89DDFF;">::</span><span style="color: #F78C6C;">class</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">35</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">36</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">query </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&lt;&lt;&lt;GQL</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">37</span><span style="color: #C3E88D;">        {</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">38</span><span style="color: #C3E88D;">          Get {</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">39</span><span style="color: #C3E88D;">            Chunk(</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">40</span><span style="color: #C3E88D;">              nearText: {</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">41</span><span style="color: #C3E88D;">                  concepts: &quot;</span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">text</span><span style="color: #C3E88D;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">42</span><span style="color: #C3E88D;">                  certainty: 0.9</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">43</span><span style="color: #C3E88D;">              }</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">44</span><span style="color: #C3E88D;">              limit: 3</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">45</span><span style="color: #C3E88D;">            ) {</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">46</span><span style="color: #C3E88D;">                content</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">47</span><span style="color: #C3E88D;">            }</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">48</span><span style="color: #C3E88D;">          }</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">49</span><span style="color: #C3E88D;">        }</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">50</span><span style="color: #89DDFF;">GQL</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">51</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">52</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">weaviate</span><span style="color: #89DDFF;">-&gt;</span><span style="color: #82AAFF;">graphql</span><span style="color: #89DDFF;">()-&gt;</span><span style="color: #82AAFF;">get</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">query</span><span style="color: #89DDFF;">);</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">53</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">54</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">if</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #82AAFF;">isset</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">errors</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">]))</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">55</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">56</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">57</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">58</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response </span><span style="color: #89DDFF;">?</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">data</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">][</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Get</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">][</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Chunk</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">]</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">59</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">60</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">61</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">protected</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">getMessages</span><span style="color: #89DDFF;">(</span><span style="color: #F78C6C;">string</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">question</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">array</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">chunks</span><span style="color: #89DDFF;">):</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">array</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">62</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">63</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">information </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">implode</span><span style="color: #89DDFF;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">\n</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">array_column</span><span style="color: #89DDFF;">($</span><span style="color: #A6ACCD;">chunks</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">content</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">));</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">64</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">65</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">messages </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">66</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">role</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">system</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">content</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">You are a helpful assistant.</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">],</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">67</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">role</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">user</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">content</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Here is some information: </span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">information</span><span style="color: #89DDFF;">],</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">68</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">[</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">role</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">user</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">content</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Please use this information to answer my question: </span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">question</span><span style="color: #89DDFF;">],</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">69</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">];</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">70</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">71</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">return</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">$</span><span style="color: #A6ACCD;">messages</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">72</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">73</span><span style="color: #89DDFF;">}</span></div></code></pre>
<h2><a id="content-conclusion" href="#content-conclusion" class="prezet-heading" title="Permalink">#</a>Conclusion</h2>
<p>In this tutorial, I’ve demonstrated how to integrate OpenAI’s GPT models, Weaviate vector search, and Laravel to create a natural language search for your documentation. While this implementation works well, there are further optimizations we can apply to enhance the system and reduce API costs.</p>
<p>One area of improvement is only updating the embeddings of your documentation content when needed. Embeddings can be expensive to compute, both in terms of time and API costs. By  only updating them when the content changes, you can save on API bills and improve response times.</p>
<p>To achieve this, you could use MD5 hashes to check whether the content has changed or not. When you receive a new content update, calculate its MD5 hash and compare it to the hash of the previous content. If the hashes are different, update the embeddings in Weaviate and store the new hash for future comparisons. This way, you’ll only update the embeddings when there’s an actual change in the content.</p>
<p>By applying these improvements, you’ll create a more efficient and cost-effective natural language search system for your documentation, while maintaining a high level of accuracy and relevance for your users.</p>
]]>
            </summary>
                                    <updated>2023-04-10T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Reconnect an unavailable Zigbee device in Home Assistant]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/reconnect-unavailable-zigbee-device-home-assistant" />
            <id>https://tim-kleyersburg.de/reconnect-unavailable-zigbee-device-home-assistant</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>If you have ever encountered a situation where a Zigbee device becomes unavailable in Home Assistant, you may be wondering how to easily reconnect it.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./unavailable-devices-480w.png 480w, /articles/img/./unavailable-devices-640w.png 640w, /articles/img/./unavailable-devices-768w.png 768w, /articles/img/./unavailable-devices-960w.png 960w, /articles/img/./unavailable-devices-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/unavailable-devices.png" alt="Screenshot of some unavailable devices in the Home Assistant UI" />
<figcaption class="prezet-figcaption">Screenshot of some unavailable devices in the Home Assistant UI</figcaption>
</figure>
<p>Although there’s no apparent method to do this in the UI, it’s surprisingly easy to accomplish!</p>
<h2><a id="content-steps-to-reconnect-an-unavailable-zigbee-device" href="#content-steps-to-reconnect-an-unavailable-zigbee-device" class="prezet-heading" title="Permalink">#</a>Steps to reconnect an unavailable Zigbee device</h2>
<ol>
<li>Open the Settings → Devices &amp; Services page</li>
<li>Click on “Add integration”</li>
<li>Select “Add Zigbee device”</li>
<li>Put your unavailable device into pairing mode</li>
</ol>
<p>Your device should now re-pair and be available again.</p>
<p>The advantage of this method is that you don’t have to remove the device from Home Assistant first, which would break all existing automations.</p>
]]>
            </summary>
                                    <updated>2023-03-12T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Setup Weaviate with Docker and Traefik]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/setup-weaviate-with-docker-and-traefik" />
            <id>https://tim-kleyersburg.de/setup-weaviate-with-docker-and-traefik</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<div class="not-prose rounded-md border-l-4 border-l-orange-300 bg-yellow-50 px-3 py-2 leading-snug dark:border-l-yellow-900 dark:bg-yellow-700 dark:text-white/90">
    <p>Update 2023-04-15: Since version 1.18 of Weaviate you can use <a href="https://weaviate.io/developers/weaviate/configuration/authentication#api-key">API key authentication</a>, thus making the header authentication plugin unnecessary.</p>

</div>

<h2><a id="content-what-is-weaviate" href="#content-what-is-weaviate" class="prezet-heading" title="Permalink">#</a>What is Weaviate?</h2>
<blockquote>
<p>Weaviate is an open-source vector search engine.
It allows you to store data objects and vector embeddings from your favorite ML-models, and scale seamlessly into billions of data objects.</p>
</blockquote>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://weaviate.io/">Source</a></p>
<p>In other words, it’s a search engine for machine learning models. It’s a helpful tool for building your own AI applications. You can use it to store and search for data objects using vector embeddings. It also integrates directly with OpenAI, allowing you to use their embedding models directly upon importing text.<br />
This enables you to do very easy near-text-searches because the query is also automatically embedded.</p>
<h2><a id="content-installing-weaviate-with-docker" href="#content-installing-weaviate-with-docker" class="prezet-heading" title="Permalink">#</a>Installing Weaviate with Docker</h2>
<p>To install Weaviate with Docker, you can use the official <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://weaviate.io/developers/weaviate/installation/docker-compose">Docker Compose Configurator</a>. This will provide you with a <code>docker-compose.yml</code> file that if perfectly pre-configured to your liking.</p>
<p>Just remember to set a persistent volume if you don’t want to lose your data when you restart the container.</p>
<h2><a id="content-integrating-with-traefik" href="#content-integrating-with-traefik" class="prezet-heading" title="Permalink">#</a>Integrating with Traefik</h2>
<p>If, like me, you are using <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://traefik.io/">Traefik</a> as a reverse proxy, you can use the following <code>docker-compose.yml</code> file to set up Weaviate with Traefik.</p>
<p>This assumes you are using an external network called <code>web</code> to route all your traffic through Traefik. The rest of the file was created using the above-mentioned configurator. Depending on your preferences the <code>environment</code> section may vary.</p>
<p>I’ve used labels to configure Traefik for this container.</p>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #FFCB6B;">---</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">version</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">3.4</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F07178;">services</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">weaviate</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">image</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">cr.weaviate.io/semitechnologies/weaviate:1.17.4</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">ports</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">8080:8080</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">restart</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">on-failure:0</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">volumes</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">/var/weaviate:/var/lib/weaviate</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">labels</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.weaviate.rule=Host(`weaviate.your-host.com`)</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.weaviate.tls=true</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.weaviate.tls.certresolver=lets-encrypt</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">environment</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">OPENAI_APIKEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">$OPENAI_APIKEY</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">QUERY_DEFAULTS_LIMIT</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">25</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">true</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">PERSISTENCE_DATA_PATH</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">/var/lib/weaviate</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">DEFAULT_VECTORIZER_MODULE</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">text2vec-openai</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">ENABLE_MODULES</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">text2vec-openai,generative-openai</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">CLUSTER_HOSTNAME</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">node1</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">web</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">web</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">true</span></div></code></pre>
<h3><a id="content-adding-a-simple-header-authentication" href="#content-adding-a-simple-header-authentication" class="prezet-heading" title="Permalink">#</a>Adding a simple header authentication</h3>
<p>I didn’t want to expose Weaviate to the internet without some kind of authentication. While Weaviate supports <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://weaviate.io/developers/weaviate/configuration/authentication">OIDC authentication</a>, this seemed overkill for my use case. I just wanted to add a simple header authentication to prevent unauthorized access.</p>
<p>To achieve this, I’ve used the plugin <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/omar-shrbajy-arive/headerauthentication">headerauthentication</a> by <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/omar-shrbajy-arive">omar-shrbajy-arive</a>. This plugin allows you to define a specific header key and value that must be present in the request to access Weaviate.</p>
<p>To use this plugin, you need to add the following to your Traefik static configuration file, assuming you are using the <code>toml</code> format:</p>
<pre><code data-theme="material-theme-palenight" data-lang="toml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">[</span><span style="color: #FFCB6B;">experimental</span><span style="color: #A6ACCD;">.</span><span style="color: #FFCB6B;">plugins</span><span style="color: #A6ACCD;">.</span><span style="color: #FFCB6B;">headerauthentication</span><span style="color: #89DDFF;">]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">  moduleName </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">github.com/omar-shrbajy-arive/headerauthentication</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">  version </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">v1.0.3</span><span style="color: #89DDFF;">&quot;</span></div></code></pre>
<p>Then you can add the following to your <code>docker-compose.yml</code> file:</p>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight has-add-lines has-diff-lines' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #FFCB6B;">---</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">version</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">3.4</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F07178;">services</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">weaviate</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">image</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">cr.weaviate.io/semitechnologies/weaviate:1.17.4</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">ports</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">8080:8080</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">restart</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">on-failure:0</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">volumes</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">/var/weaviate:/var/lib/weaviate</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">labels</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.weaviate.rule=Host(`weaviate.wacg.dev`)</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.weaviate.tls=true</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.weaviate.tls.certresolver=lets-encrypt</span><span style="color: #89DDFF;">&quot;</span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #C3E88D;">      </span><span style="color: #C3E88D;">-</span><span style="color: #C3E88D;"> </span><span style="color: #C3E88D;">&quot;</span><span style="color: #C3E88D;">traefik.http.routers.weaviate.middlewares=weaviate@docker</span><span style="color: #C3E88D;">&quot;</span><span style="color: #C3E88D;"> </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #C3E88D;">      </span><span style="color: #C3E88D;">-</span><span style="color: #C3E88D;"> </span><span style="color: #C3E88D;">&quot;</span><span style="color: #C3E88D;">traefik.http.middlewares.weaviate.plugin.headerauthentication.Header.name=X-TOKEN</span><span style="color: #C3E88D;">&quot;</span><span style="color: #C3E88D;"> </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #C3E88D;">      </span><span style="color: #C3E88D;">-</span><span style="color: #C3E88D;"> </span><span style="color: #C3E88D;">&quot;</span><span style="color: #C3E88D;">traefik.http.middlewares.weaviate.plugin.headerauthentication.Header.key=${HEADER_TOKEN}</span><span style="color: #C3E88D;">&quot;</span><span style="color: #C3E88D;"> </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">environment</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">OPENAI_APIKEY</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">$OPENAI_APIKEY</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">QUERY_DEFAULTS_LIMIT</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">25</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">true</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">PERSISTENCE_DATA_PATH</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">/var/lib/weaviate</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">DEFAULT_VECTORIZER_MODULE</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">text2vec-openai</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">ENABLE_MODULES</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">text2vec-openai,generative-openai</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #A6ACCD;">      </span><span style="color: #F07178;">CLUSTER_HOSTNAME</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">node1</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">      </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">web</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">29</span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">30</span><span style="color: #A6ACCD;">  </span><span style="color: #F07178;">web</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">31</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">true</span></div></code></pre>
<p>This configures a new middleware called <code>weaviate</code> using the <code>headerauthentication</code> plugin. It also adds a new environment variable called <code>HEADER_TOKEN</code> that will be used to set the value of the header key.</p>
<p>Please create an <code>.env</code> file which holds the value of the header key. For example:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">HEADER_TOKEN</span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;">your-token</span></div></code></pre>
<h2><a id="content-conclusion" href="#content-conclusion" class="prezet-heading" title="Permalink">#</a>Conclusion</h2>
<p>You now have a fully functional Weaviate instance running on your server. You can use it to build your own AI applications.</p>
]]>
            </summary>
                                    <updated>2023-03-07T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Clear up space used by Docker with a built-in command]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/docker-uses-too-much-space" />
            <id>https://tim-kleyersburg.de/docker-uses-too-much-space</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<h2><a id="content-introduction" href="#content-introduction" class="prezet-heading" title="Permalink">#</a>Introduction</h2>
<p>If you’ve been using Docker for a while, you may have noticed that it can quickly use up a lot of disk space. Especially after testing out a few images, updating existing ones you may find your host slowly but surely filling up.</p>
<p>In this quick tip, I’ll show you how to use the built-in command <code>docker system prune</code> to clear up space used by Docker.</p>
<h2><a id="content-what-does-docker-system-prune-do" href="#content-what-does-docker-system-prune-do" class="prezet-heading" title="Permalink">#</a>What does <code>docker system prune</code> do?</h2>
<p>To quote the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.docker.com/engine/reference/commandline/system_prune/">official documentation</a>: this command “removes unused data”.</p>
<p>In practice, this means that it will remove:</p>
<ul>
<li>all stopped containers</li>
<li>all networks not used by at least one container</li>
<li>all dangling images</li>
<li>all build cache</li>
</ul>
<p>I’ve found, that especially dangling images can use up a lot of space. If you are using my <a href="/articles/updating-home-assistant-with-docker">quicktip for updating your Home Assistant installation</a> this can quickly use up a lot of space on your SD card.</p>
<h3><a id="content-available-options" href="#content-available-options" class="prezet-heading" title="Permalink">#</a>Available options</h3>
<p><code>--all, -a</code> - Remove all unused images not just dangling ones</p>
<p><code>--force, -f</code> - Do not prompt for confirmation</p>
<p><code>--volumes</code> - Prune volumes</p>
<div class="not-prose rounded-md border-l-4 border-l-orange-300 bg-yellow-50 px-3 py-2 leading-snug dark:border-l-yellow-900 dark:bg-yellow-700 dark:text-white/90">
    <p>Be careful when using the <code>--volumes</code> option. This will remove all volumes that are not used by any containers and could potentially delete data you need!</p>

</div>

<h2><a id="content-how-to-use-it" href="#content-how-to-use-it" class="prezet-heading" title="Permalink">#</a>How to use it</h2>
<p>To use the command, simply run it in your terminal with your wanted options. For example, to remove all unused images and dangling containers, you would run:</p>
<pre><code data-theme="material-theme-palenight" data-lang="bash" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">docker</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">system</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">prune</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">--all</span></div></code></pre>
<h2><a id="content-conclusion" href="#content-conclusion" class="prezet-heading" title="Permalink">#</a>Conclusion</h2>
<p>Using the built-in command <code>docker system prune</code> is a quick and easy way to clear up space used by Docker. It’s a good idea to run it every once in a while to keep your host running smoothly and efficiently. On my Raspberry Pi I run it after updating my existing containers.</p>
]]>
            </summary>
                                    <updated>2023-01-06T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Eliminate meeting distractions in Mattermost with automatic DND mode]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/eliminate-meetings-distractions-in-mattermost" />
            <id>https://tim-kleyersburg.de/eliminate-meetings-distractions-in-mattermost</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<h2><a id="content-introduction" href="#content-introduction" class="prezet-heading" title="Permalink">#</a>Introduction</h2>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://pipedream.com">Pipedream</a> is a powerful and flexible platform for building and automating workflows. It allows you to connect a wide variety of apps and services, and trigger actions based on events or data from those services. With Pipedream, you can automate all kinds of tasks, from sending emails and texts to updating social media or setting reminders.</p>
<p>One example of how you can use Pipedream is to automatically set Mattermost’s DND (Do Not Disturb) mode when a Google Calendar event starts. This can be especially useful if you have meetings or other events that you don’t want to be interrupted during. By using Pipedream to automate this process, you can save time and hassle, and ensure that you remain focused and productive during your events.</p>
<p>In this blog post, I’ll walk you through the process of setting up this workflow in Pipedream. I’ll show you how to set up a Google Calendar trigger, how to set up a Mattermost action, and how to put it all together to create a complete workflow. By the end of this post, you’ll be able to automate the process of setting Mattermost’s DND mode when a Google Calendar event starts, using Pipedream.</p>
<h2><a id="content-setting-up-the-accounts" href="#content-setting-up-the-accounts" class="prezet-heading" title="Permalink">#</a>Setting up the accounts</h2>
<p>To get started with setting up a Pipedream workflow to automatically set Mattermost’s DND mode when a Google Calendar event starts, you’ll need to have a few things in place:</p>
<ol>
<li>
<p>A Pipedream account: If you don’t already have a Pipedream account, you can sign up for one at <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://pipedream.com">pipedream.com</a>. It’s free to get started, and you can choose a paid plan later if you need more compute time. But Pipedream’s free tier is more than enough to get started with this workflow.</p>
</li>
<li>
<p>A Mattermost instance with the right to create a <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.mattermost.com/developer/personal-access-tokens.html">Personal Access Token</a>, which is used to authenticate against the REST API. Your instance administrator can help you with this if you don’t have the right to create a Personal Access Token.</p>
</li>
<li>
<p>A Google Calendar account: Finally, you’ll need a Google Calendar account to use as the trigger for your workflow.</p>
</li>
</ol>
<p>Once you have these accounts set up, you can follow these steps to connect them to Pipedream:</p>
<p>In Pipedream, click on the “Connections” tab in the left-hand menu.</p>
<ol>
<li>
<p>Click on the “Accounts” link, click on the “Connect an app” button, and choose “Google Calendar” from the list of available connections.</p>
</li>
<li>
<p>Follow the prompts to connect your Google account to Pipedream. You’ll need to provide your Google login credentials and authorize the connection.</p>
</li>
</ol>
<div class="not-prose rounded-md border-l-4 border-l-orange-300 bg-yellow-50 px-3 py-2 leading-snug dark:border-l-yellow-900 dark:bg-yellow-700 dark:text-white/90">
    <p>You could also skip this step and set the source up when creating the workflow.</p>

</div>

<h2><a id="content-building-the-workflow" href="#content-building-the-workflow" class="prezet-heading" title="Permalink">#</a>Building the workflow</h2>
<p>Click on “Workflows” in the sidebar menu and click on the “New +” button in the top right corner to create a new workflow.</p>
<h3><a id="content-setting-up-the-google-calendar-trigger" href="#content-setting-up-the-google-calendar-trigger" class="prezet-heading" title="Permalink">#</a>Setting up the Google Calendar trigger</h3>
<p>After starting to create a new workflow, the first thing to do is add a trigger. In this case, we’ll use a Google Calendar trigger. This trigger will fire whenever a new event is added to a specific calendar.</p>
<p>Select the Google Calendar account you want to use as the trigger, and choose the specific calendar you want to use. You should now also add a timer. Pipedream will use this timer to check for new events every x minutes. I’ve used 25 minutes because I had problems with the default 15 minutes with very short events. You can adjust this to your needs.</p>
<p>Your trigger should now look like this:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./google-calendar-trigger-480w.png 480w, /articles/img/./google-calendar-trigger-640w.png 640w, /articles/img/./google-calendar-trigger-768w.png 768w, /articles/img/./google-calendar-trigger-960w.png 960w, /articles/img/./google-calendar-trigger-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/google-calendar-trigger.png" alt="Screenshot of the Google Calendar trigger" />
<figcaption class="prezet-figcaption">Screenshot of the Google Calendar trigger</figcaption>
</figure>
<p>Click on the “Create source” button to move on to the next step.</p>
<p>At this point, you should have a basic Google Calendar trigger set up in Pipedream. When an event starts on the calendar you selected, this trigger will be activated and the workflow will continue to the next step.</p>
<h3><a id="content-adding-a-node-action" href="#content-adding-a-node-action" class="prezet-heading" title="Permalink">#</a>Adding a Node action</h3>
<p>The next step is crucial for this workflow to work perfectly. Unfortunately the event is not triggered in the exact moment the event starts. This is because Google Calendar doesn’t provide a trigger for this. Instead, you’ll get a trigger for an event that starts in the next 25 minutes (or whatever timer setting you used). This is not ideal, but it’s the best we can do with the current Google Calendar API.</p>
<p>To solve this, we’ll use a Node action which will parse the event start and end time to delay the workflow until the event starts. Here is the code we’ll use:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">export</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">default</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">defineComponent</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">    </span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> </span><span style="color: #F07178;">run</span><span style="color: #89DDFF;">({</span><span style="color: #A6ACCD;"> steps</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> $ </span><span style="color: #89DDFF;">})</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F07178;">        </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">eventStartDate</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #FFCB6B;">Date</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">parse</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">steps</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">trigger</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">start</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">dateTime</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #F07178;">        </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">eventEndDate</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #FFCB6B;">Date</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">parse</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">steps</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">trigger</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">end</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">dateTime</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F07178;">        </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">triggerStartDate</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #FFCB6B;">Date</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">parse</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">steps</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">trigger</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">context</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">ts</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">        </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">delay</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">eventStartDate</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">-</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">triggerStartDate</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">if</span><span style="color: #F07178;"> (</span><span style="color: #A6ACCD;">delay</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;"> </span><span style="color: #F78C6C;">0</span><span style="color: #F07178;">) </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">$</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">flow</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">exit</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">The event already started</span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #F07178;">        </span><span style="color: #A6ACCD;">$</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">export</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">delay</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">delay</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #F07178;">        </span><span style="color: #A6ACCD;">$</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">export</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">dnd_end_time</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">eventEndDate</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #89DDFF;">}</span><span style="color: #A6ACCD;">)</span></div></code></pre>
<p>We’ll concentrate on the part inside the <code>run</code> function, the rest is normal boilerplate code. The first two lines parse the event start and end time to a JavaScript <code>Date</code> object.</p>
<p>The third line parses the trigger start time to a <code>Date</code> object.</p>
<p>With these variables in place we can now calculate how much we have to delay the workflow to continue at the exact moment the event starts.</p>
<p>Let’s subtract the trigger start time from the event start time. This will give us the delay in milliseconds. We’ll store this in the <code>delay</code> variable.</p>
<p>If the delay is under 0 we use an early return to exit the workflow in case the event already started.</p>
<p>We need to export the delay and the end time for the event. We’ll use these in the next step.</p>
<h3><a id="content-delaying-the-workflow" href="#content-delaying-the-workflow" class="prezet-heading" title="Permalink">#</a>Delaying the workflow</h3>
<p>The next step is to delay the workflow until the event starts. To do this, we’ll use the <code>Delay</code> action. This action will delay the workflow for a specified amount of time. We’ll use the delay we calculated in the previous step.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./delay-workflow-action-480w.png 480w, /articles/img/./delay-workflow-action-640w.png 640w, /articles/img/./delay-workflow-action-768w.png 768w, /articles/img/./delay-workflow-action-960w.png 960w, /articles/img/./delay-workflow-action-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/delay-workflow-action.png" alt="Screenshot of the Delay action" />
<figcaption class="prezet-figcaption">Screenshot of the Delay action</figcaption>
</figure>
<p>Make sure the set the unit to milliseconds.</p>
<h3><a id="content-set-the-dnd-mode-in-mattermost" href="#content-set-the-dnd-mode-in-mattermost" class="prezet-heading" title="Permalink">#</a>Set the DND mode in Mattermost</h3>
<p>After you’ve obtained your personal access token you are ready to build up the HTTP request we are going to make to the Mattermost REST API.</p>
<p>Use the official guide for <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.mattermost.com/developer/personal-access-tokens.html">Personal Access Tokens</a> to obtain your own token. If you can’t create a token of your own, you might need to ask your Mattermost administrator to create a token for you or to give you the right to create one.</p>
<p>Add the action “HTTP Request” with the method <code>PUT</code> and the endpoint <code>/api/v4/users/me/status</code> with the following body:</p>
<pre><code data-theme="material-theme-palenight" data-lang="json" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">user_id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">me</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">status</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">dnd</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">dnd_end_time</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">{{steps.node.dnd_end_time / 1000}}</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Note that we set a key of <code>dnd_end_time</code>. This is used to automatically end the DND mode when the event ends.</p>
<p>Your body section should now look like this:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./request-action-body-480w.png 480w, /articles/img/./request-action-body-640w.png 640w, /articles/img/./request-action-body-768w.png 768w, /articles/img/./request-action-body-960w.png 960w, /articles/img/./request-action-body-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/request-action-body.png" alt="Screenshot of the body section" />
<figcaption class="prezet-figcaption">Screenshot of the body section</figcaption>
</figure>
<p>Make sure you use your personal access token in the auth section using the <code>Bearer Token</code> authorization type.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./request-action-auth-480w.png 480w, /articles/img/./request-action-auth-640w.png 640w, /articles/img/./request-action-auth-768w.png 768w, /articles/img/./request-action-auth-960w.png 960w, /articles/img/./request-action-auth-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/request-action-auth.png" alt="Screenshot of the auth section" />
<figcaption class="prezet-figcaption">Screenshot of the auth section</figcaption>
</figure>
<p><strong>You’re good to go!</strong> After you’ve saved the workflow, it will be triggered whenever a new event is added to the calendar you selected. The workflow will delay itself until the event starts and then set the DND mode in Mattermost.</p>
<h3><a id="content-bonus-set-a-custom-status-with-the-event-name" href="#content-bonus-set-a-custom-status-with-the-event-name" class="prezet-heading" title="Permalink">#</a>Bonus: Set a custom status with the event name</h3>
<p>If you want you can also set a custom status with the event name. To do this, you’ll need to add another HTTP request action. This time, you’ll need to set the method to <code>PUT</code> and the endpoint to <code>/api/v4/users/me/status/custom</code>. You’ll also need to set the body to the following:</p>
<pre><code data-theme="material-theme-palenight" data-lang="json" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">emoji</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">calendar</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">text</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">{{steps.trigger.event.summary}}</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">expires_at</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">{{steps.trigger.event.end.dateTime}}</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Note: the API is a bit inconsistent here. The <code>dnd_end_time</code> is in seconds, but the <code>expires_at</code> is in milliseconds. This is why we needed to divide the <code>dnd_end_time</code> by 1000.</p>
]]>
            </summary>
                                    <updated>2022-12-18T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Migrate an older Mattermost installation to a new one]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/migrate-mattermost" />
            <id>https://tim-kleyersburg.de/migrate-mattermost</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>I recently had to migrate an older installation of a Mattermost instance to a new server. Unfortunately, the old install method wasn’t supported anymore and relied on a MySQL-database, which isn’t officially supported anymore.</p>
<p>Since there currently is no direct migration path from MySQL to PostgreSQL which is officially vetted by Mattermost, to following path outlines how you can manually migrate a Mattermost instance by following the steps outlined here.</p>
<h2><a id="content-migration-outline" href="#content-migration-outline" class="prezet-heading" title="Permalink">#</a>Migration outline</h2>
<ol>
<li>Upgrade current in use Mattermost instance</li>
<li>Setup new server</li>
<li>Export data from old server</li>
<li>Import data to new server</li>
<li>Migrate settings and external integrations</li>
</ol>
<h3><a id="content-1-upgrade-current-in-use-mattermost-instance" href="#content-1-upgrade-current-in-use-mattermost-instance" class="prezet-heading" title="Permalink">#</a>1. Upgrade current in use Mattermost instance</h3>
<p>Your first step should be to make sure that your current instance is using the latest Mattermost version or at least version to which you are migrating. This is important because data exports may be incompatible if you try to import a newer export into an older server or the other way around.</p>
<p>You can find instructions on how to upgrade in the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.mattermost.com/guides/deployment.html#upgrade-mattermost">official Mattermost docs</a>.</p>
<h3><a id="content-2-setup-new-server" href="#content-2-setup-new-server" class="prezet-heading" title="Permalink">#</a>2. Setup new server</h3>
<p>I’ve used the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.mattermost.com/install/installing-mattermost-omnibus.html">Mattermost Omnibus installation method</a>. If you’ve used another method the results of the following steps may vary. But since we will be using the command line utilities, which come packaged with Mattermost regardless of method of installation, the next steps should work the same.</p>
<h3><a id="content-3-export-data-from-old-server" href="#content-3-export-data-from-old-server" class="prezet-heading" title="Permalink">#</a>3. Export data from old server</h3>
<p>Now you’ll use the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.mattermost.com/manage/command-line-tools.html">mmctl command line tool</a> to export the data from the old server with the following commands:</p>
<p>First, you’ll need to authenticate the command line tools with your server:</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">auth</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">login</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://oldmattermosturl.com</span></div></code></pre>
<p>At this point you should shut off access to the old server so no new data is generated. Do this on the weekend or after-work hours.</p>
<p>The following command will create an export job. The command returns the id of the created export which you can use to check the status of the export.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">export</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">create</span></div></code></pre>
<p>Use the <code>export job show</code> command to check the status of your export by replacing <code>{id}</code> with the id from the previous command.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">export</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">job</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">show</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span><span style="color: #FFCB6B;">id</span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #FFCB6B;">---</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">Output</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #FFCB6B;">ID:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">j9fgs4h7htpu0nl14fir3ncsno</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #FFCB6B;">Status:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">success</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #FFCB6B;">Created:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">2022-10-28</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">10:55:25</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">+0000</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">UTC</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #FFCB6B;">Started:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">2022-10-28</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">10:55:31</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">+0000</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">UTC</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span><span style="color: #FFCB6B;">Data:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">map[include_attachments:true]</span></div></code></pre>
<p>After the status changes to <code>success</code>, use the next command to show all available exports. This will show  you all finished exports.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">export</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">list</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #FFCB6B;">---</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">Output</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #FFCB6B;">j9fgs4h7htpu0nl14fir3ncsno_export.zip</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #FFCB6B;">There</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">are</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">1</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">exports</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">on</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://oldmattermosturl.com/</span></div></code></pre>
<p>Copy the filename from the previous command to download the export. This will download the file into the current directory from which you’ve started this command.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">export</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">download</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span><span style="color: #FFCB6B;">filename</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>You should now have the data export on your old Mattermost server. Depending on your infrastructure and if your old and new server have access to each other you will now need to somehow transfer the zip file to the new server.<br />
In my case I used <code>rsync</code> to transfer the data to my laptop:</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">rsync</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-av</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">oldmattermosthost:/path/to/exported-file.zip</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">.</span></div></code></pre>
<h3><a id="content-4-import-data-to-new-server" href="#content-4-import-data-to-new-server" class="prezet-heading" title="Permalink">#</a>4. Import data to new server</h3>
<p>Using <code>rsync</code> again, I transferred the zip file to the new host:</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">rsync</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-av</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">./exported-file.zip</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">newmattermosthost:/path-where-the-file-should-be</span></div></code></pre>
<p>Again, the first thing you’ll need to do is authenticate the command line tool with on your new server - I’ll assume you already installed Mattermost on the new server.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">auth</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">login</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://newmattermosturl.com</span></div></code></pre>
<p>Depending on how big your data export file is you will need to make a few adjustments on the new instance as well as the server. For our team of around 15 people and an instance of around 18 months old this amounted to a backup size of around 5GB - including attachments, which is much too big for the default settings.</p>
<p>You will have to do 2 things:</p>
<ul>
<li>bump the max file upload size in the system console of Mattermost</li>
<li>update the nginx config to allow bigger upload sizes</li>
</ul>
<p>Add an admin account and open the system console. You will find the maximum file size setting by either searching for it or navigate to Environment → File Storage → Maximum File Size. Set a maximum file size that is bigger than your backup.</p>
<p>If you don’t do this, you will get an error message like this:</p>
<blockquote>
<p>Error: failed to create upload session: Unable to upload file. File is too large.</p>
</blockquote>
<p>Next, edit the nginx config file for mattermost (located at <code>/etc/nginx/conf.d/mattermost.conf</code>) and configure the <code>client_max_body_size</code> to be bigger than your zip file. There are multiple locations where you have to overwrite this setting.<br />
I used <code>sed</code> to replace all occurrences.</p>
<p>If this setting is too small you will get the following error message:</p>
<blockquote>
<p>Error: failed to upload data: AppErrorFromJSON: model.utils.decode_json.app_error<br />
413 Request Entity Too Large</p>
</blockquote>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #82AAFF;">sed</span><span style="color: #A6ACCD;"> -i </span><span style="color: #C3E88D;">-e</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;s/</span><span style="color: #A6ACCD;">50M</span><span style="color: #89DDFF;">/</span><span style="color: #A6ACCD;">4G</span><span style="color: #89DDFF;">/&#39;</span><span style="color: #A6ACCD;"> /etc/nginx/conf.d/mattermost.conf </span><span style="color: #676E95;"># replace 4G with what matches your file size</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">systemctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">reload</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">nginx</span></div></code></pre>
<div class="not-prose rounded-md border-l-4 border-l-orange-300 bg-yellow-50 px-3 py-2 leading-snug dark:border-l-yellow-900 dark:bg-yellow-700 dark:text-white/90">
    <p>Keep in mind that the config file is restored after a reboot or a restart of the Mattermost instance. This is good because you don't need to remember to change this setting back, but you also need to make sure to upload the data before restarting the server.</p>

</div>

<p>The next step is to upload the zip file to Mattermost. Although this seems counterintuitive at first glance, it’s a necessary step to make the backup available for the import command itself.<br />
Execute this command with the correct path and filename to your previously <code>rsync</code>ed file.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">import</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">upload</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">exported-file.zip</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #FFCB6B;">---</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">Output</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #FFCB6B;">Upload</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">session</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">successfully</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">created,</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ID:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">903p9ttsslrfpryorde2ah25bc</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #FFCB6B;">Import</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">file</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">successfully</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">uploaded,</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">name:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">qjtchzygoalp5whzf4p4ah36nb</span></div></code></pre>
<p>Depending on the size of the file this could take a while.</p>
<p>Next, run this command to see all available imports. This will return the filename for the following command.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">import</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">list</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">available</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #FFCB6B;">---</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">Output</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">903p9ttsslrfpryorde2ah25bc_qjtchzygoalp5whzf4p4ah36nb_export.zip</span></div></code></pre>
<p>Copy the filename of the file.<br />
This is the part where your exported data will actually be restored into your new Mattermost instance:</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">import</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">process</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span><span style="color: #FFCB6B;">filename_from_previous_command</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>This will take a little while longer than the upload because all of your data is now restored. The job is running in the background but you can check on it with the following command:</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mmctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">import</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">job</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">show</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span><span style="color: #FFCB6B;">id</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>After the job has finished all your data is restored. Because this only restores you have one last step left for a complete migration.</p>
<h3><a id="content-5-migrate-settings-and-external-integrations" href="#content-5-migrate-settings-and-external-integrations" class="prezet-heading" title="Permalink">#</a>5. Migrate settings and external integrations</h3>
<p>There is one problem with this method: since it’s not a full database backup, it won’t restore your settings and integrations.<br />
So you will have to manually migrate / setup these things to match your old instance. If you setup new integrations keep in mind that things like webhook urls and API tokens are regenerated, so you need to swap them out in your 3rd-party tools, also.</p>
<p>One last thing I’ve noticed: user images aren’t migrated, too. But this should be manageable :) It gives your users the chance to finally upload a current picture of themselves.</p>
]]>
            </summary>
                                    <updated>2022-11-20T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Create a Mattermost bot that can message specific users]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/mattermost-bot-to-message-users" />
            <id>https://tim-kleyersburg.de/mattermost-bot-to-message-users</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>You can use a Mattermost bot and the Mattermost API to automate writing messages to your users. I couldn’t find any tutorials how to make it seem like the bot messaged you or one of your users. If you want to achieve exactly that, this article is for you.</p>
<h2><a id="content-create-a-bot-integration" href="#content-create-a-bot-integration" class="prezet-heading" title="Permalink">#</a>Create a bot integration</h2>
<p>As an admin, select the grid icon in the top left of your Mattermost instance and select “Integrations”.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./integrations-480w.png 480w, /articles/img/./integrations-640w.png 640w, /articles/img/./integrations-768w.png 768w, /articles/img/./integrations-960w.png 960w, /articles/img/./integrations-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/integrations.png" alt="The grid icon dropdown in a Mattermost app" />
<figcaption class="prezet-figcaption">The grid icon dropdown in a Mattermost app</figcaption>
</figure>
<hr />
<p>You will have a couple of options to choose from:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./integrations-bot-account-480w.png 480w, /articles/img/./integrations-bot-account-640w.png 640w, /articles/img/./integrations-bot-account-768w.png 768w, /articles/img/./integrations-bot-account-960w.png 960w, /articles/img/./integrations-bot-account-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/integrations-bot-account.png" alt="The integrations menu of Mattermost" />
<figcaption class="prezet-figcaption">The integrations menu of Mattermost</figcaption>
</figure>
<p>You’ll want to select “Bot Accounts” and add a new bot account by clicking on “Add Bot Account”. Next, you will have to fill out a form. If you need more information about each of these fields, or bot accounts in general, I highly recommend the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.mattermost.com/integrations/cloud-bot-accounts.html">official documentation</a>.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./mattermost-bot-form-480w.png 480w, /articles/img/./mattermost-bot-form-640w.png 640w, /articles/img/./mattermost-bot-form-768w.png 768w, /articles/img/./mattermost-bot-form-960w.png 960w, /articles/img/./mattermost-bot-form-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/mattermost-bot-form.png" alt="A form you have to fill out to create a Mattermost bot account" />
<figcaption class="prezet-figcaption">A form you have to fill out to create a Mattermost bot account</figcaption>
</figure>
<p>After saving the form you will receive the bot token. Copy and save this someplace safe as you will not see this again. You can always generate a new token but will have to replace the token everywhere you have used it.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./mattermost-bot-token-480w.png 480w, /articles/img/./mattermost-bot-token-640w.png 640w, /articles/img/./mattermost-bot-token-768w.png 768w, /articles/img/./mattermost-bot-token-960w.png 960w, /articles/img/./mattermost-bot-token-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/mattermost-bot-token.png" alt="The success message includes your bots token" />
<figcaption class="prezet-figcaption">The success message includes your bots token</figcaption>
</figure>
<hr />
<p>Now to the fun part: sending messages with your shiny new bot! We will use Mattermost’s <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://api.mattermost.com/">REST API</a> to achieve this.</p>
<p>To access the API you will need the token you got in the previous step. Every request should send the following header, also known as Bearer or Token authentication: <code>Authorization: Bearer your-token</code>.<br />
The base url for the API at the time of writing this article is:<br />
<code>https://your-mattermost-url.com/api/v4</code>.</p>
<p>To send a message from a bot to a user you will now have to make two API requests: one that gives you the correct channel id and another one which actually sends the message.</p>
<h3><a id="content-getting-the-bot-and-the-user-id" href="#content-getting-the-bot-and-the-user-id" class="prezet-heading" title="Permalink">#</a>Getting the bot and the user id</h3>
<p>Writing a message from a bot to a user basically means that you send a message to the private channel shared by these two entities. Every channel in Mattermost has its own unique id. And direct messages are nothing else as a private channel between two people.</p>
<p>If we want to get the channel id for the combination of the bot and the user we will need their own entity ids to query the API for the correct channel id.</p>
<p>Getting the user id is simple: you can just open the System Console, and click on “Users” in the submenu “User Management”. The user id is directly below the name of your users.</p>
<p>But a bot is no normal user! So the user list unfortunately doesn’t show the bot account. But you can use the bot token to use the endpoint <span class="text-indigo-600 dark:text-indigo-300 font-semibold">GET</span>  <code>/users</code> to retrieve all users. You can then filter down the result to the username of your bot.<br />
Alternatively, if you have a lot of users, use the endpoint <span class="text-green-600 dark:text-green-300 font-semibold">POST</span>  <code>/users/search</code>.</p>
<p>Use this JSON payload to search for your bot:</p>
<pre><code data-theme="material-theme-palenight" data-lang="json" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">term</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">name-of-your-bot</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Great! Now that you have the id of your bot and your user we can move on to the next step.</p>
<h3><a id="content-getting-the-direct-channel-id" href="#content-getting-the-direct-channel-id" class="prezet-heading" title="Permalink">#</a>Getting the direct channel id</h3>
<p>Use the endpoint <span class="text-green-600 dark:text-green-300 font-semibold">POST</span> <code>/channels/direct</code> to get the correct channel id to send our message to.</p>
<p>Using the obtained bot and user id we can now send the following JSON payload to the named endpoint.</p>
<pre><code data-theme="material-theme-palenight" data-lang="json" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">[</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">bot-id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">user-id</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #89DDFF;">]</span></div></code></pre>
<p><strong>Response</strong></p>
<pre><code data-theme="material-theme-palenight" data-lang="json" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">  </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">this-is-the-channel-id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">  </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">name</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">first-user-id__second-user-id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">  ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Note down this id, that is the unique identifier of the private channel between the bot and the specified user.</p>
<h3><a id="content-sending-a-message" href="#content-sending-a-message" class="prezet-heading" title="Permalink">#</a>Sending a message</h3>
<p>Now we have all we need to finally send a message.</p>
<p>Lets now use the endpoint <span class="text-green-600 dark:text-green-300 font-semibold">POST</span> <code>/posts</code> to create a new message.</p>
<p>The minimal payload for this looks like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="json" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">channel_id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">this-is-the-channel-id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">message</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">Very thoughtful message.</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>If everything worked you will get a status <code>201 Created</code> back.</p>
<h3><a id="content-real-life-example" href="#content-real-life-example" class="prezet-heading" title="Permalink">#</a>Real life example</h3>
<p>At my work we are using such an integration to make users aware of problems with their project planning. There is a cronjob running every 15 minutes which checks the tasks of every user. If it detects new problems we can use the associated mail address to find the user, find the private channel of the bot and the user, and send them some meaningful message and a link to the planning tool to show what actual problems occurred.</p>
]]>
            </summary>
                                    <updated>2022-08-31T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Use ImageMagicks mogrify CLI to batch resize images]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/resize-images-with-mogrify" />
            <id>https://tim-kleyersburg.de/resize-images-with-mogrify</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When working with an ecommerce client, I had the requirement to display images in uniform boxes. This was way before we had things like <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://caniuse.com/object-fit">object-fit</a>. The big problem was: the images the client provided where far from uniform. Some images were landscape oriented, others in portrait, some were squared. They also differed in file size and type, some being over 5MB in size, others the opposite.</p>
<p>I wanted a quick way to resize all these images to one output format, defined as the following:</p>
<ul>
<li>end image should be 500 x 500 pixels</li>
<li>background should always be white</li>
<li>images should have a slight border of 15 pixels</li>
<li>images should be centered</li>
</ul>
<p>For this task I used the free software <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://imagemagick.org/">ImageMagick</a>, specifically its CLI tool <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://imagemagick.org/script/mogrify.php">mogrify</a></p>
<p>On Mac you can use <code>brew</code> to install the CLI with <code>brew install imagemagick</code>.</p>
<p>Our command will look like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">mogrify</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-path</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">processed</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-trim</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-resize</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">470x470&gt;</span><span style="color: #89DDFF;">&quot;</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-gravity</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">center</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">-extent</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">500x500</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-background</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">white</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-format</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">jpg</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">*</span><span style="color: #89DDFF;">&quot;</span></div></code></pre>
<p>Let’s break this command down bit by bit:</p>
<p><code>-path</code>: Path where the image should be put out (“processed” in this case)
<code>-trim</code>: Cut the image on all sides so not whitepsace is left (if the image sits on white or transparent background)
<code>-resize</code>: width x height, <code>&gt;</code> means the longest side will be used for resizing, the other side will be resized proportionally
<code>-gravity</code>: Parameter order is important as this affects the next parameter (extent). Defines how extent works
<code>-extent</code>: Extends the image to 500 x 500 pixels, our final format. Since <code>gravity</code> is set to <code>center</code> it gets extended equally on all 4 sides.
<code>-background</code>: Define a background color
<code>-format</code>: Defines the format
<code>&quot;*&quot;</code>: Double quotes ensure that the wildcard glob is not expanded by the shell, but mogrify itself.</p>
<p>If you now run this command in a folder with all your images you will have perfectly square images which can be directly used without any CSS tricks.</p>
]]>
            </summary>
                                    <updated>2022-08-09T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How to build a simple JSON API with Netlify functions]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/simple-json-api-with-netlify" />
            <id>https://tim-kleyersburg.de/simple-json-api-with-netlify</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>I recently built <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://deine-mutter.timkley.dev/">deine-mutter.timkley.dev</a>, a small project which returns a random “your mom” joke. You can use an API like this for all kinds of things. For example, I used <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://pipedream.com/https://pipedream.com/">pipedream.com</a> to create a weekly workflow which sends a random joke to my teammate. You know, keeping it professional.</p>
<p>Maybe you also need a small API like this and want to get a better grasp how to do this. In this article we’ll use <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.netlify.com/functions/overview/">Netlify functions</a> to write a simple, globally accessible API that returns some arbitrary value.</p>
<h2><a id="content-first-steps" href="#content-first-steps" class="prezet-heading" title="Permalink">#</a>First steps</h2>
<p>I’ll assume you already have an account with Netlify and maybe already published something with it. If not, I recommend the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.netlify.com/welcome/add-new-site/">“Add Site“</a> section of Netlify’s documentation.</p>
<p>Next, we’ll install the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.netlify.com/cli/get-started/">Netlify CLI</a>. Although not a hard requirement, I highly recommend installing it, because this will allow you to test your functions locally.</p>
<p>Make sure you have Node installed and run the following command:</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">npm</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">install</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">netlify-cli</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-g</span></div></code></pre>
<p>This installs the Netlify CLI globally and makes the <code>netlify</code> command available to you.</p>
<p>Next, authenticate yourself using the command <code>netlify login</code> and then we are ready to write our first function!</p>
<h3><a id="content-simple-hello-world-function" href="#content-simple-hello-world-function" class="prezet-heading" title="Permalink">#</a>Simple <code>Hello World!</code> function</h3>
<p>Create a new folder for this project and inside it put a file into the folder <code>netlify/functions</code> called <code>hello-world.js</code>. Netlify will look into this folder by default for functions to deploy. After deployment these function will be available at <code>your-domain.com/.netlify/functions/name-of-your-file-without-extension</code>.</p>
<p>Your folder structure should now look like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">hello-world</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">└──</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">netlify</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #FFCB6B;">└──</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">functions</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">        </span><span style="color: #FFCB6B;">└──</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">hello-world.js</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #F78C6C;">2</span><span style="color: #A6ACCD;"> directories, </span><span style="color: #F78C6C;">1</span><span style="color: #A6ACCD;"> file</span></div></code></pre>
<p>From the <code>hello-world</code> directory run the following command:<br />
<code>netlify functions:serve</code>. This will locally serve your functions and the output should look like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;"> ~/Code/hello-world ▶ netlify functions:serve</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">◈ Loaded function hello-world.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">◈ Functions server is listening on 9999</span></div></code></pre>
<p>The local server is now available on http://localhost:9999.</p>
<div class="not-prose rounded-md border-l-4 border-l-orange-300 bg-yellow-50 px-3 py-2 leading-snug dark:border-l-yellow-900 dark:bg-yellow-700 dark:text-white/90">
    <p>Pro-Tip: You can Cmd+Click on the function name to directly open the browser.</p>

</div>

<p>If you open the function in the browser you will see an error:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./function-error-480w.avif 480w, /articles/img/./function-error-640w.avif 640w, /articles/img/./function-error-768w.avif 768w, /articles/img/./function-error-960w.avif 960w, /articles/img/./function-error-1536w.avif 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/function-error.avif" alt="An empty file will throw an Invocation Failed error" />
<figcaption class="prezet-figcaption">An empty file will throw an Invocation Failed error</figcaption>
</figure>
<p>This is because our file is still empty and no function was exported. Every file must export an async <code>handler</code> function, like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">exports.</span><span style="color: #82AAFF;">handler</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> context</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>The function accepts two parameters: <code>event</code> and <code>context</code>. The <code>event</code> parameter is an object which contains information about things like which http-method was used, the query-string, etc.<br />
You can read more about the specific contents <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.netlify.com/functions/build-with-javascript/#synchronous-function-format">in the Netlify docs</a>.</p>
<p>The function must return an object which is compatible with the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developer.mozilla.org/en-US/docs/Web/API/Response/Response">JavaScript Response Object</a>. So, your <code>hello-world</code> example could look like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">exports.</span><span style="color: #82AAFF;">handler</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> context</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #F07178;">        statusCode</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #F78C6C;">200</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #F07178;">        body</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Hello World!</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>If you now visit the URL http://localhost:9999/.netlify/functions/hello-world you should see <code>Hello World!</code> in your browser. If you pushed these changes to your repo now (and have already configured auto-deploy in Netlify) you could also access this publicly.</p>
<p>Now that you have a working function we can add some useful functionality.</p>
<h2><a id="content-random-quote-api" href="#content-random-quote-api" class="prezet-heading" title="Permalink">#</a>Random quote API</h2>
<p>Instead of jokes let’s return some Star Wars jokes. Rename your <code>hello-world.js</code> file to <code>star-wars.js</code> and add the following content:</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">exports.</span><span style="color: #82AAFF;">handler</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> context</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">quotes</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> [</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">I find your lack of faith disturbing.</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Do. Or do not. There is no try.</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">A long time ago in a galaxy far, far away...</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F07178;">    ]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">randomQuote</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">quotes</span><span style="color: #F07178;">[</span><span style="color: #A6ACCD;">Math</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">floor</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">Math</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">random</span><span style="color: #F07178;">() </span><span style="color: #89DDFF;">*</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">quotes</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">length</span><span style="color: #F07178;">)]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">JSON</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">stringify</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">{</span><span style="color: #F07178;"> quote</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">randomQuote</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #F07178;">        statusCode</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #F78C6C;">200</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #F07178;">        body</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #F07178;">        headers</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">Content-Type</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">application/json; charset=utf-8</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">Access-Control-Allow-Origin</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">*</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Let’s talk about what’s happening here:</p>
<p>First, we assign some quotes to an array and get a random quote by using <code>Math.random()</code>.</p>
<p>Next, we’ll use <code>JSON.stringify()</code> to transform an actual object to a string which we can send with the response.</p>
<p>Lastly, the response will be sent to the browser. There are some additional headers added:</p>
<p><code>Content-Type</code>: we set the Content-Type to JSON and set the charset to UTF-8 to make sure there are no encoding issues.</p>
<p><code>Access-Control-Allow-Origin</code>: This header is responsible for defining which origins are allowed to access this endpoint. Since we want our API to be accessible by anyone, we set this to the wildcard character <code>*</code>.</p>
<h2><a id="content-configuring-the-url-of-our-api" href="#content-configuring-the-url-of-our-api" class="prezet-heading" title="Permalink">#</a>Configuring the URL of our API</h2>
<p>The only downside now is: if you deploy this, the URL of that function will be <code>your-domain.com/.netlify/functions/star-wars</code>. If you are only using this function for yourself this might be acceptable. But if you want to provide a public facing endpoint you might want something a little more readable. So we will use the redirect feature to rewrite our endpoint URL.</p>
<p>To configure your redirects, create a <code>netlify.toml</code> file in the root of your project.</p>
<p>In it, you can configure redirects like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">[[redirects]]</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">from = &quot;/star-wars&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">to = &quot;/.netlify/functions/star-wars&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">status = 200</span></div></code></pre>
<p>Now you will be able to access the API using the URL <code>your-domain.com/star-wars</code>. We set the <code>status</code> to <code>200</code> on purpose. By default, Netlify sends a 301 status code for redirects. Since this is a rewrite we will send the HTTP status code 200.</p>
<p>That’s it! After deploying you should now be able to access your API by directly visiting your configured URL or by using it with, for example fetch.</p>
<p>You can always have a look at <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/deine-mutter">my repo</a> if you’re getting stuck.</p>
]]>
            </summary>
                                    <updated>2022-07-28T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Simple and GDPR compliant website analytics with Umami]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/simple-analytics-with-umami" />
            <id>https://tim-kleyersburg.de/simple-analytics-with-umami</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>When I first started writing my blog I used <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://getinsights.io/">getinsights.io</a> for analytics to see how much traffic my site got. Seeing the first visitors finding articles on search engines was very motivating for me.</p>
<p>Using Google Analytics was never an option for me. I don’t need half of its features and I didn’t want to jump through the GDPR hoops.</p>
<p>What I wasn’t happy about when using Insights was the interface and that it still used Google services (Firebase) under the hood. It was also hard to find where the company is seated which is a small red flag for me.<br />
At some point I discovered <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://umami.is/">Umami</a>. Umami is a self-hosted, privacy-focused analytics tool which I wanted to try for some time now.</p>
<p>I’ll describe the steps I took to setup Umami on a Digital Ocean droplet, using <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://doc.traefik.io/traefik/">Traefik</a> as a proxy, providing very easy routing and HTTPS.</p>
<h2><a id="content-first-things-first-setting-up-a-vps" href="#content-first-things-first-setting-up-a-vps" class="prezet-heading" title="Permalink">#</a>First things first: Setting up a VPS</h2>
<p>I’ll use <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://digitalocean.com">DigitalOcean</a> to create a small droplet. To make my life easier I’ll use the Docker preset with the following settings:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./digitalocean-droplet-480w.png 480w, /articles/img/./digitalocean-droplet-640w.png 640w, /articles/img/./digitalocean-droplet-768w.png 768w, /articles/img/./digitalocean-droplet-960w.png 960w, /articles/img/./digitalocean-droplet-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/digitalocean-droplet.png" alt="Configuration of the Digital ocean droplet" />
<figcaption class="prezet-figcaption">Configuration of the Digital ocean droplet</figcaption>
</figure>
<p>After a few minutes our droplet is ready and we can connect to it via SSH.</p>
<h2><a id="content-defining-the-setup" href="#content-defining-the-setup" class="prezet-heading" title="Permalink">#</a>Defining the setup</h2>
<p>Here’s a list of things I’ve wanted, so I could later reuse this droplet for other small docker services:</p>
<ul>
<li>Simple way of adding new services later on</li>
<li>No hassle with HTTPS certificate creation or renewal</li>
<li>Separation of services, if possible no duplication of “management” services (like Traefik)</li>
</ul>
<p>To achieve this I used two docker-compose files in this simple setup:</p>
<ul>
<li>One for the Traefik reverse proxy</li>
<li>Another one for Umami itself</li>
</ul>
<h2><a id="content-the-code" href="#content-the-code" class="prezet-heading" title="Permalink">#</a>The code</h2>
<h3><a id="content-directory-structure" href="#content-directory-structure" class="prezet-heading" title="Permalink">#</a>Directory structure</h3>
<p>After logging into the VPS I first created two directories. One for Traefik and one for Umami:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">├── traefik</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">└── umami</span></div></code></pre>
<h3><a id="content-setting-up-traefik" href="#content-setting-up-traefik" class="prezet-heading" title="Permalink">#</a>Setting up Traefik</h3>
<p>Next, we’ll start by setting up Traefik. For this, I followed <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.digitalocean.com/community/tutorials/how-to-use-traefik-v2-as-a-reverse-proxy-for-docker-containers-on-ubuntu-20-04">this tutorial</a> from DigitalOcean to get started. I’d recommend you first read the things I’ve done differently before following that tutorial so you can decide for yourself which way you like better.</p>
<p>Instead of using a long docker command I’ve chosen to use a docker-compose file instead. That way I don’t have remember a long-ass command.</p>
<p>Here is the content of that file:</p>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #F07178;">version</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">3</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">services</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">reverse-proxy</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">image</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">traefik:v2.8</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">ports</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #89DDFF;">            </span><span style="color: #676E95;"># The HTTP port</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">80:80</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">443:443</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">volumes</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #89DDFF;">            </span><span style="color: #676E95;"># So that Traefik can listen to the Docker events</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">/var/run/docker.sock:/var/run/docker.sock:ro</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">./acme.json:/etc/traefik/acme.json</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">./traefik.toml:/etc/traefik/traefik.toml:ro</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">./traefik_dynamic.toml:/etc/traefik/traefik_dynamic.toml:ro</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">web</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">web</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">true</span></div></code></pre>
<p>Note that we’ve defined the network <code>web</code> in here. <code>external: true</code> means that this is a network managed by Docker itself and not generated while using the <code>up</code> command provided by docker-compose. To create this network run <code>docker network create web</code> before starting the service.</p>
<p>You also need to bind this network to your service so that Traefik will be able to communicate in this network.</p>
<p>If you’ve followed the linked tutorial above you can now run <code>docker-compose up -d</code>. Using the <code>-d</code> flag runs your containers detached in the background.</p>
<h2><a id="content-setting-up-umami" href="#content-setting-up-umami" class="prezet-heading" title="Permalink">#</a>Setting up Umami</h2>
<p>Now onto setting up Umami. Fortunately most of the work is already done because Umami provides a <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/umami-software/umami/blob/c5d775ce721d178af6d2ab2b959d245cb0457fdb/docker-compose.yml">docker-compose file</a> for us. But we will need to make a few changes so Umami can work correctly with Traefik.</p>
<p>Switch into the previously created <code>umami</code> folder on your VPS and create the following file, naming it <code>docker-compose.yml</code>:</p>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #F07178;">version</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">3</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">services</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">umami</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">image</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">ghcr.io/mikecao/umami:postgresql-latest</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">labels</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.routers.umami.rule=Host(`your-domain.com`)</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.routers.umami.tls=true</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.routers.umami.tls.certresolver=lets-encrypt</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">environment</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">            </span><span style="color: #F07178;">DATABASE_URL</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">postgresql://umami:umami@db:5432/umami</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">            </span><span style="color: #F07178;">DATABASE_TYPE</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">postgresql</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">            </span><span style="color: #F07178;">HASH_SALT</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">change-this-to-a-random-string</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">            </span><span style="color: #F07178;">TRACKER_SCRIPT_NAME</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">protocol</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">depends_on</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">db</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">restart</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">always</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">backend</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">web</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">db</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">image</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">postgres:12-alpine</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">environment</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #A6ACCD;">            </span><span style="color: #F07178;">POSTGRES_DB</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">umami</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">            </span><span style="color: #F07178;">POSTGRES_USER</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">umami</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #A6ACCD;">            </span><span style="color: #F07178;">POSTGRES_PASSWORD</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">umami</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">volumes</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">./sql/schema.postgresql.sql:/docker-entrypoint-initdb.d/schema.postgresql.sql:ro</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">umami-db-data:/var/lib/postgresql/data</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">29</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">restart</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">always</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">30</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">31</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">backend</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">32</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">33</span><span style="color: #F07178;">volumes</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">34</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">umami-db-data</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">35</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">36</span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">37</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">backend</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">38</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">false</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">39</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">web</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">40</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">true</span></div></code></pre>
<p>There are a few changes here in comparison to the original file. I’ll explain them in order of appearance and what you’ll have to change for it to work correctly for your own setup.</p>
<h3><a id="content-traefik-labels" href="#content-traefik-labels" class="prezet-heading" title="Permalink">#</a>Traefik Labels</h3>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #F07178;">labels</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.routers.umami.rule=Host(`your-domain.com`)</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.routers.umami.tls=true</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">traefik.http.routers.umami.tls.certresolver=lets-encrypt</span><span style="color: #89DDFF;">&#39;</span></div></code></pre>
<p>These labels will be read by Traefik to configure the router for Umami dynamically when its containers are started. The <code>umami</code> part in the beginning of the string creates a new router for Umami to use.</p>
<h4><code>traefik.http.routers.umami.rule=Host(your-domain.com)</code></h4>
<p>This label creates a <code>Host</code> rule. This means, that if the incoming traffic matches the host <code>your-domain.com</code> it will be forwarded to Umami. Please change this to the domain you plan to use and make sure you’ve added an A-record which is pointing to the IP address of your VPS.</p>
<h4><code>traefik.http.routers.umami.tls=true</code></h4>
<p>This label specifies that we want TLS activated.</p>
<h4><code>traefik.http.routers.umami.tls.certresolver=lets-encrypt</code></h4>
<p>And this label tells Traefik to resolve the certificate with Let’s Encrypt.</p>
<h3><a id="content-environment-values" href="#content-environment-values" class="prezet-heading" title="Permalink">#</a>Environment values</h3>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #F07178;">environment</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">HASH_SALT</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">change-this-to-a-random-string</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">TRACKER_SCRIPT_NAME</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">protocol</span></div></code></pre>
<p><code>DATABASE_URL</code> and <code>DATABASE_TYPE</code> haven’t changed because the defaults are fine in this scenario.</p>
<p><code>HASH_SALT</code> is used to generate unique values, replace this with a random string.</p>
<p><code>TRACKER_SCRIPT_NAME</code> can be used to rename the tracker script. By default it is called <code>umami.js</code> which unfortunately is blocked by default by some adblockers or the privacy-focused browser <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://brave.com">Brave</a>. I renamed mine to <code>protocol</code> which seems to work fine.</p>
<h3><a id="content-networks" href="#content-networks" class="prezet-heading" title="Permalink">#</a>Networks</h3>
<p>This step is very important for everything to work correctly. If you’ve attempted setting up Traefik before and ran in 502 Bad Gateway or 504 Bad Gateway errors, this should help you.</p>
<p>Lets start with the root level network definitions:</p>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">backend</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">false</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">web</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">external</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">true</span></div></code></pre>
<p>Instead of just one network <code>web</code>, which we need so Traefik can route the web traffic to the Node server, we also have need a second network.</p>
<p>Normally docker-compose creates a default network for our services to communicate with each other. But since we defined the networks ourselves, this default network is not created. This unfortunately means that the Node server now has no way to communicate with the database service.</p>
<p>That’s why we defined <code>backend</code> as a second network. You can see that we set this one explicitly to <em>not</em> be external. This is the default but when I come back to this configuration in a few weeks or months I don’t have to guess the value.</p>
<p>In addition to defining these networks we also have to configure which services use these networks. In our case, we have two services: <code>umami</code> is the service that is running the Node server and therefore responsible for serving the dashboard and tracking script.<br />
And <code>db</code> runs the database server.</p>
<p><code>umami</code> needs to join the <code>web</code> and <code>backend</code> network. <code>db</code> only needs <code>backend</code>.</p>
<p>Our network diagram now looks like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #F07178;">traefik</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">reverse-proxy</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">web</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F07178;">umami</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">umami</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">web</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">backend</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">db</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">networks</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">backend</span></div></code></pre>
<p>This ensures that Traefik can route traffic to the Umami frontend and also, that Umami can communicate internally with its database.</p>
<p>Since the <code>backend</code> network is an internal network this also ensures that the database service is not reachable publicly which is good for security.</p>
<h2><a id="content-all-together-now" href="#content-all-together-now" class="prezet-heading" title="Permalink">#</a>All together now!</h2>
<p>You can now run <code>docker-compose up -d</code> from the directory the docker-compose file for Traefik is located as well as for Umami. After a few seconds you should be able to visit the Umami dashboard on the domain you’ve connected.</p>
<p>Once again many thanks to <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/pitkley">my brother</a> for explaining new things to me ❤️.</p>
]]>
            </summary>
                                    <updated>2022-07-11T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Create long time statistics from Home Assistant data]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/longtime-statistics-home-assistant" />
            <id>https://tim-kleyersburg.de/longtime-statistics-home-assistant</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>This article is for you, if:</p>
<ul>
<li>you want to visualise Home Assistant statistics over a longer period than the dashboard allows</li>
<li>you want to know more about how the database of Home Assistant works generally</li>
</ul>
<p>You’ll learn the following:</p>
<ul>
<li>How the default database of Home Assistant works</li>
<li>How to connect to the database and run simple queries</li>
<li>Use a little more complex SQL to generate daily data</li>
<li>How to generate a chart using a free, open-source tool</li>
</ul>
<p>If you want to skip the basics you can jump to <a href="#querying-and-exporting-data">Querying and exporting data</a>.</p>
<h2><a id="content-home-assistant-database" href="#content-home-assistant-database" class="prezet-heading" title="Permalink">#</a>Home Assistant database</h2>
<p>If you did not change any settings, Home Assistant uses an <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.sqlite.org/index.html">SQLite</a> database, stored in your configuration directory.</p>
<p>There are a few tables which Home Assistant uses to record all things that happened in your smart home. Because it is using an event driven approach you can basically see everything that has happened since you first installed Home Assistant.</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.home-assistant.io/docs/backend/database/">Official documentation</a></p>
<h2><a id="content-connecting-to-the-database" href="#content-connecting-to-the-database" class="prezet-heading" title="Permalink">#</a>Connecting to the database</h2>
<div class="not-prose rounded-md border-l-4 border-l-orange-300 bg-yellow-50 px-3 py-2 leading-snug dark:border-l-yellow-900 dark:bg-yellow-700 dark:text-white/90">
    <p>To follow the next steps you need to have SSH access to the machine running Home Assistant.</p>

</div>

<p>I’m using <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://tableplus.com/">TablePlus</a> for Mac to connect to the database. If you are on Windows a popular alternativ is <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.heidisql.com/">HeidiSQL</a>, which also supports SQLite.</p>
<p>To connect to the database, create a connection that looks like the following:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./tableplus-connection-details-480w.png 480w, /articles/img/./tableplus-connection-details-640w.png 640w, /articles/img/./tableplus-connection-details-768w.png 768w, /articles/img/./tableplus-connection-details-960w.png 960w, /articles/img/./tableplus-connection-details-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/tableplus-connection-details.png" alt="Screenshot of the connection details in TablePlus" />
<figcaption class="prezet-figcaption">Screenshot of the connection details in TablePlus</figcaption>
</figure>
<p>The important bits are:</p>
<p><strong>Over SSH</strong>: you will connect the database using an SSH tunnel. This means that TablePlus will first connect to the machine with SSH before connecting to the database itself.</p>
<p><strong>Database path</strong>: Enter the absolute path to the database file which is used by Home Assistant. Most likely this will be a path like this:<br />
<code>/home/pi/path-to-configuration-folder-of-home-assistant/home-assistant_v2.db</code></p>
<p>Click on “Test” to make sure you’ve filled out everything correctly and click “Connect” to drop into the database.</p>
<h2><a id="content-understanding-the-structure" href="#content-understanding-the-structure" class="prezet-heading" title="Permalink">#</a>Understanding the structure</h2>
<p>Compared to many other systems, Home Assistant doesn’t have that many tables to provide all the features it does.</p>
<p>In part, this comes from a good database architecture, but also from making use of JSON fields to store additional data, which would otherwise need many additional fields in the tables.</p>
<p>These are the tables Home Assistant uses, as of version <code>2022.6.4</code>.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./home-assistant-tables-480w.png 480w, /articles/img/./home-assistant-tables-640w.png 640w, /articles/img/./home-assistant-tables-768w.png 768w, /articles/img/./home-assistant-tables-960w.png 960w, /articles/img/./home-assistant-tables-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/home-assistant-tables.png" alt="Screenshot of the tables used by Home Assistant" />
<figcaption class="prezet-figcaption">Screenshot of the tables used by Home Assistant</figcaption>
</figure>
<p>I won’t explain every table, we’ll focus on States, Events and Statistics.</p>
<h3><a id="content-events-and-eventdata" href="#content-events-and-eventdata" class="prezet-heading" title="Permalink">#</a><code>events</code> and <code>event_data</code></h3>
<p>Events are things that can happen in your Home Assistant installation. This includes system things like the loading of a component or when an automation was triggered. The <code>events</code> table only includes metadata like the event type, when the event was triggered and a few more fields. But most importantly it includes a foreign key <code>data_id</code> which references the <code>event_data</code> table.</p>
<p>The <code>event_data</code> uses a JSON field which contains all data the event contained. If, for example, you turn a light on from the dashboard, behind the scenes a <code>call_service</code> event is fired. The event data contains data like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="json" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">domain</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">light</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">service</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">turn_on</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C792EA;">service_data</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">&quot;</span><span style="color: #FFCB6B;">entity_id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">light.nightdesk</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">&quot;</span><span style="color: #FFCB6B;">brightness_pct</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">55</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span><span style="color: #89DDFF;">}</span></div></code></pre>
<h3><a id="content-states-and-stateattributes" href="#content-states-and-stateattributes" class="prezet-heading" title="Permalink">#</a><code>states</code> and <code>state_attributes</code></h3>
<p>While events not necessarily need to have a relation to a specific entity, states do. States describe the concrete state an entity can have, like how bright your light is.</p>
<p>Like with events the <code>states</code> table contains metadata and <code>state_attributes</code> more details about the state which was changed.</p>
<h3><a id="content-statistics" href="#content-statistics" class="prezet-heading" title="Permalink">#</a><code>statistics</code></h3>
<p>This table contains statistical data about sensors. It references the specific sensor ID with the foreign key <code>metadata_id</code>. If you take a look into <code>statistics_meta</code> you’ll can find the numeric ID for your specific sensor. You’ll need this in the next step.</p>
<h2><a id="content-querying-and-exporting-data" href="#content-querying-and-exporting-data" class="prezet-heading" title="Permalink">#</a>Querying and exporting data</h2>
<p>Now lets assume we want to get all statistics about a speficic sensor. This is the query we would write:</p>
<pre><code data-theme="material-theme-palenight" data-lang="sql" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #F78C6C;">SELECT</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">*</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #F78C6C;">FROM</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">statistics</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #F78C6C;">WHERE</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">metadata_id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> id</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #F78C6C;">ORDER BY</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">id</span><span style="color: #89DDFF;">&quot;</span></div></code></pre>
<p>Make sure to replace <code>id</code> with the correct ID of the sensor you want to query.</p>
<p>Since the statistics platform writes to this table every hour, depending on the age of your sensor, you will get a lot of rows back. My sensor is about 2 months old, which resulted in 1.600 rows of data. Since I wanted to visualize how a sensors internal battery declined over the period of multiple weeks one data point per day will be enough:</p>
<pre><code data-theme="material-theme-palenight" data-lang="sql" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #F78C6C;">SELECT</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">date</span><span style="color: #A6ACCD;">(created), mean</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #676E95;">/* using the date function we can strip the time from the created field */</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #F78C6C;">FROM</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">statistics</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #F78C6C;">WHERE</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">metadata_id</span><span style="color: #89DDFF;">&quot;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> id</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #F78C6C;">GROUP BY</span><span style="color: #A6ACCD;"> </span><span style="color: #F78C6C;">date</span><span style="color: #A6ACCD;">(created)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #676E95;">/* also grouping by only the date ensures only one row per day */</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #F78C6C;">ORDER BY</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">id</span><span style="color: #89DDFF;">&quot;</span></div></code></pre>
<p>The result should look like this:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./grouped-result-480w.png 480w, /articles/img/./grouped-result-640w.png 640w, /articles/img/./grouped-result-768w.png 768w, /articles/img/./grouped-result-960w.png 960w, /articles/img/./grouped-result-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/grouped-result.png" alt="Screenshot of the statistical values after grouping them by day" />
<figcaption class="prezet-figcaption">Screenshot of the statistical values after grouping them by day</figcaption>
</figure>
<p>Next, export the data as a csv file.</p>
<h2><a id="content-visualising-data-with-rawgraphs" href="#content-visualising-data-with-rawgraphs" class="prezet-heading" title="Permalink">#</a>Visualising data with RAWGraphs</h2>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.rawgraphs.io/">RAWGraphs</a> is a great online tool to quickly visualise data.</p>
<p>After uploading your recently created csv file, RAWGraphs gives you a lot of options for correctly parsing your data. It is pretty smart about finding the correct settings for you. If you want to visualise data over periods of time make sure to format the date column as date:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./rawgraphs-data-settings-480w.png 480w, /articles/img/./rawgraphs-data-settings-640w.png 640w, /articles/img/./rawgraphs-data-settings-768w.png 768w, /articles/img/./rawgraphs-data-settings-960w.png 960w, /articles/img/./rawgraphs-data-settings-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/rawgraphs-data-settings.png" alt="Screenshot of the RAWGraphs data formatting settings" />
<figcaption class="prezet-figcaption">Screenshot of the RAWGraphs data formatting settings</figcaption>
</figure>
<p>In the next step, you have to choose a chart type. For the purpose of visualising the decline of a battery I chose the line chart.</p>
<p>Now you need to define the dimension mapping. Drag the date column into the x-axis and the mean column into the y-axis:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./chart-variables-480w.png 480w, /articles/img/./chart-variables-640w.png 640w, /articles/img/./chart-variables-768w.png 768w, /articles/img/./chart-variables-960w.png 960w, /articles/img/./chart-variables-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/chart-variables.png" alt="Screenshot of RAWGraphs chart variables" />
<figcaption class="prezet-figcaption">Screenshot of RAWGraphs chart variables</figcaption>
</figure>
<p>With these settings, you should now get an auto-generated chart like this:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./final-chart-480w.png 480w, /articles/img/./final-chart-640w.png 640w, /articles/img/./final-chart-768w.png 768w, /articles/img/./final-chart-960w.png 960w, /articles/img/./final-chart-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/final-chart.png" alt="Screenshot of the final chart" />
<figcaption class="prezet-figcaption">Screenshot of the final chart</figcaption>
</figure>
]]>
            </summary>
                                    <updated>2022-06-18T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How to update your docker-containers running Home Assistant]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/updating-home-assistant-with-docker" />
            <id>https://tim-kleyersburg.de/updating-home-assistant-with-docker</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>From time to time you’ll want to update your Home Assistant instance you’ve <a href="/articles/home-assistant-with-docker-2022/">previously set up</a> to the latest version.</p>
<p>Using docker compose this is really simple, you just have to run two simple commands and need a little patience. If you’ve used <a href="/articles/home-assistant-with-docker-2022/">my guide</a> to run Home Assistant this works with nearly no downtime.</p>
<h2><a id="content-how-to-update-your-containers" href="#content-how-to-update-your-containers" class="prezet-heading" title="Permalink">#</a>How to update your containers</h2>
<p>SSH into the server running your Home Assistant instance and navigate to the folder where you’ve saved the <code>docker-compose.yml</code> file to.</p>
<h3><a id="content-updating-the-docker-images" href="#content-updating-the-docker-images" class="prezet-heading" title="Permalink">#</a>Updating the docker images</h3>
<p>Run the following command:</p>
<pre><code data-theme="material-theme-palenight" data-lang="sh" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">docker-compose</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">pull</span></div></code></pre>
<p>This will pull the latest images used in your <code>docker-compose.yml</code> file.<br />
While you wait, why don’t you read <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.docker.com/compose/reference/pull/">the official documentation</a>? 🙂</p>
<div class="not-prose rounded-md border-l-4 border-l-orange-300 bg-yellow-50 px-3 py-2 leading-snug dark:border-l-yellow-900 dark:bg-yellow-700 dark:text-white/90">
    <p>This will <em>not</em> interrupt the running containers just yet, so your Home Assistant instance is still available through this process.</p>

</div>

<h3><a id="content-recreate-the-home-assistant-instance" href="#content-recreate-the-home-assistant-instance" class="prezet-heading" title="Permalink">#</a>Recreate the Home Assistant instance</h3>
<p>When this process is finished you can now recreate the containers running the same command as for the first time you started your containers:</p>
<pre><code data-theme="material-theme-palenight" data-lang="sh" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">docker-compose</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">up</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-d</span></div></code></pre>
<p>This will recreate the containers with the newest images that got pulled before.</p>
<p>If you’ve done everything like described above, the output should look something like this:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./update-commands-480w.jpg 480w, /articles/img/./update-commands-640w.jpg 640w, /articles/img/./update-commands-768w.jpg 768w, /articles/img/./update-commands-960w.jpg 960w, /articles/img/./update-commands-1536w.jpg 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/update-commands.jpg" alt="Screenshot of the commands outputted in a terminal window" />
<figcaption class="prezet-figcaption">Screenshot of the commands outputted in a terminal window</figcaption>
</figure>
<p>Now revisit your Home Assistant dashboard and enjoy the newest version 😎</p>
]]>
            </summary>
                                    <updated>2022-06-06T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Alpine Expression Error: Cannot set properties of null (setting _x_dataStack)]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/alpine-expression-error-x-datastack" />
            <id>https://tim-kleyersburg.de/alpine-expression-error-x-datastack</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./alpine-expression-error-480w.png 480w, /articles/img/./alpine-expression-error-640w.png 640w, /articles/img/./alpine-expression-error-768w.png 768w, /articles/img/./alpine-expression-error-960w.png 960w, /articles/img/./alpine-expression-error-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/alpine-expression-error.png" alt="Screenshot of a console showing a warning about an Alpine expression error" />
<figcaption class="prezet-figcaption">Screenshot of a console showing a warning about an Alpine expression error</figcaption>
</figure>
<p>If you ran into the above error before, one of the most common causes is forgetting to define exactly one root element.</p>
<p>So, if your code looks like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="html" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">template</span><span style="color: #89DDFF;"> </span><span style="color: #C792EA;">x-if</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">loading</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">&gt;</span><span style="color: #A6ACCD;"> Loading... </span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">template</span><span style="color: #89DDFF;">&gt;</span></div></code></pre>
<p>You need to change it to this (instead of a <code>div</code> you may use any other valid HTML element):</p>
<pre><code data-theme="material-theme-palenight" data-lang="html" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">template</span><span style="color: #89DDFF;"> </span><span style="color: #C792EA;">x-if</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">loading</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">div</span><span style="color: #89DDFF;">&gt;</span><span style="color: #A6ACCD;">Loading...</span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">div</span><span style="color: #89DDFF;">&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">template</span><span style="color: #89DDFF;">&gt;</span></div></code></pre>
<p>As stated in the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://alpinejs.dev/directives/if">Alpine.js docs</a>, <code>template</code> tags may only contain one root element, and text in itself does not qualify as an element.</p>
]]>
            </summary>
                                    <updated>2022-05-21T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[JavaScript: Custom Events explained]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/custom-events-in-javascript" />
            <id>https://tim-kleyersburg.de/custom-events-in-javascript</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>In every bigger project I’ve worked on (especially in e-commerce) there comes a time where you need to tie different parts of your JavaScript together. Maybe you want to track something when a user interacts with your site or does something of value (like adding a product to the cart). Or you need to trigger some behaviour in a totally different component.</p>
<p>The most straightforward approach is to just add the needed functionality to the code where the new behaviour should happen:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">addToCart</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">productId</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// add product to cart</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// code you&#39;ll often see in Google Tag Manager integrations</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">dataLayer</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">push</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #F07178;">        event</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">addToCart</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #89DDFF;">        </span><span style="color: #676E95;">// ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>If you are only doing this for one integration it might be fine. But imagine you need to do this for multiple services and things will become messy very quickly:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">addToCart</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">productId</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// add product to cart</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">dataLayer</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">push</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F07178;">        event</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">addToCart</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #89DDFF;">        </span><span style="color: #676E95;">// ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">otherIntegration</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">trackAddToCart</span><span style="color: #F07178;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">anotherServiceWhichNeedsMuchMoreWork</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">startTransaction</span><span style="color: #F07178;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">anotherServiceWhichNeedsMuchMoreWork</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">addProduct</span><span style="color: #F07178;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">anotherServiceWhichNeedsMuchMoreWork</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">sendTransaction</span><span style="color: #F07178;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>If you keep doing this your site <em>will</em> become harder to maintain because you can never be sure if you might be breaking functionality in some of the added parts.</p>
<p>Lets take a look at an alternative, very flexible approach using native JavaScript events.</p>
<h2><a id="content-what-are-events" href="#content-what-are-events" class="prezet-heading" title="Permalink">#</a>What are events?</h2>
<p>Basically events are things that can happen on your site. You can listen for these events to happen and act on them. Browsers implement a lot of events that cover every interaction you can have with a site.</p>
<h3><a id="content-built-in-events" href="#content-built-in-events" class="prezet-heading" title="Permalink">#</a>Built-in events</h3>
<p>These are the events like mouse clicks, taps, key presses and so on. If you want, paste the following snippet into the console of your browser dev tools. It’ll log every event that is happening to the console.</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">Object</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">keys</span><span style="color: #A6ACCD;">(window)</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">forEach</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">key</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">if</span><span style="color: #F07178;"> (</span><span style="color: #89DDFF;">/</span><span style="color: #C3E88D;">.</span><span style="color: #89DDFF;">/</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">test</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">key</span><span style="color: #F07178;">)) </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #F07178;">        </span><span style="color: #A6ACCD;">window</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">addEventListener</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">key</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">slice</span><span style="color: #F07178;">(</span><span style="color: #F78C6C;">2</span><span style="color: #F07178;">)</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">)</span><span style="color: #F07178;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #F07178;">            </span><span style="color: #A6ACCD;">console</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">log</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">key</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">event</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #89DDFF;">}</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span><span style="color: #676E95;">// ~~stolen~~ kindly borrowed from https://stackoverflow.com/a/61399370</span></div></code></pre>
<p>You probably added event listeners before to let something happen on events like a <code>click</code> by using <code>element.addEventListener('click')</code>.</p>
<p>You maybe know you can add built-in events to HTML elements by using the <code>onX</code> attributes, where <code>X</code> is substituted be the name of the event.</p>
<p>These attributes don’t depend on any additional JavaScript to work. When this attribute exists it will always work. Try adding <code>onclick=&quot;alert('Hello!')&quot;</code> to any element with your browsers dev tools, click on it and you will see the alert.</p>
<h3><a id="content-custom-events" href="#content-custom-events" class="prezet-heading" title="Permalink">#</a>Custom events</h3>
<p>Everthing which is not a standard interaction can be a custom event. In an e-commerce site this could be the add-to-cart functionality. Most likely this action will be triggered by a built-in event, like a click, but by using a custom event you gain a lot of flexibility because your not dependant on that click anymore.<br />
Custom events have to be dispatched manually when the specific event actually happens.</p>
<p>First, define your custom event. It needs a name, I’ll use <code>custom-event</code> in this case, and accepts an optional second object parameter which includes all properties a normal event can set as well as an additional <code>detail</code> property which can return details about your specific event.</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> customEvent </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">new</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">CustomEvent</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">custom-event</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span><span style="color: #A6ACCD;"> </span><span style="color: #F07178;">detail</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">your-details</span><span style="color: #89DDFF;">&#39;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">}</span><span style="color: #A6ACCD;">)</span></div></code></pre>
<p>Secondly, you’ll need to dispatch the event. Events can be dispatched from any <code>HTMLElement</code>. Depending on your use case you’ll either use a specific element or just use the window element:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">window</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">dispatchEvent</span><span style="color: #A6ACCD;">(customEvent)</span></div></code></pre>
<p>That’s all there is to it. Just define a new event, give it a name and an optional payload, dispatch it and you have a global custom event which you can now use in other parts of your site.</p>
<p>Lets rewrite our first example to something more maintainable:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">addToCart</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">productId</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// add product to cart</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">addToCartEvent</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">new</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">CustomEvent</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">add-to-cart</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #F07178;">        detail</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span><span style="color: #F07178;"> productId</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">productId</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">window</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">dispatchEvent</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">addToCartEvent</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">9</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>I like to use a little helper function for creating and dispatching global events for better readability.<br />
I also like the ability to pass the original event (this can be useful if you dispatch your custom event with a built-in attribute like <code>onsubmit</code> and want to have access to the original event).</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">customEvent</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">name</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> payload </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">null,</span><span style="color: #A6ACCD;"> originalEvent </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">null)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// options should be an object with:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// name: &#39;string&#39;,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// payload: &#39;object&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// originalEvent: &#39;this&#39;, if you need the actual event target</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">customEvent</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">new</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">CustomEvent</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">name</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">        detail</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">            payload</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">payload</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #F07178;">            originalEvent</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">originalEvent</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">window</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">dispatchEvent</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">customEvent</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Let’s now use our helper to dispatch the event:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">addToCart</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">productId</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// add product to cart</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #F07178;">    </span><span style="color: #82AAFF;">customEvent</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">add-to-cart</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span><span style="color: #F07178;"> productId</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">productId</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Instead of writing all the tracking and other logic, which has nothing to do with actually adding the product to the cart, we are now simply dispatching a custom event in one line.</p>
<p>This keeps our <code>addToCart</code> function small and clean but still puts us in a position to add more to it. If you are familiar with the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design">SOLID principles</a> of programming you maybe recognise the <code>S</code> (Single responsibility) and <code>O</code> (Open for extension, closed for modification) of this. By just raising an event we don’t break the single responsibility principle and at the same time allow extension because we can now react to the dispatched event without the need to modify our original code.</p>
<p>In another part of our code (maybe in a file called <code>tracking.js</code>) we can now write the following code:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">addToCartHandler</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// the handler will accept the event</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// you can access the payload from the `detail` property</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">productId</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">detail</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">productId</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// do your thing, like tracking or any other functionality</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #676E95;">// attach your custom event listener</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">window</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">addEventListener</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">add-to-cart</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> addToCartHandler)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #676E95;">// 🎉 done!</span></div></code></pre>
<p>You can attach as many handlers as you want and therefore append on your add-to-cart functionality as much as you need. If the need comes up to add another tracking library you can just attach another event listener and can leave your add-to-cart functionality completely unchanged, thus reducing the chance of introducing nasty bugs 🐛.</p>
<h2><a id="content-interoperability-between-frameworks" href="#content-interoperability-between-frameworks" class="prezet-heading" title="Permalink">#</a>Interoperability between frameworks</h2>
<p>Since custom events are native to the browser nothing prevents you from creating bridges between multiple frameworks.</p>
<p>You could, for example, listen for a custom event dispatched by a Vue component in your native javascript bundle or even a simple inline script tag. You just need to make sure you attach your event listeners <em>before</em> dispatching the events. Otherwise nothing will happen.</p>
<p>There is one particular use-case where we leveraged custom events within a Vue application: replacing HTML directly from a server request while maintaining interactivity.</p>
<p>We have an e-commerce application which uses Vue as its framework of choice, but uses renderless components. For the most part there are no backend APIs, so changing something like the price of an article based on the choice of a user means we’ll have to swap out some parts with server side rendered HTML.</p>
<p>As you may know, this breaks any Vue-based event handlers because they are destroyed with the swapped out HTML leaving you with dumb old HTML.</p>
<p>What we did is, we swapped out Vue specific handlers like <code>v-on:click</code> with built-in event attributes and use the helper function to dispatch a global event:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight has-add-lines has-remove-lines has-diff-lines' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #f07178;">&lt;button v-on.click=&quot;chooseOption({&#39;size&#39;: &#39;xl&#39;})&quot;&gt; </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #C3E88D;">&lt;button onclick=&quot;customEvent(&#39;choose-option&#39;, {&#39;size&#39;: &#39;xl&#39;})&quot;&gt; </span></div></code></pre>
<p>Quick recap on the <code>onX</code> attributes: you don’t have to attach any event listeners yourself, thus eliminating the need for some extra step which re-initializes event listeners after swapping out the HTML. In combination with a custom event that makes them perfect for this use-case because you can just dynamically add HTML with these attributes and they just work.</p>
<p>Another benefit is that you don’t need to wire up every piece of HTML with a dedicated click handler to make it do something. Instead it’ll just fire your custom event for which you defined your handlers beforehand.</p>
<p>So, within our relevant Vue component we can now do something like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #FFCB6B;">methods</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">    </span><span style="color: #82AAFF;">chooseOption</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">optionsObject</span><span style="color: #F07178;">) </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #89DDFF;">        </span><span style="color: #676E95;">// handle loading and swapping out the</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #89DDFF;">        </span><span style="color: #676E95;">// HTML of the product card</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #89DDFF;">        </span><span style="color: #676E95;">// ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #676E95;">// Use the `created` lifecycle hook to attach our event listener</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #82AAFF;">created</span><span style="color: #A6ACCD;">() </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// we&#39;ll attach an event listener for the name of your custom</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// event and use our existing method to defer to it</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">window</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">addEventListener</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">choose-option</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">event</span><span style="color: #F07178;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">this.</span><span style="color: #82AAFF;">chooseOption</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">detail</span><span style="color: #F07178;">))</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>I skipped all other parts, like checking for the correct product id and so on but I hope you’ll get the idea.</p>
<p>One thing that’s great about this approach is that by choosing to use the browsers API for events you are able to dispatch and listen for events from every part of your app as long as you make sure to always define your listeners before dispatching events.</p>
<p>Below you can find an interactive demonstration of how this works. It uses Vue for managing the cart state, AlpineJS for the logging of our events and you can add more buttons dynamically and see everything still works as expected.</p>
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="GRrVyZG" data-user="timkley" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/timkley/pen/GRrVyZG">
  Custom Events</a> by Tim (<a href="https://codepen.io/timkley">@timkley</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<h3><a id="content-resources" href="#content-resources" class="prezet-heading" title="Permalink">#</a>Resources</h3>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developer.mozilla.org/en-US/docs/Web/API/Event">Event - Web APIs | MDN</a><br />
<a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent">CustomEvent - Web APIs | MDN</a></p>
]]>
            </summary>
                                    <updated>2022-04-14T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Home Assistant with Docker on Raspberry Pi - the 2024 guide]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/home-assistant-with-docker-2024" />
            <id>https://tim-kleyersburg.de/home-assistant-with-docker-2024</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Update 2024-06-04: updated the guide for the “new” year.</p>
<p>Finally, a Raspberry Pi 4 with 8GB RAM came into stock that wasn’t outrageously priced.</p>
<p>Quick tip: remember where you’ve placed your Micro SD card from your last bricked Pi so you don’t have to wait for the delivery of a new one.</p>
<p>I’m assuming you have basic knowledge of Linux (for example, how to connect to another computer with SSH), so forgive me if I don’t explain everything down to the last detail. Don’t hesitate to message me on X if you have any queries 🙂.</p>
<h2><a id="content-the-guide" href="#content-the-guide" class="prezet-heading" title="Permalink">#</a>The guide</h2>
<p>We’ll follow these steps to get a working installation of Home Assistant running on your local network:</p>
<ol>
<li>Install Raspberry Pi OS with Raspberry Pi Imager.</li>
<li>Connect to and update your Raspberry Pi.</li>
<li>Install Docker.</li>
<li>Run Home Assistant as a Docker container.</li>
</ol>
<h3><a id="content-1-install-raspberry-pi-os-with-raspberry-pi-imager" href="#content-1-install-raspberry-pi-os-with-raspberry-pi-imager" class="prezet-heading" title="Permalink">#</a>1. Install Raspberry Pi OS with Raspberry Pi Imager</h3>
<p>While I’m not typically a fan of installers, preferring to understand what’s happening during each step, I highly recommend using the Imager tool for installing Raspberry Pi OS on your SD card in this instance. It saves time and prevents headaches during the initial configuration.</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.raspberrypi.com/software/">Download Raspberry Pi Imager</a> for your operating system.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./raspberry-pi-imager-home-screen-480w.jpg 480w, /articles/img/./raspberry-pi-imager-home-screen-640w.jpg 640w, /articles/img/./raspberry-pi-imager-home-screen-768w.jpg 768w, /articles/img/./raspberry-pi-imager-home-screen-960w.jpg 960w, /articles/img/./raspberry-pi-imager-home-screen-1536w.jpg 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/raspberry-pi-imager-home-screen.jpg" alt="Screenshot of the home screen of Raspberry Pi Imager" />
<figcaption class="prezet-figcaption">Screenshot of the home screen of Raspberry Pi Imager</figcaption>
</figure>
<ol>
<li>Select the OS of your choice; I opted for Raspberry Pi OS Lite 64 Bit (important as the Docker images we’ll be using don’t work on 32-bit systems).</li>
<li>If you haven’t already, insert your SD card into your computer and select it here.</li>
<li>Click on the small cog in the bottom right corner to configure the installation with the following settings:</li>
</ol>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./raspberry-pi-imager-settings-480w.jpg 480w, /articles/img/./raspberry-pi-imager-settings-640w.jpg 640w, /articles/img/./raspberry-pi-imager-settings-768w.jpg 768w, /articles/img/./raspberry-pi-imager-settings-960w.jpg 960w, /articles/img/./raspberry-pi-imager-settings-1536w.jpg 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/raspberry-pi-imager-settings.jpg" alt="Screenshot of the advanced options of Raspberry Pi Imager" />
<figcaption class="prezet-figcaption">Screenshot of the advanced options of Raspberry Pi Imager</figcaption>
</figure>
<p>I customised a few things:</p>
<ul>
<li>Updated hostname to rpi for brevity.</li>
<li>Enabled SSH and set an authorised key (if you already have a default key present, this will be filled in automatically).</li>
<li>Configured wireless LAN with my network details. I plan to hardwire the Pi eventually, but avoiding cables made setup easier.</li>
<li>Set locale settings to match my timezone and preferred keyboard layout.</li>
</ul>
<p>Remember to press <strong>Save</strong> before proceeding. You are now ready to write everything to your SD card, which may take a few minutes depending on its speed.</p>
<p>Once complete, insert the card into your Raspberry Pi and continue.</p>
<h3><a id="content-2-connecting-and-updating-your-raspberry-pi" href="#content-2-connecting-and-updating-your-raspberry-pi" class="prezet-heading" title="Permalink">#</a>2. Connecting and updating your Raspberry Pi</h3>
<p>After inserting the SD card, power up your Pi by connecting the power supply. Allow it 1 or 2 minutes to fully boot.</p>
<p>You can then connect using your preferred terminal:</p>
<pre><code data-theme="material-theme-palenight" data-lang="sh" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">ssh</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">pi@rpi.local</span></div></code></pre>
<p>Change the username (<code>pi</code>) and hostname (<code>rpi</code>) according to what you selected in the options dialogue. Alternatively, use the IP address assigned by your router.</p>
<p>To update packages and upgrade your Pi to the latest version, run:</p>
<pre><code data-theme="material-theme-palenight" data-lang="sh" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">apt</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">update</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;&amp;</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">apt</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">full-upgrade</span></div></code></pre>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.raspberrypi.com/documentation/computers/os.html#updating-and-upgrading-raspberry-pi-os">Official source</a></p>
<p>Important: Ensure you reboot before continuing. This isn’t optional; skipping this step will likely result in Docker installation failure.</p>
<pre><code data-theme="material-theme-palenight" data-lang="sh" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">reboot</span></div></code></pre>
<p>Your Pi will reboot, ready for the next step.</p>
<h3><a id="content-3-installing-docker" href="#content-3-installing-docker" class="prezet-heading" title="Permalink">#</a>3. Installing Docker</h3>
<p>We’ll use Docker’s straightforward install script. The following command is all that’s needed:</p>
<pre><code data-theme="material-theme-palenight" data-lang="sh" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">curl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-sSL</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://get.docker.com</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">|</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">sh</span></div></code></pre>
<p>By default, non-root users don’t have rights to run containers, so we’ll add our current user (<code>pi</code>) to the <code>docker</code> user group, eliminating the need for <code>sudo</code> every time we want to run a container.</p>
<pre><code data-theme="material-theme-palenight" data-lang="sh" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">usermod</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-aG</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">docker</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">USER</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Run <code>groups ${USER}</code> to verify that this has worked. You should see the user group you just added at the end of the line.</p>
<p>I highly recommend enabling the Docker system service. This ensures Docker automatically starts whenever you reboot your system and will also start containers configured with a <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://docs.docker.com/compose/compose-file/#restart">restart-policy</a> of <code>always</code> or <code>unless-stopped.</code> We’ll configure our Home Assistant container in this way.</p>
<p>Enable the Docker system service by running:</p>
<pre><code data-theme="material-theme-palenight" data-lang="sh" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">systemctl</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">enable</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">docker</span></div></code></pre>
<h3><a id="content-4-running-home-assistant-as-docker-container" href="#content-4-running-home-assistant-as-docker-container" class="prezet-heading" title="Permalink">#</a>4. Running Home Assistant as Docker container</h3>
<p>Finally, we’re at the stage of running Home Assistant in a Docker container accessible from your local network.</p>
<p>Create a file called <code>docker-compose.yml</code> in a folder called <code>docker/homeAssistant</code> within your home folder, resulting in this structure:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">pi@rpi:~ $ tree -L 3</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">└── docker</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">    └── homeAssistant</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">        └── docker-compose.yml</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #A6ACCD;">2 directories, 1 file</span></div></code></pre>
<p>Input the following into the <code>docker-compose.yml</code> file. You’ll likely need to change the timezone to match yours. If you’ve used a different directory structure, adjust your volumes configuration accordingly.</p>
<pre><code data-theme="material-theme-palenight" data-lang="yaml" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #FFCB6B;">---</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">version</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">2.1</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F07178;">services</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">homeassistant</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">image</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">lscr.io/linuxserver/homeassistant</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">container_name</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">homeassistant</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">network_mode</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">host</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">environment</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">PUID=1000</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">PGID=1000</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">TZ=Europe/Berlin</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">volumes</span><span style="color: #89DDFF;">:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">-</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">/home/pi/docker/homeAssistant/data:/config</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">restart</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">unless-stopped</span></div></code></pre>
<p>The last option, <code>restart: unless-stopped</code>, ensures our container reboots after restarting your Pi unless you’ve manually stopped it.</p>
<p>In theory, you’re now ready to start your container and begin configuring Home Assistant.</p>
<p>From the directory <code>/home/pi/docker/homeAssistant</code>, run:</p>
<pre><code data-theme="material-theme-palenight" data-lang="sh" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">sudo</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">docker</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">compose</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">up</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-d</span></div></code></pre>
<p>The <code>-d</code> flag runs the container in <code>detached</code> mode in the background.</p>
<p>If you see no errors appear, congratulations! Access Home Assistant’s management backend by navigating to <code>http://rpi.local:8123</code> (Adjust URL according to your hostname if different). You can now start the onboarding process; for this, I highly recommend referring to the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.home-assistant.io/getting-started/onboarding">official documentation</a>.</p>
]]>
            </summary>
                                    <updated>2022-04-02T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[11ty quick tip: Nunjucks include in markdown without indentation]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/eleventyjs-no-markdown-code-for-includes" />
            <id>https://tim-kleyersburg.de/eleventyjs-no-markdown-code-for-includes</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p><strong>TL;DR:</strong> Scroll to the bottom to see how to use nunjucks whitespace control to correctly render an include within a markdown file.</p>
<hr />
<p>When I was writing my <a href="/resources/articlesxy-with-cloudflare-workers.md">Create an API proxy with Cloudflare Workers</a> article I wanted to dynamically include the widget for what I last scrobbled so everyone could see what the purpose should be.</p>
<p>I knew I could set <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.11ty.dev/docs/config/#default-template-engine-for-markdown-files">a default template engine for markdown files</a>, which is used to parse the files before markdown renders the rest of the file. Since I’m using <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://mozilla.github.io/nunjucks/">Nunjucks</a> for the rest of my site I changed the default from <code>liquid</code> to <code>njk</code> in my <code>.eleventy.js</code> configuration file:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">module.exports</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">eleventyConfig</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #F07178;">        markdownTemplateEngine</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">njk</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Next, I tried to simply include the same widget I was including on my homepage by writing:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">```jinja-html</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">{% include &#39;last-tweet.njk&#39; %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">```</span></div></code></pre>
<p>Unfortunately, this wasn’t working as expected and looked like this:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./broken-nunjucks-include-480w.jpg 480w, /articles/img/./broken-nunjucks-include-640w.jpg 640w, /articles/img/./broken-nunjucks-include-768w.jpg 768w, /articles/img/./broken-nunjucks-include-960w.jpg 960w, /articles/img/./broken-nunjucks-include-1536w.jpg 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/broken-nunjucks-include.jpg" alt="Screenshot of the broken Nunjucks include" />
<figcaption class="prezet-figcaption">Screenshot of the broken Nunjucks include</figcaption>
</figure>
<p>At first I thought it just included the raw code, but then realized that the outer parts seemed to work as expected but then markdowns <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://spec.commonmark.org/0.28/#indented-code-blocks">Indented code blocks</a> feature kicked in.<br />
This is also mentioned in the 11ty docs as a <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.11ty.dev/docs/languages/markdown/#there-are-extra-and-in-my-output">common pitfall</a>.</p>
<p>The docs pointed me in the right direction but I just couldn’t find the real culprit of why it wasn’t working as expected.</p>
<p>To understand what the problem was let’s take a quick look how I implemented that Last Scrobble widget: Basically, we have two templates. One that provides the structure of the card and one that extends it to provide the individual content. I hate to repeat myself so I reach for this pattern as often as I can.</p>
<p><code>_last-thing.njk</code></p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">```jinja-html</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">&lt;div&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    &lt;div&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">        {% block link %}{% endblock %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">    &lt;/div&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #A6ACCD;">    {% block content %}{% endblock %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #A6ACCD;">&lt;/div&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span><span style="color: #A6ACCD;">```</span></div></code></pre>
<p><code>last-scrobble.njk</code></p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #A6ACCD;">```jinja-html</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">{% extends &#39;_last-thing.njk&#39; %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">{% block link %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">    &lt;a href=&quot;https://www.last.fm/user/Timmotheus&quot;&gt;@timmotheus&lt;/a&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">{% endblock %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">{% block content %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">    Dynamic track title and artist</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">{% endblock %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">```</span></div></code></pre>
<p>Turns out: my problem was my notorious need for correctly indenting everything. When providing the content for the blocks I naturally indented everything between the <code>block</code> statements, therefore adding to much indentation. Changing it to the following solved my problem:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight has-add-lines has-remove-lines has-diff-lines' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #A6ACCD;">```</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">{% extends &#39;_last-thing.njk&#39; %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">{% block link %}</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #f07178;">    &lt;a href=&quot;https://www.last.fm/user/Timmotheus&quot;&gt;@timmotheus&lt;/a&gt; </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #C3E88D;">&lt;a href=&quot;https://www.last.fm/user/Timmotheus&quot;&gt;@timmotheus&lt;/a&gt; </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">{% endblock %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">{% block content %}</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #f07178;">    Dynamic track title and artist </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #C3E88D;">Dynamic track title and artist </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">{% endblock %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">```</span></div></code></pre>
<p>But that’s ugly. <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://mozilla.github.io/nunjucks/templating.html#whitespace-control">Whitespace control</a> to the rescue! Quoting from the docs:</p>
<blockquote>
<p>Occasionally you don’t want the extra whitespace, but you still want to format the template cleanly, which requires whitespace.</p>
</blockquote>
<p>Yep, that’s what I wanted. My first instinct was to use it on the <code>include</code>. But that was wrong, because my extra whitespace was clearly coming from my blocks. So I changed my implementation of <code>last-tweet.njk</code> to this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight has-add-lines has-remove-lines has-diff-lines' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #A6ACCD;">```</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">{% extends &#39;_last-thing.njk&#39; %}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #f07178;">{% block link %} </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #C3E88D;">{%- block link -%} </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">    &lt;a href=&quot;https://www.last.fm/user/Timmotheus&quot;&gt;@timmotheus&lt;/a&gt;</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #f07178;">{% endblock %} </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #C3E88D;">{%- endblock -%} </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #f07178;">{% block content %} </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #C3E88D;">{%- block content -%} </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">    Dynamic track title and artist</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #f07178;">{% endblock %} </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #C3E88D;">{%- endblock -%} </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">```</span></div></code></pre>
<p><strong>That’s it!</strong> You should now be able to include a nunjucks template without any code indentation from markdown messing up your HTML.</p>
]]>
            </summary>
                                    <updated>2022-03-19T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Create an API proxy with Cloudflare Workers]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/api-proxy-with-cloudflare-workers" />
            <id>https://tim-kleyersburg.de/api-proxy-with-cloudflare-workers</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>{% from ‘macros.njk’ import alert %}</p>
<p>In this article we’ll discover how to use <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://workers.cloudflare.com/">Cloudflare Workers</a> to consume third party APIs without the need for your own server or compromising on security.<br />
I used this to create this dynamic widget on my homepage to show which song I listened to last, scrobbled from Spotify to <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://last.fm">Last.fm</a>:</p>
<div class="not-prose text-base my-4">
  {% include 'last-scrobble.njk' %}
</div>
<h2><a id="content-why-dont-you-use-javascript" href="#content-why-dont-you-use-javascript" class="prezet-heading" title="Permalink">#</a>Why don’t you use Javascript?</h2>
<p>You might ask yourself, since we are just writing plain JavaScript: why not use it directly from our bundle? In case of the Last.fm API this might actually not be the worst idea in the world, since it’s a read-only API. You can’t change anything on behalf of the user, only consume.</p>
<p>But: with Twitters API, for example, you might actually be able to post a tweet on behalf of a user when you get access to their API keys.</p>
<p>So, the short answer is: security. You don’t want your API keys floating around in public. (I actually made this mistake in the past and <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/tim-kleyersburg.de/blob/ecee30507d388362c2c6171196b382f0669ee762/api/twitter.php#L3-L4">pushed my API keys to GitHub</a>. I created new keys since then, so no problem there, but this was danger zone ☠️).</p>
<h2><a id="content-but-this-could-be-solved-by---insert-server-side-language-here--" href="#content-but-this-could-be-solved-by---insert-server-side-language-here--" class="prezet-heading" title="Permalink">#</a>But this could be solved by - insert server side language here -</h2>
<p>Indeed, it could. But using a server side language also means you have to manage a server and write a backend to provide this functionality. And while I love using something like Laravel: if you just need a simple endpoint which returns some data, this might be overkill.</p>
<p>Instead, we’ll use a serverless function on Cloudflare so achieve the same thing, but without the unneeded, and in my case unwanted, overhead.</p>
<h2><a id="content-heres-what-were-gonna-do" href="#content-heres-what-were-gonna-do" class="prezet-heading" title="Permalink">#</a>Here’s what we’re gonna do</h2>
<ol>
<li>Create a new account on Cloudflare</li>
<li>Install <code>wrangler</code>, Cloudflares CLI</li>
<li>Login and generate a new project</li>
<li>Set up a development environment</li>
<li>Write the code</li>
<li>Deploy 🎉</li>
</ol>
<h3><a id="content-create-a-new-account-on-cloudflare" href="#content-create-a-new-account-on-cloudflare" class="prezet-heading" title="Permalink">#</a>Create a new account on Cloudflare</h3>
<p>Or don’t, if you already have one. Cloudflare offers a generous free tier for Workers of 100,000 free requests <em>per day</em>. This should be more than enough, even for ambitious hobby projects. Looking at my stats I had 6 request today. And it’s early evening now. More than enough headroom for me!</p>
<h3><a id="content-install-wrangler-cloudflares-cli" href="#content-install-wrangler-cloudflares-cli" class="prezet-heading" title="Permalink">#</a>Install <code>wrangler</code>, Cloudflares CLI</h3>
<p>Next you need to install <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/cloudflare/wrangler">Cloudflare Wrangler</a>. Wrangler is Cloudlfares CLI tool you’ll use to generate projects, manage secrets and deploy workers to your account.</p>
<p>Run the following command from your preferred shell to install <code>wrangler</code>.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">npm</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">i</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">@cloudflare/wrangler</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">-g</span></div></code></pre>
<blockquote>
<p>For up to date information about the installation process please <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developers.cloudflare.com/workers/">refer to the Cloudflares docs</a>.</p>
</blockquote>
<p>After you’ve successfully installed <code>wrangler</code> you’ll want to log in so <code>wrangler</code> has access to your account and can publish new projects on your behalf. Run the following command to authenticate with Cloudlfare.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">wrangler</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">login</span></div></code></pre>
<p>Now that you are logged in you can generate a new project with the following command:</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">wrangler</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">generate</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">your-worker</span></div></code></pre>
<p>If you don’t specify any other options the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/cloudflare/worker-template">default starter pack</a> will be used. This default template uses JavaScript as its language of choice and its what we’ll use to build our serverless proxy.</p>
<p>After creating your worker, change into the newly created directory <code>your-worker</code> (or the name you specified) and you are ready to go.</p>
<h3><a id="content-set-up-a-development-environment" href="#content-set-up-a-development-environment" class="prezet-heading" title="Permalink">#</a>Set up a development environment</h3>
<p>Before actually writing any code we’ll configure our project with our individual account id and set an appropriate name. This is needed for using wranglers <code>dev</code> command (allowing you to preview what you’re doing) and also for deploying later on. All the hard work of running your function will happen on Cloudflares edge workers linked to your account which are routed to your local machine.</p>
<p>Go ahead and open the file <code>wrangler.toml</code> in your favorite editor. It should look something like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">name = &quot;helloworld&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">type = &quot;javascript&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">account_id = &quot;&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">workers_dev = true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #A6ACCD;">route = &quot;&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #A6ACCD;">zone_id = &quot;&quot;</span></div></code></pre>
<p>Next, run <code>wrangler whoami</code> to get your account ID:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">wrangler whoami</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">+--------------+----------------------------------+</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">| Account Name | Account ID                       |</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">+--------------+----------------------------------+</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #A6ACCD;">| Your Account | ${yourAccountId}                  |</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #A6ACCD;">+--------------+----------------------------------+</span></div></code></pre>
<p>Copy the value of your account ID and change your <code>wrangler.toml</code> like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight has-add-lines has-remove-lines has-diff-lines' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #f07178;">name = &quot;helloworld&quot; </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #C3E88D;">name = &quot;your-worker-name&quot; </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">type = &quot;javascript&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #f07178;">account_id = &quot;&quot; </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #C3E88D;">account_id = &quot;your-account-id&quot; </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #A6ACCD;">workers_dev = true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span><span style="color: #A6ACCD;">route = &quot;&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">9</span><span style="color: #A6ACCD;">zone_id = &quot;&quot;</span></div></code></pre>
<p>I’ll take the following bit directly from Cloudflares <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developers.cloudflare.com/workers/get-started/guide/#5a-understanding-hello-world">Get Started</a> guide to explain how a worker fundamentally works:</p>
<blockquote>
<p>Fundamentally, a Workers application consists of two parts:</p>
<ol>
<li>An event listener that listens for <code>FetchEvents</code>, and</li>
<li>An event handler that returns a <code>Response</code> object which is passed to the event’s <code>.respondWith()</code> method.
When a request is received on one of Cloudflare’s edge servers for a URL matching a Workers script, it passes the request to the Workers runtime. This dispatches a FetchEvent in the isolate where the script is running.</li>
</ol>
</blockquote>
<p>In fact, the following code, directly taken from the basic cloudflare template, does exactly these two things described above:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #676E95;">// 1. listen for fetch events</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #82AAFF;">addEventListener</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">fetch</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">respondWith</span><span style="color: #F07178;">(</span><span style="color: #82AAFF;">handleRequest</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">request</span><span style="color: #F07178;">))</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #89DDFF;">}</span><span style="color: #A6ACCD;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #676E95;">// 2. event handler which returns a Response object</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">handleRequest</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">request</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">new</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">Response</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">Hello worker!</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">        headers</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">content-type</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">text/plain</span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>If you want to preview this script you can now run <code>wrangler dev</code> to boot up a preview environment which you can access from localhost. This will deploy the worker to an edge worker and provide you with a local URL to see it in action.</p>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">wrangler</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">dev</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #FFCB6B;">💁</span><span style="color: #A6ACCD;">  </span><span style="color: #C3E88D;">watching</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">./</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #FFCB6B;">👂</span><span style="color: #A6ACCD;">  </span><span style="color: #C3E88D;">Listening</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">on</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">http://127.0.0.1:8787</span></div></code></pre>
<p>You can now visit <code>http://127.0.0.1:8787</code> in a browser or use an API client like <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://insomnia.rest/products/insomnia">Insomnia</a> to inspect and debug your endpoint. Changes to your code are directly reflected so you don’t have to manually redeploy your code to preview it.</p>
<h3><a id="content-write-the-code" href="#content-write-the-code" class="prezet-heading" title="Permalink">#</a>Write the code</h3>
<p>We finally left the setup part behind us and are ready to write some actual code! If you are familiar with JavaScript, especially on the Node.js end, you should have no problems following the next steps.</p>
<p>Basically all we want to do is make a <code>fetch</code> request to a predefined endpoint and return the result. If you are impatient you can find the finished code on <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/lastfm-cloudflare-worker">GitHub</a>.</p>
<p>We’ll gradually build up our <code>handleRequest</code> method step by step so you can follow along.</p>
<blockquote>
<p>Note: For these steps to work without errors you’ll need to provide the API key as a secret. To do this run the command <code>wrangler secret put LASTFM_API_KEY</code> and paste your key. If you don’t have an API key you can obtain one from <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.last.fm/api/account/create">Last.fm’s developer portal</a>.</p>
</blockquote>
<hr />
<h4>Step 1: Build the request URL which we’ll fetch later</h4>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #676E95;">// Note that we are using an `async` function. This will become important from step 2 onward</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">handleRequest</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// You can find the API documentation here: https://www.last.fm/api/show/user.getRecentTracks</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// We use template strings to interpolate our secret API key into the URL</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">url</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">http://ws.audioscrobbler.com/2.0/?format=json&amp;method=user.getrecenttracks&amp;user=your-username&amp;limit=1&amp;api_key=</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">LASTFM_API_KEY</span><span style="color: #89DDFF;">}`</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #89DDFF;">}</span></div></code></pre>
<hr />
<h4>Step 2: Use <code>fetch</code> to get a response</h4>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">handleRequest</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">url</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">http://ws.audioscrobbler.com/2.0/?format=json&amp;method=user.getrecenttracks&amp;user=your-username&amp;limit=1&amp;api_key=</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">LASTFM_API_KEY</span><span style="color: #89DDFF;">}`</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// we&#39;ll use `fetch` in combination with `await` so we don&#39;t have to manually resolve the returned `Promise`</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// this is why we defined the whole function as `async`, so we can use `await`</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">await</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">fetch</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">url</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// `fetch` will resolve to a [Response object](https://developer.mozilla.org/en-US/docs/Web/API/Response)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// We will use the `json` method to return the responses results as a JavaScript object</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// note that we&#39;ll again use `await` since .json() returns a `Promise`</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">result</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">await</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">json</span><span style="color: #F07178;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #89DDFF;">}</span></div></code></pre>
<hr />
<h4>Step 3: Return a new response with the fetched data</h4>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">handleRequest</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">url</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">http://ws.audioscrobbler.com/2.0/?format=json&amp;method=user.getrecenttracks&amp;user=your-username&amp;limit=1&amp;api_key=</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">LASTFM_API_KEY</span><span style="color: #89DDFF;">}`</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">await</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">fetch</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">url</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">result</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">await</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">json</span><span style="color: #F07178;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">new</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">Response</span><span style="color: #F07178;">(</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #89DDFF;">        </span><span style="color: #676E95;">// we&#39;ll use JSON.stringify() to convert the returned JavaScript object to a string which can be sent in a response</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">        </span><span style="color: #A6ACCD;">JSON</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">stringify</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">response</span><span style="color: #F07178;">)</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #F07178;">            headers</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #89DDFF;">                </span><span style="color: #676E95;">// we&#39;ll set a CORS header to allow access to this resource from everywhere</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #F07178;">                </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">Access-Control-Allow-Origin</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">*</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #F07178;">    )</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #89DDFF;">}</span></div></code></pre>
<blockquote>
<p>This is the first time you should actually see a response in your browser or API client, be proud of yourself 🎉</p>
</blockquote>
<hr />
<h4>Step 4: Cache and cleanup</h4>
<p>Currently we will make a request to the Last.FM API every time our serverless function is invoked. This is excessive given that a new song can only be scrobbled every few minutes. To not overuse our worker we’ll implement caching using the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developers.cloudflare.com/workers/runtime-apis/cache/">Cache Runtime API</a>.</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">handleRequest</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// Initialize the default cache</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">cache</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">caches</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">default</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// use .match() to see if we have a cache hit, if so return the caches response early</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">let</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">await</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">cache</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">match</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">request</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">if</span><span style="color: #F07178;"> (</span><span style="color: #A6ACCD;">response</span><span style="color: #F07178;">) </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// we&#39;ll chain our await calls to get the JSON response in one line</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">lastfmResponse</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">await</span><span style="color: #F07178;"> (</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">await</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">fetch</span><span style="color: #F07178;">(</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">http://ws.audioscrobbler.com/2.0/?format=json&amp;method=user.getrecenttracks&amp;user=timmotheus&amp;limit=1&amp;api_key=</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">LASTFM_API_KEY</span><span style="color: #89DDFF;">}`</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #F07178;">        )</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #F07178;">    )</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">json</span><span style="color: #F07178;">()</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">response</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">new</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">Response</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">JSON</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">stringify</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">response</span><span style="color: #F07178;">)</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #F07178;">        headers</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">Access-Control-Allow-Origin</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">*</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #89DDFF;">            </span><span style="color: #676E95;">// We set a max-age of 300 seconds which is equivalent to 5 minutes.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #89DDFF;">            </span><span style="color: #676E95;">// If the last response is older than that the cache.match() call returns nothing and and a new response is fetched</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">Cache-Control</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">max-age: 300</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">26</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">27</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// before returning the response we put a clone of our response object into the cache so it can be resolved later</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">28</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">waitUntil</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">cache</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">put</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">event</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">request</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">clone</span><span style="color: #F07178;">()))</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">29</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">30</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">31</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>That should be it! We now have a functioning API proxy which we’ll deploy in the next step.</p>
<h2><a id="content-deploy-our-code" href="#content-deploy-our-code" class="prezet-heading" title="Permalink">#</a>Deploy our code</h2>
<p>Deploying our code with the <code>wrangler</code> CLI is as simple as running <code>wrangler publish</code>. Yep, that’s it. If you don’t need a custom domain this is all it takes to publish your code on Cloudflare. Read on if you want to use your own domain to deploy your worker.</p>
<h3><a id="content-using-a-custom-domain" href="#content-using-a-custom-domain" class="prezet-heading" title="Permalink">#</a>Using a custom domain</h3>
<p>To use a custom domain you’ll first need to make a few changes in the Cloudflare dashboard as well as your <code>wrangler.toml</code> file. We’ll set up a new environment (called <code>production</code>) to provide the necessary configurations for deploying to our own domain.</p>
<p>Assuming you want to use a custom subdomain for your workers named <code>workers</code> you will first need to setup a new DNS record.</p>
<p>It should look like this:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./dns-record-for-cloudflare-workers-480w.png 480w, /articles/img/./dns-record-for-cloudflare-workers-640w.png 640w, /articles/img/./dns-record-for-cloudflare-workers-768w.png 768w, /articles/img/./dns-record-for-cloudflare-workers-960w.png 960w, /articles/img/./dns-record-for-cloudflare-workers-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/dns-record-for-cloudflare-workers.png" alt="Screenshot of a Cloudflare workers DNS record" />
<figcaption class="prezet-figcaption">Screenshot of a Cloudflare workers DNS record</figcaption>
</figure>
<p>This makes sure that routing works correctly.</p>
<p>Next, make the following changes to your <code>wrangler.toml</code></p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight has-add-lines has-diff-lines' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #A6ACCD;">name = &quot;your-worker-name&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">type = &quot;javascript&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">account_id = &quot;your-account-id&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">workers_dev = true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">route = &quot;&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">zone_id = &quot;&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #C3E88D;">[env.production] </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #C3E88D;">route = &quot;workers.your-domain.com/last-scrobble&quot; </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #C3E88D;">zone_id = &quot;your-zone-id&quot; </span></div></code></pre>
<p>To find your zone id, log in to your Cloudflare account, choose your site and look on the right sidebar of your dashboard. Under the section “API” you will find your zone id. This makes sure the worker is published for the correct domain.</p>
<p>Next, you’ll need to add the API key for Last.fm as a secret to the new environment. For this, run the command <code>wrangler secret put LASTFM_API_KEY --env production</code> and enter your key.</p>
<p>You are now ready to deploy your worker to your own domain running the command <code>wrangler publish --env production</code>!</p>
<hr />
<p>I hope this helped you to get your first worker deployed to Cloudflare! If you don’t want to do the work if yourself just fork the repo <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/lastfm-cloudflare-worker">of my own worker</a>. If you have any question hit my up <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://x.com/timkley">on Twitter</a></p>
]]>
            </summary>
                                    <updated>2022-03-18T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Web performance for non-tech-people]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/web-performance-for-nontech-people" />
            <id>https://tim-kleyersburg.de/web-performance-for-nontech-people</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>With the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://blog.chromium.org/2020/05/introducing-web-vitals-essential-metrics.html">introduction of Web Vitals</a> by Google in May 2020, almost everyone
has probably heard of or got in touch with web performance. Either because you are a developer looking at and developing against these new metrics or because you do
client work and every client waved their bad Google performance report in your face.</p>
<h2><a id="content-what-has-changed" href="#content-what-has-changed" class="prezet-heading" title="Permalink">#</a>What has changed?</h2>
<p>It’s not like performance reports didn’t exist prior to May 2020. Chances are, you also used <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://pagespeed.web.dev/">PageSpeed Insights</a> back in the day to
generate reports. The metrics used back then were technical and in many cases very straightforward to optimise. Basically the reports told you what to do very clearly:</p>
<blockquote>
<p>“Your server sent the website to slow, serve it faster!”<br />
“These images are too big, make them smaller!”<br />
“You only use 50% of the CSS, don’t do that.”</p>
</blockquote>
<p>These are, of course, good suggestions which help to improve your websites performance. But that wasn’t enough for Google and shouldn’t be enough for you.</p>
<p>Instead of focusing on technical aspects of web performance, Google introduced new metrics that where designed to measure impacts on <em>user experience</em> and <em>perceived</em> performance.</p>
<p>Shortly after the announcement, Google tools like <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a> and PageSpeed Insights (which is based on Lighthouse but more accessible to non-dev people) were updated to reflect these changes, leading to bad performance reports for many websites.</p>
<h2><a id="content-why-should-you-care" href="#content-why-should-you-care" class="prezet-heading" title="Permalink">#</a>Why should you care?</h2>
<p>You’ve probably been at the other end of this topic: clicking a link in a search machine and waiting for the page to load.<br />
When it’s finally loaded, you want to click on a link in the first paragraph but realise that the page is still loading. So you wait another second.<br />
Your next click opens an ad, which popped up just before the paragraph you were reading.</p>
<p>If this happens to me, 9 out of 10 times I’ll leave the page and get my information elsewhere.</p>
<p>Another part I want to emphasise, is inclusion. In many cases, developers work with a stable internet connection and good hardware. This prevents them from experiencing what it feels like to use their website on a slow 3G network with a 5 year old device. And this is something the visitors may not be able to change. Take a good look at your target audience so you can make an informed decision about whether or not it is acceptable to have a slow loading website.</p>
<p>Also, the internet consumes a lot of electricity. And your website needs electricity, too. By optimising the performance you make sure to only use what you actually need. Apparently I <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.websitecarbon.com/website/tim-kleyersburg-de/">should plant a tree</a> to accommodate for the CO2 generated by my website (assuming I have 10.000 visitors per month. As if.).</p>
<p>So, unless you <em>want</em> to provide a shitty user experience <em>and</em> destroy the planet, read on.</p>
<h2><a id="content-so-what-exactly-is-performance" href="#content-so-what-exactly-is-performance" class="prezet-heading" title="Permalink">#</a>So what exactly <em>is</em> performance?</h2>
<p>Performance is a very big word and its implications are very broad, even broader since the introduction of Web Vitals, because now bad user experience can cause penalties for your performance rating, too.</p>
<p>So let’s break it down into smaller parts to better understand the differences and responsibilities. Take a look at what happens when you request a webpage:</p>
<ol>
<li>You type into the address bar or click a link to e.g., www.tim-kleyersburg.de.</li>
<li>The domain name system (DNS) resolves this name to an IP address (the address of a server).</li>
<li>The <strong>request</strong> is forwarded to this IP address.</li>
<li>Now the server processes your request and does all the work needed so you can see a webpage.</li>
<li>After the servers work is done, it sends a <strong>response</strong> back to you. This response contains the HTML of the page you want to look at.</li>
<li>Now your browser starts processing this response. It also starts downloading assets (like CSS for styles, JavaScript for interactivity, or images for
something beautiful to look at).</li>
<li>After all this is done you can finally view the webpage in all its glory.</li>
</ol>
<p>For simplicity’s sake we’ll only focus on points 4 to 7.</p>
<p>Typically, everything that happens <em>before</em> your browser receives the response, is called “backend” and everything that happens <em>after</em> is called “frontend” so we’ll use these terms to keep it simple.</p>
<h3><a id="content-backend-4-and-5" href="#content-backend-4-and-5" class="prezet-heading" title="Permalink">#</a>Backend (4. and 5.)</h3>
<p>Imagine an online store that sells guitars (because I like guitars). You want to take a look at a specific guitar so you click on the link to get to the product page. Now the backend has to do all the work necessary to give you a helpful response. It fetches information about the product from a database, calculates the correct price based on your location and gets stock information for different vendors. All this takes time to compute. This timespan is called “<a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://web.dev/time-to-first-byte/">Time to first byte</a>” or TTFB and is one audit run by Lighthouse to assess your final performance score.</p>
<p>Since all of this computation happens in the “backend”, there is no way to extract more information from this measurement without a developer looking into what exactly causes a high TTFB. All these online tools you can use to measure performance, can only tell you “your server took x seconds to send a response”. Nothing more.</p>
<p>A good strategy for a good TTFB is to aggressively cache the response and skip the computation part altogether. There are pages which are better suited for this (like this website) than others (like an e-commerce site).<br />
Rule of thumb: if your content rarely changes and is not personalised (for a logged in user) or parts can change at any time (like a price), a static cache is a great way to achieve a really good TTFB score.</p>
<h3><a id="content-frontend-6-and-7" href="#content-frontend-6-and-7" class="prezet-heading" title="Permalink">#</a>Frontend (6. and 7.)</h3>
<p>Now it gets more complex. Browsers can do a lot more these days so there are more areas that can negatively affect your performance scores.</p>
<p>We’ll break it down further to complete the puzzle piece by piece.</p>
<p><em>I’m fully aware, there is still a lot more to web performance than the following explanations. Please keep in mind, that the audience for this article are people with a non-tech background.</em></p>
<p>There are two big parts which very directly affect frontend performance: scripts and assets (like images or videos).</p>
<h4>Assets</h4>
<p>Images can make up a big part of your website. The average website of today <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://httparchive.org/reports/page-weight">weighs about 2MB</a> and images make up about <em>half of it</em>. Let’s not take videos into account right now to avoid another layer of complexity.</p>
<p>Since images make up half of your website, it seems natural to care about how they affect the performance of your website. The size of your website has a direct influence on the performance because the more contents need to be loaded, the longer it takes.</p>
<h4>Scripts</h4>
<p>JavaScript (not to be mixed up with Java) is a programming language. It is used for a lot of things concerning websites. It’s used to create interactivity, load more content after the initial page has loaded, track visitors, display ads and much more.</p>
<p>Each of these things can have an impact on the performance and therefore affect performance metrics. By executing scripts you are using computing power which can have a negative impact on performance. Fetching content after the first page load can lead to layout shifts and render-blocking scripts can completely halt the rendering of a page, resulting in an unusable page until the script is done.</p>
<p>In commercial sites, there are also most likely a lot of 3rd-party-scripts. <strong>Every script can have a negative impact on performance</strong>, and should therefore be evaluated by you, if this negative impact is worth the potential uplift in other areas.</p>
<h2><a id="content-how-to-measure-and-interpret-frontend-performance" href="#content-how-to-measure-and-interpret-frontend-performance" class="prezet-heading" title="Permalink">#</a>How to measure and interpret frontend performance</h2>
<p>As outlined above: the tools at our disposal have no way to measure backend performance. There are tools for that, like <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.blackfire.io/">Blackfire for PHP</a>, which are, most of the time, focused on developers that care about performance while developing.</p>
<h3><a id="content-tools" href="#content-tools" class="prezet-heading" title="Permalink">#</a>Tools</h3>
<p>There are many tools you can use to measure frontend performance. I’ve used the following which are all well-known and well-used:</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://web.dev/measure/">web.dev/measure</a> <em>by Google</em></p>
<p>This tool uses Lighthouse, which is also built into Google Chrome, to run an audit of your webpage. It provides nearly the same output as Chrome but you can do it online in any browser. If you need accurate data (the tests are performed <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://web.dev/lab-and-field-data-differences/">in a lab environment</a>) use this tool. Because it uses a predefined set of network and device conditions, the results can’t fluctuate like a local test or a test with field data (see PageSpeed Insights) can.</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://pagespeed.web.dev/">pagespeed.web.dev</a> <em>by Google</em></p>
<p>PageSpeed Insights (PSI) is almost identical to web.dev/measure with one important difference: PSI uses <em>field data</em> provided by Chrome to give you a better understanding of how your website performs for <em>your actual visitors</em>. While a lab test can help you enhance your metrics in a reproducible way so you can actually see what works and what doesn’t, a test with field data can surface problems your real users are having. Maybe your average user has a much slower network than you do.</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://waterfaller.dev/">waterfaller.dev</a></p>
<p>With waterfaller, you get actionable insight on what and how to improve specific Core Web Vital metrics. It also shows you a waterfall (hence the name) of your requests, this enables you to better understand how and what your website exactly loads when, and how it impacts metrics. What I really liked about using this tool is, that it gives you a clear path to take. Instead of giving you a report with all the things you’ve done wrong it provides you with help on how to do these things right.</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://yellowlab.tools/result/g691h5f4kf">yellowlab.tools</a></p>
<p>Yellow Lab Tools doesn’t analyse Core Web Vitals but a lot other factors that are playing into bad performance, like how many requests you performed, if you have oversized images and more. It’s a great addition to the other tools because it can surface quick wins.</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://webhint.io">webhint.io</a> <em>by Microsoft</em></p>
<p>webhint can be used as a <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://webhint.io/docs/user-guide/extensions/extension-browser/">browser extension</a>, as CLI using <code>npx</code> or <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://webhint.io/docs/user-guide/extensions/vscode-webhint/">directly in VS Code</a>. It not only scans for performance issues but also for things to improve in terms of accessibility, security and more. It is backed up by a lot of documentation which explains what you could and should do.</p>
<hr />
<p>But what good is the best hammer if you don’t know what a nail is. So let’s dive deeper into some metrics, focusing again on the Core Web Vitals.</p>
<h3><a id="content-the-metrics" href="#content-the-metrics" class="prezet-heading" title="Permalink">#</a>The metrics</h3>
<p>The following metrics where promoted to be much more important for your performance score than they have been before.</p>
<p><strong>Largest Contentful Paint (LCP)</strong><br />
Measures perceived load speed and marks the point in the page load timeline when the page’s main content has likely loaded.</p>
<p>Oftentimes this will likely be a hero image of some kind and you can optimise this yourself without talking to your developers.</p>
<p>This metric falls in the “loading” category.</p>
<p><strong>Time to Interactive (TTI)</strong><br />
Time to interactive is the amount of time it takes for the page to become fully interactive. Fully interactive means that the page is displaying useful content, event handlers for most visible page elements are registered and the page responds to user interactions within 50 milliseconds.</p>
<p>For a website to be usable your computer has to do a lot of work. All these things take up time from your CPU and only when these things are finished your page is considered “interactive”. Most of the time scripts are responsible for needing much CPU power, so investigate them first.</p>
<p>This metric falls in the “interactivity” category.</p>
<p><strong>Cumulative Layout Shift (CLS)</strong><br />
Measures visual stability and quantifies the amount of unexpected layout shift of visible page content.</p>
<p>Think back to that ad that loads later than the rest of the page, causing the articles paragraph to <em>shift</em> down. But it is not only ads: slow loading images, custom fonts or scripts that load more content can cause layouts to shift unexpectedly.</p>
<p>This metric falls in “visual stability” the category.</p>
<p>There are a lot more metrics than the ones listed here, but I think these cause the most confusion for a lot of people. If you are interested in learning more about the other metrics you can find them <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://web.dev/metrics/">here</a>.</p>
<hr />
<h2><a id="content-best-practices-performance" href="#content-best-practices-performance" class="prezet-heading" title="Permalink">#</a>Best practices performance</h2>
<p>There are a lot of things you can do for performance. In my daily business I’ve found that <em>awareness</em> from everyone who works on a project is the most important thing. If you are aware of the problems your changes can cause you’ll avoid creating these problems altogether. Instead of remedying an issue you’ll prevent it from happening.</p>
<p>So my best advice is: involve the whole team. Always ask “will this affect performance?” and if the answer is “yes” explore how can minimise the negative impact or find a solution that won’t impact performance.</p>
<p>Another thing I always advocate is: think mobile first. Mobile devices are used more than desktop devices these days. So creating experiences with the bigger part of the audience in mind not only creates more usable websites, but because performance is that much more important on mobile devices chances are much higher you’ll provide a good experience from the start!</p>
<p>But let’s take a look at a few actionable insights.</p>
<h3><a id="content-images" href="#content-images" class="prezet-heading" title="Permalink">#</a>Images</h3>
<p>When optimising images you should take the following things into account:</p>
<ul>
<li>👩‍🎨 <strong>Are my images properly cropped and sized?</strong>
You don’t want to just provide the raw image as it came from Adobe Stock or your camera. Make sure to crop it to only what is needed and has a maximum width
of your websites viewport.</li>
<li>👩‍🎨 <strong>Am I using modern file formats, such as <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://caniuse.com/webp"><code>webp</code></a> or <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://caniuse.com/avif"><code>avif</code></a>?</strong>
Please make sure to provide fallbacks for older browsers before making a complete switch to modern formats.</li>
<li>👩‍💻 <strong>Am I taking proper care of serving different images for different devices?</strong>
Devices with a high-dpi screen need images with more pixels so they appear sharp. But these are much bigger than their average-dpi counterpart and therefore
take longer to load. You should only load these big images if a high-dpi device is used.</li>
</ul>
<blockquote>
<p>👩‍🎨 = designer<br />
👩‍💻 = developer</p>
</blockquote>
<p>A designer can take care of generating images in the appropriate sizes and formats while a developer should take care of implementing <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images">responsive images</a>. There also are systems which can take care of automatically generating appropriately sized images. Most of the time it’s easier to manually generate the images needed than to setup a complex automatic process.</p>
<h3><a id="content-scripts" href="#content-scripts" class="prezet-heading" title="Permalink">#</a>Scripts</h3>
<blockquote>
<p>I’ll use some of the weak explanations used by performance tools because a deep dive into script performance is definitely not “non-tech”. But I’ll try to provide actionable insights along the way.</p>
</blockquote>
<ul>
<li><strong>Am I only loading scripts I need?</strong>
When auditing web pages for bad performance many times 3rd party scripts are the culprit for good performance. These script load big libraries like jQuery to perform simple tasks and cause a slow down of your page. Evaluate carefully what scripts you include in your page.</li>
<li><strong>Are your own scripts optimised?</strong>
Everything that applies to 3rd party scripts also applies to your own scripts but with one big advantage: you probably have your scripts under control and can rip out old jQuery dependencies. This is more easily said then done, of course. Give your developers time to evaluate and prepare concepts how to do bigger updates.</li>
<li><strong>Are you loading your scripts correctly?</strong>
There are many ways to trigger the loading of a script file. Some of them are good (because they don’t block page rendering for example) and some are not that good (because they <em>do</em> block your page from rendering correctly). If you can, always use the “defer” attribute so the rendering of your page is not blocked.</li>
</ul>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./optimum-head-order-480w.jpg 480w, /articles/img/./optimum-head-order-640w.jpg 640w, /articles/img/./optimum-head-order-768w.jpg 768w, /articles/img/./optimum-head-order-960w.jpg 960w, /articles/img/./optimum-head-order-1536w.jpg 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/optimum-head-order.jpg" alt="Optimum head are, according to @csswizardy" />
<figcaption class="prezet-figcaption">Optimum head are, according to @csswizardy</figcaption>
</figure>
<h2><a id="content-get-your-whole-team-on-board" href="#content-get-your-whole-team-on-board" class="prezet-heading" title="Permalink">#</a>Get your whole team on board</h2>
<p>I mentioned awareness to be one of the key factors. If you have to permanently fight against bad performance because not everyone is aware of the problem, you’re going to have a bad time.</p>
<p>Educate your team on the importance, what they can do from the start to prevent it, and how to fix existing issues.</p>
]]>
            </summary>
                                    <updated>2022-02-16T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[UniFi network at home: how to easily setup]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/unifi-network-for-homeoffice" />
            <id>https://tim-kleyersburg.de/unifi-network-for-homeoffice</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<blockquote>
<p>This small tutorial is directed at people who want a great wireless network experience in their own home but this setup might also be suitable for small businesses with just a few access points.</p>
</blockquote>
<p>We recently moved into a new flat. COVID doesn’t seem to go away in the near future and our company will offer working completely remote with or without the virus. So having a good internet and network connection was one of my personal priorities. We were lucky enough to get a fiber connection installed just in time for our move.<br />
Our electrician made sure to run LAN cables in every room for maximum flexibility.</p>
<p>But for our laptops and mobile devices, LAN wasn’t gonna cut it. We also have two LAN outlets in the ceiling of our hallway, which were deliberately installed there so I could mount two access points.</p>
<p>We already use <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://ui.com/">Ubiquiti</a> devices at my company. Ubiquity has a very good reputation (and rightfully so), so using Ubiquity gear was pretty much set from the start.</p>
<h2><a id="content-shopping-list" href="#content-shopping-list" class="prezet-heading" title="Permalink">#</a>Shopping list</h2>
<p>My shopping list wasn’t big:</p>
<ul>
<li>2x <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://store.ui.com/products/unifi-ap-6-lite">Access Point WiFi 6 Lite</a></li>
<li>2x <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://store.ui.com/collections/operator-accessories/products/u-poe-af">PoE Injector, 802.3af</a></li>
<li>4x Cat6 Patch cables</li>
</ul>
<blockquote>
<p>Note: depending on your situation you might need more (or less) cables. The access point itself has one outlet (PoE, power over ethernet) and the injector has two. One for LAN intake and one the connect it to the access point.</p>
</blockquote>
<p>It took some time for me to get my hands on these because, unfortunately, the access points were sold out whenever I wanted to order.</p>
<p>But after some patience they where back in stock so I pulled the trigger. Ordering in the Ubiquity store was simple and delivery was fast. And somehow much cheaper than buying from a retailer 🤷‍♂️.</p>
<h2><a id="content-unpacking" href="#content-unpacking" class="prezet-heading" title="Permalink">#</a>Unpacking</h2>
<p>The access points ship with a wall/ceiling mounting kit. The packaging was very thoughtful. All screws where in a small package you could access directly when opening the box. The flap in which it was stored is used to pull out the inner packaging and reveals the access point which itself is covered with a plastic cover for protection. The access point feels very high quality and has a smooth touch surface (I love those!).</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./access-point-wifi6-lite-480w.jpg 480w, /articles/img/./access-point-wifi6-lite-640w.jpg 640w, /articles/img/./access-point-wifi6-lite-768w.jpg 768w, /articles/img/./access-point-wifi6-lite-960w.jpg 960w, /articles/img/./access-point-wifi6-lite-1536w.jpg 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/access-point-wifi6-lite.jpg" alt="Photograph of the Access Point Wifi 6 Lite" />
<figcaption class="prezet-figcaption">Photograph of the Access Point Wifi 6 Lite</figcaption>
</figure>
<p>All in all the quality was even better than I expected (and I knew what I was getting into).</p>
<h2><a id="content-setting-up-the-network-controller" href="#content-setting-up-the-network-controller" class="prezet-heading" title="Permalink">#</a>Setting up the Network Controller</h2>
<p>Last time I set up the network in my company I used a local controller on my MacBook. Since I forgot to store the backup this resulted in the need to completely reinstall the whole network when I decided to reinstall macOS.</p>
<p>So this time I wanted to setup a controller on a VPS so this won’t happen again. Also, it would enable me to access the controller remotely.</p>
<p>After trying some manual setups it turned out that there is a super simple way for setting up everything without any hassle, on any Ubuntu version, with HTTPS enabled without the need to tinker around with correct Java versions.</p>
<h3><a id="content-get-a-vps-or-similar-server" href="#content-get-a-vps-or-similar-server" class="prezet-heading" title="Permalink">#</a>Get a VPS or similar server</h3>
<p>You’ll need a (fresh) Ubuntu installation for the next steps. These are the settings I chose for my DigitalOcean droplet:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./digital-ocean-droplet-settings-480w.jpg 480w, /articles/img/./digital-ocean-droplet-settings-640w.jpg 640w, /articles/img/./digital-ocean-droplet-settings-768w.jpg 768w, /articles/img/./digital-ocean-droplet-settings-960w.jpg 960w, /articles/img/./digital-ocean-droplet-settings-1536w.jpg 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/digital-ocean-droplet-settings.jpg" alt="Screenshot of my droplet settings" />
<figcaption class="prezet-figcaption">Screenshot of my droplet settings</figcaption>
</figure>
<p>Although a server with 2 GB of RAM is recommended I didn’t have any problems with the smaller option. If you want to make extra sure everything keeps running smoothly better directly select the bigger option for 10$.</p>
<p>I didn’t activate the auto backup functionality because I plan to just backup the UniFi Controller settings from time to time.</p>
<p>You could also use <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.hetzner.com/cloud">Hetzner Cloud</a> or <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.netcup.de/vserver/vps.php">Netcup</a> if those better suit your budget or needs.</p>
<p>To access your network controller make sure to point an A record (like <code>network.your-domain.com</code>) to your servers IP address. This is necessary if you want to access your network controller remotely with Let’s Encrypt activated.</p>
<h3><a id="content-installing-the-unifi-network-application" href="#content-installing-the-unifi-network-application" class="prezet-heading" title="Permalink">#</a>Installing the UniFi Network Application</h3>
<div class="not-prose rounded-md border-l-4 border-l-orange-300 bg-yellow-50 px-3 py-2 leading-snug dark:border-l-yellow-900 dark:bg-yellow-700 dark:text-white/90">
    <p>The following steps where successfully tested on Ubuntu 22.04 and with v7.2.92 of the Unifi Controller</p>

</div>

<p>Community member <em>AmazedMender16</em> <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://community.ui.com/questions/UniFi-Installation-Scripts-or-UniFi-Easy-Update-Script-or-UniFi-Lets-Encrypt-or-UniFi-Easy-Encrypt-/ccbc7530-dd61-40a7-82ec-22b17f027776">has provided an easy installation script</a> which takes away all the hassle of installing the network controller.</p>
<p>Basically you’ll need to do the following steps:</p>
<ol>
<li>Login as root to the provisioned machine</li>
<li>Download and execute the install and the encrypt script with the following two commands</li>
</ol>
<pre><code data-theme="material-theme-palenight" data-lang="shell" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">rm</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">unifi-latest.sh</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;&gt;</span><span style="color: #A6ACCD;"> /dev/null</span><span style="color: #89DDFF;">;</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">wget</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://get.glennr.nl/unifi/install/install_latest/unifi-latest.sh</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;&amp;</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">bash</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">unifi-latest.sh</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #FFCB6B;">rm</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">unifi-easy-encrypt.sh</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;&gt;</span><span style="color: #A6ACCD;"> /dev/null</span><span style="color: #89DDFF;">;</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">wget</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">https://get.glennr.nl/unifi/extra/unifi-easy-encrypt.sh</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&amp;&amp;</span><span style="color: #A6ACCD;"> </span><span style="color: #FFCB6B;">bash</span><span style="color: #A6ACCD;"> </span><span style="color: #C3E88D;">unifi-easy-encrypt.sh</span></div></code></pre>
<p>You’ll find all available script options in the forum thread I linked above, if you provide none yourself the script uses defaults or asks for your input (like which domain name to use).</p>
<p>This process takes about 5 to 10 minutes from start to finish. You can then access your network controller with the domain you specified. The default ports are 8080 for an unsecured connection and 8443 for a secured connection. If you’ve used the encrypt command above you will be automatically redirected to HTTPS.</p>
<p>You now need to set up a new administrator account which you’ll use to access your controller. I used a local account for this since I didn’t want to create a Ubiquity account and didn’t need one for remote access since now the network controller itself was already remotely accessible.</p>
<p>After you finished these setup steps your network controller is ready to adopt your access points 🎉!</p>
<h2><a id="content-adopting-your-access-points" href="#content-adopting-your-access-points" class="prezet-heading" title="Permalink">#</a>Adopting your access points</h2>
<p>You could adopt your access points with the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.ui.com/download-software/">UniFi Network Mobile app</a> available for iPhone and Android if you have a controller in the same network as your access points. But this doesn’t work with a remote controller.</p>
<p>UniFi access points use default “inform URL” to announce their presence to local controllers. A remote controller doesn’t match the default inform URL, so you’ll have to manually set the inform URL for your access points.</p>
<p>I found a great solution in this blog post: <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://blog.ktz.me/how-to-adopt-a-unifi-ap-with-a-remote-controller/">How to adopt a UniFi AP with a remote controller</a></p>
<p>You’ll need to:</p>
<ol>
<li>Find out the IP address of your access point (this can be done in your router)</li>
<li>Connect with ssh: <code>ssh ubnt@ip-address</code>
The default user for a not adopted access point is “ubnt” and the default password is also “ubnt”.</li>
<li>After successfully logging in you can set the inform URL with this command:
<code>set-inform https://your-domain:8443/inform</code></li>
</ol>
<p>If everything worked you should get a message that your access point was announced for adoption. In your network controller the device should pop up, ready for adaption.</p>
<h3><a id="content-pitfall-when-adopting-more-than-one-access-point" href="#content-pitfall-when-adopting-more-than-one-access-point" class="prezet-heading" title="Permalink">#</a>Pitfall when adopting more than one access point</h3>
<p>After adopting the first access point I quickly set up a new wireless network with the new access point. This all worked so well that I deactivated the wireless connection in my Fritz Box and swapped everything over to the UniFi network.</p>
<p>The next day I wanted to adopt the second one. It didn’t work. I just couldn’t get a connection to the device.</p>
<p>Neither a reset, different cables or other IT tricks (like desperately staring at it for 2 minutes) worked.</p>
<p>After setting it up as a standalone access point I updated the firmware to the latest version. In hindsight I can’t tell if this was necessary but didn’t want to not tell you.</p>
<p>But it still didn’t work. The following was my thought process I went through to find the culprit and <em>finally</em> fix it:</p>
<ul>
<li>I connected to the standalone access points WLAN</li>
<li>ssh’ed into the AP using the credentials provided by the UniFi Network App on my iPhone</li>
<li>was stunned I could log in without any problems</li>
<li>reset the AP to the factory defaults using <code>set-default</code></li>
<li>reconnected to my UniFi WLAN (this will be important)</li>
<li>tried ssh’ing into the AP using the default user (<code>ubnt</code>, same password) which didn’t work. Seems like the firmware upgrade did nothing.</li>
<li>realising what changed. Yesterday I used the WLAN directly from the router. To set up the first access point. Could this be the reason I can’t get a connection?</li>
<li>reactivated the wireless connection in the Fritz Box and connected to it.</li>
<li>ssh’ed into the AP. This time it worked!</li>
</ul>
<p>Now I could just follow the steps outlined above, and now I have 2 working access points 🎉</p>
]]>
            </summary>
                                    <updated>2022-01-06T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Going all in with Jamstack and Eleventy]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/eleventyjs-is-great" />
            <id>https://tim-kleyersburg.de/eleventyjs-is-great</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>I wanted to create a new version of my website for quite some time. <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/tim-kleyersburg.de/tree/54da506cab1f437faf98c4c87e5c89dd82b99222">The old version</a> was just one site which was also statically generated HTML. I was using <code>gulp</code> along with a plugin so I could use <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://twig.symfony.com/doc/2.x/">Twig templates</a>, mainly because I hate to repeat myself in templates, even if it’s just a few hundred lines of markup.</p>
<p>I learned the hard way that missing abstraction is the source of many bugs that never should have occurred in the first place. In my early days it was much easier to just copy and paste the (seemingly final) pieces of code, swap out their contents and <em>be done with it</em> ™.</p>
<p>Jokes on me: I never should have assumed there’s such a thing as <em>being done</em> when it comes to the web. Everything is subject to change, at any time. Some may call this a curse, to me it’s one of the greatest things about the web. Every mistake can be undone, nothing is final. Accepting this premise greatly reduced my anxiety about shipping the perfect thing on the first try.</p>
<blockquote>
<p>Ship early, iterate, improve.</p>
</blockquote>
<p>I don’t know about you, but I know what <em>I</em> did last summer. I wanted to redesign my website and start to write articles about things I care.</p>
<p>At first I tried to come up with a solution with my existing setup but couldn’t really see how to integrate a blog into my site.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./screenshot-old-site-480w.jpg 480w, /articles/img/./screenshot-old-site-640w.jpg 640w, /articles/img/./screenshot-old-site-768w.jpg 768w, /articles/img/./screenshot-old-site-960w.jpg 960w, /articles/img/./screenshot-old-site-1536w.jpg 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/screenshot-old-site.jpg" alt="Screenshot of my previous site" />
<figcaption class="prezet-figcaption">Screenshot of my previous site</figcaption>
</figure>
<p>Like every good developer I naturally questioned my whole stack. Technology moves fast, especially when it comes to the web. My site was 2 or 3 years old, a Methusalem in web-years (that’s kinda like dog-years). Frontend tooling moved fast and there where a bunch of options I explored in my head.</p>
<p>Developer experience is important to me. I want to be able to make my own decisions as well as be pampered with relevant features that make my life easier and not reinvent the wheel.</p>
<p>I knew I did not want to install a CMS. At first <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://statamic.com">Statamic</a> seemed like an obvious choice. We are using it for every new site we build in <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.wacg.de">our agency</a> and I like everything about it.<br />
But: a CMS also has drawbacks. In my case I didn’t want another system I have to manage. I didn’t want its shiny cool features. I just wanted to create content and present myself as easy as possible.</p>
<p>So I explored something else.</p>
<h2><a id="content-jamstack-the-elephant-in-the-room" href="#content-jamstack-the-elephant-in-the-room" class="prezet-heading" title="Permalink">#</a>Jamstack: the elephant in the room</h2>
<p>As an avid reader of <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://css-tricks.com/">CSS-Tricks</a> the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://jamstack.org/">Jamstack</a> was a buzzword I’ve read a few times before. But it never really sparked my attention. Working mainly with the LAMP-stack (Linux, Apache, MySQL, PHP) at work, because the dynamic component of PHP always was a necessity, another “stack” seemed like something that would just waste my (very limited) mental capacity.</p>
<p>But: my personal site didn’t need PHP and I had no interest in using PHP for it.</p>
<h3><a id="content-what-is-the-jamstack" href="#content-what-is-the-jamstack" class="prezet-heading" title="Permalink">#</a>What is the Jamstack?</h3>
<p>Personally, what pushed me in the wrong direction how to think about the Jamstack was the word “stack” itself. It sounded like many parts of some server environment I was unfamiliar with. Which sounded like a lot to learn.</p>
<p>But that’s not the case.</p>
<blockquote>
<p>Jamstack is an <em>architecture</em> designed to make the web faster, more secure, and easier to scale.</p>
</blockquote>
<p>Source: <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://jamstack.org">jamstack.org</a>, emphasis by me.</p>
<p>It’s not about Apache, Linux or PHP. It’s basically about static HTML being served as fast as possible. And since it’s static it can also be served by a CDN from multiple locations with great speed and security.</p>
<p>Writing static HTML sounds like a nightmare if your website contains more than one page. I’d rather have a slower site and don’t have to change static HTML for every page of my website. I’d use a server side language like PHP to dynamically create the HTML.</p>
<p>Fortunately we don’t have to choose one over the other! One part of Jamstack is “Pre-rendering”, which essentially means to create all the HTML before serving it, not in the run-time.</p>
<h2><a id="content-static-site-generators--especially-eleventy" href="#content-static-site-generators--especially-eleventy" class="prezet-heading" title="Permalink">#</a>Static Site Generators – especially Eleventy</h2>
<p>This is where Static Site Generators (SSG) come in. One of many SSGs is <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://11ty.dev">Eleventy</a>. It seems like Eleventy really gained traction in the last year, I definitely read more about it in the last months (altough this could be because my interest in it increased). After about half a year of using it I can say: it is all I wanted from my previous setup but is so much more mature. It doesn’t dictate how I have to structure my website, what CSS framework I should use or what the best base markup is.</p>
<p>Eleventy focuses on your content and not on the layout. <em>Bring Your Own HTML</em> was never so easy. And in the end it’ll output static HTML which you can host, probably, anywhere.</p>
<p>If you are just starting your journey into web programming and are learning HTML and CSS for the first time, also learning about servers, content management systems, databases, and more might be overwhelming. Eleventy, with its flat learning curve, could be the perfect companion on your journey.<br />
You can start very small and add more and more features (if you want and need) on top of it.</p>
<h3><a id="content-resources" href="#content-resources" class="prezet-heading" title="Permalink">#</a>Resources</h3>
<p>There are a lot of great tutorials on the web on how to use Eleventy. These are some that helped me the most when starting out:</p>
<p>🔗 <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.11ty.dev/docs/">11ty.dev/docs/</a></p>
<p>The official documentation is a good place to start. It explains all concepts in detail and can be used as a reference.</p>
<p>🔗 <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://11ty.rocks/">11ty.rocks</a></p>
<p>11ty.rocks is a collection of many great resources, tutorials, plugins and much more, created by <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://x.com/5t3ph">Stephanie Eckles</a>. The site itself is, of course, also built with Eleventy. You can browse the source code on <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/5t3ph/11ty-rocks">GitHub</a>.</p>
<p>🔗 <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://learneleventyfromscratch.com/">learneleventyfromscratch.com</a></p>
<p>Learn Eleventy From Scratch was the tutorial I learned to most from. It is a 31 lessons tutorial which covers nearly every use case you could think of. If you already know some HTML and CSS, this should get you very far.</p>
]]>
            </summary>
                                    <updated>2022-01-03T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Easy dark mode with TailwindCSS]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/easy-darkmode-with-tailwind" />
            <id>https://tim-kleyersburg.de/easy-darkmode-with-tailwind</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Implementing dark mode with TailwindCSS is not that new. Experimental support was added in <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/tailwindlabs/tailwindcss/pull/2279">September 2020</a> and being promoted to a first class citizen in November 2020 when TailwindCSS was updated to v2.</p>
<p>I’m currently using version 3 of TailwindCSS with the amazing <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://tailwindcss.com/docs/typography-plugin">Typography plugin</a>. Dark mode is already activated because I am using the JIT (just-in-time) mode and is leveraging <code>prefers-color-scheme</code> to automatically switch between light and dark mode.</p>
<p>Since my site is very simple the changes were just a few lines of code. Just use the <code>dark</code> modifier in front of all classes that should be applied when the user prefers dark mode. In my case, most work was done with these few lines of code:</p>
<pre><code data-theme="material-theme-palenight" data-lang="html" class='torchlight has-add-lines has-remove-lines has-diff-lines' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">body</span><span style="color: #89DDFF;"> </span><span style="color: #C792EA;">class</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">bg-gray-50</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">&gt;</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #f07178;">    </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">body</span><span style="color: #89DDFF;"> </span><span style="color: #C792EA;">class</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">bg-gray-50 dark:bg-gray-800 dark:text-gray-100</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">&gt;</span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #C3E88D;">        </span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">body</span><span style="color: #89DDFF;">&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">body</span><span style="color: #89DDFF;">&gt;</span></div></code></pre>
<pre><code data-theme="material-theme-palenight" data-lang="postcss" class='torchlight has-add-lines has-remove-lines has-diff-lines' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #FFCB6B;">a</span><span style="color: #A6ACCD;"> {</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #f07178;">    </span><span style="color: #f07178;">@apply </span><span style="color: #f07178;">hover</span><span style="color: #f07178;">:text-gray-700 </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #C3E88D;">    </span><span style="color: #C3E88D;">@apply </span><span style="color: #C3E88D;">hover</span><span style="color: #C3E88D;">:text-gray-700 dark:hover:text-gray-400 </span><span style="color: #C3E88D;">// ;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">}</span></div></code></pre>
<p>After updating the Typography plugin to the latest version using <code>npm install -D @tailwindcss/typography@latest</code> the only change needed to use the default inverted dark mode is to add <em>one</em> class:</p>
<pre><code data-theme="material-theme-palenight" data-lang="html" class='torchlight has-add-lines has-remove-lines has-diff-lines' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">div</span><span style="color: #89DDFF;"> </span><span style="color: #C792EA;">class</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">prose</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">&gt;</span></div><div class='line line-remove line-has-background' style='background-color: #ff9cac20'><span style="color:#f07178; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #f07178;">    </span></div><div class='line line-add line-has-background' style='background-color: #89DDFF20'><span style="color:#C3E88D; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #C3E88D;">    </span><span style="color: #C3E88D;">&lt;</span><span style="color: #C3E88D;">div</span><span style="color: #C3E88D;"> </span><span style="color: #C3E88D;">class</span><span style="color: #C3E88D;">=</span><span style="color: #C3E88D;">&quot;</span><span style="color: #C3E88D;">prose dark:prose-invert</span><span style="color: #C3E88D;">&quot;</span><span style="color: #C3E88D;">&gt;</span><span style="color: #C3E88D;">&lt;!-- [tl! ++] --&gt;</span><span style="color: #C3E88D;">&lt;/</span><span style="color: #C3E88D;">div</span><span style="color: #C3E88D;">&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">div</span><span style="color: #89DDFF;">&gt;</span></div></code></pre>
<p>Now, to get all the details right, go through your site to spot everything that is not properly styled when using dark mode.</p>
<blockquote>
<p><em>Tip:</em> Using Chrome with the dev tools open, hit <code>Cmd + Shift + P</code> to open the command palette. Type <code>prefers-color-scheme</code> and choose the simulate option for <code>dark</code> or <code>light</code> to quickly review your changes without needing to change your system preferences.</p>
</blockquote>
<p>This may be the trickiest part if your site is larger. In my case <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/tim-kleyersburg.de/pull/13/files">I needed to change 6 files or 8 lines</a> to make it everything look good.</p>
]]>
            </summary>
                                    <updated>2022-01-01T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Moving from GitLab to GitHub]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/gitlab-to-github" />
            <id>https://tim-kleyersburg.de/gitlab-to-github</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>Since years I’ve been a big fan of GitLab. I started there because I could host my own private repos without any costs. At this time, GitHub only allowed private repos for paying customers.</p>
<p>I started using GitHub for my own projects in August 2021 with GitHubs announcement of <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.blog/changelog/2021-08-11-codespaces-is-generally-available-for-team-and-enterprise/">Codespaces being available for everyone</a> with a teams plan.</p>
<p>Just hitting <code>.</code> in the browser and having a full-blown VS Code instance at my fingertips had a real wow-effect for me. I really dig GitHubs interface, but if you just want to navigate a project to get a feel for it, I nearly always cloned the repo to my machine so I could use my IDE.<br />
Not anymore.</p>
<p>For work I could see the use of Codespaces as a development environment on demand, so I started playing around with GitHub a lot more. Spoiler alert: it wasn’t Codespaces or the full-blown VS Code instance that kept me there.</p>
<p>Although I knew my way around GitHub’s interface, I never used it to maintain a repository. Moving my repos to the same place where I explorer other people’s code was something I wanted to do for some time.</p>
<p>So the following thoughts are what drove me to GitHub and what kept me there.</p>
<h2><a id="content-user-interface" href="#content-user-interface" class="prezet-heading" title="Permalink">#</a>User interface</h2>
<p>GitHub has done so much in the last years to improve their interface. Compared to GitLab it feels light and much more modern.</p>
<p>The addition that you could click through code to get to function definitions or where a variable was declared made open source projects much more accessible in my opinion.</p>
<p>I often browse GitHub on my mobile phone after seeing some interesting tweets. GitHub does a much better job presenting the code and is much faster navigation-wise.</p>
<h2><a id="content-importing-a-project-from-gitlab" href="#content-importing-a-project-from-gitlab" class="prezet-heading" title="Permalink">#</a>Importing a project from GitLab</h2>
<p>Getting a project into GitHub is relatively simple since the addition of the import dialogue:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./importing-a-project-480w.png 480w, /articles/img/./importing-a-project-640w.png 640w, /articles/img/./importing-a-project-768w.png 768w, /articles/img/./importing-a-project-960w.png 960w, /articles/img/./importing-a-project-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/importing-a-project.png" alt="The add dropdown with the import option highlighted" />
<figcaption class="prezet-figcaption">The add dropdown with the import option highlighted</figcaption>
</figure>
<p>In the following dialogue you need to provide your previous repository’s clone URL, add a new name (or just use the one from GitLab), choose of you want to make the repo private or public and you are good to go.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./github-import-dialogue-480w.png 480w, /articles/img/./github-import-dialogue-640w.png 640w, /articles/img/./github-import-dialogue-768w.png 768w, /articles/img/./github-import-dialogue-960w.png 960w, /articles/img/./github-import-dialogue-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/github-import-dialogue.png" alt="The GitHub import dialogue" />
<figcaption class="prezet-figcaption">The GitHub import dialogue</figcaption>
</figure>
<p>You will be asked to authenticate yourself. These are your GitLab credentials.<br />
If you are getting the error <span class="bg-red-100 dark:bg-red-800"><code>No source repositories were detected at https://gitlab.com/timkley/your-repo. Please check the URL and try again.</code></span> you maybe copied the wrong URL. In my case though the problem was the activated two-factor-authentication on GitLab. The 2FA flow is not supported when importing to GitHub so you need to turn 2FA off in GitLab to be able to import your repositories.</p>
<p>The import itself is done in the background so you don’t have to keep the tab open, you’ll get an email after the import is done.<br />
Depending on the size of your repository this can take a few minutes. My repos are pretty small and the imports never took longer than a minute or two.</p>
<h2><a id="content-continuous-integration--continuous-deployment" href="#content-continuous-integration--continuous-deployment" class="prezet-heading" title="Permalink">#</a>Continuous integration / continuous deployment</h2>
<p>I’ve been using GitLabs shared runners for CI/CD pipelines for a few years now and think I got a good grasp at it (at least for my needs).</p>
<p>I set up automatic testing and deployment with it. And after a few months of iterating I now have a set of in good shape <code>.gitlab-ci.yml</code> files I can always reference when starting a new project.</p>
<p>I didn’t want to loose this functionality, so with the rising of GitHub Workflows I was curious to see how challenging the setup on GitHub would be.</p>
<p>Spoiler: It wasn’t challenging at all.</p>
<p>Maybe my needs aren’t that special or hard to solve. But the ease of setting up, for example, automatic testing for a Laravel application really impressed me. Where I tried to get it working for hours on GitLab, keeping countless tutorials and StackOverflow threads open in other tabs, I could find a ready to use <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/marketplace/actions/laravel-tests">GitHub Action in the Marketplace</a>.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./green-action-480w.png 480w, /articles/img/./green-action-640w.png 640w, /articles/img/./green-action-768w.png 768w, /articles/img/./green-action-960w.png 960w, /articles/img/./green-action-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/green-action.png" alt="The test workflow returning green" />
<figcaption class="prezet-figcaption">The test workflow returning green</figcaption>
</figure>
<p>For deployment I currently use <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://deployer.org">Deployer</a>. Of course there was <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/marketplace/actions/action-deployer-php">an action readily available</a> on the GitHub marketplace.</p>
<p>As with GitLab I struggled getting access to my repo. In this case, as last time, I got the following error:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">Host key verification failed.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">fatal: Could not read from remote repository.</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">Please make sure you have the correct access rights</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">and the repository exists.</span></div></code></pre>
<blockquote>
<p>I don’t like the presentation of this error message. The last part about making sure to having the correct access rights is very misleading if you miss the first part.</p>
</blockquote>
<p>In my <code>deploy.php</code> I had already set <code>StrictHostKeyChecking</code> as SSH option <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://deployer.org/docs/6.x/hosts">like described in the docs</a> but to no luck.</p>
<p>After logging into the host I wanted to deploy to and running a <code>git</code> command my <code>known_hosts</code> file was correctly updated and the next automatic deployment with my configured GitHub Action just worked:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./deployment-works-480w.png 480w, /articles/img/./deployment-works-640w.png 640w, /articles/img/./deployment-works-768w.png 768w, /articles/img/./deployment-works-960w.png 960w, /articles/img/./deployment-works-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/deployment-works.png" alt="Deployment works" />
<figcaption class="prezet-figcaption">Deployment works</figcaption>
</figure>
<p>To be fair: I learned a lot about CI pipelines in general in the last years, so maybe this comparison isn’t that fair.</p>
<p>But it emphasises one main advantage of GitHub: the community and open source approach in general. In the last years I maybe saw one or two open source projects hosted on GitLab. If you see open source code, you probably are surfing GitHub.</p>
<p>People solve their needs and open source the code they write for it in the process, making lifes of people like me a little (or sometimes a lot) easier.</p>
<h3><a id="content-downsides" href="#content-downsides" class="prezet-heading" title="Permalink">#</a>Downsides</h3>
<p>The whole concept of workflows, actions, steps is a little different than pipelines and jobs on GitLab. It’s confusing at first to get the semantics right and I’m still searching for a way to trigger a step in a workflow manually after the first steps where successful.</p>
<p>But: it gets easier with every project. After finishing the setup of my first Laravel project the transition of another project went smoothly. Much smoother than my second project on GitLab 😉.</p>
<p>I’ve gotten much better at adapting to new circumstances lately. In many cases you can achieve the same, or even better, result if you change your point of view. You’ll explorer new approaches from which you might benefit. So this may be a little philosophical, but: with every downside, every problem you solve comes an, maybee unseen, upside.</p>
<p>Looking at it from a business point of view (my agency also moved to GitHub), GitHub workflows are sill not as powerful as GitLab. Things like organization wide actions aren’t a thing, making it harder to keep your workflows DRY. |<a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/github/roadmap/issues/98">It is planned for Q4 2021</a>, though. Some of our repos still live with GitLab because of this. I don’t want to adjust multiple files by hand, so we are patiently waiting.</p>
<h2><a id="content-community-and-open-source" href="#content-community-and-open-source" class="prezet-heading" title="Permalink">#</a>Community and open source</h2>
<p>I mentioned the community aspect of GitHub a few times before. Having the privilege of browsing others people’s code, learning from them and maybe even contribute to open source projects has become more important to me.</p>
<p>It was around 2002 when I wrote my first piece of HTML. It took me this long (yes, that’s 19 years 🤯) to gain enough confidence in my skills to publicly build and show things I’ve done. And I think there is currently no better platform than GitHub for this.</p>
<h2><a id="content-final-words" href="#content-final-words" class="prezet-heading" title="Permalink">#</a>Final words</h2>
<p>My hope is that my move to GitHub helps my motiviation of giving back after years of using open source for my own projects and that, as many others before me, I can provide some value, may it be through an article like this one or contributions to open source in general.</p>
<p>Since I switched to GitHub I published two open source projects! I even got a few stars on one of them 😉. I’m planning to open source some of my other projects, too. For some I had more hopes that they would take of, but since this probably won’t happen the most useful thing I could do with them is make them open source.</p>
<h3><a id="content-my-recent-open-source-projects" href="#content-my-recent-open-source-projects" class="prezet-heading" title="Permalink">#</a>My recent open source projects</h3>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/eleventy-plugin-torchlight">An Eleventy plugin for the great Torchlight.dev</a></p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/awork-php-sdk">A PHP SDK for awork.io</a></p>
]]>
            </summary>
                                    <updated>2021-11-05T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[You should learn Vim!]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/you-should-learn-vim" />
            <id>https://tim-kleyersburg.de/you-should-learn-vim</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>You probably heard of <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.vim.org/">Vim</a>. Chances are you probably got stuck in Vim and angrily tried to exit it by smashing your mouse and keyboard.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./vim-meme-480w.jpg 480w, /articles/img/./vim-meme-640w.jpg 640w, /articles/img/./vim-meme-768w.jpg 768w, /articles/img/./vim-meme-960w.jpg 960w, /articles/img/./vim-meme-1536w.jpg 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/vim-meme.jpg" alt="Picture of a Tweet mocking how it looks when first users of Vim try to quit it." />
<figcaption class="prezet-figcaption">Picture of a Tweet mocking how it looks when first users of Vim try to quit it.</figcaption>
</figure>
<p>You maybe know by now to exit Vim by typing <code>:q</code> (or <code>:q!</code> if you want to be really sure Vim quits).<br />
Typing a colon tells Vim you want to enter a command (given you are in the so-called “normal mode”, which is the default mode of Vim after startup).</p>
<p>But let’s cycle back: why would you want to learn Vim? It feels complicated and the situations where you really need it seem to be scarce.</p>
<p>For me, it was exactly these situations where you are connected to a remote server over SSH to just make a quick edit to a config and the only editor installed is Vim. It wasn’t quick or easy at all for me. I always dreaded entering <code>vim example.conf</code> because, although knowing how to enter “input mode”, leaving it and quitting Vim, I somehow got stuck 2 out of 3 times anyway.</p>
<p>This frustrated me. But it wasn’t until my brother sat down with me to explain the basics of Vim that I started to get a good grasp of it. I think this is mainly because he explains things to me like I’m 5 years old and has nearly unlimited patience with my (probably dumb) questions.</p>
<p>This article’s aim isn’t to teach you Vim. Countless resources do a much better job explaining it than I could. <strong>I just want to provide you some insights into how learning Vim made me a faster and better developer</strong>. At the end of the article you’ll find resources that helped me to understand Vim better.</p>
<h2><a id="content-quick-introduction-to-vim" href="#content-quick-introduction-to-vim" class="prezet-heading" title="Permalink">#</a>Quick introduction to Vim</h2>
<p>Know how I said this article won’t be a tutorial in the last paragraph? Just stay with me for a quick introduction so we all are on the same page (and by writing about it forcing me to get my facts straight and learn some basics in the process).</p>
<p>A normal, “modeless” editor is like Notepad on Windows: it only has one “mode” where you can enter text.<br />
Vim falls in the category of “modal editors”. Instead of only having one mode it consists of multiple “modes” which you can use to efficiently edit text or code.</p>
<h3><a id="content-normal-mode" href="#content-normal-mode" class="prezet-heading" title="Permalink">#</a>Normal mode</h3>
<p>This is the default mode that Vim starts in. If you start typing, unexpected things will happen because <em>your keys have completely different meanings</em> in normal mode.
You can use them to navigate (using <code>h</code>, <code>j</code>, <code>k</code> and <code>l</code>), enter insert mode (by hitting <code>i</code>) or start typing commands by typing <code>:</code>. You will see the colon appear in the bottom left corner of the screen.</p>
<blockquote>
<p>The Normal mode is for executing commands (delete words, lines, paragraphs and a lot more). Basically, it’s used to edit your text (or code).
<a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://thevaluable.dev/phpstorm-vs-vim/">How did I Replace PhpStorm by Vim in my Heart, Matthieu Cneude</a></p>
</blockquote>
<h3><a id="content-insert-mode" href="#content-insert-mode" class="prezet-heading" title="Permalink">#</a>Insert mode</h3>
<p>This is the mode in which you have this familiar feeling because you can <em>just type away</em> like in your editor / IDE. Nothing bad happens when you type, you just see the characters happily appear on the screen as you type them. Until you hit <code>Esc</code> and fall back to “normal mode”.</p>
<h3><a id="content-visual-mode" href="#content-visual-mode" class="prezet-heading" title="Permalink">#</a>Visual mode</h3>
<p>The visual mode (which you can get into by hitting <code>v</code> when in the normal mode) works similarly to normal mode. The difference is that you are selecting the underlying text/code when moving through the code which gives you the possibility to copy, cut or just delete the selected pieces of code.</p>
<blockquote>
<p>Typing <code>V</code> (<code>Shift</code>-<code>v</code>) puts you in visual-<em>line</em> mode and also selects <em>the whole line under the cursor</em>. In visual-line mode you select whole lines which makes it super easy to move some blocks of code around.</p>
</blockquote>
<h3><a id="content-motions" href="#content-motions" class="prezet-heading" title="Permalink">#</a>Motions</h3>
<p>Although not the perfect synonym you could also call it “movements”. You hit one or more specific keys to get where you want to be. And you get there fast, after learning the basics. Some common motions are <code>e</code> to move <em>forward</em> to the end of the next word and <code>b</code> to move <em>backwards</em> to the beginning of a word.
There are many more motions which let you quickly get were you want to go, without ever touching your mouse. This applies to all of Vim: your hands stay on the keyboard much more, therefore reducing the time spent moving your right hand to the mouse, searching for what you want to click on, and getting back to typing.
This may seem negligible, but since you do this so often throughout your work day, you can really save a significant amount of time. There’s also the accompanying feeling of productivity and efficiency that makes this so compelling to me.</p>
<h2><a id="content-why-did-i-want-to-learn-vim" href="#content-why-did-i-want-to-learn-vim" class="prezet-heading" title="Permalink">#</a>Why did I want to learn Vim?</h2>
<p>Two driving factors pushed me to learn Vim.</p>
<p>One: my frustration not being able to edit a damn config file on a server without the fear of breaking it. Even after many years of programming.</p>
<p>Second: seeing developers like <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://x.com/jeffrey_way">Jeffrey Way</a> on <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://laracasts.com">Laracasts</a> or my brother editing code like magic.</p>
<p>How did they do it? The most common denominator was the block cursor I knew from Vim’s normal mode.</p>
<p>All in all, I had the feeling I was being held back by my not existing knowledge of the tools with which I program. I’d gotten very good at hitting nails with the wrong side of the hammer – and hitting my finger all the time. But I was <em>fast</em>. I learned how to type with more than 2 fingers early on, so I was almost always faster than my colleagues. But never as fast as some of the people I looked up to in my programming sphere.</p>
<h2><a id="content-why-is-vim-faster" href="#content-why-is-vim-faster" class="prezet-heading" title="Permalink">#</a>Why is Vim faster?</h2>
<p>Being faster can be a motivator to learn Vim. It certainly was for me, after I saw <em>just how much faster I could be</em>.<br />
It’s the little things that let you feel like you are in much more control. Most of the time your mental process is very similar to how Vim works.</p>
<p>An example: I write a lot of HTML, so working with tags is one of those things I need to do all the time. Selecting the whole tag, changing the content inside a paragraph tag, or just deleting the <code>&lt;div&gt;</code> altogether.</p>
<p>Let’s compare the process:</p>
<h3><a id="content-normal-modal-editor" href="#content-normal-modal-editor" class="prezet-heading" title="Permalink">#</a>Normal (modal) Editor</h3>
<ol>
<li>Go to tag</li>
<li>Select all of its content with the mouse or keyboard (be careful not the select one of the start or end braces <code>&lt;/&gt;</code>, fucking up your whole markup in the process).</li>
<li>Finally change it.</li>
</ol>
<h3><a id="content-vim--modeless-editor" href="#content-vim--modeless-editor" class="prezet-heading" title="Permalink">#</a>Vim / modeless editor</h3>
<ol>
<li>Place cursor somewhere in the tag while in normal mode</li>
<li>Type <code>cit</code></li>
<li>Boom💥</li>
</ol>
<p><code>cit</code> means <strong>c</strong>hange <strong>i</strong>nner <strong>t</strong>ag. I use it all the time, and it was easy to remember because it just does what it says. Just remember you want to change the inner tag, and you won’t forget what to type.</p>
<p>The above process doesn’t look so much different written down, but I would argue the Vim way is 5 times faster. Maybe 10 times if you use your mouse to select what you want to change.</p>
<blockquote>
<p>Vim takes so much pain out of the boring editing stuff when writing code that the experience of writing code becomes much more enjoyable. I now spend more time coding than I did before because I don’t waste valuable time moving code blocks around or fiddling with the mouse to change some characters.<br />
The added time spent on programming probably also had and has a positive impact on my continued learning how to program.</p>
</blockquote>
<h2><a id="content-how-i-got-into-vim-and-got-better-with-it" href="#content-how-i-got-into-vim-and-got-better-with-it" class="prezet-heading" title="Permalink">#</a>How I got into Vim and got better with it</h2>
<p>After my crash course, I installed/activated Vim mode in all my editors (I use PHPStorm and VSCode) and just forced myself to use it. After the first week (in which I felt very unproductive) I got used to it and quickly got back to the speed I had before.
It clicked the first time I changed a file on a remote server. I felt so comfortable! And I was quick compared to before. This experience was what kept me going.</p>
<p>I took another lesson from my brother. He showed me some more advanced motions and commands. But I plateaued quickly. Don’t get me wrong: I still got faster from day to day because I got used to it. Where I had to think in the beginning to hit the correct keys I can now mainly count on my muscle memory.</p>
<h3><a id="content-resources" href="#content-resources" class="prezet-heading" title="Permalink">#</a>Resources</h3>
<p>Afar from the personal conversations with my brother, where I could also ask questions or get another explanation, the following resources helped me a lot to better understand Vim, see how other people use it and what their learning process looked like.</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://laracasts.com/series/vim-mastery"><strong>Vim Mastery by Jeffrey Way on laracasts.com</strong></a> (paid)<br />
I always recommend Jeffrey and Laracasts. Laracasts started as a video learning platform for Laravel but also has many great series about everything around programming.
Vim Mastery is a great course.</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://thevaluable.dev/vim-beginner/"><strong>Vim for Beginners by Matthieu Cneude</strong></a> (free)<br />
This is a complete series not only for beginners but also advanced or expert users.
In my opinion, Matthieu has a great mindset when it comes to how to use the tools at your hand, how to customize them to suit your needs and get the most out of them.</p>
<p><a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://youtu.be/wlR5gYd6um0"><strong>Mastering the Vim language</strong></a> (free, video)<br />
A great talk (a bit longy with ~ 30 minutes of playtime) by Chris Toomey for beginners. Chris is a splendid speaker, so the video doesn’t feel that long.</p>
<h2><a id="content-final-words" href="#content-final-words" class="prezet-heading" title="Permalink">#</a>Final words</h2>
<p>If you now feel like you could also level up your skills: just do it! Activate Vim mode in VSCode or whatever editor you use <em>and just start</em>! It’s easier than you think and very rewarding in its own way.</p>
<p>Matthieu phrased it perfectly in his <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://thevaluable.dev/vim-beginner/">Vim for Beginners</a> article:</p>
<blockquote>
<p>To me, Vim is the gamification of coding.</p>
</blockquote>
<p>It really is. Like in a game you need to learn when to push the right buttons to get the outcome you want. It is a lot of fun after the first few steps because you feel how much you are progressing.</p>
<p>I hope you give Vim a chance!</p>
<hr />
<p>A special thanks to <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/pitkley">pitkley</a> for taking the time and <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/timkley/tim-kleyersburg.de/pull/8#pullrequestreview-775661926">reviewing my first draft</a> so thoroughly. I, again, learned some small things I wasn’t aware of before 🥰.</p>
]]>
            </summary>
                                    <updated>2021-10-17T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Customisable TailwindCSS colours without build step]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/tailwindcss-with-css-variables" />
            <id>https://tim-kleyersburg.de/tailwindcss-with-css-variables</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<p>One of our clients provides a marketplace. You can book a package and get a complete website with content, e-commerce functionality and the possibility to add your own content and define your brand colours.</p>
<p>The project was built with <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://tailwindcss.com">TailwindCSS</a> and our config looked something like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">module.exports</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">theme</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">extend</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">            </span><span style="color: #F07178;">colors</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">primary-dark</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">#1c3d5a</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">                </span><span style="color: #F07178;">primary</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">#3490dc</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">primary-light</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">#bcdefa</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// ... rest of the config</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Our first problem was with semantics. The first layout consisted of one color in different shades. It made sense to group these colours together. But if the clients choose 3 very different colours, because that’s their corporate design, these keywords would loose meaning.</p>
<p>This is one of the very few and still very manageable downsides of using TailwindCSS. You have to search&amp;replace in all your template files if you want to rename a colour. But most of the time your good to go with a regular expression like this one:</p>
<pre><code data-theme="material-theme-palenight" data-lang="regex" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">(?:$|^|)(your-color-name)(?:$|^|)</span></div></code></pre>
<blockquote>
<p>I don’t understand RegEx enough to explain this one in detail. I <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://regex101.com/library/1COSOf">just googled it</a>.</p>
</blockquote>
<p>In the end we didn’t change the names because the entry page still used the original layout, so in development it would only loose meaning when manually choosing a marketplace vendor. We could live with that.</p>
<h2><a id="content-css-variables-to-the-rescue" href="#content-css-variables-to-the-rescue" class="prezet-heading" title="Permalink">#</a>CSS Variables to the rescue!</h2>
<p>After initial thoughts of generating the accompanying CSS files for each vendor (the dev ops guy hated that idea) we put together a proof of concept using <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties">CSS Variables</a>.</p>
<p>We changed our <code>tailwind.config.js</code> to this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">module.exports</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">theme</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">        </span><span style="color: #F07178;">extend</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">            </span><span style="color: #F07178;">colors</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">primary-dark</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">var(--primary-dark)</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">                </span><span style="color: #F07178;">primary</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">var(--primary)</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">                </span><span style="color: #89DDFF;">&#39;</span><span style="color: #F07178;">primary-light</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">var(--primary-light)</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">            </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// ... rest of the config</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>Now we could just set the colours from our server side rendered templates like this:</p>
<pre><code data-theme="material-theme-palenight" data-lang="html" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">style</span><span style="color: #89DDFF;">&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">:</span><span style="color: #C792EA;">root</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #A6ACCD;">        --primary-dark</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">#</span><span style="color: #A6ACCD;">1c3d5a</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #A6ACCD;">        --primary</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">#</span><span style="color: #A6ACCD;">3490dc</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">        --primary-light</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">#</span><span style="color: #A6ACCD;">bcdefa</span><span style="color: #89DDFF;">;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">style</span><span style="color: #89DDFF;">&gt;</span></div></code></pre>
<blockquote>
<p>The particular colors are coming from the database where all vendors are stored.</p>
</blockquote>
<p>And that’s it for the frontend part! Now every vendor can set his own colours in his backend and the change live as soon as the settings were saved.</p>
<h3><a id="content-live-preview" href="#content-live-preview" class="prezet-heading" title="Permalink">#</a>Live preview</h3>
<p>With just using CSS we could also provide a live preview in the frontend for logged in vendors so they could see how their colour combination would look.</p>
<p>Setting a CSS variable from JavaScript is as simple as calling the <code>setProperty</code> method with the name of your variable and the new value on whichever element makes sense for your application.</p>
<p>Since we set those variables for <code>:root</code> the <code>documentElement</code> made sense for us:</p>
<pre><code data-theme="material-theme-palenight" data-lang="js" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #A6ACCD;">document</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">documentElement</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">style</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">setProperty</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">--variable-name</span><span style="color: #89DDFF;">`</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> newValue)</span></div></code></pre>
<h3><a id="content-codepen-example" href="#content-codepen-example" class="prezet-heading" title="Permalink">#</a>CodePen example</h3>
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="zYzNdRX" data-user="timkley" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/timkley/pen/zYzNdRX">
  Change CSS custom properties with J</a> by Tim (<a href="https://codepen.io/timkley">@timkley</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<h3><a id="content-internet-explorer-support" href="#content-internet-explorer-support" class="prezet-heading" title="Permalink">#</a>Internet Explorer support</h3>
<p>In an earlier iteration we still needed to support Internet Explorer 11. Fortunately this isn’t the case anymore, but if you want or need to support IE11 you can use the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/jhildenbiddle/css-vars-ponyfill">this ponyfill</a>.</p>
<p>After successful installation it was literally a one-liner, we only had to call <code>cssVars()</code> when we detected we had an IE11 user on the site.</p>
<p>We’re still glad we could remove this dependency from our bundle.</p>
]]>
            </summary>
                                    <updated>2021-09-08T00:00:00+00:00</updated>
        </entry>
            <entry>
            <title><![CDATA[From Vue to Alpine.js: Our journey]]></title>
            <link rel="alternate" href="https://tim-kleyersburg.de/articles/from-vue-to-alpinejs" />
            <id>https://tim-kleyersburg.de/from-vue-to-alpinejs</id>
            <author>
                <name><![CDATA[Tim Kleyersburg]]></name>
            </author>
            <summary type="html">
                <![CDATA[<h2><a id="content-the-problem" href="#content-the-problem" class="prezet-heading" title="Permalink">#</a>The problem</h2>
<p>We relaunched the e-commerce site from one of our clients in the end of 2019. It was a big relaunch, impacting the overall design, template and frontend architecture.<br />
The only thing pretty much left unchanged was the backend.</p>
<p>The main goals defined with the client were:</p>
<ul>
<li>optimise PageSpeed metrics</li>
<li>Improve usability and therefore conversion rate</li>
</ul>
<p>After months of implementing, the client, and we, were happy with the results. We hit green ratings in all 4 of Lighthouse’s categories and the conversion rate improved significantly.</p>
<p>That was until Google decided to change how Lighthouse calculates the performance score (<a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://web.dev/lighthouse-whats-new-6.0/">What’s New in Lighthouse 6.0</a>). Our scores dropped from solid green ratings to red.</p>
<p>As a quick reminder: in addition to things like TTFB (Time to first byte) and overall network performance things like file sizes, optimised CSS or webfonts, Lighthouse moved the focus to frontend stuff like „Time To Interactive“ or „Largest Contentful Paint“. As the web becomes more and more interactive the perceived performance becomes more important. So, in theory, we agreed with Google’s step to include those new metrics. Although it’s comparing apples to oranges when Google presents nearly non-interactive sites like blogs as „good examples“.</p>
<p>After the first meetings with the client we postponed optimising for the new Lighthouse metrics. After analysing what devices our visitors most commonly used we couldn’t rationalise investing much time into a problem all our competitors faced also.</p>
<p>That changed with Google’s announcement that some of these new metrics would impact search ranking (<a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developers.google.com/search/blog/2020/11/timing-for-page-experience">Timing for bringing page experience to Google Search</a>).</p>
<p>It was clear we shouldn’t postpone this issue further (this was late 2020 / early 2021).</p>
<p>We talked to the client again and decided we could gain a significant competitors advantage and of course perceived speed for the end users.</p>
<h2><a id="content-how-we-analyzed" href="#content-how-we-analyzed" class="prezet-heading" title="Permalink">#</a>How we analyzed</h2>
<p>We now needed more data. To be frank: until this point the deeper performance metrics were never our biggest concern so we had some catching up to do.<br />
Using Google Chrome we analysed the website with a mix of the built in Lighthouse app as well as the Performance tab in DevTools.</p>
<h2><a id="content-our-setup-at-this-time" href="#content-our-setup-at-this-time" class="prezet-heading" title="Permalink">#</a>Our setup at this time</h2>
<p>When we did the relaunch we completely reimplemented the frontend architecture. We were using Vue 2 as our javascript framework of choice and TailwindCSS.
Everything was bundled by Symfony Encore (Webpack).</p>
<p>The site was no SPA, instead we wrapped the whole site with a <code>#app</code> div which we bound the root instance to. We used renderless components (<a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://adamwathan.me/renderless-components-in-vuejs/">Renderless Components in Vue.js</a>) so we could write most of our templates in Twig and also make easy use of server side variables without the need of writing an API.</p>
<pre><code data-theme="material-theme-palenight" data-lang="html" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">notepad-star</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #89DDFF;">    </span><span style="color: #C792EA;">:product-id</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">{{ product_id }}</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #89DDFF;">    </span><span style="color: #C792EA;">:initial-star</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">{{ is_stared(product_id) ? &#39;true&#39; : &#39;false&#39; }}</span><span style="color: #89DDFF;">&quot;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #89DDFF;">&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">div</span><span style="color: #89DDFF;">&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #A6ACCD;">        </span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">button</span><span style="color: #89DDFF;"> </span><span style="color: #C792EA;">@click.prevent</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">toggle</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">&gt;</span><span style="color: #A6ACCD;">Toggle</span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">button</span><span style="color: #89DDFF;">&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">div</span><span style="color: #89DDFF;">&gt;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">8</span><span style="color: #89DDFF;">&lt;/</span><span style="color: #F07178;">notepad-star</span><span style="color: #89DDFF;">&gt;</span></div></code></pre>
<blockquote>
<p><code>product_id</code> is a server side variable and <code>is_stared(product_id)</code> a Twig functions. Both are passed into the Vue component as props.</p>
</blockquote>
<h2><a id="content-analysing-the-problem" href="#content-analysing-the-problem" class="prezet-heading" title="Permalink">#</a>Analysing the problem</h2>
<p>Our process looked like this:</p>
<ol>
<li>Do performance report in chrome</li>
<li>Look at the numbers</li>
<li>Change something</li>
<li>Create new report to confirm or refute our assumption</li>
</ol>
<p>The most significant part of the Performance Report was the „Evaluating scripts“ part. It seemed like the browser had a lot of work to do when evaluating our javascript bundle.</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./performance-report-before-480w.png 480w, /articles/img/./performance-report-before-640w.png 640w, /articles/img/./performance-report-before-768w.png 768w, /articles/img/./performance-report-before-960w.png 960w, /articles/img/./performance-report-before-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/performance-report-before.png" alt="Screenshot of Lighthouse report" />
<figcaption class="prezet-figcaption">Screenshot of Lighthouse report</figcaption>
</figure>
<p><em>Live environment</em></p>
<p>Our first step was to comment out the script tag so see how that improved our metrics.<br />
Turns out, pretty significantly:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./performance-report-no-scripts-480w.png 480w, /articles/img/./performance-report-no-scripts-640w.png 640w, /articles/img/./performance-report-no-scripts-768w.png 768w, /articles/img/./performance-report-no-scripts-960w.png 960w, /articles/img/./performance-report-no-scripts-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/performance-report-no-scripts.png" alt="Screenshot of Lighthouse report without scripts" />
<figcaption class="prezet-figcaption">Screenshot of Lighthouse report without scripts</figcaption>
</figure>
<p><em>Note: This report was generated in our development environment. We have a difference of about 10-15% to the live environment.</em></p>
<h2><a id="content-what-needed-to-be-done" href="#content-what-needed-to-be-done" class="prezet-heading" title="Permalink">#</a>What needed to be done</h2>
<p>We identified the following things needed the most attention:</p>
<ol>
<li>Optimising preloading of key assets</li>
<li>Minimising blocking time</li>
<li>Optimising time to interactive</li>
<li>Minimising main thread work</li>
</ol>
<h2><a id="content-part-1-optimising-preloading" href="#content-part-1-optimising-preloading" class="prezet-heading" title="Permalink">#</a>Part 1: Optimising preloading</h2>
<p>We experienced some render blocking because of quick and dirty implementations without proper performance testing for the Google Tag Manager and our CCM.</p>
<p>We tested different combinations of preloading and pre-connecting and ended up with the following results:</p>
<ul>
<li>Preload for key assets like the CCM script</li>
<li>Preconnect for GTM</li>
<li>Preloading of our own key assets (like webfonts or our main css/js file)</li>
</ul>
<p>The following tools were used:</p>
<ul>
<li>Lighthouse: provides direct insight which assets should be preloaded</li>
<li>Firefox: the devtools tell you which fonts you are preloading but aren‘t used within the first seconds</li>
</ul>
<h2><a id="content-part-2-4-optimising-the-rest" href="#content-part-2-4-optimising-the-rest" class="prezet-heading" title="Permalink">#</a>Part 2-4: Optimising the rest</h2>
<p>After we optimised the preloading we knew the only parts left that are impacting our key metrics could only be our own assets as in our javascript bundle.</p>
<p>All these metrics are somewhat connected to each other because of this.</p>
<h3><a id="content-finding-the-problem" href="#content-finding-the-problem" class="prezet-heading" title="Permalink">#</a>Finding the problem</h3>
<p>Before optimising we first needed to understand the problem on a deeper level. As mentioned earlier we are using renderless components for all our Vue components and are wrapping our whole site with the Vue instance. This gives us the benefit of simple global state management. We can also simply sprinkle in some interaction by adding another mixin, for example:</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">export</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> searchOverlay </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">data</span><span style="color: #89DDFF;">()</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">4</span><span style="color: #F07178;">            showSearchOverlay</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #FF9CAC;">false</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">5</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">6</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">7</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p><em>Example of global state / functionality provided by a mixin</em></p>
<h3><a id="content-different-versions-of-vue" href="#content-different-versions-of-vue" class="prezet-heading" title="Permalink">#</a>Different versions of Vue</h3>
<p>Vue comes in two different „flavours“: the runtime-only-build and the one with included template compiler.<br />
The runtime-only build is much smaller. It can only be used if you are using Single-File-Components. Those will be included in your bundle and therefore make
the template compiler unnecessary.<br />
The template compiler enables us to provide templates from our templating engine (Twig) into the default slots of our renderless components.</p>
<p>But: because we were wrapping the complete site, Vue has to evaluate every DOM node it finds (around 4,500 nodes for the homepage).<br />
That’s why we had such a long evaluate script time.</p>
<p>Now that we better understood the root cause we could start evaluating paths to mitigate this issue.</p>
<p>Unfortunately we couldn’t find a way to significantly improve the performance with our current architecture. There is just no good way to switch to the runtime-only build with our template architecture and backend structure.</p>
<h3><a id="content-evaluating-the-needs" href="#content-evaluating-the-needs" class="prezet-heading" title="Permalink">#</a>Evaluating the needs</h3>
<p>Next, we put together the components and interactivity we currently provide on the site to get a bird’s eye view of the things we need from a new solution.</p>
<p>Here are some examples of components we have on the site:</p>
<ul>
<li>Live search</li>
<li>Dynamic offcanvas cart</li>
<li>A flyout menu</li>
<li>Modals</li>
</ul>
<p>We also have some smaller functions (previously provided by mixins). Those functions are mostly used for things that don‘t need a separate component because they hold little to no state but should be easily be triggerable from everywhere, like:</p>
<ul>
<li>Dynamically changing a product variant</li>
<li>Opening the shipping modal</li>
<li>Showing / hiding a global information banner</li>
</ul>
<p>One thing all these things had in common: many of them needed to communicate with each other.</p>
<p>The components were not the most complex, mostly providing interactivity or preventing site reloads.</p>
<p>What we needed (and wanted) from a new framework was:</p>
<ul>
<li>Reactivity (templates rerender when data changes)</li>
<li>Event system for easy communication between components</li>
<li>Small footprint</li>
</ul>
<h3><a id="content-enter-alpinejs" href="#content-enter-alpinejs" class="prezet-heading" title="Permalink">#</a>Enter Alpine.js</h3>
<p>We already used <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://alpinejs.dev">Alpine.js</a> in other projects to provide interactivity and really liked it. Since we were already using <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://tailwindcss.com">TailwindCSS</a>, the claim “Like TailwindCSS for Javascript” really resonated with us.</p>
<p>We weren’t sure if Alpine.js could handle such a big e-commerce site. So our next step was to build a proof of concept to see if the hardest parts could be handled.</p>
<p>We rebuild the most important components like our offcanvas component, the dynamic cart and the main menu. These components used all the above mentioned needs. So if we could reintegrate these components we could be very confident that all other components could be rewritten also.</p>
<p>After about a day’s work we were super happy with the results. We hit a few roadblocks along the way, but since most of our logic is of course Javascript (although in Vue) the translation was very straight-forward for the most parts.</p>
<p>We settled on the following architecture:</p>
<pre><code data-theme="material-theme-palenight" data-lang="text" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #A6ACCD;">js/</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">├── components/</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">│   ├── cart.js</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">│   ├── mobileMenu.js</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">│   └── ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">├── enums/</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #A6ACCD;">│   ├── events.js</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #A6ACCD;">│   └── ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #A6ACCD;">├── helper/</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #A6ACCD;">│   └── customEvent.js</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">├── providers/</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #A6ACCD;">│   ├── cart.js</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #A6ACCD;">│   ├── googleTagManager.js</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #A6ACCD;">│   └── ...</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">└── stores/</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">    ├── cart.js</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #A6ACCD;">    ├── global.js</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #A6ACCD;">    └── ...</span></div></code></pre>
<h4><code>components</code></h4>
<p>Components are defined as window-scoped function which return an object to be used in Alpines <code>x-data</code> attribute to initialize a component.</p>
<p>This is a dumbed down example of our modal component, read on how we use the <code>customEvent</code> function and “<code>enums</code>”.</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">import</span><span style="color: #A6ACCD;"> customEvent </span><span style="color: #89DDFF;">from</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">@/helper/customEvent</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #89DDFF;">import</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">MODAL_OPEN</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">MODAL_OPENED</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">MODAL_CLOSE</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">}</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">from</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">@/enums/events</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #A6ACCD;">window</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">modal</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">()</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #A6ACCD;"> (</span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">open</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #FF9CAC;">false</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">init</span><span style="color: #89DDFF;">()</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">if</span><span style="color: #F07178;"> (</span><span style="color: #89DDFF;">this.</span><span style="color: #A6ACCD;">instantDisplay</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">!==</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">undefined</span><span style="color: #F07178;">) </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">this.</span><span style="color: #A6ACCD;">open</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #FF9CAC;">true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">close</span><span style="color: #89DDFF;">()</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">this.</span><span style="color: #A6ACCD;">open</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #FF9CAC;">false</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #F07178;">        </span><span style="color: #82AAFF;">customEvent</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">MODAL_CLOSE</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">this.</span><span style="color: #A6ACCD;">name</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">wrapper</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #A6ACCD;">        </span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> [</span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">@</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">MODAL_OPEN</span><span style="color: #89DDFF;">}</span><span style="color: #C3E88D;">.window</span><span style="color: #89DDFF;">`</span><span style="color: #A6ACCD;">]</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">e</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">if</span><span style="color: #F07178;"> (</span><span style="color: #A6ACCD;">modalToOpen</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">!==</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">e</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">payload</span><span style="color: #89DDFF;">.</span><span style="color: #A6ACCD;">name</span><span style="color: #F07178;">) </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #F07178;">                </span><span style="color: #89DDFF;">return</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">19</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">20</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">21</span><span style="color: #F07178;">            </span><span style="color: #82AAFF;">customEvent</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">MODAL_OPENED</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">this.</span><span style="color: #A6ACCD;">name</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">22</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">this.</span><span style="color: #A6ACCD;">open</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #FF9CAC;">true</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">23</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">24</span><span style="color: #A6ACCD;">    </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">25</span><span style="color: #89DDFF;">}</span><span style="color: #A6ACCD;">)</span></div></code></pre>
<h4><code>enums</code></h4>
<p>These are not really enums. It’s just a helper file which holds constants which we can use throughout the codebase without fear of breaking something when an event is renamed.</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> MODAL_OPEN </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">modal-open</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">2</span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> MODAL_OPENED </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">modal-opened</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">3</span><span style="color: #C792EA;">const</span><span style="color: #A6ACCD;"> MODAL_CLOSE </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">modal-close</span><span style="color: #89DDFF;">&#39;</span></div></code></pre>
<h4><code>helper</code></h4>
<p>Helper functions which we can import from anywhere that hold no state.</p>
<p>This is what our <code>customEvent</code> helper looks like.</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">export</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">default</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">name</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> payload </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">null,</span><span style="color: #A6ACCD;"> originalEvent </span><span style="color: #89DDFF;">=</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">null)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// options should be an object with:</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// name: &#39;string&#39;,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// payload: &#39;object&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #89DDFF;">    </span><span style="color: #676E95;">// originalEvent: &#39;this&#39;, if you need the actual target that was clicked</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">const</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">customEvent</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">new</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">CustomEvent</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">name</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">        detail</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">            payload</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">payload</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #F07178;">            originalEvent</span><span style="color: #89DDFF;">:</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">originalEvent</span><span style="color: #89DDFF;">,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span><span style="color: #F07178;">    </span><span style="color: #A6ACCD;">window</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">dispatchEvent</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">customEvent</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #89DDFF;">}</span></div></code></pre>
<p>This little helper gives us a lot of flexibility and we use it from anywhere (directly in the HTML for example) without the need of defining countless Alpine components.</p>
<p>This just uses the standard <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent">CustomEvent API</a> under the hood</p>
<p>We made the function available in the window-scope so we can just use it in an <code>onclick</code> attribute:</p>
<pre><code data-theme="material-theme-palenight" data-lang="html" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">1</span><span style="color: #89DDFF;">&lt;</span><span style="color: #F07178;">button</span><span style="color: #89DDFF;"> </span><span style="color: #C792EA;">type</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #C3E88D;">button</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;"> </span><span style="color: #C792EA;">onclick</span><span style="color: #89DDFF;">=</span><span style="color: #89DDFF;">&quot;</span><span style="color: #82AAFF;">customEvent</span><span style="color: #C3E88D;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">name</span><span style="color: #89DDFF;">&#39;,</span><span style="color: #C3E88D;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">payload</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">)</span><span style="color: #89DDFF;">&quot;</span><span style="color: #89DDFF;">&gt;&lt;/</span><span style="color: #F07178;">button</span><span style="color: #89DDFF;">&gt;</span></div></code></pre>
<h4><code>providers</code></h4>
<p>Providers provide reusable functionality to provide data. Think of it as the client side API layer. As with the helper functions these functions should hold no state and just be consumed by our components.</p>
<p>This is what the provider for the live search roughly looks like:</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #89DDFF;">import</span><span style="color: #A6ACCD;"> customEvent </span><span style="color: #89DDFF;">from</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">@/helper/customEvent</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #89DDFF;">import</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">SEARCH_GET</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">}</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">from</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">@/enums/events</span><span style="color: #89DDFF;">&#39;</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #C792EA;">async</span><span style="color: #A6ACCD;"> </span><span style="color: #C792EA;">function</span><span style="color: #A6ACCD;"> </span><span style="color: #82AAFF;">getResultFor</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">searchTerm</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #F07178;">    </span><span style="color: #C792EA;">let</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">result</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">undefined</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">await</span><span style="color: #F07178;"> </span><span style="color: #82AAFF;">fetch</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">`</span><span style="color: #C3E88D;">/search?q=</span><span style="color: #89DDFF;">${</span><span style="color: #A6ACCD;">searchTerm</span><span style="color: #89DDFF;">}`</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">then</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">)</span><span style="color: #F07178;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">response</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">json</span><span style="color: #F07178;">())</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">then</span><span style="color: #F07178;">(</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">data</span><span style="color: #89DDFF;">)</span><span style="color: #F07178;"> </span><span style="color: #C792EA;">=&gt;</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #F07178;">            </span><span style="color: #A6ACCD;">result</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">data</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">11</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">}</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">12</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">13</span><span style="color: #F07178;">    </span><span style="color: #82AAFF;">customEvent</span><span style="color: #F07178;">(</span><span style="color: #A6ACCD;">SEARCH_GET</span><span style="color: #89DDFF;">,</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">result</span><span style="color: #F07178;">)</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">14</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">15</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">return</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">result</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">16</span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">17</span>&nbsp;</div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">18</span><span style="color: #89DDFF;">export</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">getResultFor</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">}</span></div></code></pre>
<h4><code>stores</code></h4>
<p>Since we depend on Alpine.js 2.8 we use <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/ryangjchandler/spruce">Spruce</a> for global state management.</p>
<p>We have one store for each part, these are the few lines of code we use to manage the state of our mega menu:</p>
<pre><code data-theme="material-theme-palenight" data-lang="javascript" class='torchlight' style='background-color: #292D3E; --theme-selection-background: #00000080;'><!-- Syntax highlighted by torchlight.dev --><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 1</span><span style="color: #A6ACCD;">Spruce</span><span style="color: #89DDFF;">.</span><span style="color: #82AAFF;">store</span><span style="color: #A6ACCD;">(</span><span style="color: #89DDFF;">&#39;</span><span style="color: #C3E88D;">megamenu</span><span style="color: #89DDFF;">&#39;</span><span style="color: #89DDFF;">,</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 2</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">activeId</span><span style="color: #89DDFF;">:</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">null,</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 3</span><span style="color: #A6ACCD;">    </span><span style="color: #F07178;">toggle</span><span style="color: #89DDFF;">(</span><span style="color: #A6ACCD;">id</span><span style="color: #89DDFF;">)</span><span style="color: #A6ACCD;"> </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 4</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">if</span><span style="color: #F07178;"> (</span><span style="color: #A6ACCD;">id</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">===</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">this.</span><span style="color: #A6ACCD;">activeId</span><span style="color: #F07178;">) </span><span style="color: #89DDFF;">{</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 5</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">this.</span><span style="color: #A6ACCD;">activeId</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">null</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 6</span><span style="color: #F07178;">            </span><span style="color: #89DDFF;">return</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 7</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">}</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 8</span><span style="color: #F07178;">        </span><span style="color: #89DDFF;">this.</span><span style="color: #A6ACCD;">activeId</span><span style="color: #F07178;"> </span><span style="color: #89DDFF;">=</span><span style="color: #F07178;"> </span><span style="color: #A6ACCD;">id</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number"> 9</span><span style="color: #F07178;">    </span><span style="color: #89DDFF;">},</span></div><div class='line'><span style="color:#3A3F58; text-align: right; -webkit-user-select: none; user-select: none;" class="line-number">10</span><span style="color: #89DDFF;">}</span><span style="color: #A6ACCD;">)</span></div></code></pre>
<h3><a id="content-comparison-between-old-and-new-metrics" href="#content-comparison-between-old-and-new-metrics" class="prezet-heading" title="Permalink">#</a>Comparison between old and new metrics</h3>
<p>After having settled on an architecture and implementing our most complex components without problems we were very confident we are on the right path. The metrics looked promising, gaining 15-20 percentage points in most performance categories.</p>
<p>We were eager to get complete metrics after implementing all other components. My heart-rate elevated a little when I pressed “Generate Report” in the Lighthouse tab.<br />
We expected we wouldn’t come to the 56 points completely without scripts, but here we are:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./performance-report-with-alpine-480w.png 480w, /articles/img/./performance-report-with-alpine-640w.png 640w, /articles/img/./performance-report-with-alpine-768w.png 768w, /articles/img/./performance-report-with-alpine-960w.png 960w, /articles/img/./performance-report-with-alpine-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/performance-report-with-alpine.png" alt="Screenshot of Lighthouse report after conversion to Alpine.js" />
<figcaption class="prezet-figcaption">Screenshot of Lighthouse report after conversion to Alpine.js</figcaption>
</figure>
<p>This again was our dev environment, thus many opportunities shown here wouldn’t apply in the live environment.</p>
<p>Pleased with the results we did some final tests, cleaned up the code and planned the release for the next Monday.</p>
<p>The “Merge”-Button was pressed at 8:24AM. We did a last Lighthouse test before, our performance-score was down to 28 at that time (I don’t exactly know what caused the drop of around 10 points).</p>
<p>Deployment rolled through. The site still worked. And we scheduled another Lighthouse test.</p>
<p>I’ll let the result speak for itself:</p>
<figure class="prezet-figure"><img x-zoomable srcset="/articles/img/./performance-graph-480w.png 480w, /articles/img/./performance-graph-640w.png 640w, /articles/img/./performance-graph-768w.png 768w, /articles/img/./performance-graph-960w.png 960w, /articles/img/./performance-graph-1536w.png 1536w" sizes="92vw, (max-width: 1024px) 92vw, 768px" loading="lazy" decoding="async" fetchpriority="auto" class="prezet-image" src="/articles/img/performance-graph.png" alt="Performance graph" />
<figcaption class="prezet-figcaption">Performance graph</figcaption>
</figure>
<p>After Go-Live we noticed a few small things we optimized the following days, landing us a solid score of <strong>62</strong> in the end.</p>
<p>What a ride!</p>
<p>This won’t be the end of our journey, though. We now laid the groundwork to further improvide the page experience for the users.</p>
<h2><a id="content-honorable-mention-debugbear" href="#content-honorable-mention-debugbear" class="prezet-heading" title="Permalink">#</a>Honorable mention: Debugbear</h2>
<p>While doing all this we noticed we needed some kind of monitoring of our metrics.</p>
<p>While researching ways to accomplish this with our CI/CD pipeline, through manual tests or a script running the <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://github.com/GoogleChrome/lighthouse#using-the-node-cli">Lighthouse Node CLI</a> we stumbled upon <a rel="nofollow noopener noreferrer" target="_blank" class="external-link" href="https://www.debugbear.com">Debugbear</a>.</p>
<p>Debugbear is a service which monitors your core web vitals, does Lighthouse tests and can compare all of these results to either your competitors or your historical data, providing great insights into what changed between two tests.</p>
<p>Debugbear helped us not only in better understanding what caused problems, but also in having confidence into our optimizations.</p>
<p>Debugbear provides great value for its dollars. And Matt is a great guy, we had problems with our credit card, he generously renewed our trial multiple times so we could continue to test everything without having to fear the deadline.</p>
<h2><a id="content-final-words" href="#content-final-words" class="prezet-heading" title="Permalink">#</a>Final words</h2>
<p>This concludes the first part of our journey.</p>
<p>The results are great and we are confident in our decision. Vue just wasn’t the right fit for a project of this kind and honestly: the decision to use Vue like this might have seemed like it was a good one a few years back, but it never was.</p>
<p>This was totally our fault. Vue is a great framework and we still use it. But we now have another great tool in our belt which may be better suited than Vue.</p>
]]>
            </summary>
                                    <updated>2021-08-04T00:00:00+00:00</updated>
        </entry>
    </feed>
