<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>Thomas Schoffelen</title>
      <link>https://schof.co</link>
      <description></description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://schof.co/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Sun, 15 Feb 2026 00:00:00 +0000</lastBuildDate>
      <item>
          <title>Using Worker Threads in Node.js</title>
          <pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/using-worker-threads-in-nodejs/</link>
          <guid>https://schof.co/using-worker-threads-in-nodejs/</guid>
          <description xml:base="https://schof.co/using-worker-threads-in-nodejs/">&lt;p&gt;I had a use case recently where I was building a custom framework for managing background jobs. It needed to be possible for the user to cancel a long-running background job, which should cancel its execution.&lt;&#x2F;p&gt;
&lt;p&gt;In Node, that&#x27;s actually pretty hard to do. If you have a bunch of promises running that are doing things like HTTP requests, there are only very minimal options available to cancel these.&lt;&#x2F;p&gt;
&lt;p&gt;The &#x27;official&#x27; one would be to create an &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;AbortController&quot;&gt;&lt;code&gt;AbortController&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; and pass this along to the job, and make sure that every HTTP request uses it. Given the fact that there&#x27;s a lot of third-party SDK calls in my background jobs, this was unfeasible.&lt;&#x2F;p&gt;
&lt;p&gt;What is one other sure-fire way to stop a JS process from continuing to run? &lt;code&gt;process.exit()&lt;&#x2F;code&gt; works, but obviously you don&#x27;t want to kill the entire server, so that option is excluded as well.&lt;&#x2F;p&gt;
&lt;p&gt;Unless... the job runs in a separate process. You could so something like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;spawn &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;node:child_process&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;child &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;spawn&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;node&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;.&#x2F;background-task.js&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;child&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;stdout&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;on&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;data&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, () &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;* ... *&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;child&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;stderr&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;on&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;data&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, () &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;* ... *&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;child&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;on&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;close&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;exitCode&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;* ... *&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;However, there&#x27;s an even easier way: &lt;a href=&quot;https:&#x2F;&#x2F;nodejs.org&#x2F;api&#x2F;worker_threads.html#new-workerfilename-options&quot;&gt;worker threads&lt;&#x2F;a&gt;!&lt;&#x2F;p&gt;
&lt;p&gt;Worker threads are similar to child processes, but they can share memory and communicate with each other. They also have a very cool system to share data:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Worker&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isMainThread&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;parentPort&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;workerData &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;node:worker_threads&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isMainThread&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; We&amp;#39;re in the main thread, so let&amp;#39;s create a worker:
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;worker &lt;&#x2F;span&gt;&lt;span&gt;= new Worker(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt;.meta.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;, {
&lt;&#x2F;span&gt;&lt;span&gt;		workerData: { hello: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;world&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;worker&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;on&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;message&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;message&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;message&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;error&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;console&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;error&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;worker error:&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;message&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;error&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;		}
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;worker&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;on&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;exit&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;code&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;console&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;log&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;worker exited with code&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;code&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;else &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; We are a worker:
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;workerData&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;	
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Check for cancellation and exit early if needed:
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;setInterval&lt;&#x2F;span&gt;&lt;span&gt;(() &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;checkJobWasCancelled&lt;&#x2F;span&gt;&lt;span&gt;()) {
&lt;&#x2F;span&gt;&lt;span&gt;			process.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;exit&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;130&lt;&#x2F;span&gt;&lt;span&gt;); &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; unique exit code to mark user cancellation
&lt;&#x2F;span&gt;&lt;span&gt;		}
&lt;&#x2F;span&gt;&lt;span&gt;	}, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1000&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;	
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Run the actual worker job:
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;try &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;doSomething&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;	} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;catch &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; On error, post back a message to the parent with
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; error info, then end thread
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;parentPort&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;postMessage&lt;&#x2F;span&gt;&lt;span&gt;({ error: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;message &lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;span&gt;		process.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;exit&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;	}
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The cool thing about this system is that you can have a single file that both acts as the worker as well as the worker orchestrator. This helps with bundling through ESBuild or similar, where you might not know the path at which your worker code ends up.&lt;&#x2F;p&gt;
&lt;p&gt;I successfully used this in AWS Lambda to cancel my background jobs, without triggering a failed Lambda invocation every time I exited the process early.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;
&lt;script&gt;document.querySelectorAll(&#x27;h1, h2, h3, h4, h5, h6&#x27;).forEach(heading =&gt; { if (!heading.textContent.includes(&#x27;%% fold %%&#x27;)) return; const details = document.createElement(&#x27;details&#x27;); const summary = document.createElement(&#x27;summary&#x27;); summary.innerHTML = heading.innerHTML.replace(&#x27;%% fold %%&#x27;, &#x27;&#x27;).trim(); details.appendChild(summary); const content = document.createElement(&#x27;div&#x27;); details.appendChild(content); let sibling = heading.nextElementSibling; const headingLevel = parseInt(heading.tagName[1]); while (sibling) { const next = sibling.nextElementSibling; if (&#x2F;^H[1-6]$&#x2F;.test(sibling.tagName) &amp;&amp; parseInt(sibling.tagName[1]) &lt;= headingLevel) break; if (sibling.textContent.includes(&#x27;%% endfold %%&#x27;) || sibling.textContent.includes(&#x27;%% fold %%&#x27;) || sibling.textContent.includes(&#x27;❧&#x27;)) break; content.appendChild(sibling); sibling = next; } heading.replaceWith(details); });&lt;&#x2F;script&gt;</description>
      </item>
      <item>
          <title>Focussing on Longevity</title>
          <pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/focussing-on-longevity/</link>
          <guid>https://schof.co/focussing-on-longevity/</guid>
          <description xml:base="https://schof.co/focussing-on-longevity/">&lt;p&gt;One of the things I really enjoy about the co-founders I&#x27;ve currently surrounded myself with, is our shared goal of longevity.&lt;&#x2F;p&gt;
&lt;p&gt;We&#x27;re not trying to build businesses that have hockeystick growth projections, or stressing about month-on-month revenue growth. We&#x27;re focussing on building what we believe in, and attracting people that share those beliefs to use our products.&lt;&#x2F;p&gt;
&lt;p&gt;I believe very strongly that that&#x27;s the only consistent way to build something long-lasting. Premature growth, both in revenue or in team size, will eventually lead to instability. I like staying small until we&#x27;re certain about what we&#x27;re doing.&lt;&#x2F;p&gt;
&lt;p&gt;That of course is easier if you have previous businesses that are profitable, and are able to pour those profits into taking time to build something new slowly.&lt;&#x2F;p&gt;
&lt;p&gt;I want my company to be like a family farm that still exists 90 years later, not like a startup that has to lay off 200 people because it grew slightly less quick last quarter.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;n0N7Zu-Jj&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;
&lt;script&gt;document.querySelectorAll(&#x27;h1, h2, h3, h4, h5, h6&#x27;).forEach(heading =&gt; { if (!heading.textContent.includes(&#x27;%% fold %%&#x27;)) return; const details = document.createElement(&#x27;details&#x27;); const summary = document.createElement(&#x27;summary&#x27;); summary.innerHTML = heading.innerHTML.replace(&#x27;%% fold %%&#x27;, &#x27;&#x27;).trim(); details.appendChild(summary); const content = document.createElement(&#x27;div&#x27;); details.appendChild(content); let sibling = heading.nextElementSibling; const headingLevel = parseInt(heading.tagName[1]); while (sibling) { const next = sibling.nextElementSibling; if (&#x2F;^H[1-6]$&#x2F;.test(sibling.tagName) &amp;&amp; parseInt(sibling.tagName[1]) &lt;= headingLevel) break; if (sibling.textContent.includes(&#x27;%% endfold %%&#x27;) || sibling.textContent.includes(&#x27;%% fold %%&#x27;) || sibling.textContent.includes(&#x27;❧&#x27;)) break; content.appendChild(sibling); sibling = next; } heading.replaceWith(details); });&lt;&#x2F;script&gt;</description>
      </item>
      <item>
          <title>Conditionally Loading Native Modules in Expo</title>
          <pubDate>Mon, 02 Feb 2026 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/conditionally-loading-native-modules-in-expo/</link>
          <guid>https://schof.co/conditionally-loading-native-modules-in-expo/</guid>
          <description xml:base="https://schof.co/conditionally-loading-native-modules-in-expo/">&lt;p&gt;I used to be a &#x27;plain&#x27; React Native guy, but I&#x27;ve really grown to enjoy using &lt;a href=&quot;https:&#x2F;&#x2F;expo.dev&quot;&gt;Expo&lt;&#x2F;a&gt; for the last few apps I&#x27;ve built.&lt;&#x2F;p&gt;
&lt;p&gt;Much less compile errors to deal with, and I really love using the &lt;strong&gt;Expo Go&lt;&#x2F;strong&gt; app to quickly debug an app without having to wait for a development build to finish or opening Xcode.&lt;&#x2F;p&gt;
&lt;p&gt;Of course, some apps need custom packages with native functionality that isn&#x27;t available in Expo Go. Depending on what type of functionality they offer, they might be optional for debugging.&lt;&#x2F;p&gt;
&lt;p&gt;Recently, I had to use the &lt;a href=&quot;https:&#x2F;&#x2F;intercom.com&quot;&gt;Intercom&lt;&#x2F;a&gt; SDK in an app. Their &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;intercom&#x2F;intercom-react-native&quot;&gt;React Native package&lt;&#x2F;a&gt; is great, but it doesn&#x27;t work with Expo Go, since it relies on their native iOS and Android SDKs.&lt;&#x2F;p&gt;
&lt;p&gt;As soon as you &lt;code&gt;import&lt;&#x2F;code&gt; it in your code anywhere, your app will crash if you try to open it in Expo Go. Annoying, especially since I don&#x27;t really need the Intercom functionality to debug my app.&lt;&#x2F;p&gt;
&lt;p&gt;The solution? Conditionally import it, based on whether the native module exists:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Linking&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;NativeModules&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Platform &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react-native&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getIntercom &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;() &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;NativeModules&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;IntercomEventEmitter&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;console&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;log&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Intercom SDK not available&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;  }
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;default&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Intercom &lt;&#x2F;span&gt;&lt;span&gt;} = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span&gt;import(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;@intercom&#x2F;intercom-react-native&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Intercom&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we have the best of both worlds – I can still debug all of the other app functionality in Expo Go, but in a native build, the Intercom SDK loads correctly.&lt;&#x2F;p&gt;
&lt;p&gt;Anywhere I want to call any of the Intercom SDK methods, I&#x27;d just do:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;intercom &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getIntercom&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;intercom&lt;&#x2F;span&gt;&lt;span&gt;?.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;presentMessageComposer&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;
&lt;script&gt;document.querySelectorAll(&#x27;h1, h2, h3, h4, h5, h6&#x27;).forEach(heading =&gt; { if (!heading.textContent.includes(&#x27;%% fold %%&#x27;)) return; const details = document.createElement(&#x27;details&#x27;); const summary = document.createElement(&#x27;summary&#x27;); summary.innerHTML = heading.innerHTML.replace(&#x27;%% fold %%&#x27;, &#x27;&#x27;).trim(); details.appendChild(summary); const content = document.createElement(&#x27;div&#x27;); details.appendChild(content); let sibling = heading.nextElementSibling; const headingLevel = parseInt(heading.tagName[1]); while (sibling) { const next = sibling.nextElementSibling; if (&#x2F;^H[1-6]$&#x2F;.test(sibling.tagName) &amp;&amp; parseInt(sibling.tagName[1]) &lt;= headingLevel) break; if (sibling.textContent.includes(&#x27;%% endfold %%&#x27;) || sibling.textContent.includes(&#x27;%% fold %%&#x27;) || sibling.textContent.includes(&#x27;❧&#x27;)) break; content.appendChild(sibling); sibling = next; } heading.replaceWith(details); });&lt;&#x2F;script&gt;</description>
      </item>
      <item>
          <title>A Short Introduction to QTI</title>
          <pubDate>Mon, 05 Jan 2026 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/introduction-to-qti/</link>
          <guid>https://schof.co/introduction-to-qti/</guid>
          <description xml:base="https://schof.co/introduction-to-qti/">&lt;p&gt;QTI stands for &lt;strong&gt;Question and Test Interoperability&lt;&#x2F;strong&gt;, and is a standard for exchanging assessment content (and results) between systems. It has developed over the last few decades into one of the most complex, wonderful standards we have in education tech.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s one of those standards though that is really awesome, but hard to get a basic understanding of, without spending lots of time reading all of the documents that make up the specification.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve spent the last few months learning all about it as part of building QTI 3.0 import and export into our assessment platform &lt;a href=&quot;https:&#x2F;&#x2F;examplary.ai&#x2F;&quot;&gt;Examplary&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Let me give you a short introduction, going from the outside in, and highlighting some of the things I really love about how the specification is put together.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;packaging&quot;&gt;Packaging&lt;&#x2F;h2&gt;
&lt;p&gt;In most situations, you&#x27;ll come across QTI content in the form of a QTI content package, which you would export from one system (e.g. Canvas or Examplary) to import into another system (e.g. Tao Testing or Cloud Assess).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source srcset=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;hC06ZW0YD&quot; media=&quot;(prefers-color-scheme: dark)&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;7O38ljUeI&quot; alt=&quot;Drawing&quot; &#x2F;&gt;&lt;&#x2F;picture&gt;&lt;&#x2F;p&gt;
&lt;p&gt;These content packages are ZIP files which contains XML files describing the test and questions, and any other resources required for those questions (images, videos, style sheets).&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s also always a file called &lt;code&gt;imsmanifest.xml&lt;&#x2F;code&gt;. This file acts as an &lt;a href=&quot;https:&#x2F;&#x2F;www.1edtech.org&#x2F;standards&#x2F;content-packaging&quot;&gt;index card&lt;&#x2F;a&gt;, telling the system reading the file what resources are in the zip file, what types they have, and where they are located.&lt;&#x2F;p&gt;
&lt;p&gt;It also can contain metadata about the educational content, using &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Learning_object_metadata&quot;&gt;Learning Object Metadata (LOM)&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-example-imsmanifest-xml-simplified-fold&quot;&gt;An example &lt;code&gt;imsmanifest.xml&lt;&#x2F;code&gt; (simplified) %% fold %%&lt;&#x2F;h3&gt;
&lt;pre data-lang=&quot;xml&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&lt;span&gt;&amp;lt;?&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;xml &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;version&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;1.0&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;encoding&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;UTF-8&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;?&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;manifest &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;xmlns&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;http:&#x2F;&#x2F;www.imsglobal.org&#x2F;xsd&#x2F;qti&#x2F;qtiv3p0&#x2F;imscp_v1p1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;schema&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;QTI Package&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;schema&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;schemaversion&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;3.0.0&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;schemaversion&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;organizations&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;resources&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		 &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&amp;lt;!-- This is our main entry - the test --&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;         &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;assessment.xml&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;imsqti_test_xmlv3p0&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;test1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;            &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;assessment.xml&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;            &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;dependency &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifierref&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;question1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;            &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;dependency &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifierref&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;question2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;resource&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&amp;lt;!-- Resources for questions are referenced in assessment.xml 
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;        by idenfitier, and the lines below tell the target app where
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;        to find the content for each question --&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;question1&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;imsqti_item_xmlv3p0&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;question1.xml&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;            &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;question1.xml&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;            &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;images&#x2F;sign.png&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;resource&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;resource &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;question2&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;imsqti_item_xmlv3p0&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;question2.xml&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;            &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;question2.xml&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;resource&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;resources&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;manifest&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;❧ Misc fact: This &lt;a href=&quot;https:&#x2F;&#x2F;www.imsglobal.org&#x2F;packaging&#x2F;index.html&quot;&gt;format&lt;&#x2F;a&gt; for packaging content is not specific to QTI. It&#x27;s part of another standard that is also used for the SCORM and &lt;a href=&quot;https:&#x2F;&#x2F;www.imsglobal.org&#x2F;cc&#x2F;index.html&quot;&gt;Common Cartridge&lt;&#x2F;a&gt; standards, although they all have slightly different requirements for what should be contained in the manifest file.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;assessment-structure&quot;&gt;Assessment structure&lt;&#x2F;h2&gt;
&lt;p&gt;One of those packages can contain multiple assessments, which in turn can contain parts, sections, and assessment items (the actual questions):&lt;&#x2F;p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source srcset=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;nmOf9Fdes&quot; media=&quot;(prefers-color-scheme: dark)&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;84iog_xSP&quot; alt=&quot;Drawing&quot; &#x2F;&gt;&lt;&#x2F;picture&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Within a test part, you decide how you want the student to navigate through items, e.g. either one after the other, without allowing the student to jump back and forth (&lt;code&gt;linear&lt;&#x2F;code&gt;) or &lt;code&gt;nonlinear&lt;&#x2F;code&gt; to provide more freedom.&lt;&#x2F;p&gt;
&lt;p&gt;Most tests have a single part, and one or more sections about different topics.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;adaptiveness&quot;&gt;Adaptiveness&lt;&#x2F;h2&gt;
&lt;p&gt;Sections, and individual items can have &lt;code&gt;preconditions&lt;&#x2F;code&gt; (e.g., &quot;only show
this if score &amp;lt; 70%&quot;) and branching rules, which determine the flow of the test.&lt;&#x2F;p&gt;
&lt;p&gt;This means you can create adaptive structures, for example making sure that a student that scores poorly on a certain topic gets more follow-up questions about that topic. This is really powerful!&lt;&#x2F;p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source srcset=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;H9vi3U5wG&quot; media=&quot;(prefers-color-scheme: dark)&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;kHxOgwLqk&quot; alt=&quot;Drawing&quot; &#x2F;&gt;&lt;&#x2F;picture&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;items-and-interactions&quot;&gt;Items and interactions&lt;&#x2F;h2&gt;
&lt;p&gt;Let&#x27;s dive into the items: the actual questions that make up the test.&lt;&#x2F;p&gt;
&lt;p&gt;The cool thing about QTI is that these can be fully self-contained documents. Each one can be its own file, contain any HTML you want, and then use some custom XML tags to allow interaction.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source srcset=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;1mJyxAlun&quot; media=&quot;(prefers-color-scheme: dark)&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;cmwPizi7h&quot; alt=&quot;Drawing&quot; &#x2F;&gt;&lt;&#x2F;picture&gt;&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s about two dozen built-in interaction types, which you can mix and match and use in combination with your content:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-assessment-item&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;  ...
&lt;&#x2F;span&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-item-body&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Look at the text in the picture.&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;img &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;src&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;images&#x2F;sign.png&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-choice-interaction &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;max-choices&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;1&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;response-identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;RESPONSE&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-prompt&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;What does it say?&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-prompt&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-simple-choice &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;A&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;You must stay with your luggage at all times.&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-simple-choice&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-simple-choice &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;B&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Do not let someone else look after your luggage.&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-simple-choice&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-simple-choice &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;C&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Remember your luggage when you leave.&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-simple-choice&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-choice-interaction&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-item-body&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-assessment-item&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;portable-custom-interactions&quot;&gt;Portable custom interactions&lt;&#x2F;h2&gt;
&lt;p&gt;If some combination of the built-in interaction types somehow doesn&#x27;t fit your needs, QTI supports something called &lt;em&gt;Portable Custom Interactions&lt;&#x2F;em&gt; (PCIs), which allow you to write scripts to display custom UIs to the user.&lt;&#x2F;p&gt;
&lt;p&gt;They&#x27;re not as easy to set up as &lt;a href=&quot;https:&#x2F;&#x2F;developers.examplary.ai&#x2F;question-types&#x2F;&quot;&gt;Examplary custom question types&lt;&#x2F;a&gt;, but the cool thing is they are completely portable - you usually include them in the ZIP file, so that they can be played even when you move over to a different LMS or assessment platform.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source srcset=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;uiex9aVGq&quot; media=&quot;(prefers-color-scheme: dark)&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;T2_QQZH_g&quot; alt=&quot;Drawing&quot; &#x2F;&gt;&lt;&#x2F;picture&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;❧ Random note: if you export custom question type content from Examplary as QTI, we automatically generate a PCI version of that Examplary question type and package it up in the ZIP. This was very fun to build!&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;response-mapping&quot;&gt;Response mapping&lt;&#x2F;h2&gt;
&lt;p&gt;The same flexibility is afforded in terms of how responses are marked.&lt;&#x2F;p&gt;
&lt;p&gt;You can set up a simple matching system, where a specific answer leads to a specific score and a specific feedback text, but you can also be very complex with your scoring rules.&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s almost an entire programming language embedded in the QTI markup to describe almost any desired outcome.&lt;&#x2F;p&gt;
&lt;p&gt;Thankfully there are also some defaults built in, so that you don&#x27;t need to define something like this for each question that simply has a &quot;matches correct answer&quot; scoring mechanism:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-response-condition&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-response-if&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&amp;lt;!-- If the variable &amp;#39;RESPONSE&amp;#39; matches the correct answer --&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-match&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;			&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-variable &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;RESPONSE&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;			&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-correct &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;RESPONSE&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-match&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&amp;lt;!-- Set &amp;#39;SCORE&amp;#39; to 1 --&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-set-outcome-value &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;SCORE&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;			&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-base-value &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;base-type&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;float&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;1&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-base-value&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-set-outcome-value&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-response-if&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-response-else&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&amp;lt;!-- Otherwise set &amp;#39;SCORE&amp;#39; to 0 --&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-set-outcome-value &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;SCORE&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;			&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-base-value &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;base-type&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;float&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;0&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-base-value&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-set-outcome-value&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-response-else&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;qti-response-condition&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;reviewer-content&quot;&gt;Reviewer content&lt;&#x2F;h2&gt;
&lt;p&gt;You can embed content into QTI items that are only visible for certain groups of users.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;picture&gt;&lt;source srcset=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;gitGX-wz0&quot; media=&quot;(prefers-color-scheme: dark)&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;hyRAaW2kk&quot; alt=&quot;Drawing&quot; &#x2F;&gt;&lt;&#x2F;picture&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&#x27;Rubric&#x27; blocks, which are meant to display things like instructions, scoring info and additional context, can be configured to be viewed only by a specific audience:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;author&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;candidate&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;proctor&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;scorer&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;testConstructor&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tutor&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This is very useful for adding detailed scoring rubrics, which can live alongside the content of the question. It can also be used for private notes by the question writer, to denote references or resources.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;&#x2F;h2&gt;
&lt;p&gt;There&#x27;s so much more I could cover, including response-specific feedback, companion materials, test time limits, and accessibility features, but those are better understood in context of the original specification documents.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Official specification documents:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;📄 &lt;a href=&quot;https:&#x2F;&#x2F;www.imsglobal.org&#x2F;spec&#x2F;qti&#x2F;v3p0&#x2F;oview&quot;&gt;Question and Test Interoperability (QTI) Overview&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;A quick overview of where QTI fits in the ecosystem, with links to all other relevant specification docs.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;📄 &lt;a href=&quot;https:&#x2F;&#x2F;www.imsglobal.org&#x2F;spec&#x2F;qti&#x2F;v3p0&#x2F;guide&quot;&gt;QTI 3 Beginner&#x27;s Guide&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;A very simple beginner&#x27;s guide. I only started reading this way too late in the process. Essential reading.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;📄 &lt;a href=&quot;https:&#x2F;&#x2F;www.imsglobal.org&#x2F;spec&#x2F;qti&#x2F;v3p0&#x2F;impl&quot;&gt;QTI v3 Best Practices and Implementation Guide&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;This is the core document you&#x27;d use to build any type of technical integration. It shows the full width and breadth of what&#x27;s possible in QTI.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;📄 &lt;a href=&quot;https:&#x2F;&#x2F;www.imsglobal.org&#x2F;sites&#x2F;default&#x2F;files&#x2F;spec&#x2F;qti&#x2F;v3&#x2F;info&#x2F;index.html&quot;&gt;Assessment, Section and Item Information Model&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;The actual technical specification of the information model. Not worth reading, unless you&#x27;re implementing QTI importer&#x2F;exporter yourself.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Useful tools:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;🌍 &lt;a href=&quot;https:&#x2F;&#x2F;qti.citolab.nl&#x2F;landing&quot;&gt;Citolab QTI Playground&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;A great tool by our friends at Citolab, makes it easy to test your QTI packages and convert them from QTI 2 to 3.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;🌍 &lt;a href=&quot;https:&#x2F;&#x2F;www.onyx-editor.com&#x2F;onyxeditor&#x2F;editor?4&quot;&gt;ONYX Editor&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;Online assessment editor. Only supports QTI 2.1, but was useful in helping me understand how certain concepts mapped from the XML markup to UI.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;
&lt;script&gt;document.querySelectorAll(&#x27;h1, h2, h3, h4, h5, h6&#x27;).forEach(heading =&gt; { if (!heading.textContent.includes(&#x27;%% fold %%&#x27;)) return; const details = document.createElement(&#x27;details&#x27;); const summary = document.createElement(&#x27;summary&#x27;); summary.innerHTML = heading.innerHTML.replace(&#x27;%% fold %%&#x27;, &#x27;&#x27;).trim(); details.appendChild(summary); const content = document.createElement(&#x27;div&#x27;); details.appendChild(content); let sibling = heading.nextElementSibling; const headingLevel = parseInt(heading.tagName[1]); while (sibling) { const next = sibling.nextElementSibling; if (&#x2F;^H[1-6]$&#x2F;.test(sibling.tagName) &amp;&amp; parseInt(sibling.tagName[1]) &lt;= headingLevel) break; if (sibling.textContent.includes(&#x27;%% endfold %%&#x27;) || sibling.textContent.includes(&#x27;%% fold %%&#x27;) || sibling.textContent.includes(&#x27;❧&#x27;)) break; content.appendChild(sibling); sibling = next; } heading.replaceWith(details); });&lt;&#x2F;script&gt;</description>
      </item>
      <item>
          <title>Reviewing the Year&#x27;s Daily Notes</title>
          <pubDate>Thu, 01 Jan 2026 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/reviewing-the-years-daily-notes/</link>
          <guid>https://schof.co/reviewing-the-years-daily-notes/</guid>
          <description xml:base="https://schof.co/reviewing-the-years-daily-notes/">&lt;p&gt;My &lt;a href=&quot;https:&#x2F;&#x2F;help.obsidian.md&#x2F;plugins&#x2F;daily-notes&quot;&gt;daily notes&lt;&#x2F;a&gt; in Obsidian are the cornerstone of my note taking and journaling.&lt;&#x2F;p&gt;
&lt;p&gt;I don&#x27;t really review old daily notes too often, but once a year, in December, I&#x27;ll look back at the past year&#x27;s notes day-by-day and write a little overview of the important events and things I learned in that past year.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s a great way to reflect on what the year has brought me, and remind myself of all of the beautiful experiences I&#x27;ve had.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s also great to look back on old links and resources I&#x27;ve saved for later, that I never actually managed to look at. It&#x27;s a lot of work, but it&#x27;s also very fulfilling.&lt;&#x2F;p&gt;
&lt;p&gt;On to a new year, and maybe at some point (end of the decade?), I&#x27;ll review these yearly review notes!&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;vJ9FyhjZg&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Implementing the Repository Model with DynamoDB and Turbine</title>
          <pubDate>Wed, 17 Dec 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/implementing-the-repository-model-with-dynamodb-and-turbine/</link>
          <guid>https://schof.co/implementing-the-repository-model-with-dynamodb-and-turbine/</guid>
          <description xml:base="https://schof.co/implementing-the-repository-model-with-dynamodb-and-turbine/">&lt;p&gt;When you&#x27;re building a new product at a small startup, speed and flexibility are of the essence. It&#x27;s important to recognise which architectural patterns make sense, and which ones should be reserved until you have a better understand of the product you&#x27;re building.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;introducing-the-repository-pattern&quot;&gt;Introducing the Repository pattern&lt;&#x2F;h2&gt;
&lt;p&gt;One pattern that I found is essential to implement from the start is the &lt;strong&gt;Repository pattern&lt;&#x2F;strong&gt; from Domain-Driven Development (DDD). This pattern states that you should keep your data access layer and data storage layers separate:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;2rCPegoEr&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;br&gt;
&lt;p&gt;What does this look like in practice? Let&#x27;s say we have a HTTP endpoint that returns a list of users:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;app&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&#x2F;users&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;req&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Items &lt;&#x2F;span&gt;&lt;span&gt;} = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;dynamodbClient&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;query&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;		Table: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;users-production&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		Index: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;type-org&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		KeyConditionExpression: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;type = &amp;#39;user&amp;#39; AND org = :org&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		ExpressionAttributeValues: {
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;:org&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;req&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;orgId
&lt;&#x2F;span&gt;&lt;span&gt;		},
&lt;&#x2F;span&gt;&lt;span&gt;		Limit: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;10
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;	
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;json&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Items&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;map&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;* ... *&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;));
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That works, but at some point in the development of your app, you might realise that since user data is highly relational, you might want to switch to using a SQL-based database to store them.&lt;&#x2F;p&gt;
&lt;p&gt;Or maybe you simply want to switch the database structure and the access pattern for this data.&lt;&#x2F;p&gt;
&lt;p&gt;Either of these things would be a show-stopping amount of work if you don&#x27;t abstract data access from the start!&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Side note: you might be thinking &quot;But hey, I don&#x27;t access my database directly, I already use some query library to access it. That&#x27;s an abstraction, right?&quot; – it is, but it usually is quite a &lt;strong&gt;leaky abstraction&lt;&#x2F;strong&gt;, in the sense that these libraries often expose APIs specific to the database you&#x27;re using. You might still want to use a separate Repository layer in addition in such cases.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-a-good-repository-looks-like&quot;&gt;What a good repository looks like&lt;&#x2F;h2&gt;
&lt;p&gt;Ideally, you want to have the above code look more like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;app&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&#x2F;users&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;req&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;users &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Users&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;listByOrg&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;req&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;orgId&lt;&#x2F;span&gt;&lt;span&gt;, { limit: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;10 &lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;span&gt;	
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;json&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is so much nicer! And now you&#x27;d only have to update the once place when you need to switch up your data storage model or engine.&lt;&#x2F;p&gt;
&lt;p&gt;A few rules:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Repositories should not leak any specifics about the underlying data storage.&lt;&#x2F;li&gt;
&lt;li&gt;Repository methods should be specific and fulfil use cases related to entities, not related to data storage patterns.&lt;&#x2F;li&gt;
&lt;li&gt;Repositories should contain as little business logic as possible.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;An example signature of our entire users repository might look like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export class &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Users &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;public static &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;listAll&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;public static &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;listByOrg&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;orgId&lt;&#x2F;span&gt;&lt;span&gt;: string);
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;public static &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getById&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;: string);
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;public static &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getByEmail&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;email&lt;&#x2F;span&gt;&lt;span&gt;: string);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;public static &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;updateAvatar&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;: string, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;: string);
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;public static &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;updateBio&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;: string, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;bio&lt;&#x2F;span&gt;&lt;span&gt;: string);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;public static &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;delete&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;: string);	
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;dynamodb-and-turbine&quot;&gt;DynamoDB and Turbine&lt;&#x2F;h2&gt;
&lt;p&gt;My favourite database to start most new products with is DynamoDB – it&#x27;s virtually infinitely scalable, I don&#x27;t have to think about hosting, it&#x27;s robust, supports event-based triggers using &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;amazondynamodb&#x2F;latest&#x2F;developerguide&#x2F;Streams.html&quot;&gt;DynamoDB stream&lt;&#x2F;a&gt; and in most cases doesn&#x27;t require complex migration setups to get started with.&lt;&#x2F;p&gt;
&lt;p&gt;The DynamoDB SDK itself is a bit verbose though, and if you had to write all of the repository methods using direct calls, your class file would get very long very quickly, especially when it comes to data validation and schemas.&lt;&#x2F;p&gt;
&lt;p&gt;I use &lt;strong&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tschoffelen&#x2F;turbine&quot;&gt;Turbine&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;, a simple layer on top of DynamoDB to streamline these calls and validation logic. It introduces the concept of multiple types of entities, ideal for &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;blogs&#x2F;compute&#x2F;creating-a-single-table-design-with-amazon-dynamodb&#x2F;&quot;&gt;single-table design&lt;&#x2F;a&gt;. There&#x27;s many like it, but this one is mine 😁.&lt;&#x2F;p&gt;
&lt;p&gt;To get started with it, we create a shared &lt;code&gt;table.ts&lt;&#x2F;code&gt; file, which specifies how Turbine should connect to the DynamoDB table:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;defineTable &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;dynamodb-turbine&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;table &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;defineTable&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;	name: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;my-table-production&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; usually a environment var!
&lt;&#x2F;span&gt;&lt;span&gt;	indexes: {
&lt;&#x2F;span&gt;&lt;span&gt;		table: {
&lt;&#x2F;span&gt;&lt;span&gt;			hashKey: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;pk&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;			rangeKey: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;sk&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;		},
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;type-org&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;			hashKey: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;			rangeKey: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;org&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;		}
&lt;&#x2F;span&gt;&lt;span&gt;	}
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We then define our entities, each in a separate file. Take this &lt;code&gt;user-entity.ts&lt;&#x2F;code&gt; file:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;defineEntity&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Entity &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;dynamodb-turbine&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;table &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;.&#x2F;table&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;z &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;zod&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; We define the entity, but we should only interact with
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; it through the Users repository class methods.
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;userEntity &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;defineEntity&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;table&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	schema: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;z&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;object&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;		id: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;z&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;string&lt;&#x2F;span&gt;&lt;span&gt;(),
&lt;&#x2F;span&gt;&lt;span&gt;		orgId: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;z&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;string&lt;&#x2F;span&gt;&lt;span&gt;(),
&lt;&#x2F;span&gt;&lt;span&gt;		name: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;z&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;string&lt;&#x2F;span&gt;&lt;span&gt;(),
&lt;&#x2F;span&gt;&lt;span&gt;		bio: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;z&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;string&lt;&#x2F;span&gt;&lt;span&gt;().&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;optional&lt;&#x2F;span&gt;&lt;span&gt;(),
&lt;&#x2F;span&gt;&lt;span&gt;		avatarUrl: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;z&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;().&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;optional&lt;&#x2F;span&gt;&lt;span&gt;(),
&lt;&#x2F;span&gt;&lt;span&gt;		createdAt: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;z&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iso&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;datetime&lt;&#x2F;span&gt;&lt;span&gt;(),
&lt;&#x2F;span&gt;&lt;span&gt;		updatedAt: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;z&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iso&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;datetime&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;	}),
&lt;&#x2F;span&gt;&lt;span&gt;	keys: {
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;: () &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;User&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;pk&lt;&#x2F;span&gt;&lt;span&gt;: (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;entity&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`org#${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;entity&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;orgId&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;sk&lt;&#x2F;span&gt;&lt;span&gt;: (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;entity&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`user#${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;entity&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.id}`&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;createdAt&lt;&#x2F;span&gt;&lt;span&gt;: (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;entity&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;entity&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;createdAt &lt;&#x2F;span&gt;&lt;span&gt;|| new Date().&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;toISOString&lt;&#x2F;span&gt;&lt;span&gt;(),
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;updatedAt&lt;&#x2F;span&gt;&lt;span&gt;: (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;entity&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;new Date().&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;toISOString&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;	},
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The great thing about this is that you can also export the type for &lt;code&gt;User&lt;&#x2F;code&gt;, as defined in the schema, for easy TypeScript type checking:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;type Instance &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;dynamodb-turbine&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export type &lt;&#x2F;span&gt;&lt;span&gt;User = Instance&amp;lt;typeof userEntity&amp;gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we can implement our actual &lt;code&gt;Users&lt;&#x2F;code&gt; repository:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export class &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Users &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;public static async &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;listByOrg&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;orgId&lt;&#x2F;span&gt;&lt;span&gt;: string): Promise&amp;lt;User[]&amp;gt; {
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;entity&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;queryAll&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;			{
&lt;&#x2F;span&gt;&lt;span&gt;				index: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;type-org&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;				type: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;user&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;				org: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;orgId
&lt;&#x2F;span&gt;&lt;span&gt;			},
&lt;&#x2F;span&gt;&lt;span&gt;			{
&lt;&#x2F;span&gt;&lt;span&gt;				filters: {
&lt;&#x2F;span&gt;&lt;span&gt;					deletedAt: { notExists: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;true &lt;&#x2F;span&gt;&lt;span&gt;},
&lt;&#x2F;span&gt;&lt;span&gt;				}
&lt;&#x2F;span&gt;&lt;span&gt;			}
&lt;&#x2F;span&gt;&lt;span&gt;		);
&lt;&#x2F;span&gt;&lt;span&gt;	}
&lt;&#x2F;span&gt;&lt;span&gt;	
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;	
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;public static async &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;delete&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;userId&lt;&#x2F;span&gt;&lt;span&gt;: string) {
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;user &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;entity&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;queryOne&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;			pk: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`user#${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;userId&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`
&lt;&#x2F;span&gt;&lt;span&gt;		}, {
&lt;&#x2F;span&gt;&lt;span&gt;			filters: {
&lt;&#x2F;span&gt;&lt;span&gt;				deletedAt: { notExists: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;true &lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;			}
&lt;&#x2F;span&gt;&lt;span&gt;		});
&lt;&#x2F;span&gt;&lt;span&gt;		
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;user&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;throw &lt;&#x2F;span&gt;&lt;span&gt;new Error(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;User does not exist.&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;		
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;user&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;			deletedAt: new Date().&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;toISOString&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;		});
&lt;&#x2F;span&gt;&lt;span&gt;	}
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This class is the only thing the rest of our codebase interacts with when it needs to access user information.&lt;&#x2F;p&gt;
&lt;p&gt;Notice how this pattern makes it easy to abstract away the fact that we&#x27;re doing soft deletes in the database! The code calling &lt;code&gt;Users.listByOrg()&lt;&#x2F;code&gt; doesn&#x27;t need to be aware of that fact, or anything else about how users are stored in the database.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Copy Rich Text to the Clipboard in JavaScript</title>
          <pubDate>Fri, 21 Nov 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/copy-rich-text-to-the-clipboard-in-javascript/</link>
          <guid>https://schof.co/copy-rich-text-to-the-clipboard-in-javascript/</guid>
          <description xml:base="https://schof.co/copy-rich-text-to-the-clipboard-in-javascript/">&lt;p&gt;I really like how in &lt;a href=&quot;https:&#x2F;&#x2F;linear.app&quot;&gt;Linear&lt;&#x2F;a&gt;, if you press &lt;kbd&gt;Cmd + C&lt;&#x2F;kbd&gt;, which is listed as &#x27;Copy issue title&#x27;, it will copy the title as plain text, but also as an HTML link, so that if you paste it in a Google Doc or in Obsidian, it will show a link to that issue.&lt;&#x2F;p&gt;
&lt;p&gt;I recently wanted to implement the same thing, but in the reverse, for Street Art Cities, where you can click &#x27;Copy map link&#x27; to get a link to the map with the current map center point coordinates and zoom level.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;DQzQS4Uf4&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;If you paste it into a text message, it&#x27;s just a URL, but if you paste it in a place that can deal with HTML, it&#x27;s a link with the text &#x27;Street Art Cities map&#x27;.&lt;&#x2F;p&gt;
&lt;p&gt;Doing that in Javascript is pretty easy, once you figure out the slightly weird syntax:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;streetartcities.com&#x2F;...&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span&gt;navigator.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipboard&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;write&lt;&#x2F;span&gt;&lt;span&gt;([
&lt;&#x2F;span&gt;&lt;span&gt;	new ClipboardItem({
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;text&#x2F;plain&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: new Blob(
&lt;&#x2F;span&gt;&lt;span&gt;			[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;], 
&lt;&#x2F;span&gt;&lt;span&gt;			{ type: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;text&#x2F;plain&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;		),
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;text&#x2F;html&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: new Blob(
&lt;&#x2F;span&gt;&lt;span&gt;			[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`&amp;lt;a href=&amp;quot;${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}&amp;quot;&amp;gt;Street Art Cities map&amp;lt;&#x2F;a&amp;gt;`&lt;&#x2F;span&gt;&lt;span&gt;],
&lt;&#x2F;span&gt;&lt;span&gt;			{ type: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;text&#x2F;html&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;		)
&lt;&#x2F;span&gt;&lt;span&gt;	})
&lt;&#x2F;span&gt;&lt;span&gt;]);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There&#x27;s lots of one-item arrays in here, but overall it&#x27;s quite neat that browsers give you this functionality!&lt;&#x2F;p&gt;
&lt;p&gt;This is also how you could copy an image to the clipboard, you just create a &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;ClipboardItem&quot;&gt;&lt;code&gt;ClipboardItem&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; with an &lt;code&gt;image&#x2F;jpeg&lt;&#x2F;code&gt; entry, or similar.&lt;&#x2F;p&gt;
&lt;p&gt;For production, you might want to wrap this into a &lt;code&gt;try-catch&lt;&#x2F;code&gt; with a more generic version, for browsers that don&#x27;t support this yet, or when you&#x27;re running in an insecure context (&lt;code&gt;ClipboardItem&lt;&#x2F;code&gt; is only available through HTTPS, although some browsers seem to make an exception for &lt;code&gt;localhost&lt;&#x2F;code&gt;, thankfully).&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a more hardened version, with some toasts to give feedback to the user:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;streetartcities.com&#x2F;...&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;try &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span&gt;navigator.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipboard&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;write&lt;&#x2F;span&gt;&lt;span&gt;([
&lt;&#x2F;span&gt;&lt;span&gt;		new ClipboardItem({
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;text&#x2F;plain&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: new Blob(
&lt;&#x2F;span&gt;&lt;span&gt;				[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;], 
&lt;&#x2F;span&gt;&lt;span&gt;				{ type: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;text&#x2F;plain&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;			),
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;text&#x2F;html&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: new Blob(
&lt;&#x2F;span&gt;&lt;span&gt;				[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`&amp;lt;a href=&amp;quot;${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}&amp;quot;&amp;gt;Street Art Cities map&amp;lt;&#x2F;a&amp;gt;`&lt;&#x2F;span&gt;&lt;span&gt;],
&lt;&#x2F;span&gt;&lt;span&gt;				{ type: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;text&#x2F;html&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;			)
&lt;&#x2F;span&gt;&lt;span&gt;		})
&lt;&#x2F;span&gt;&lt;span&gt;	]);
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;toast&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;success&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Copied to clipboard&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;catch &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;error&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Try the simpler .writeText() if it failed
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;try &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span&gt;navigator.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipboard&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;writeText&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;toast&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;success&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Copied to clipboard&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;	} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;catch &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;error&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Or show an error if we still can&amp;#39;t hack it
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;toast&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;error&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`Failed to copy link to clipboard: ${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;error&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;message&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;	}
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When you&#x27;re building stuff like this, this little &lt;a href=&quot;https:&#x2F;&#x2F;evercoder.github.io&#x2F;clipboard-inspector&#x2F;&quot;&gt;Clipboard Inspector&lt;&#x2F;a&gt; tool is super useful to see the exact contents of the clipboard:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;xMWF78SL5&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Premature Abstraction of Entity Names</title>
          <pubDate>Sun, 09 Nov 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/premature-abstraction-of-entity-names/</link>
          <guid>https://schof.co/premature-abstraction-of-entity-names/</guid>
          <description xml:base="https://schof.co/premature-abstraction-of-entity-names/">&lt;p&gt;When I started building the current iteration of the Street Art Cities platform a few years ago, I decided on the following core entities:&lt;&#x2F;p&gt;
&lt;center&gt;&lt;svg viewBox=&quot;0 0 400 200&quot; xmlns=&quot;http:&#x2F;&#x2F;www.w3.org&#x2F;2000&#x2F;svg&quot; style=&quot;max-width:400px;margin:auto 0; height: auto; display: block;&quot;&gt;
  &lt;!-- Site Entity (left box) --&gt;
  &lt;rect x=&quot;50&quot; y=&quot;75&quot; width=&quot;100&quot; height=&quot;50&quot; rx=&quot;5&quot; ry=&quot;5&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&#x2F;&gt;
  &lt;text x=&quot;100&quot; y=&quot;105&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; font-family=&quot;inherit, sans-serif&quot; font-size=&quot;16&quot;&gt;Site&lt;&#x2F;text&gt;
  &lt;!-- Marker Entity (right box) --&gt;
  &lt;rect x=&quot;250&quot; y=&quot;75&quot; width=&quot;100&quot; height=&quot;50&quot; rx=&quot;5&quot; ry=&quot;5&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&#x2F;&gt;
  &lt;text x=&quot;300&quot; y=&quot;105&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; font-family=&quot;inherit, sans-serif&quot; font-size=&quot;16&quot;&gt;Marker&lt;&#x2F;text&gt;
  &lt;!-- Relationship line --&gt;
  &lt;line x1=&quot;150&quot; y1=&quot;100&quot; x2=&quot;250&quot; y2=&quot;100&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&#x2F;&gt;
  &lt;!-- Left side: || (exactly one) - moved further from box --&gt;
  &lt;line x1=&quot;160&quot; y1=&quot;95&quot; x2=&quot;160&quot; y2=&quot;105&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&#x2F;&gt;
  &lt;line x1=&quot;165&quot; y1=&quot;95&quot; x2=&quot;165&quot; y2=&quot;105&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&#x2F;&gt;
  &lt;!-- Right side: o{ (zero or many) - moved further from box --&gt;
  &lt;circle cx=&quot;230&quot; cy=&quot;100&quot; r=&quot;4&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&#x2F;&gt;
  &lt;line x1=&quot;240&quot; y1=&quot;95&quot; x2=&quot;240&quot; y2=&quot;100&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&#x2F;&gt;
  &lt;line x1=&quot;240&quot; y1=&quot;105&quot; x2=&quot;240&quot; y2=&quot;100&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&#x2F;&gt;
&lt;&#x2F;svg&gt;&lt;&#x2F;center&gt;
&lt;p&gt;A &lt;strong&gt;Site&lt;&#x2F;strong&gt; can have multiple &lt;strong&gt;Markers&lt;&#x2F;strong&gt;, and a &lt;strong&gt;Marker&lt;&#x2F;strong&gt; is connected to a single &lt;strong&gt;Site&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Both of these are somewhat abstract entities. A Marker has a &lt;code&gt;type&lt;&#x2F;code&gt; attribute, which can be either &lt;code&gt;artwork&lt;&#x2F;code&gt; or &lt;code&gt;place&lt;&#x2F;code&gt;. Places are things that aren’t artworks, but still relevant to display on the map, and has a bunch of sub-types: Hall of Fame, Can Shop, Skate Park, Street Art Bookshop, etc.&lt;&#x2F;p&gt;
&lt;p&gt;Sites work the same way, but at launch there was only one type: &lt;code&gt;city&lt;&#x2F;code&gt;. Each Site represents a city that someone can have permission to add markers to, and holds a lot of metadata to render &lt;code&gt;streetartcities.com&#x2F;cities&#x2F;{siteId}&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;In my mind, there might be other types of Sites in the future - maybe a street art festival would have their own Site, or a gallery. Maybe there would be some other type of Site that I hadn’t thought about yet.&lt;&#x2F;p&gt;
&lt;p&gt;This turned out to be a useless abstraction, since all of those other structures ended up being built as separate entities, as you always want a Marker connected to a specific city, even when it’s &lt;strong&gt;also&lt;&#x2F;strong&gt; part of a festival or gallery.&lt;&#x2F;p&gt;
&lt;p&gt;Trying to be clever here was a major distraction, and over time, as multiple people worked on the codebase, we started using the works &lt;code&gt;site&lt;&#x2F;code&gt; and &lt;code&gt;city&lt;&#x2F;code&gt; interchangeably in variable names and comments, which is a bit messy.&lt;&#x2F;p&gt;
&lt;p&gt;It would have been so much better to stick to the use cases we fully understood, and have gone with a City entity. If we would have had to refactor later, that would have been easier than ending up with a situation where we’re using not using &lt;a href=&quot;https:&#x2F;&#x2F;martinfowler.com&#x2F;bliki&#x2F;UbiquitousLanguage.html&quot;&gt;ubiquitous language&lt;&#x2F;a&gt; for this entity in our model.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Building a Custom Rate Limiter for Hono</title>
          <pubDate>Sun, 02 Nov 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/building-a-custom-rate-limiter-for-hono/</link>
          <guid>https://schof.co/building-a-custom-rate-limiter-for-hono/</guid>
          <description xml:base="https://schof.co/building-a-custom-rate-limiter-for-hono/">&lt;p&gt;There&#x27;s a bunch of &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rhinobase&#x2F;hono-rate-limiter&quot;&gt;cool&lt;&#x2F;a&gt; rate limiting packages out there for the &lt;a href=&quot;https:&#x2F;&#x2F;hono.dev&quot;&gt;Hono&lt;&#x2F;a&gt; web framework, but I wanted something simple that worked for my specific use case, where I want to rate limit per user.&lt;&#x2F;p&gt;
&lt;p&gt;To get started, let&#x27;s set up a new middleware:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;createMiddleware &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hono&#x2F;factory&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;rateLimitUser &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;callsPerMinute&lt;&#x2F;span&gt;&lt;span&gt;: number) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;createMiddleware&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;{
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Variables&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;userId&lt;&#x2F;span&gt;&lt;span&gt;?: string;
&lt;&#x2F;span&gt;&lt;span&gt;	    };
&lt;&#x2F;span&gt;&lt;span&gt;	}&amp;gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;next&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Our logic will go here
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;next&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Using &lt;code&gt;createMiddleware&lt;&#x2F;code&gt; is not strictly speaking necessary, but helps a lot with making sure our middleware has the correct typings. We can define which variables and bindings we expect to exist in the context.&lt;&#x2F;p&gt;
&lt;p&gt;Next, we want to figure out the current user and the route for which this middleware is invoked, so that we can create a unique cache key to keep count of the number of requests:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;routePath &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hono&#x2F;route&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;userId &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;userId&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;) || &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;anonymous&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;route &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;req&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.method} ${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;routePath&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;)}`&lt;&#x2F;span&gt;&lt;span&gt;; 
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; -&amp;gt; &amp;quot;GET &#x2F;recipes&#x2F;:id&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We want our rate limit to be per minute, so we can combine these with the current date, rounded down to the nearest minute, to get our unique cache key:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;minuteMarker &lt;&#x2F;span&gt;&lt;span&gt;= Math.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;floor&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Date&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;now&lt;&#x2F;span&gt;&lt;span&gt;() &#x2F; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;60000&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cacheKey &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`rate-limit:${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;route&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}:${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;userId&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}:${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;minuteMarker&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Depending on your implementation, you might want to use a remote cache server (if you&#x27;re running in a serverless environment, or if you&#x27;re running multiple instances of the web server), or a simple in-memory cache.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;HTTPException &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hono&#x2F;http-exception&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;newCount &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Cache&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;increment&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cacheKey&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;newCount &lt;&#x2F;span&gt;&lt;span&gt;&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;callsPerMinute&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;throw &lt;&#x2F;span&gt;&lt;span&gt;new HTTPException(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;429&lt;&#x2F;span&gt;&lt;span&gt;, {
&lt;&#x2F;span&gt;&lt;span&gt;		res: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;json&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;			error: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Rate limit exceeded. Please wait a minute.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		}),
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note how we use &lt;code&gt;throw HTTPException()&lt;&#x2F;code&gt; here rather than &lt;code&gt;return c.json()&lt;&#x2F;code&gt;, because we want Hono to stop processing the request and immediately return this particular response if the rate limit has been exceeded.&lt;&#x2F;p&gt;
&lt;p&gt;Theoretically, that&#x27;s all you need to make it functional, but I would add two more things to make it production-ready:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rate-limit-http-headers&quot;&gt;Rate limit HTTP headers&lt;&#x2F;h2&gt;
&lt;p&gt;When your API is consumed by external developers, or if you want to display more detailed information in your UI about rate limits, it might be useful to return some HTTP headers with rate limit info.&lt;&#x2F;p&gt;
&lt;p&gt;You might add something like this just above the &lt;code&gt;if&lt;&#x2F;code&gt; statement in the previous snippet:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;now &lt;&#x2F;span&gt;&lt;span&gt;= Math.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;floor&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Date&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;now&lt;&#x2F;span&gt;&lt;span&gt;() &#x2F; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1000&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;secondsUntilRefresh &lt;&#x2F;span&gt;&lt;span&gt;= Math.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;round&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;60 &lt;&#x2F;span&gt;&lt;span&gt;- (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;now &lt;&#x2F;span&gt;&lt;span&gt;- &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;minuteMarker &lt;&#x2F;span&gt;&lt;span&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;60&lt;&#x2F;span&gt;&lt;span&gt;));
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;callsRemaining &lt;&#x2F;span&gt;&lt;span&gt;= Math.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;max&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;callsPerMinute &lt;&#x2F;span&gt;&lt;span&gt;- &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;count&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;.headers.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;X-RateLimit-Limit&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;callsPerMinute&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;.headers.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;X-RateLimit-Remaining&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;callsRemaining&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;.headers.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;X-RateLimit-Reset&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;secondsUntilRefresh&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;These headers are not part of the official HTTP spec (as indicated by the &lt;code&gt;X-&lt;&#x2F;code&gt; prefix), but used by a lot of APIs and developer tools.&lt;&#x2F;p&gt;
&lt;p&gt;There is also &lt;a href=&quot;https:&#x2F;&#x2F;www.ietf.org&#x2F;archive&#x2F;id&#x2F;draft-ietf-httpapi-ratelimit-headers-10.html&quot;&gt;a new specification&lt;&#x2F;a&gt; in the works to create an official standard, which you might want to support as well:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;.headers.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;RateLimit-Policy&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`&amp;quot;default&amp;quot;;q=${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;callsPerMinute&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;};w=60`
&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;.headers.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;RateLimit&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`&amp;quot;default&amp;quot;;r=${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;remaining&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;};t=${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;secondsUntilRefresh&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`
&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In the case of failure, you might also want to send the &lt;code&gt;Retry-After&lt;&#x2F;code&gt; header, which is a standard way of telling clients to wait until a specified time or number of seconds before retrying:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;.headers.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Retry-After&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;secondsUntilRefresh&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Lots of headers! It might not make sense to send all of them, especially when response size is an important factor in your use case.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;adding-openapi-docs-support&quot;&gt;Adding OpenAPI docs support&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m a big fan of the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rhinobase&#x2F;hono-openapi&quot;&gt;&lt;code&gt;hono-openapi&lt;&#x2F;code&gt; package&lt;&#x2F;a&gt;, which makes it easy to generate an &lt;a href=&quot;https:&#x2F;&#x2F;spec.openapis.org&#x2F;oas&#x2F;latest.html&quot;&gt;OpenAPI&lt;&#x2F;a&gt; spec for your API.&lt;&#x2F;p&gt;
&lt;p&gt;Although rate limits are not an official part of the OpenAPI specification, it often is added using a &lt;code&gt;x-ratelimits&lt;&#x2F;code&gt; key. To make sure these are added in the spec generated by &lt;code&gt;hono-openapi&lt;&#x2F;code&gt;, we can have our middleware return some metadata:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;uniqueSymbol &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;as &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;openApiSpec &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hono-openapi&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;rateLimitUser &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;callsPerMinute&lt;&#x2F;span&gt;&lt;span&gt;: number) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;middleware &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;createMiddleware&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;{
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...	
&lt;&#x2F;span&gt;&lt;span&gt;	}&amp;gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;next&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;  
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Attach OpenAPI spec info
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Object&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;assign&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;middleware&lt;&#x2F;span&gt;&lt;span&gt;, {
&lt;&#x2F;span&gt;&lt;span&gt;		[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;openApiSpec&lt;&#x2F;span&gt;&lt;span&gt;]: {
&lt;&#x2F;span&gt;&lt;span&gt;			spec: {
&lt;&#x2F;span&gt;&lt;span&gt;				&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;x-ratelimit&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;					limit: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;callsPerMinute&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;					window: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;60&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;				},
&lt;&#x2F;span&gt;&lt;span&gt;			},
&lt;&#x2F;span&gt;&lt;span&gt;		},
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;using-our-middleware&quot;&gt;Using our middleware&lt;&#x2F;h2&gt;
&lt;p&gt;Okay, let&#x27;s finally actually use it:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Hono &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hono&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;api &lt;&#x2F;span&gt;&lt;span&gt;= new Hono();
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;api&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&#x2F;hello&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;rateLimitUser&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;), (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;body&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Hello world!&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now let&#x27;s call it:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span&gt; http:&#x2F;&#x2F;localhost:3000&#x2F;hello
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Hello&lt;&#x2F;span&gt;&lt;span&gt; world!
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span&gt; http:&#x2F;&#x2F;localhost:3000&#x2F;hello
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Hello&lt;&#x2F;span&gt;&lt;span&gt; world!
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span&gt; http:&#x2F;&#x2F;localhost:3000&#x2F;hello
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;error&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Rate limit exceeded. Please wait a minute.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>A Story of a Story of Recursion</title>
          <pubDate>Sat, 23 Aug 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/a-story-of-a-story-of-recursion/</link>
          <guid>https://schof.co/a-story-of-a-story-of-recursion/</guid>
          <description xml:base="https://schof.co/a-story-of-a-story-of-recursion/">&lt;p&gt;When I was a child, one time my dad told me a story about a group of robbers sitting under a blade of grass. The story went something like this:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the depth of night, seven robbers sat hidden in the shadow of a blade of grass. The leader of the group stood up and said &quot;Pedro, tell us a story.&quot;&lt;br &#x2F;&gt;
Pedro stood up and started speaking: &quot;In the depth of night, seven robbers sat hidden in the shadow of a blade of grass...&quot;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;I loved this story and the &lt;a href=&quot;https:&#x2F;&#x2F;en.m.wikipedia.org&#x2F;wiki&#x2F;Recursion&quot;&gt;recursive&lt;&#x2F;a&gt; nature of it. My dad would take a lot of joy in making the story, which apparently is an old joke from the 50s, more and more elaborate, and turn it from three sentences into a 20 minute tale.&lt;&#x2F;p&gt;
&lt;p&gt;This is not just repetition – each time you actually go a level deeper. You could imagine the story ending like this:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;He finished his story, sat down, and took a swig of his ale.&quot; Pedro finished his story, sat down, and took a swig of his ale.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Recursion in storytelling still really grabs me. I found another example this morning, in the lyrics of a song by &lt;em&gt;RUBII&lt;&#x2F;em&gt; called &lt;a href=&quot;https:&#x2F;&#x2F;genius.com&#x2F;Rubii-botb-lyrics&quot;&gt;BOTB&lt;&#x2F;a&gt; that I really like:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;I&#x27;ve got some big bossin&#x27; b&#x27;s, and some boys on the block&lt;br &#x2F;&gt;
I call them a couple G&#x27;s, and they think that I&#x27;m hot&lt;br &#x2F;&gt;
...&lt;br &#x2F;&gt;
There&#x27;s no debating, the consensus is&lt;br &#x2F;&gt;
I&#x27;ve got some big bossin&#x27; b&#x27;s, and some boys on the block&lt;br &#x2F;&gt;
...&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Makes an already great song even better!&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Creating Modern Wordpress Themes</title>
          <pubDate>Tue, 19 Aug 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/creating-modern-wordpress-themes/</link>
          <guid>https://schof.co/creating-modern-wordpress-themes/</guid>
          <description xml:base="https://schof.co/creating-modern-wordpress-themes/">&lt;p&gt;I started out as a developer building Wordpress websites when I was in my early teens. Since then, I haven&#x27;t really touched Wordpress for a while, other than occasionally having to make some small tweaks to one of those ancient websites.&lt;&#x2F;p&gt;
&lt;p&gt;Modern Wordpress is something completely different, though. I had heard of &lt;a href=&quot;https:&#x2F;&#x2F;wordpress.org&#x2F;gutenberg&#x2F;&quot;&gt;Gutenberg&lt;&#x2F;a&gt;, and tried it in its early stages a few years ago, but I did not actively work with it until this week. It&#x27;s awesome. The Wordpress &lt;a href=&quot;https:&#x2F;&#x2F;wordpress.org&#x2F;documentation&#x2F;article&#x2F;site-editor&#x2F;&quot;&gt;full-site editing&lt;&#x2F;a&gt; experience is as good or better than Squarespace and Shopify&#x27;s visual site editors.&lt;&#x2F;p&gt;
&lt;p&gt;It basically allows you to build a whole site without needing any custom code in a lot of cases. If you do want to build a custom theme, where&#x27;s how to get started:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-you-might-want-a-custom-theme&quot;&gt;Why you might want a custom theme&lt;&#x2F;h2&gt;
&lt;p&gt;Themes allow you to add a few things that you might want to use to enhance your experience using the site editor:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https:&#x2F;&#x2F;developer.wordpress.org&#x2F;themes&#x2F;global-settings-and-styles&#x2F;&quot;&gt;Theme settings and styles&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; allow you to set default colors, fonts and spacing&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https:&#x2F;&#x2F;developer.wordpress.org&#x2F;themes&#x2F;patterns&#x2F;introduction-to-patterns&#x2F;&quot;&gt;Patterns&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; allow you to create reusable groups of blocks (think a hero section, a pricing table, a navigation bar).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https:&#x2F;&#x2F;developer.wordpress.org&#x2F;block-editor&#x2F;&quot;&gt;Custom blocks&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; to add specific functionality, like a testimonials slider or a specific styled embed.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;creating-our-theme&quot;&gt;Creating our theme&lt;&#x2F;h2&gt;
&lt;p&gt;Let&#x27;s start by creating an empty directory for our theme. To build any theme in Wordpress, you first need to create a &lt;code&gt;style.css&lt;&#x2F;code&gt; file that will define our theme. Fill it with something like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;*
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;Theme Name: my-website
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;Theme URI: https:&#x2F;&#x2F;my-website.com
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;Description: Custom theme for my-website.com, based on &amp;#39;Twenty Twenty-Four&amp;#39;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;Author: Includable
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;Author URI: https:&#x2F;&#x2F;includable.com
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;Template: twentytwentyfour
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;Version: 1.0.0
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;*&#x2F;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It only acts to register the theme. This file is sufficient to show your theme as an option in the dashboard.&lt;&#x2F;p&gt;
&lt;p&gt;We extend the built-in theme &lt;code&gt;twentytwentyfour&lt;&#x2F;code&gt; here to inherit all of its default styles and patterns.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;running-locally&quot;&gt;Running locally&lt;&#x2F;h2&gt;
&lt;p&gt;Now, let&#x27;s create a &lt;code&gt;.wp-env.json&lt;&#x2F;code&gt; file, so that we can run our theme in a local Wordpress installation to test and develop it:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;core&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;themes&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This simply says that the Wordpress core should be downloaded, and the current directory should be installed as a theme on the test site.&lt;&#x2F;p&gt;
&lt;p&gt;A simple &lt;code&gt;npx @wordpress&#x2F;env start&lt;&#x2F;code&gt; in this directory will spin up a set of Docker containers, and within a minute or two you&#x27;ll have a local Wordpress installation running on &lt;code&gt;http:&#x2F;&#x2F;localhost:8888&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;theme-settings-and-styles&quot;&gt;Theme settings and styles&lt;&#x2F;h2&gt;
&lt;p&gt;You might want to change some of the defaults of this theme, like the color palette that is available. Doing so is easy through creating a &lt;code&gt;theme.json&lt;&#x2F;code&gt; file. Any keys not specified will inherit their values from the parent theme:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;json&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-json &quot;&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;$schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;schemas.wp.org&#x2F;wp&#x2F;6.5&#x2F;theme.json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;version&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;settings&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;color&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;palette&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [
&lt;&#x2F;span&gt;&lt;span&gt;				{
&lt;&#x2F;span&gt;&lt;span&gt;					&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;color&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;#F05B4D&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;					&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;My red&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;					&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;slug&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;red&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;				},
&lt;&#x2F;span&gt;&lt;span&gt;				{
&lt;&#x2F;span&gt;&lt;span&gt;					&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;color&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;#ECB251&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;					&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;My yellow&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;					&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;slug&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;yellow&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;				}
&lt;&#x2F;span&gt;&lt;span&gt;			]
&lt;&#x2F;span&gt;&lt;span&gt;		}
&lt;&#x2F;span&gt;&lt;span&gt;	}
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can find out the other available settings &lt;a href=&quot;https:&#x2F;&#x2F;developer.wordpress.org&#x2F;themes&#x2F;global-settings-and-styles&#x2F;settings&#x2F;&quot;&gt;here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I usually tend to also add the following:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;typography.fontFamilies&lt;&#x2F;code&gt; to set the available fonts. If you extend the theme &lt;code&gt;twentytwentyfour&lt;&#x2F;code&gt;, you want to have at least two fonts with the slugs &lt;code&gt;body&lt;&#x2F;code&gt; and &lt;code&gt;heading&lt;&#x2F;code&gt; defined, as those are referenced in the default styles of the theme.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;layout.contentSize&lt;&#x2F;code&gt; and &lt;code&gt;layout.wideSize&lt;&#x2F;code&gt; control the max width of containers on the page.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;loading-custom-css&quot;&gt;Loading custom CSS&lt;&#x2F;h2&gt;
&lt;p&gt;If you want to use &lt;code&gt;style.css&lt;&#x2F;code&gt; as a stylesheet to override some default CSS, you need to &lt;a href=&quot;https:&#x2F;&#x2F;developer.wordpress.org&#x2F;reference&#x2F;functions&#x2F;wp_enqueue_style&#x2F;&quot;&gt;enqueue&lt;&#x2F;a&gt; the stylesheet. The easiest way to do this is to add a &lt;code&gt;functions.php&lt;&#x2F;code&gt; file to your theme, and add the following:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;php&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-php &quot;&gt;&lt;code class=&quot;language-php&quot; data-lang=&quot;php&quot;&gt;&lt;span style=&quot;color:#be5046;&quot;&gt;&amp;lt;?php
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;function &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;my_website_enqueue_styles&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;wp_enqueue_style&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;my-website-style&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;get_stylesheet_directory_uri&lt;&#x2F;span&gt;&lt;span&gt;() . &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;&#x2F;style.css&amp;#39;
&lt;&#x2F;span&gt;&lt;span&gt;    );
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;add_action&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;wp_enqueue_scripts&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;my_website_enqueue_styles&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;add_action&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;enqueue_block_editor_assets&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;my_website_enqueue_styles&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that we execute this for two different &lt;a href=&quot;https:&#x2F;&#x2F;developer.wordpress.org&#x2F;plugins&#x2F;hooks&#x2F;&quot;&gt;hooks&lt;&#x2F;a&gt;: when the front-end loads (&lt;code&gt;wp_enqueue_scripts&lt;&#x2F;code&gt;) to get it added to the &lt;code&gt;&amp;lt;head&amp;gt;&lt;&#x2F;code&gt; of the public website, and in the site editor (&lt;code&gt;enqueue_block_editor_assets&lt;&#x2F;code&gt;), so that we can see those styles reflected whilst editing in the dashboard as well.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;adding-custom-blocks&quot;&gt;Adding custom blocks&lt;&#x2F;h2&gt;
&lt;p&gt;If you want to add custom blocks, your best bet is to use the &lt;a href=&quot;https:&#x2F;&#x2F;developer.wordpress.org&#x2F;block-editor&#x2F;reference-guides&#x2F;packages&#x2F;packages-create-block&#x2F;&quot;&gt;&lt;code&gt;@wordpress&#x2F;create-block&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; CLI to scaffold block files. After running this, you can copy the &lt;code&gt;src&lt;&#x2F;code&gt; directory of the result, as well as the &lt;code&gt;package.json&lt;&#x2F;code&gt; file into your theme directory.&lt;&#x2F;p&gt;
&lt;p&gt;You&#x27;ll now have a setup that lets you edit your custom block(s) and automatically build them by running &lt;code&gt;npm run build&lt;&#x2F;code&gt;, or re-build them automatically as you edit the files using &lt;code&gt;npm start&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;To actually register the blocks to be usable in the editor, you need to add some more code to your &lt;code&gt;functions.php&lt;&#x2F;code&gt; file:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;php&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-php &quot;&gt;&lt;code class=&quot;language-php&quot; data-lang=&quot;php&quot;&gt;&lt;span style=&quot;color:#be5046;&quot;&gt;&amp;lt;?php
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;function &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;my_website_create_blocks&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;wp_register_block_types_from_metadata_collection&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;__DIR__ &lt;&#x2F;span&gt;&lt;span&gt;. &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;&#x2F;build&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;__DIR__ &lt;&#x2F;span&gt;&lt;span&gt;. &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;&#x2F;build&#x2F;blocks-manifest.php&amp;#39;
&lt;&#x2F;span&gt;&lt;span&gt;    );
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;add_action&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;init&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;my_website_create_blocks&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will register any block referenced in the auto-generated &lt;code&gt;build&#x2F;blocks-manifest.php&lt;&#x2F;code&gt; file.&lt;&#x2F;p&gt;
&lt;p&gt;This all is really fun and relatively easy - building blocks and then immediately getting to use them yourself is quite fun, although editing them and refreshing the site editor takes a bit of getting used to if you&#x27;re used to hot reloading React applications.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Building AI Workflows Using Alfred</title>
          <pubDate>Sun, 10 Aug 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/building-ai-workflows-using-alfred/</link>
          <guid>https://schof.co/building-ai-workflows-using-alfred/</guid>
          <description xml:base="https://schof.co/building-ai-workflows-using-alfred/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.alfredapp.com&quot;&gt;Alfred&lt;&#x2F;a&gt; has long been my launcher of choice, and I have dozens of custom workflows built for it, including lots of internal tools that help me quickly look up items in the databases of my various projects.&lt;&#x2F;p&gt;
&lt;p&gt;More and more, I&#x27;m building simple AI workflows within Alfred, so I thought I&#x27;d share how I go about it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;things-todo-workflow&quot;&gt;Things todo workflow&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s an example of a workflow that takes some text, and turns it into an actionable task in &lt;a href=&quot;https:&#x2F;&#x2F;culturedcode.com&#x2F;things&#x2F;&quot;&gt;Things&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;1KC19ZD&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;I use this one a lot to copy over Slack messages I&#x27;ve received or sent (e.g. &quot;cool, I&#x27;ll send the newsletter on Monday&quot;), and have it turn this into a Things task (&quot;Send newsletter on Monday&quot;).&lt;&#x2F;p&gt;
&lt;p&gt;After adding a keyword entry point, I added a little &quot;Args and vars&quot; utility to hold the prompt:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;D2ZR_7s&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;This is useful, as it allows me to change the text of the prompt, without having to dive into the &#x27;script&#x27; stage. The full prompt in this example looks like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;plain&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-plain &quot;&gt;&lt;code class=&quot;language-plain&quot; data-lang=&quot;plain&quot;&gt;&lt;span&gt;Extract the title of a task from the text pasted in by the user. Make it actionable, e.g. including a verb. If the text passed in is already in this format, you don&amp;#39;t need to change anything, other than cleaning up capitalisation and punctuation. Don&amp;#39;t include a period at the end of the task title. 
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;Be as specific as possible with the title of the task, and extract as much information from the input as possible.
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;User input:
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;{query}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I pass through the original query as the variable &lt;code&gt;description&lt;&#x2F;code&gt;, so that I can add it to my task in Things for some additional context.&lt;&#x2F;p&gt;
&lt;p&gt;The &#x27;Run script&#x27; stage looks like this:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;bpBh4_C&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;It&#x27;s a simple bash script that builds a JSON payload, then extracts the response text. Here it is in full:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;#!&#x2F;bin&#x2F;bash
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Configuration
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;API_KEY&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;YOUR API KEY&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;MODEL&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;gemini-2.5-flash-lite&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;API_URL&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;generativelanguage.googleapis.com&#x2F;v1beta&#x2F;models&#x2F;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;MODEL&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;:generateContent&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Get input from Alfred
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;query&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Prepare the request body
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;request_body&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cat &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;EOF
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;{
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;  &amp;quot;contents&amp;quot;: [
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;    {
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;      &amp;quot;parts&amp;quot;: [
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;        {
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;          &amp;quot;text&amp;quot;: &amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;query&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;        }
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;      ]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;    }
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;  ]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;EOF
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Send request to Gemini API
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;response&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;curl -s -X&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt; POST &amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;API_URL&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;?key=$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;API_KEY&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot; \
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;	-H &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Content-Type: application&#x2F;json&amp;quot; \
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;	-d &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;request_body&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;)
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Extract and output the text response
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;echo &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;response&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;grep -o &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;&amp;quot;text&amp;quot;: &amp;quot;[^&amp;quot;]*&amp;quot;&amp;#39; &lt;&#x2F;span&gt;&lt;span&gt;| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;head -1 &lt;&#x2F;span&gt;&lt;span&gt;| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;sed &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;s&#x2F;&amp;quot;text&amp;quot;: &amp;quot;\(.*\)&amp;quot;&#x2F;\1&#x2F;&amp;#39;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I found Gemini&#x27;s 2.5 Flash Lite model to be best for this, since it&#x27;s fast and small, and the prompt is simple enough to produce decent results. If you have a more complex prompt, you might want to go for Gemini Flash or Gemini Pro instead.&lt;&#x2F;p&gt;
&lt;p&gt;The last step simply involves opening Things with its &lt;a href=&quot;https:&#x2F;&#x2F;culturedcode.com&#x2F;things&#x2F;support&#x2F;articles&#x2F;2803573&#x2F;&quot;&gt;URL scheme&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;CDZ9jrh&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;h2 id=&quot;other-uses&quot;&gt;Other uses&lt;&#x2F;h2&gt;
&lt;p&gt;I have similar scripts to do other things. The main one I like is &quot;create event from text&quot;, which allows me to quickly create a calendar event from some text.&lt;&#x2F;p&gt;
&lt;p&gt;I tend to copy some text from a web page or text message, and have it create a calendar event for me.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Building a Plugin System for a React App</title>
          <pubDate>Wed, 30 Jul 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/building-a-plugin-system-for-a-react-app/</link>
          <guid>https://schof.co/building-a-plugin-system-for-a-react-app/</guid>
          <description xml:base="https://schof.co/building-a-plugin-system-for-a-react-app/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;examplary.ai&quot;&gt;Examplary&lt;&#x2F;a&gt; is a new tool we&#x27;re building to help teachers create better tests and exams.&lt;&#x2F;p&gt;
&lt;p&gt;One of my philosophies about product development is that the more integration and extensibility options you create within a product, the more opportunities you&#x27;re creating for a community to flourish around it. As part of that, I wanted to make it possible to create custom question types.&lt;&#x2F;p&gt;
&lt;p&gt;When creating an exam, you have a few default question types you can choose from - single line text, multiple choice, essay, etc. In the future, I envision also being able to add programming questions, diagramming questions, and all sorts of subject-specific questions.&lt;&#x2F;p&gt;
&lt;p&gt;Rather than having to implement these all within the core platform, I wanted to give other developers the opportunity to build these question types, and make them available on the platform.&lt;&#x2F;p&gt;
&lt;p&gt;So after a few weeks of thinking about it, within a few days, I built an MVP, including &lt;a href=&quot;https:&#x2F;&#x2F;developers.examplary.ai&#x2F;question-types&#x2F;&quot;&gt;developer documentation&lt;&#x2F;a&gt;, a &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;@examplary&#x2F;question-type-bundler&quot;&gt;CLI tool&lt;&#x2F;a&gt; for local development and publishing, and an open-sourced &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;examplary-ai&#x2F;default-questions-pack&quot;&gt;default question types&lt;&#x2F;a&gt; pack.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;4UOLnLN&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;h2 id=&quot;anatomy-of-a-question-type&quot;&gt;Anatomy of a question type&lt;&#x2F;h2&gt;
&lt;p&gt;Under the hood, question types consist of a JSON file with metadata, and one or more React components that are shown within the Examplary UI in different scenarios (e.g. one to display the question to a student when taking a test online, one for the print version of the question, and one to show the student&#x27;s answer in the teacher&#x27;s review tool).&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#282c34;color:#abb2bf;&quot;&gt;&lt;code&gt;&lt;span&gt;my-custom-question-type&#x2F;  
&lt;&#x2F;span&gt;&lt;span&gt;├── question-type.json  
&lt;&#x2F;span&gt;&lt;span&gt;├── icon.svg  
&lt;&#x2F;span&gt;&lt;span&gt;├── component-assessment.jsx  
&lt;&#x2F;span&gt;&lt;span&gt;└── component-print.jsx
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;&#x2F;h2&gt;
&lt;p&gt;It took me a long time to think about the best way to get arbitrary React components rendered within our front-end. After all, modern React components require some transpiling and bundling to make them executable in a browser.&lt;&#x2F;p&gt;
&lt;p&gt;I didn&#x27;t want to have developers creating a question type have to deal with Vite or &lt;a href=&quot;https:&#x2F;&#x2F;esbuild.github.io&quot;&gt;Esbuild&lt;&#x2F;a&gt; directly though, so I decided to build a simple CLI tool in Node that uses Esbuild under the hood to bundle the React components in a format I can load within the main Examplary app. It also will detect any &lt;a href=&quot;https:&#x2F;&#x2F;tailwindcss.com&quot;&gt;Tailwind CSS&lt;&#x2F;a&gt; classes used in the component, and bundle the resulting CSS styles alongside the component.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;@examplary&#x2F;question-type-bundler&quot;&gt;CLI&lt;&#x2F;a&gt; then uploads these to our file storage, and &lt;a href=&quot;https:&#x2F;&#x2F;developers.examplary.ai&#x2F;rest-api&#x2F;post-question-types&quot;&gt;calls the Examplary API&lt;&#x2F;a&gt; to insert or update the question type definition in our database.&lt;&#x2F;p&gt;
&lt;p&gt;In the metadata file, you can specify whether you want to make your question type available for use by all Examplary users, or only within the workspace your API key is tied to.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;under-the-hood-bundling&quot;&gt;Under the hood: bundling&lt;&#x2F;h2&gt;
&lt;p&gt;My description of the bundling might make it seem like this is a long, difficult process, but I was astonished how few lines of code were required for this.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s what the main JS bundling looks like (omitting some boring code that generates and saves source maps):&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;build &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;esbuild&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;buildComponent &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;build&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;    entryPoints: [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file&lt;&#x2F;span&gt;&lt;span&gt;],
&lt;&#x2F;span&gt;&lt;span&gt;    bundle: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    write: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    minify: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    platform: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;browser&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    format: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;cjs&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    external: [
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;@examplary&#x2F;ui&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react-dom&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react&#x2F;jsx-runtime&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react-dom&#x2F;client&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    ],
&lt;&#x2F;span&gt;&lt;span&gt;  });
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;js &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;outputFiles&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;].text;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;js&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;By specifying React and &lt;code&gt;@examplary&#x2F;ui&lt;&#x2F;code&gt; as external, we&#x27;re telling the bundler not to bundle references to them, but instead to keep &lt;code&gt;require()&lt;&#x2F;code&gt; statements to load those libraries at run-time. Since we already include them in our main app front-end, we don&#x27;t want to double up and make our bundle sizes much larger!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;under-the-hood-front-end-rendering&quot;&gt;Under the hood: front-end rendering&lt;&#x2F;h2&gt;
&lt;p&gt;The actual rendering of the bundled components is a bit more complicated.&lt;&#x2F;p&gt;
&lt;p&gt;Since we&#x27;ve specified CommonJS as the format for bundling, the code expects to be executed in an environment where the &lt;code&gt;module&lt;&#x2F;code&gt; object and the &lt;code&gt;require&lt;&#x2F;code&gt; function exist.&lt;&#x2F;p&gt;
&lt;p&gt;For our purposes, we can implement minimal versions of these, like so:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;as &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;React &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;react&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;as &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;ExamplaryUI &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;@examplary&#x2F;ui&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;module &lt;&#x2F;span&gt;&lt;span&gt;= { 
&lt;&#x2F;span&gt;&lt;span&gt;  exports: {}
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;require &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;name &lt;&#x2F;span&gt;&lt;span&gt;=== &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;React&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;name &lt;&#x2F;span&gt;&lt;span&gt;=== &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;@examplary&#x2F;ui&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;ExamplaryUI&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;throw &lt;&#x2F;span&gt;&lt;span&gt;new Error(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`Module not found: ${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we can execute our bundled code within the context of these two variables. There&#x27;s a few ways to do that, and none of them seem very clean.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s the one that will probably make some Eslint rules shout:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Execute our bundled code in context
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;scopedJs &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`const { require, module } = this; ${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;js&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;new Function(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;scopedJs&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;call&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;module&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;require
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; If it worked, module.exports should now be populated!
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component &lt;&#x2F;span&gt;&lt;span&gt;= module.exports.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;default&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;throw &lt;&#x2F;span&gt;&lt;span&gt;new RuntimeError(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;No default export found!&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Now you can render &amp;lt;Component &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;What this does is create a new function from a string (similar to the dangerous &lt;code&gt;eval&lt;&#x2F;code&gt;!), and then set &lt;code&gt;this&lt;&#x2F;code&gt; to be whatever you pass into &lt;code&gt;.call()&lt;&#x2F;code&gt;. We then destruct &lt;code&gt;this&lt;&#x2F;code&gt; so we have &lt;code&gt;module&lt;&#x2F;code&gt; and &lt;code&gt;require&lt;&#x2F;code&gt; variables in the scope.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;In production, you might want to consider running this code in an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;&#x2F;code&gt; to shield it from the rest of your application&lt;&#x2F;strong&gt;, and you probably want to throw in some &lt;code&gt;try ... catch&lt;&#x2F;code&gt; statements to display error messages if something goes wrong at any step of this process.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;under-the-hood-styles&quot;&gt;Under the hood: styles&lt;&#x2F;h2&gt;
&lt;p&gt;Since the rest of Examplary uses Tailwind, I also wanted the option of using Tailwind class names in my question type components.&lt;&#x2F;p&gt;
&lt;p&gt;Tailwind usually scans your files to dynamically only create the CSS you need, given the class names you use.&lt;&#x2F;p&gt;
&lt;p&gt;Including a line like this in your React component:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;input className&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;mt-4&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Causes Tailwind to spit out this in your stylesheet:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;css&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-css &quot;&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;.mt-4 &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  margin-top: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;calc&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;var&lt;&#x2F;span&gt;&lt;span&gt;(--spacing) * &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Of course, Tailwind won&#x27;t know about the class names used in question types, since the code for those isn&#x27;t part of the main application&#x27;s codebase.&lt;&#x2F;p&gt;
&lt;p&gt;There are also static builds of Tailwind that include all default styles, but those are really bulky, plus they don&#x27;t allow some of the more dynamic options in Tailwind I tend to rely on, like being able to specify any value by including it within square brackets:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;input className&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;h-[158px]&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So instead, I decided to run the Tailwind bundler over the React components as part of the question type bundling process. Again, the code is a lot simpler than you might expect:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;tailwindcss &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;@tailwindcss&#x2F;postcss&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;postcss &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;postcss&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;buildStyles &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;() &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;inputCSS &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`@import &amp;quot;tailwindcss&amp;quot;;`&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;result &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;postcss&lt;&#x2F;span&gt;&lt;span&gt;([
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;tailwindcss&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;      optimize: { minify: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;true &lt;&#x2F;span&gt;&lt;span&gt;},
&lt;&#x2F;span&gt;&lt;span&gt;    }),
&lt;&#x2F;span&gt;&lt;span&gt;  ]).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;process&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;inputCSS&lt;&#x2F;span&gt;&lt;span&gt;, { from: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;styles.css&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;result&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;css&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That&#x27;s all that&#x27;s needed to generate a Tailwind CSS file based on the classes used in files in the current working directory.&lt;&#x2F;p&gt;
&lt;p&gt;In our production version, we do a bit more to remove Tailwind&#x27;s default CSS reset styles (the &lt;a href=&quot;https:&#x2F;&#x2F;tailwindcss.com&#x2F;docs&#x2F;preflight#disabling-preflight&quot;&gt;&#x27;preflight styles&#x27;&lt;&#x2F;a&gt;) to ensure the bundle is as small as possible, and so that it doesn&#x27;t override any of our custom theme in the main app.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;I do really think that extensible software is better software, but I also simply really enjoyed this challenge.&lt;&#x2F;p&gt;
&lt;p&gt;Building developer tools that are easy to use and abstract all the complexity is quite fun, and this was a project where I spent almost more time upfront thinking out how the whole thing would work than building it. Having those puzzle pieces slot into place (almost always whilst in the shower or dozing off to sleep) is an amazing feeling.&lt;&#x2F;p&gt;
&lt;p&gt;Now we&#x27;ll just have to wait and see if anyone other than me will ever use this to build custom question types 😁&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Changing My Mind on Tech Monopolies</title>
          <pubDate>Sun, 27 Jul 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/changing-my-mind-on-tech-monopolies/</link>
          <guid>https://schof.co/changing-my-mind-on-tech-monopolies/</guid>
          <description xml:base="https://schof.co/changing-my-mind-on-tech-monopolies/">&lt;p&gt;When &lt;span title=&quot;Internal link: John Oliver&quot;&gt;John Oliver&lt;&#x2F;span&gt; did a segment on &lt;span title=&quot;Internal link: Last Week Tonight&quot;&gt;Last Week Tonight&lt;&#x2F;span&gt; about tech monopolies a few years ago, I was quite hesitant about the &quot;break up big tech&quot; message. It seemed unrealistic and difficult to achieve to me.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve fully changed my mind on this now – breaking up big tech companies, no matter how difficult (although there is sufficient relevant precedent) – is the only effective way to:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Restore a healthy tech startup ecosystem, that isn&#x27;t fully based on hype cycles around AI and cryptocurrency, and actually allows small, sustainable (tech) companies to sprout up and stick around.&lt;&#x2F;li&gt;
&lt;li&gt;Create space for new online communities to thrive, like the way Tumblr felt before 2015, or all of the small self-hosted forums that used to be very popular at the start of the century.&lt;&#x2F;li&gt;
&lt;li&gt;Undo some harmful legislation, like parts of the UK&#x27;s Online Safety Act, which can only feasibly implemented in markets where there are just a few large companies that own most of an online space.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Not likely to happen anytime soon, but the changes in the EU so far have been great. Meta giving people the option to pay for Instagram to get an ad-free experience is an amazing thing (although it&#x27;s more expensive than it has a right to be, I wonder how the European Commission feels about the pricing), and I hope to see more of that type of stuff.&lt;&#x2F;p&gt;
&lt;p&gt;Hopefully, the changes the EU are making will force big companies to also roll out those changes elsewhere. I also want access to alternative app stores and ad-free social media platforms!&lt;&#x2F;p&gt;
&lt;iframe width=&quot;560&quot; style=&quot;width: 100%&quot; height=&quot;315&quot; src=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;jXf04bhcjbg?si=whlX6DJPhy-LoZPA&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Force Multipliers Versus Form Factors</title>
          <pubDate>Fri, 20 Jun 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/force-multipliers-versus-form-factors/</link>
          <guid>https://schof.co/force-multipliers-versus-form-factors/</guid>
          <description xml:base="https://schof.co/force-multipliers-versus-form-factors/">&lt;p&gt;Whenever people talk about GenAI as the &#x27;next big thing&#x27;, the lineage of previous &#x27;big things&#x27; seems very variable on the speaker. Usually it&#x27;s something like the personal computer, the web, smartphones, and now GenAI.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s a similar version of this in &lt;a href=&quot;https:&#x2F;&#x2F;www.ben-evans.com&#x2F;presentations&quot;&gt;a (excellent!) deck from Benedict Evans&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;tqj-MkI&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;I always find these somewhat debatable, and instinctively want to push back against it. I realised today that&#x27;s because when we show a progression like this, we&#x27;re mixing two concepts: &lt;strong&gt;human capability force multipliers&lt;&#x2F;strong&gt;, and &lt;strong&gt;technology form factors&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;If I think about major force multipliers for what we as humans are capable of doing on this planet, the main items that come to mind for me are:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Electromechanics&lt;&#x2F;li&gt;
&lt;li&gt;Integrated circuits and processors&lt;&#x2F;li&gt;
&lt;li&gt;The technology powering the internet&lt;&#x2F;li&gt;
&lt;li&gt;Generative AI&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;These are wholly separate from the form factors those things have taken over the decades:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Mainframes&lt;&#x2F;li&gt;
&lt;li&gt;Personal computers&lt;&#x2F;li&gt;
&lt;li&gt;Smartphones&lt;&#x2F;li&gt;
&lt;li&gt;Wearables?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This is still not a very clean separation, but to me makes the argument about LLMs being the &lt;em&gt;next big thing&lt;&#x2F;em&gt; a lot more convincing.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Using Gemini in My Todo Flow</title>
          <pubDate>Tue, 17 Jun 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/using-gemini-in-my-todo-flow/</link>
          <guid>https://schof.co/using-gemini-in-my-todo-flow/</guid>
          <description xml:base="https://schof.co/using-gemini-in-my-todo-flow/">&lt;p&gt;Maybe it&#x27;s taken too long, but I more and more am actually integrating little LLM tools into my daily workflows, other than coding and chat.&lt;&#x2F;p&gt;
&lt;p&gt;This week, I&#x27;ve added Gemini into my flow to add tasks into &lt;a href=&quot;https:&#x2F;&#x2F;culturedcode.com&#x2F;things&#x2F;&quot;&gt;Things&lt;&#x2F;a&gt;. I often will receive a Slack message that contains a task, or will reply to someone saying &quot;Okay, I&#x27;ll take care of doing X later today.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;Rather than having to turn this into a task in Things, I use an &lt;a href=&quot;https:&#x2F;&#x2F;www.alfredapp.com&#x2F;&quot;&gt;Alfred&lt;&#x2F;a&gt; workflow to have Gemini turn the message into an actionable task:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;XHzluuN&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;The flow for this is very simple:
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;-GDr1eU&quot; alt=&quot;Image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;An &#x27;Arg and vars&#x27; block contains the prompt:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;text&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-text &quot;&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;Extract the title of a task from the text pasted in by the user. Make 
&lt;&#x2F;span&gt;&lt;span&gt;it actionable, e.g. including a verb. If the text passed in is already 
&lt;&#x2F;span&gt;&lt;span&gt;in this format, you don&amp;#39;t need to change anything, other than cleaning 
&lt;&#x2F;span&gt;&lt;span&gt;up capitalisation and punctuation. Don&amp;#39;t include a period at the end 
&lt;&#x2F;span&gt;&lt;span&gt;of the task title. 
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;Be as specific as possible with the title of the task, and extract as 
&lt;&#x2F;span&gt;&lt;span&gt;much information from the input as possible.
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;User input:
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;{query}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And then a simple bash script runs it through Gemini 2.5 Flash:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Configuration
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;API_KEY&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;YOUR API KEY HERE&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;MODEL&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;gemini-2.5-flash-preview-05-20&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;API_URL&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;generativelanguage.googleapis.com&#x2F;v1beta&#x2F;models&#x2F;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;MODEL&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;:generateContent&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Get input from Alfred
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;query&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Prepare the request body
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;request_body&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cat &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;EOF
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;{
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;  &amp;quot;contents&amp;quot;: [
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;    {
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;      &amp;quot;parts&amp;quot;: [
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;        {
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;          &amp;quot;text&amp;quot;: &amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;query&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;        }
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;      ]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;    }
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;  ]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;EOF
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Send request to Gemini API
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;response&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;curl -s -X&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt; POST &amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;API_URL&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;?key=$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;API_KEY&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot; \
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;	-H &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Content-Type: application&#x2F;json&amp;quot; \
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;	-d &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;request_body&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;)
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;# Extract and output the text response
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;echo &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;$&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;response&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;grep -o &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;&amp;quot;text&amp;quot;: &amp;quot;[^&amp;quot;]*&amp;quot;&amp;#39; &lt;&#x2F;span&gt;&lt;span&gt;| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;head -1 &lt;&#x2F;span&gt;&lt;span&gt;| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;sed &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;s&#x2F;&amp;quot;text&amp;quot;: &amp;quot;\(.*\)&amp;quot;&#x2F;\1&#x2F;&amp;#39;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Rendering a PDF Page to a JPEG in Node.js</title>
          <pubDate>Fri, 06 Jun 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/rendering-a-pdf-page-to-a-jpeg-in-nodejs/</link>
          <guid>https://schof.co/rendering-a-pdf-page-to-a-jpeg-in-nodejs/</guid>
          <description xml:base="https://schof.co/rendering-a-pdf-page-to-a-jpeg-in-nodejs/">&lt;p&gt;&lt;code&gt;PDF.js&lt;&#x2F;code&gt; is one of those libraries that seems amazing in its scope – it&#x27;s able to render basically any PDF you throw at it into an HTML canvas, with all of the weirdness of the PDF file format.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s also confusing and not super well documented.&lt;&#x2F;p&gt;
&lt;p&gt;I had to implement a use case where I wanted to let users select a section of a page from a PDF to embed it as a &#x27;clipping&#x27; in their text document. My rich text editor of choice, &lt;a href=&quot;https:&#x2F;&#x2F;tiptap.dev&#x2F;&quot;&gt;TipTap&lt;&#x2F;a&gt;, made it very easy to create a custom node type for that, which would allow the user (or an AI prompt response) to insert a custom HTML element that looked like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;page-clipping &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;url&#x2F;pdf-link.pdf&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;page&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;1&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;x&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;0.0444&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;y&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;0.2133&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;width&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;0.1022&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;height&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;0.1340&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;page-clipping&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The dimensions here are fractions of the total page width&#x2F;height (eg width=&quot;1&quot; is full width, and x=&quot;0.5&quot; is horizontally halfway on the page). Coordinates start at the top left corner of the page, so x=&quot;0&quot; and y=&quot;0&quot; is the top left corner of the page, and x=&quot;1&quot; and y=&quot;1&quot; is the bottom right corner of the page.&lt;&#x2F;p&gt;
&lt;p&gt;This was then rendered using a &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;Web_components&quot;&gt;web component&lt;&#x2F;a&gt; into a &lt;code&gt;&amp;lt;img&#x2F;&amp;gt;&lt;&#x2F;code&gt; that pointed at an API endpoint. In the endpoint, the following code would download the PDF, crop it, and output it as a JPEG image:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Download the PDF
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;response &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;fetch&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;params&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;response&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;arrayBuffer&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Load it into PDF.js
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;doc &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pdfjsLib&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getDocument&lt;&#x2F;span&gt;&lt;span&gt;({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data &lt;&#x2F;span&gt;&lt;span&gt;}).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;promise&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;page &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;doc&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getPage&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pageNumber&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;viewport &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;page&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getViewport&lt;&#x2F;span&gt;&lt;span&gt;({ scale: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;2.0 &lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipX &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;x &lt;&#x2F;span&gt;&lt;span&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;viewport&lt;&#x2F;span&gt;&lt;span&gt;.width;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipY &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;y &lt;&#x2F;span&gt;&lt;span&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;viewport&lt;&#x2F;span&gt;&lt;span&gt;.height;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipWidth &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;width &lt;&#x2F;span&gt;&lt;span&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;viewport&lt;&#x2F;span&gt;&lt;span&gt;.width;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipHeight &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;height &lt;&#x2F;span&gt;&lt;span&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;viewport&lt;&#x2F;span&gt;&lt;span&gt;.height;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Render into a new canvas
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;canvas &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;doc&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;canvasFactory&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;create&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipWidth&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipHeight&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;renderViewport &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;viewport&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;clone&lt;&#x2F;span&gt;&lt;span&gt;({ dontFlip: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;false &lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;renderContext &lt;&#x2F;span&gt;&lt;span&gt;= {
&lt;&#x2F;span&gt;&lt;span&gt;  canvasContext: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;canvas&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;  viewport: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;renderViewport&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; translate so (clipX, clipY) is at (0,0)
&lt;&#x2F;span&gt;&lt;span&gt;  transform: [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;, -&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipX&lt;&#x2F;span&gt;&lt;span&gt;, -&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipY&lt;&#x2F;span&gt;&lt;span&gt;],
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;page&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;render&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;renderContext&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;promise&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Convert to image
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;image &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;canvas&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;canvas&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;toBuffer&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;image&#x2F;jpeg&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Integrating Safe Exam Browser</title>
          <pubDate>Sun, 01 Jun 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/integrating-safe-exam-browser/</link>
          <guid>https://schof.co/integrating-safe-exam-browser/</guid>
          <description xml:base="https://schof.co/integrating-safe-exam-browser/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;safeexambrowser.org&#x2F;news_en.html&quot;&gt;Safe Exam Browser&lt;&#x2F;a&gt; is an app that can be installed on Mac, Windows and iOS to create a safe test taking environment for students that prevents some basic ways of cheating, e.g. by switching to other apps, or copying and pasting content.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s supported by Moodle and various other e-learning tools.&lt;&#x2F;p&gt;
&lt;p&gt;Implementing it is not super difficult, but is not made very easy by the fact that the &lt;a href=&quot;https:&#x2F;&#x2F;safeexambrowser.org&#x2F;developer&#x2F;overview.html&quot;&gt;developer documentation&lt;&#x2F;a&gt; on the website is not very clear, and there&#x27;s many different methods.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s how to do it using the &#x27;config keys&#x27; method, which at the time of writing seems to be the preferred way.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;creating-a-settings-file&quot;&gt;Creating a settings file&lt;&#x2F;h2&gt;
&lt;p&gt;The easiest way to get users to open the Safe Exam Browser with the correct URL and settings is to provide a link to a XML settings file, which can be opened through the &lt;code&gt;seb:&#x2F;&#x2F;&lt;&#x2F;code&gt; or &lt;code&gt;sebs:&#x2F;&#x2F;&lt;&#x2F;code&gt; URL scheme to open the app:&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#282c34;color:#abb2bf;&quot;&gt;&lt;code&gt;&lt;span&gt;sebs:&#x2F;&#x2F;domain.com&#x2F;example&#x2F;config.seb
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Would open SEB if it is installed and downloads the XML config from &lt;code&gt;https:&#x2F;&#x2F;domain.com&#x2F;example&#x2F;config.seb&lt;&#x2F;code&gt; (note the &lt;code&gt;s&lt;&#x2F;code&gt; in the &lt;code&gt;sebs:&#x2F;&#x2F;&lt;&#x2F;code&gt; URL scheme, you might want to use &lt;code&gt;seb:&#x2F;&#x2F;&lt;&#x2F;code&gt; if you&#x27;re on localhost and need HTTP instead of HTTPS).&lt;&#x2F;p&gt;
&lt;p&gt;This file should contain a Apple-style XML &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Property_list&quot;&gt;property list&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;xml&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-xml &quot;&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&lt;span&gt;&amp;lt;?&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;xml &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;version&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;1.0&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;encoding&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;UTF-8&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;?&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;DOCTYPE &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;plist &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;PUBLIC &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;-&#x2F;&#x2F;Apple&#x2F;&#x2F;DTD PLIST 1.0&#x2F;&#x2F;EN&amp;quot; &amp;quot;http:&#x2F;&#x2F;www.apple.com&#x2F;DTDs&#x2F;PropertyList-1.0.dtd&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;plist &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;version&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;1.0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;dict&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;startURL&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;string&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;http:&#x2F;&#x2F;example.com&#x2F;...&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;string&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;sendBrowserExamKey&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;browserWindowWebView&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;3&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;allowPreferencesWindow&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;allowQuit&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;showTaskBar&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;allowWlan&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;showTime&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;allowSpellCheck&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;showReloadButton&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;showInputLanguage&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;audioControlEnabled&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;audioMute&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;browserMediaCaptureCamera&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;browserMediaCaptureMicrophone&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;browserWindowAllowReload&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;examSessionClearCookiesOnStart&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;dict&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;plist&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Hosting this file and linking to it is enough to get your &lt;code&gt;startURL&lt;&#x2F;code&gt; to show in Safe Exam Browser.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;checking-if-we-re-running-in-safe-exam-browser&quot;&gt;Checking if we&#x27;re running in Safe Exam Browser&lt;&#x2F;h2&gt;
&lt;p&gt;You probably want to check if your page is opened in Safe Exam Browser, though.&lt;&#x2F;p&gt;
&lt;p&gt;The current recommended way to do so is in JavaScript:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isSafeExamBrowser &lt;&#x2F;span&gt;&lt;span&gt;=
&lt;&#x2F;span&gt;&lt;span&gt;	typeof window.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;SafeExamBrowser &lt;&#x2F;span&gt;&lt;span&gt;!== &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;undefined&amp;#39; &lt;&#x2F;span&gt;&lt;span&gt;&amp;amp;&amp;amp;
&lt;&#x2F;span&gt;&lt;span&gt;	window.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;SafeExamBrowser&lt;&#x2F;span&gt;&lt;span&gt;.version;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;console&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;log&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Safe Exam Browser in use?&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isSafeExamBrowser&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;checking-the-configuration-is-valid&quot;&gt;Checking the configuration is valid&lt;&#x2F;h2&gt;
&lt;p&gt;Okay, so that is enough to know whether you&#x27;re running in SEB, but someone could have manually gone to your website in SEB and not used your provided configuration. To check if the settings are equal to what is expected, you should check the &lt;code&gt;configKey&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;expectedConfigKey &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;generateConfigKey&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;settings&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;validConfiguration &lt;&#x2F;span&gt;&lt;span&gt;= 
&lt;&#x2F;span&gt;&lt;span&gt;	window.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;SafeExamBrowser&lt;&#x2F;span&gt;&lt;span&gt;.security.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;configKey &lt;&#x2F;span&gt;&lt;span&gt;=== &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;expectedConfigKey&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Generating the config key is a multi-step process, that looks like this:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Convert the XML options into a JSON object&lt;&#x2F;li&gt;
&lt;li&gt;Remove the &lt;code&gt;originatorVersion&lt;&#x2F;code&gt; field if it exists&lt;&#x2F;li&gt;
&lt;li&gt;Sort the JSON keys alphabetically&lt;&#x2F;li&gt;
&lt;li&gt;Generate a SHA256 hash from the SEB-JSON string&lt;&#x2F;li&gt;
&lt;li&gt;Encode that hash as Base-16&lt;&#x2F;li&gt;
&lt;li&gt;Concatenate the expected URL and the generated hash&lt;&#x2F;li&gt;
&lt;li&gt;Generate a new SHA256 hash from this, and output it as a lowercased Base-16 HEX string&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;To simplify this a bit, I created &lt;a href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;tschoffelen&#x2F;5fa4ca1841ee09f96c9a0f21b4787eff&quot;&gt;a simple JS class&lt;&#x2F;a&gt; that takes care of all of this for me. It&#x27;s not the cleanest code, but it does the job.&lt;&#x2F;p&gt;
&lt;p&gt;The usage is as follows:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;plist &lt;&#x2F;span&gt;&lt;span&gt;= new Plist({
&lt;&#x2F;span&gt;&lt;span&gt;	startURL: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`https:&#x2F;&#x2F;example.com&#x2F;...`&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	sendBrowserExamKey: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	browserWindowWebView: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;3&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	allowPreferencesWindow: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; etc...
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Generate XML payload:
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;xmlOutput &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;plist&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;toXML&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; -&amp;gt; &amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;...
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Get config key
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; (based on `startURL` above)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;configKey &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;plist&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getConfigKey&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; -&amp;gt; b07b99ebdab710432638fc111cc406881593fd5dc...
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>You Don&#x27;t Need Everything in One Place</title>
          <pubDate>Sat, 31 May 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/you-dont-need-everything-in-one-place/</link>
          <guid>https://schof.co/you-dont-need-everything-in-one-place/</guid>
          <description xml:base="https://schof.co/you-dont-need-everything-in-one-place/">&lt;p&gt;There&#x27;s this urge that I – and based on the subreddits I visit, a lot of people that are into productivity tools – have, of wanting to find a perfect system where all of your notes and data are in one place.&lt;&#x2F;p&gt;
&lt;p&gt;That makes you look for tools that do everything. I long thought that my perfect notes app could also hold all of my tasks and contacts and meeting info.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s not true. This starts the fact that there&#x27;s not one tool that does all of those things well. But it doesn&#x27;t end there: there&#x27;s also different contexts in which you access information.&lt;&#x2F;p&gt;
&lt;p&gt;A practical example of that is how I use &lt;a href=&quot;https:&#x2F;&#x2F;getdrafts.com&#x2F;&quot;&gt;Drafts&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;obsidian.md&quot;&gt;Obsidian&lt;&#x2F;a&gt;. I started out using Drafts mainly as a quick input mechanism for notes that will end up in Obsidian. It&#x27;s perfect for that - so much quicker to enter something into Drafts on my phone than to put things into Obsidian.&lt;&#x2F;p&gt;
&lt;p&gt;But I realised that advantage doesn&#x27;t so much come from how Drafts is set up, but more that I treat it as an unorganised space. Quick thoughts, quotes, links, shopping lists - all of those things are in Drafts.&lt;&#x2F;p&gt;
&lt;p&gt;Some of those might make it into Obsidian when they turn into something more: an exhaustive list, a full blog post. But a lot of those notes in Drafts never will turn into that, and never should go into Obsidian.&lt;&#x2F;p&gt;
&lt;p&gt;I don&#x27;t need everything in one place, it&#x27;s much better to have things in different places for different contexts, and giving up on the &#x27;all in one place&#x27; mentality creates a lot more mental space.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Keeping Your Environments Distinct with Separate Favicons</title>
          <pubDate>Mon, 26 May 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/keeping-your-environments-distinct-with-separate-favicons/</link>
          <guid>https://schof.co/keeping-your-environments-distinct-with-separate-favicons/</guid>
          <description xml:base="https://schof.co/keeping-your-environments-distinct-with-separate-favicons/">&lt;p&gt;When you&#x27;re working on a web app that runs in multiple environments (dev, staging, production), it&#x27;s easy to get confused and get frustrated about a change not working, only to realise that you&#x27;ve been refreshing the production app rather than your locally running version for the last 5 minutes.&lt;&#x2F;p&gt;
&lt;p&gt;To keep it easier to separate them, I like to set custom titles and favicons based on the stage:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;yG4-hfT&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;In &lt;a href=&quot;https:&#x2F;&#x2F;vite.dev&#x2F;&quot;&gt;Vite&lt;&#x2F;a&gt;, it&#x27;s pretty easy to do so.&lt;&#x2F;p&gt;
&lt;p&gt;First, we configure some variables in our &lt;code&gt;vite.config.js&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;defineConfig &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;vite&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export default &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;defineConfig&lt;&#x2F;span&gt;&lt;span&gt;(({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;mode &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    define: {
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;import.meta.env.VITE_STAGE_SUFFIX&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: JSON.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;stringify&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;mode &lt;&#x2F;span&gt;&lt;span&gt;=== &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;production&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;? &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`-${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;mode&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      ),
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;import.meta.env.VITE_TITLE_PREFIX&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: JSON.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;stringify&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;mode &lt;&#x2F;span&gt;&lt;span&gt;=== &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;production&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;? &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`(${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;mode&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}) `&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      ),
&lt;&#x2F;span&gt;&lt;span&gt;    },
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;  };
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we can use these variables in our &lt;code&gt;index.html&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span&gt;&amp;lt;!doctype html&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;html &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;lang&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;en&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;head&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;meta &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;charset&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;UTF-8&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;title&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;%VITE_TITLE_PREFIX%Examplary&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;title&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;link
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;rel&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;icon&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&#x2F;images&#x2F;favicon%VITE_STAGE_SUFFIX%.svg&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;image&#x2F;svg+xml&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;    &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will load &lt;code&gt;&#x2F;images&#x2F;favicon.svg&lt;&#x2F;code&gt; in production, and &lt;code&gt;&#x2F;images&#x2F;favicon-{mode}.svg&lt;&#x2F;code&gt; in every other mode.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Staying in Control Using Remote Config</title>
          <pubDate>Sun, 25 May 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/staying-in-control-using-remote-config/</link>
          <guid>https://schof.co/staying-in-control-using-remote-config/</guid>
          <description xml:base="https://schof.co/staying-in-control-using-remote-config/">&lt;p&gt;Apple and Google&#x27;s app review process for getting a new version of an app released into the App Store and Google Play is pretty fast these days, often taking less than 48 hours, and frequently even under 24 hours.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s still a long time to wait if you need to deal with an urgent bug or have information you want to communicate with your app users. That&#x27;s why it&#x27;s important to build in some controls into your production mobile apps to be able to deal with situations where you want to make small adjustments.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;feature-flags&quot;&gt;Feature flags&lt;&#x2F;h2&gt;
&lt;p&gt;The easiest one of these is feature flags, also sometimes known as feature toggles. These allow you to remotely toggle on or off certain functionality in your app.&lt;&#x2F;p&gt;
&lt;p&gt;This is often used as a way to keep beta functionality hidden until it&#x27;s ready for a full audience release, or when feature releases need to be synced up with marketing activities. It&#x27;s also super useful for dealing with unexpected issues, where turning off a certain set of features for a limited time might help resolve a crash.&lt;&#x2F;p&gt;
&lt;p&gt;Services like &lt;a href=&quot;https:&#x2F;&#x2F;launchdarkly.com&#x2F;&quot;&gt;LaunchDarkly&lt;&#x2F;a&gt; will let you configure these on a per-user level, so you can roll out a certain feature only to a subset of users, or gradually increase roll-out across the full audience over a certain period of time, allowing you to stop it if any issues occur.&lt;&#x2F;p&gt;
&lt;p&gt;For &lt;a href=&quot;https:&#x2F;&#x2F;streetartcities.com&quot;&gt;Street Art Cities&lt;&#x2F;a&gt;, I&#x27;ve opted for a more low-tech solution: a JSON file hosted in AWS S3. Admins on the platform have a simple dashboard where the feature toggles are available, and the app downloads an updated version of the feature flags file every time it is launched.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;p1ackRA&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;h2 id=&quot;dynamic-content&quot;&gt;Dynamic content&lt;&#x2F;h2&gt;
&lt;p&gt;In addition, we&#x27;ve done something that allows us to display dynamic content in various areas of the app. This is super useful to quickly add information for users regarding a bug, but also to insert marketing content. The &#x27;Book a tour&#x27; button below is an example of this dynamic content:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;OVBFs-O&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;This is again powered by JSON files living in AWS S3. These contain definitions of banners and buttons, and the app will try to load files based on the area of the app it&#x27;s in, e.g. &lt;code&gt;...&#x2F;media&#x2F;global&#x2F;above_feed.json&lt;&#x2F;code&gt; or &lt;code&gt;...&#x2F;media&#x2F;cities&#x2F;paris.json&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;If a file doesn&#x27;t exist and a 404 response is returned, the app won&#x27;t show any content.&lt;&#x2F;p&gt;
&lt;p&gt;This is an example of one of these files, specifically meant to show a &#x27;Best of 2024 Awards&#x27; banner above the homepage feed of the app:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;[
&lt;&#x2F;span&gt;&lt;span&gt;    {
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;awards2024&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;appearance&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;banner&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;conditions&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [
&lt;&#x2F;span&gt;&lt;span&gt;          [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;dateAfter&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;2025-01-01T00:00:00Z&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;],
&lt;&#x2F;span&gt;&lt;span&gt;          [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;dateBefore&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;2025-01-27T20:40:00Z&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span&gt;      ],
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;touchable&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;attr&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;          &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;style&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;borderRadius&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;12&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;backgroundColor&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;#e62d37&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;          }
&lt;&#x2F;span&gt;&lt;span&gt;        },
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;action&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;          &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;web&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;          &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;http:&#x2F;&#x2F;streetartcities.com&#x2F;awards&#x2F;2024&#x2F;inline&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;        },
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [
&lt;&#x2F;span&gt;&lt;span&gt;	      {
&lt;&#x2F;span&gt;&lt;span&gt;            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;image&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;streetartcities.com&#x2F;...&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;attr&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;              &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;resizeMode&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;cover&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;              &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;style&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;                &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;height&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;84
&lt;&#x2F;span&gt;&lt;span&gt;              }
&lt;&#x2F;span&gt;&lt;span&gt;            }
&lt;&#x2F;span&gt;&lt;span&gt;          },
&lt;&#x2F;span&gt;&lt;span&gt;          {
&lt;&#x2F;span&gt;&lt;span&gt;            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;view&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;attr&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;              &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;style&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {
&lt;&#x2F;span&gt;&lt;span&gt;                &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;flexDirection&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;row&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;              }
&lt;&#x2F;span&gt;&lt;span&gt;            },
&lt;&#x2F;span&gt;&lt;span&gt;            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [
&lt;&#x2F;span&gt;&lt;span&gt;              &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ... etc etc ...
&lt;&#x2F;span&gt;&lt;span&gt;            ]
&lt;&#x2F;span&gt;&lt;span&gt;          }
&lt;&#x2F;span&gt;&lt;span&gt;        ]
&lt;&#x2F;span&gt;&lt;span&gt;      }
&lt;&#x2F;span&gt;&lt;span&gt;    }
&lt;&#x2F;span&gt;&lt;span&gt;  ]
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A simple conditional checking system validates if each item should be displayed, allowing us to only show these items during certain dates, or for users using certain versions of the app:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Platform &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react-native&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pkg &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;@&#x2F;package.json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;checkCondition &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;condition&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;user &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value&lt;&#x2F;span&gt;&lt;span&gt;] = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;condition&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;  
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;switch &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;case &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;platform&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value &lt;&#x2F;span&gt;&lt;span&gt;=== &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Platform&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;OS&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;case &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;osVersionAfter&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value &lt;&#x2F;span&gt;&lt;span&gt;&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Platform&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Version&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;case &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;osVersionBefore&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Platform&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Version&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;case &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;appVersionAfter&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value &lt;&#x2F;span&gt;&lt;span&gt;&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pkg&lt;&#x2F;span&gt;&lt;span&gt;.version;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;case &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;appVersionBefore&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pkg&lt;&#x2F;span&gt;&lt;span&gt;.version;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;case &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;dateAfter&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;new Date(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;valueOf&lt;&#x2F;span&gt;&lt;span&gt;() &amp;lt; new Date().&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;valueOf&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;case &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;dateBefore&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;new Date(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;valueOf&lt;&#x2F;span&gt;&lt;span&gt;() &amp;gt; new Date().&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;valueOf&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;case &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hasRole&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;user&lt;&#x2F;span&gt;&lt;span&gt;?.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;roles&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;includes&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;default&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;  }
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;shouldShowMediaItem &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;item&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;user&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;item&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;conditions &lt;&#x2F;span&gt;&lt;span&gt;||
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;item&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;conditions&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;every&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;condition&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;checkCondition&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;condition&lt;&#x2F;span&gt;&lt;span&gt;));
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The content itself is rendered using some basic logic that converts the JSON into React Native views, touchables and images, with some pre-configured actions when the items are tapped: open a web page, navigate to a screen or open a new screen with content specified in the JSON.&lt;&#x2F;p&gt;
&lt;p&gt;The flexibility of this system is so helpful, and has saved me from disaster multiple times. I keep extending this system and adding new areas in the app where this data can be pulled in.&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s also a hidden staging area in the app, where I can test and preview content, before it goes live for the production app users audience:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;ygy3tp2&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Norms and Rules in Online Communities</title>
          <pubDate>Sun, 18 May 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/norms-and-rules-in-online-communities/</link>
          <guid>https://schof.co/norms-and-rules-in-online-communities/</guid>
          <description xml:base="https://schof.co/norms-and-rules-in-online-communities/">&lt;p&gt;I&#x27;ve been spending a lot of time recently thinking about the governance of online communities. One helpful starting point for this is the &lt;a href=&quot;https:&#x2F;&#x2F;academic.oup.com&#x2F;jcmc&#x2F;article&#x2F;10&#x2F;4&#x2F;JCMC10410&#x2F;4614449&quot;&gt;Preece &amp;amp; Maloney-Krichmar&lt;&#x2F;a&gt; definition of online communities:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;em&gt;people&lt;&#x2F;em&gt; who come together for a particular &lt;em&gt;purpose&lt;&#x2F;em&gt;, and who are guided by &lt;em&gt;policies&lt;&#x2F;em&gt; (including norms and rules) and supported by &lt;em&gt;software&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;My thinking has always been that communities need to develop their own norms, and should enforce those themselves. Tools might be provided to help with this (possibly to a select number of moderators within the community), but the majority of the responsibility lies with the community itself.&lt;&#x2F;p&gt;
&lt;p&gt;In trying to grow some of the online communities I help facilitate, I realised it isn&#x27;t quite as simple. Yes - tech that enforces hard rules (you can do X, you can&#x27;t do Y) might actually serve a purpose in certain scenarios.&lt;&#x2F;p&gt;
&lt;p&gt;Especially at the early stages of a community (or a newly created sub-community), being prescriptive with rules and enforcing them from the top down might be a good practice, as self-enforcement and development of community norms can happen efficiently within a pre-defined framework than within a fully blank space.&lt;&#x2F;p&gt;
&lt;p&gt;I still feel however like software should not be the primary driver of any policies, but applied as a tool: the community needs to feel like they are in control for there to be an environment where collective norms can grow.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Building Dynamic GraphQL Queries in Flexible Feed</title>
          <pubDate>Fri, 16 May 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/building-dynamic-graphql-queries-in-flexible-feed/</link>
          <guid>https://schof.co/building-dynamic-graphql-queries-in-flexible-feed/</guid>
          <description xml:base="https://schof.co/building-dynamic-graphql-queries-in-flexible-feed/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;apps.shopify.com&#x2F;inventory-feed&quot;&gt;Flexible Feed&lt;&#x2F;a&gt; is a small Shopify app I built a few years ago. It helps merchants create XML and CSV feeds to use with Google Shopping and other similar platforms.&lt;&#x2F;p&gt;
&lt;p&gt;At the core of it is this simple feed attribute mapping UI:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;17kvWpT&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;Here, you can configure what fields are available in the feed, and where the data for this field should be pulled from.&lt;&#x2F;p&gt;
&lt;p&gt;Even though it looks pretty minimal - there&#x27;s a lot of stuff happening behind the scenes here! The &#x27;Shopify value&#x27; field accepts almost any Javascript expression, allowing you to build really complicated constructions, with &lt;code&gt;if&lt;&#x2F;code&gt; statements, concatenating fields, and anything else you can do in a programming language.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;determining-what-fields-we-need&quot;&gt;Determining what fields we need&lt;&#x2F;h2&gt;
&lt;p&gt;Somehow, from this JS expression, we need to get to a GraphQL query we can run against Shopify&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;shopify.dev&#x2F;docs&#x2F;api&#x2F;admin-graphql&#x2F;latest&#x2F;queries&#x2F;productVariants&quot;&gt;&lt;code&gt;productVariants&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; edge.&lt;&#x2F;p&gt;
&lt;p&gt;To do so, we first parse each JavaScript expression into an AST (abstract syntax tree), using the wonderful &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;acornjs&#x2F;acorn&quot;&gt;&lt;code&gt;acorn&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; package. This allows us to extract all the identifiers used in the expression:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;as &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;acorn &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;acorn&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;as &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;walk &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;acorn-walk&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Get all identifiers in a JS expression string
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getIdentifiers &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;expression&lt;&#x2F;span&gt;&lt;span&gt;: string) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifiers &lt;&#x2F;span&gt;&lt;span&gt;= new Set&amp;lt;string&amp;gt;();
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;walk&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;ancestor&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;acorn&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;parse&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;expression&lt;&#x2F;span&gt;&lt;span&gt;, { ecmaVersion: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;2020&lt;&#x2F;span&gt;&lt;span&gt;, }),
&lt;&#x2F;span&gt;&lt;span&gt;		{
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;Identifier&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;node&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;				&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifiers&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;add&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;node&lt;&#x2F;span&gt;&lt;span&gt;.name);
&lt;&#x2F;span&gt;&lt;span&gt;			},
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;MemberExpression&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;node&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;s&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;ancestors&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;				&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;node&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;property&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;				&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; simplified slightly for brevity
&lt;&#x2F;span&gt;&lt;span&gt;				&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifiers&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;add&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;node&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;property&lt;&#x2F;span&gt;&lt;span&gt;.name);
&lt;&#x2F;span&gt;&lt;span&gt;			}
&lt;&#x2F;span&gt;&lt;span&gt;		}
&lt;&#x2F;span&gt;&lt;span&gt;	);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Array&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifiers&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For an expression like &lt;code&gt;inventoryLevel.available &amp;gt; 2 ? &quot;in stock&quot; : &quot;low stock&quot;&lt;&#x2F;code&gt;, this results in a list of identifiers that looks like &lt;code&gt;[&quot;inventoryLevel&quot;, &quot;inventoryLevel.available&quot;]&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;We only need the lowest level identifiers, so a little &lt;code&gt;filter&lt;&#x2F;code&gt; can be used to end up only with &lt;code&gt;[&quot;inventoryLevel.available&quot;]&lt;&#x2F;code&gt; in the example above:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Filter out all identifiers that have common ancestors
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;filteredIdentifiers &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifiers&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;filter&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;	(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifiers&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;some&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;other&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;other&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;startsWith&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifier &lt;&#x2F;span&gt;&lt;span&gt;+ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;	)
&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;generating-graphql-queries&quot;&gt;Generating GraphQL queries&lt;&#x2F;h2&gt;
&lt;p&gt;Once we have the identifiers used in each of the field mapping expressions, we can start turning this into a GraphQL query that includes certain fields based on which identifiers are needed.&lt;&#x2F;p&gt;
&lt;p&gt;Building complex GraphQL queries by hand is difficult, especially if they need to be mostly dynamic. The package &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;json-to-graphql-query&quot;&gt;&lt;code&gt;json-to-graphql-query&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; is really handy to get around this (and a godsend for people like me that never fully got to grips with the GraphQL syntax to begin with).&lt;&#x2F;p&gt;
&lt;p&gt;By looping through the identifiers, splitting at each dot, and going a level deeper each time), we can slowly build a JSON-style GraphQL query:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Our base query (massively shortened)
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;jsonQuery &lt;&#x2F;span&gt;&lt;span&gt;= {
&lt;&#x2F;span&gt;&lt;span&gt;	productVariants: {
&lt;&#x2F;span&gt;&lt;span&gt;		__args: {
&lt;&#x2F;span&gt;&lt;span&gt;			first: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;100&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		},
&lt;&#x2F;span&gt;&lt;span&gt;		edges: {
&lt;&#x2F;span&gt;&lt;span&gt;			node: {
&lt;&#x2F;span&gt;&lt;span&gt;				id: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;true
&lt;&#x2F;span&gt;&lt;span&gt;			}
&lt;&#x2F;span&gt;&lt;span&gt;		}
&lt;&#x2F;span&gt;&lt;span&gt;	}
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Loop through each identifier, and add it to the query
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;for &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifier &lt;&#x2F;span&gt;&lt;span&gt;of &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;filteredIdentifiers&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;parts &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;identifier&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;split&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;	
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;current &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;jsonQuery&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;productVariants&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;edges&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;node&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;for &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;index&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;part&lt;&#x2F;span&gt;&lt;span&gt;] of &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Object&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;entries&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;parts&lt;&#x2F;span&gt;&lt;span&gt;)) {
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isLastPart &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Number&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;index&lt;&#x2F;span&gt;&lt;span&gt;) === &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;parts&lt;&#x2F;span&gt;&lt;span&gt;.length - &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;		
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isLastPart&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;current&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;part&lt;&#x2F;span&gt;&lt;span&gt;] = {};
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;current &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;current&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;part&lt;&#x2F;span&gt;&lt;span&gt;];
&lt;&#x2F;span&gt;&lt;span&gt;		} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;else &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;current&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;part&lt;&#x2F;span&gt;&lt;span&gt;] = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;true&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;		}
&lt;&#x2F;span&gt;&lt;span&gt;	}
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I&#x27;ve omitted all the very complicated and ugly field mapping code here, mapping alternative names for fields (this app was originally built based on the Shopify Admin REST API), and a lot of shortcuts (e.g. &lt;code&gt;inventoryLevel&lt;&#x2F;code&gt; from above is available at &lt;code&gt;edges.node.inventoryItem.inventoryLevel.nodes.item&lt;&#x2F;code&gt; in reality).&lt;&#x2F;p&gt;
&lt;p&gt;Once we have a JSON object where every field we want included in the query is defined as a key with value &lt;code&gt;true&lt;&#x2F;code&gt;, we can then easily convert it into a GraphQL string:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;jsonToGraphQLQuery &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;json-to-graphql-query&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;query &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;jsonToGraphQLQuery&lt;&#x2F;span&gt;&lt;span&gt;({ query: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;jsonQuery &lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;fetching-data-and-mapping-fields&quot;&gt;Fetching data and mapping fields&lt;&#x2F;h2&gt;
&lt;p&gt;Now all that&#x27;s left is to run the query, paginate through all of the results, and then call our user-inputted JS expressions with the resulting data to get the eventual feed output.&lt;&#x2F;p&gt;
&lt;p&gt;To make sure we can run user-defined code safely, we use a very locked down AWS Lambda environment, and a package called &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nyariv&#x2F;SandboxJS&quot;&gt;&lt;code&gt;SandboxJS&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, which runs the code in an environment that only has access to certain JS built-ins, and the data we pass into it, which looks something like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Sandbox &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;@nyariv&#x2F;sandboxjs&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;sandbox &lt;&#x2F;span&gt;&lt;span&gt;= new Sandbox();
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;code &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`return ${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;mapping&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;shopifyField&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;};`&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;expression &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;sandbox&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;compile&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;code&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;result &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;expression&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;run&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;productVariant&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Using AI in GitHub Pull Requests</title>
          <pubDate>Fri, 16 May 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/using-ai-in-github-pull-requests/</link>
          <guid>https://schof.co/using-ai-in-github-pull-requests/</guid>
          <description xml:base="https://schof.co/using-ai-in-github-pull-requests/">&lt;p&gt;We&#x27;ve been talking for a while about how we can be better at informing the &lt;a href=&quot;https:&#x2F;&#x2F;streetartcities.com&quot;&gt;Street Art Cities&lt;&#x2F;a&gt; community about new features and changes to our platform.&lt;&#x2F;p&gt;
&lt;p&gt;Our main way of sharing this info is &lt;a href=&quot;https:&#x2F;&#x2F;streetart.community&#x2F;&quot;&gt;through our forum&lt;&#x2F;a&gt;, but sometimes it&#x27;s hard to remember to share an update about a small change.&lt;&#x2F;p&gt;
&lt;p&gt;To encourage ourselves to do so, I built a little bot that will automatically create a draft forum post for every GitHub PR that gets opened, with a simple link to continue editing and post it.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;sG4kcg8&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;To implement this, I relied on &lt;a href=&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;github-models&#x2F;prototyping-with-ai-models&quot;&gt;GitHub Models&lt;&#x2F;a&gt; and some JavaScript, which gets triggered through a GitHub Actions workflow.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-script&quot;&gt;The script&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m writing some JavaScript that will be executed by the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;actions&#x2F;github-script&quot;&gt;&lt;code&gt;actions&#x2F;github-script&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; action. It has access to an already authorized version an the GitHub API client, making it very easy to do the API calls we need to do.&lt;&#x2F;p&gt;
&lt;p&gt;First, we get the full details of the Pull Request:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pullRequest &lt;&#x2F;span&gt;&lt;span&gt;} = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;github&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;rest&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pulls&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;	owner: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;owner&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	repo: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	pull_number: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;issue&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;number&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can then use those to form our very rudimentary prompt (shortened slightly for clarity) and run it through the GitHub Models API:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Build our prompt with the PR data
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;prompt &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;You are a GitHub bot that helps developers write forum posts based on their pull request descriptions.
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;The forum post will be a public announcement of the new feature or change, for a non-technical audience.
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;The platform this is for is called Street Art Cities, and the forum is located at https:&#x2F;&#x2F;streetart.community.
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;Use informal, simple and friendly language, and make it sound exciting. Our focus is on community, street art and culture.
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;The pull request title is: &amp;quot;${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pullRequest&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.title}&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;The pull request body is: &amp;quot;${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pullRequest&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.body &lt;&#x2F;span&gt;&lt;span&gt;|| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;No description provided.&amp;quot;}&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;Output a JSON object with the following fields:
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;- title: The title of the forum post
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;- body: The body of the forum post, including a short description of the feature or change, and any relevant links or images. You can use Markdown syntax for formatting.
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Do the API call to GPT-4.1
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;llmResponse &lt;&#x2F;span&gt;&lt;span&gt;} = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;github&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;request&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;	method: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;POST&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	url: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;models.github.ai&#x2F;inference&#x2F;chat&#x2F;completions&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	data: {
&lt;&#x2F;span&gt;&lt;span&gt;		model: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;openai&#x2F;gpt-4.1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		messages: [
&lt;&#x2F;span&gt;&lt;span&gt;			{
&lt;&#x2F;span&gt;&lt;span&gt;				role: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;user&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;				content: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;prompt&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;			},
&lt;&#x2F;span&gt;&lt;span&gt;		],
&lt;&#x2F;span&gt;&lt;span&gt;	},
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Extract the JSON object from the response
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;suggestion &lt;&#x2F;span&gt;&lt;span&gt;= JSON.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;parse&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;llmResponse&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;choices&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;].&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;message&lt;&#x2F;span&gt;&lt;span&gt;.content);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The only thing left is to add our comment to the Pull Request thread:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Set the content for our body
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;commentBody &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;💡 Suggested forum post for this feature update:
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;	
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;gt; **${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;suggestion&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.title}**
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;gt; ${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;suggestion&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.body}
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Get existing comments on the PR
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;comments &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;github&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;rest&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;issues&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;listComments&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;	owner: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;owner&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	repo: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	issue_number: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;issue&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;number&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Check if the bot already posted a comment
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;existingComment &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;comments&lt;&#x2F;span&gt;&lt;span&gt;.data.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;find&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;comment&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;comment&lt;&#x2F;span&gt;&lt;span&gt;.body.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;includes&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Suggested forum post&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;),
&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; If not, create a new comment, otherwise update the existing one
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;existingComment&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;github&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;rest&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;issues&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;createComment&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;		owner: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;owner&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		repo: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		issue_number: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;issue&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;number&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		body: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;commentBody&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;else &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;github&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;rest&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;issues&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;updateComment&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;		owner: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;owner&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		repo: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;repo&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		comment_id: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;existingComment&lt;&#x2F;span&gt;&lt;span&gt;.id,
&lt;&#x2F;span&gt;&lt;span&gt;		body: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;commentBody&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;github-actions-workflow&quot;&gt;Github Actions workflow&lt;&#x2F;h2&gt;
&lt;p&gt;To get the script to run at the right moment, with the right creds, we can set up a GitHub Actions workflow that looks like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;PR Comment - Forum Suggestion
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;on&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pull_request&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;types&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;opened&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;edited&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;jobs&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;forum-suggestion&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;runs-on&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;ubuntu-latest
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;permissions&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pull-requests&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;write
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;models&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;read
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;steps&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      - &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;Comment on PR
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;uses&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;actions&#x2F;github-script@v7
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;with&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;          &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;github-token&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;${{ secrets.GITHUB_TOKEN }}
&lt;&#x2F;span&gt;&lt;span&gt;          &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;script&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;|
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;            &#x2F;&#x2F; The JS script from above
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;What I especially love about this method is that there&#x27;s no need to do anything in terms of configuring API credentials at any point, it&#x27;s all just built-in!&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Musing on Writing</title>
          <pubDate>Thu, 24 Apr 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/musing-on-writing/</link>
          <guid>https://schof.co/musing-on-writing/</guid>
          <description xml:base="https://schof.co/musing-on-writing/">&lt;p&gt;I&#x27;ve been reminiscing a lot lately. I&#x27;ve always done that, but these moments when you make big life changes cause that to be so much more intense. It makes you appreciate what you have, what the world around you contains.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve been thinking a lot about what I used to do back in high school, during classes that I found boring. I&#x27;d write these stories, all very short, I couldn&#x27;t write something that had a plot for the life of me. They&#x27;d be just a description of a situation, either one I&#x27;d noticed and spent time examining in real life, or this imaginary situation I&#x27;d want to encounter one day, trying to figure out how I&#x27;d respond to it, what I would bring to that situation.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;d describe scenes, and then rewrite the same paragraph over and over and over again, trying to find better words to describe the things I thought were important, trying to impart some emotion onto a description of people sitting in a room, of people walking past buildings on a street. I&#x27;d try to turn it from something meaningless and everyday into something that was new and unique, filled with feeling, in the way Nabokov in his autobiography was able to make catching butterflies as a young boy in a Russian forest sound magical and filled with emotion that he&#x27;s able to impart on the reader.&lt;&#x2F;p&gt;
&lt;p&gt;I haven&#x27;t done that for a long time - just sitting down and spending an hour on a single paragraph, just for my joy. Most writing I do as an adult is very perfunctory, a means to an end: a company blog post about a product launch, a support article or an email to set up a meeting.&lt;&#x2F;p&gt;
&lt;p&gt;I want to go back to a place where I feel like I have the time to just write for myself, not with the goal of publishing it for the world to see. This blog is a bit like that: I never expect anyone to read most of what I publish on here, it&#x27;s an archive of things I learn and emotions I want to store for safekeeping. But writing for my blog is still a means to an end, trying to make writing something &#x27;productive&#x27; that forms an end product that could be consumed.&lt;&#x2F;p&gt;
&lt;p&gt;I want to go back to scribbling in a notebook, writing stories just for the sake of exercising the creative part of my brain, between class notes with physics formulas and misspelled French words.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>TraceStack: Building an Open-Source Serverless Observability Tool</title>
          <pubDate>Sat, 29 Mar 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/tracestack-open-source-observability/</link>
          <guid>https://schof.co/tracestack-open-source-observability/</guid>
          <description xml:base="https://schof.co/tracestack-open-source-observability/">&lt;p&gt;Last summer, I started building &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;includable&#x2F;trace-stack&quot;&gt;TraceStack&lt;&#x2F;a&gt;: A serverless observability tool for AWS Lambda.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;0q5NbUF&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;My goal was to build an open-source self-hosted alternative to &lt;a href=&quot;https:&#x2F;&#x2F;lumigo.io&quot;&gt;Lumigo&lt;&#x2F;a&gt; or &lt;a href=&quot;https:&#x2F;&#x2F;newrelic.com&quot;&gt;New Relic&lt;&#x2F;a&gt;, focusing on tracing Lambda invocations of Javascript functions. I have a lot of small projects I run, where some observability would be nice, but I quickly run into the limits of what free tiers of these hosted platforms provide.&lt;&#x2F;p&gt;
&lt;p&gt;This project was super exciting to work on. I built a slick user interface along with a &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;includable&#x2F;trace-stack#getting-started&quot;&gt;self-installer package&lt;&#x2F;a&gt; that makes deployment incredibly easy – just one command and you&#x27;re up and running.&lt;&#x2F;p&gt;
&lt;p&gt;In the process though, I learned a lot about observability. And I realised some of my assumptions were wrong at the start of this project.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ll probably not continue working on it any time soon, but I learned a lot!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;throughput-and-database-choice&quot;&gt;Throughput and database choice&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;amazondynamodb&#x2F;latest&#x2F;developerguide&#x2F;Introduction.html&quot;&gt;DynamoDB&lt;&#x2F;a&gt; isn&#x27;t a &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Online_analytical_processing&quot;&gt;OLAP database&lt;&#x2F;a&gt;. I already knew that, but it&#x27;s also the serverless database offering from AWS that I&#x27;m the most familiar with, and I love that it fully scales from zero in terms of costs.&lt;&#x2F;p&gt;
&lt;p&gt;I wanted this to be something you could deploy from day one on a side project, without worrying about observability adding to the cost of that project.&lt;&#x2F;p&gt;
&lt;p&gt;And for the most part, I was able to build a table design that didn&#x27;t require any more flexibility in how I was querying the data than DynamoDB allows. There is some relationality in the data, but it&#x27;s limited to only a few entities, and very hierarchical:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;INIng2O&quot; alt=&quot;Drawing&quot; &#x2F;&gt;
&lt;p&gt;By pre-computing some fields, and storing some duplicate data (each &lt;code&gt;Trace&lt;&#x2F;code&gt; that errored is also stored as a separate &lt;code&gt;Error&lt;&#x2F;code&gt; item), I was able to make this work quite performantly in DynamoDB.&lt;&#x2F;p&gt;
&lt;p&gt;However, what I didn&#x27;t appreciate, was how much it would cost to store this data. A single function call might have hundreds of spans (a span representing a database or API call), and therefore kilobytes of data – for a single request!&lt;&#x2F;p&gt;
&lt;p&gt;This quickly adds up, and started to be noticeable on the AWS bill for &lt;a href=&quot;https:&#x2F;&#x2F;streetartcities.com&quot;&gt;Street Art Cities&lt;&#x2F;a&gt;, even though our total Lambda invocation count is relatively low, compared to some of the other projects I work on.&lt;&#x2F;p&gt;
&lt;p&gt;I spent some time on deduplication of data and trimming spans, but quickly entered trade-off territory, where I was choosing between cost optimisation and helpfulness of the trace data.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;opentelemetry-and-wide-events&quot;&gt;OpenTelemetry and wide events&lt;&#x2F;h2&gt;
&lt;p&gt;At the same time, I started learning more about &lt;a href=&quot;https:&#x2F;&#x2F;opentelemetry.io&#x2F;docs&#x2F;concepts&#x2F;observability-primer&#x2F;&quot;&gt;OpenTelemetry&lt;&#x2F;a&gt; and the practice of &lt;a href=&quot;https:&#x2F;&#x2F;jeremymorrell.dev&#x2F;blog&#x2F;a-practitioners-guide-to-wide-events&#x2F;&quot;&gt;instrumenting using Wide Events&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The more I played around with this principle, and platforms like &lt;a href=&quot;https:&#x2F;&#x2F;www.honeycomb.io&quot;&gt;Honeycomb&lt;&#x2F;a&gt;, the more I realised that was the way forward for my own observability needs. I started adding OpenTelemetry support to my &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;includable&#x2F;serverless-middleware?tab=readme-ov-file#opentelemetry-span-enrichment&quot;&gt;serverless-middleware&lt;&#x2F;a&gt; and thinking in terms of attributes I wanted to add to my events.&lt;&#x2F;p&gt;
&lt;p&gt;This model doesn&#x27;t work at all with TraceStack, which is built on a DB that doesn&#x27;t allow querying on arbitrary fields, and uses a proprietary tracing format (I used &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lumigo-io&#x2F;lumigo-node&quot;&gt;Lumigo&#x27;s open source Node tracer&lt;&#x2F;a&gt; as the basis for the code that is automatically added to each Lambda to trace it).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;if-i-was-starting-over&quot;&gt;If I was starting over&lt;&#x2F;h2&gt;
&lt;p&gt;I don&#x27;t think I&#x27;ll have space anytime soon to start working on a new version of TraceStack that incorporates some of these ideas, but if I knew what I know now when building it in the first place, I&#x27;d probably take a completely different approach:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Deploy it as a container, rather than Lambda functions and a DynamoDB table. This not only takes away any lambda invocation charges and reduces the linear nature of the cost profile, but also means there&#x27;s only a single resource added to the target AWS account, so that we don&#x27;t add a bunch of resources that might conflict with the actual app&#x27;s infrastructure or confuse developers.&lt;&#x2F;li&gt;
&lt;li&gt;In that container, I&#x27;d probably run an instance of &lt;a href=&quot;https:&#x2F;&#x2F;clickhouse.com&quot;&gt;ClickHouse&lt;&#x2F;a&gt;. I&#x27;ve been using ClickHouse a bit recently, and really like it as a database for this purpose. It&#x27;s very good at real-time data ingest, and aggregation across massive amounts of data.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;There&#x27;s also just the question of whether self-hosting observability tools is what you should want as a developer. The worst thing would be to have to debug why your observability tools isn&#x27;t working whilst trying to solve a real issue.&lt;&#x2F;p&gt;
&lt;p&gt;But it would be an interesting option as a cheap alternative for small-scale apps and services that can&#x27;t afford any of the industry standard tools.&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s some tools that get closer to the approach I described above, like &lt;a href=&quot;https:&#x2F;&#x2F;zipkin.io&#x2F;&quot;&gt;Zipkin&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;www.jaegertracing.io&#x2F;&quot;&gt;Jaeger&lt;&#x2F;a&gt;, but to my eye, their UIs leave a lot to be desired.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;
</description>
      </item>
      <item>
          <title>AI Concepts for Developers</title>
          <pubDate>Sat, 01 Feb 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/ai-concepts-for-developers/</link>
          <guid>https://schof.co/ai-concepts-for-developers/</guid>
          <description xml:base="https://schof.co/ai-concepts-for-developers/">&lt;p&gt;I&#x27;ve been doing some diagrams to explain certain AI app development concepts to my team. Here they are, if they&#x27;re useful to anyone else. Created using the delightful FigJam app from &lt;a href=&quot;https:&#x2F;&#x2F;figma.com&quot;&gt;Figma&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;retrieval-augmented-generation-rag&quot;&gt;Retrieval-augmented generation (RAG)&lt;&#x2F;h2&gt;
&lt;p&gt;Retrieval-augmented generation (RAG) is when an AI looks up information from a database or documents to help it give better, more accurate answers.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;Yf9di0J&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;h2 id=&quot;tools-actions&quot;&gt;Tools&#x2F;actions&lt;&#x2F;h2&gt;
&lt;p&gt;Tools or actions are used to give the LLM access to real-time data or let it perform actions based on user input.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;AeuoPv6&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;style&gt;img { border: 1px solid #eee; border-radius: 0.75rem; }&lt;&#x2F;style&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Lazy Loading Routes with Vite and React Router v7</title>
          <pubDate>Sun, 19 Jan 2025 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/lazy-loading-routes-with-vite-and-react-router-v7/</link>
          <guid>https://schof.co/lazy-loading-routes-with-vite-and-react-router-v7/</guid>
          <description xml:base="https://schof.co/lazy-loading-routes-with-vite-and-react-router-v7/">&lt;p&gt;Recently, we switched the &lt;a href=&quot;https:&#x2F;&#x2F;streetartcities.com&quot;&gt;Street Art Cities&lt;&#x2F;a&gt; dashboard (where users upload artworks, create routes, view insights, etc.) from a massive monolithic &lt;a href=&quot;https:&#x2F;&#x2F;nextjs.org&#x2F;&quot;&gt;Next.js&lt;&#x2F;a&gt; app – that also contained the many pages of our website! – to a standalone React app using &lt;a href=&quot;https:&#x2F;&#x2F;reactrouter.com&#x2F;&quot;&gt;React Router&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;vite.dev&#x2F;&quot;&gt;Vite&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;We have dashboard for various different roles - hunter, artist, country manager, admin, etc. To make sure not everyone needs to load all the Javascript for each dashboard, even if they don&#x27;t have access to it, I set up chunking and lazy loading for various sub-routes of the dashboard.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;updating-our-router&quot;&gt;Updating our router&lt;&#x2F;h2&gt;
&lt;p&gt;Previously, our main router looked like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;HunterRoutes &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;.&#x2F;routes&#x2F;hunter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;AdminRoutes &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;.&#x2F;routes&#x2F;admin&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;App &lt;&#x2F;span&gt;&lt;span&gt;= () &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;Routes&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Route path&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hunter&#x2F;*&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;element&lt;&#x2F;span&gt;&lt;span&gt;={&amp;lt;HunterRoutes &#x2F;&amp;gt;} &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Route path&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;admin&#x2F;*&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;element&lt;&#x2F;span&gt;&lt;span&gt;={&amp;lt;AdminRoutes &#x2F;&amp;gt;} &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Routes&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;  );
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is a perfect setup for lazy loading, since the various dashboards are already clumped together using sub-routes.&lt;&#x2F;p&gt;
&lt;p&gt;We can combine Vite&#x27;s dynamic &lt;a href=&quot;https:&#x2F;&#x2F;vite.dev&#x2F;guide&#x2F;features.html#dynamic-import&quot;&gt;&lt;code&gt;import()&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; function and React&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;react.dev&#x2F;reference&#x2F;react&#x2F;lazy&quot;&gt;&lt;code&gt;lazy()&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; to get this done. I&#x27;ve started by introducing a helper component that combines them:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;Lazy &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;component &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;React&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;lazy&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;component&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Suspense fallback&lt;&#x2F;span&gt;&lt;span&gt;={&amp;lt;LoadingSpinner &#x2F;&amp;gt;}&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component &lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Suspense&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;  );
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can use it like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;HunterRoutes &lt;&#x2F;span&gt;&lt;span&gt;= () &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;import(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;.&#x2F;routes&#x2F;hunter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;AdminRoutes &lt;&#x2F;span&gt;&lt;span&gt;= () &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;import(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;.&#x2F;routes&#x2F;admin&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;App &lt;&#x2F;span&gt;&lt;span&gt;= () &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;Routes&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Route path&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hunter&#x2F;*&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;element&lt;&#x2F;span&gt;&lt;span&gt;={
&lt;&#x2F;span&gt;&lt;span&gt;	    &amp;lt;Lazy &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;component&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;HunterRoutes&lt;&#x2F;span&gt;&lt;span&gt;} &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	  } &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Route path&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;admin&#x2F;*&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;element&lt;&#x2F;span&gt;&lt;span&gt;={
&lt;&#x2F;span&gt;&lt;span&gt;	    &amp;lt;Lazy &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;component&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;AdminRoutes&lt;&#x2F;span&gt;&lt;span&gt;} &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	  } &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	  &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Routes&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;  );
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;&#x2F;strong&gt;: we need to make sure we define the &lt;code&gt;import()&lt;&#x2F;code&gt; with a pre-set value. Before, I tried to abstract this further by passing &lt;code&gt;path&lt;&#x2F;code&gt; prop into the &lt;code&gt;&amp;lt;Lazy&amp;gt;&lt;&#x2F;code&gt; component, and having the &lt;code&gt;import()&lt;&#x2F;code&gt; in the component, but that means Vite can&#x27;t resolve the reference during build-time.&lt;&#x2F;p&gt;
&lt;p&gt;That means that even though it works in development mode, in production it will try to load &lt;code&gt;.&#x2F;routes&#x2F;hunter&lt;&#x2F;code&gt;, which probably won&#x27;t exist in your bundled code!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chunking-strategy&quot;&gt;Chunking strategy&lt;&#x2F;h2&gt;
&lt;p&gt;By default, Vite (and &lt;a href=&quot;https:&#x2F;&#x2F;rollupjs.org&#x2F;&quot;&gt;Rollup&lt;&#x2F;a&gt;, which Vite uses under the hood) will try to intelligently chunk our dependencies. It&#x27;s not always amazing at it though, so in cases like this, where there is a clear-cut distinction in when users will need certain bits of code.&lt;&#x2F;p&gt;
&lt;p&gt;Rollup allows you to define a &lt;a href=&quot;https:&#x2F;&#x2F;rollupjs.org&#x2F;configuration-options&#x2F;#output-manualchunks&quot;&gt;&lt;code&gt;manualChunks&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; configuration option where you can pass a function that determines the chunk a certain file of source could should be put into.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s how you can use this in the &lt;code&gt;vite.config.js&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export default &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;defineConfig&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;  build: {
&lt;&#x2F;span&gt;&lt;span&gt;    rollupOptions: {
&lt;&#x2F;span&gt;&lt;span&gt;      output: {
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;manualChunks&lt;&#x2F;span&gt;&lt;span&gt;: (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;          &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;match &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;&#x2F;src\&#x2F;routes\&#x2F;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;[\w-]&lt;&#x2F;span&gt;&lt;span&gt;+&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;)\&#x2F;&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;exec&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;          &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;match&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;            &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;match&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;];
&lt;&#x2F;span&gt;&lt;span&gt;          }
&lt;&#x2F;span&gt;&lt;span&gt;        },
&lt;&#x2F;span&gt;&lt;span&gt;      },
&lt;&#x2F;span&gt;&lt;span&gt;    },
&lt;&#x2F;span&gt;&lt;span&gt;  },
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The parameter &lt;code&gt;id&lt;&#x2F;code&gt; here refers to the full path for the input file. In this example, we match files like &lt;code&gt;src&#x2F;routes&#x2F;admin&#x2F;AdminDashboard.jsx&lt;&#x2F;code&gt; and puts them in a chunk based on the first directory within &lt;code&gt;src&#x2F;routes&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The result is a chunk per dashboard section:
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;BUcRgnJ&quot; alt=&quot;Image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This is a massive improvement over the &#x27;before&#x27; state:
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;qjnmKor&quot; alt=&quot;Image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;That means that all of our hunters and artists no longer need to load the almost 3 megabytes of JS that is only used in the admin dashboard!&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;
</description>
      </item>
      <item>
          <title>Setting Up Syntax Highlighting in HubSpot CMS</title>
          <pubDate>Sat, 21 Sep 2024 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/setting-up-syntax-highlighting-in-hubspot-cms/</link>
          <guid>https://schof.co/setting-up-syntax-highlighting-in-hubspot-cms/</guid>
          <description xml:base="https://schof.co/setting-up-syntax-highlighting-in-hubspot-cms/">&lt;p&gt;I&#x27;ve been building out our &lt;a href=&quot;https:&#x2F;&#x2F;stockroom.near.st&#x2F;?utm_source=schof&quot;&gt;NearSt Engineering Blog&lt;&#x2F;a&gt; over the last couple of days, and one of the things I ran into is the need to show syntax-highlighted code in some of the blog posts.&lt;&#x2F;p&gt;
&lt;p&gt;HubSpot CMS doesn&#x27;t have a built-in way to do that, but you can easily add this yourself.&lt;&#x2F;p&gt;
&lt;p&gt;In the HubSpot Design Manager, you&#x27;ll want to find your blog post template (often called something like &lt;code&gt;blog-post.html&lt;&#x2F;code&gt; in the &lt;code&gt;template&lt;&#x2F;code&gt; folder of your theme). At the bottom of that, just before the &lt;code&gt;{% endblock body %}&lt;&#x2F;code&gt; statement, add this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;link &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;rel&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;stylesheet&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;cdnjs.cloudflare.com&#x2F;ajax&#x2F;libs&#x2F;highlight.js&#x2F;11.9.0&#x2F;styles&#x2F;default.min.css&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;link &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;rel&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;stylesheet&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;cdnjs.cloudflare.com&#x2F;ajax&#x2F;libs&#x2F;highlight.js&#x2F;11.9.0&#x2F;styles&#x2F;atom-one-dark.min.css&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;script &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;src&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;cdnjs.cloudflare.com&#x2F;ajax&#x2F;libs&#x2F;highlight.js&#x2F;11.9.0&#x2F;highlight.min.js&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;script&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;script&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;brPlugin &lt;&#x2F;span&gt;&lt;span&gt;= {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;before:highlightBlock&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: ({ block }) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;block&lt;&#x2F;span&gt;&lt;span&gt;.innerHTML = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;block&lt;&#x2F;span&gt;&lt;span&gt;.innerHTML
&lt;&#x2F;span&gt;&lt;span&gt;        .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;replace&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;&#x2F;\n&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;g&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;        .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;replace&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;&#x2F;&amp;lt;br&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;[ &#x2F;]&lt;&#x2F;span&gt;&lt;span&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;&amp;gt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;g&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;\n&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;        .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;replace&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;&#x2F;\t&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;g&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;    &amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;    },
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;after:highlightBlock&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: ({ result }) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;result&lt;&#x2F;span&gt;&lt;span&gt;.value = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;result&lt;&#x2F;span&gt;&lt;span&gt;.value.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;replace&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;&#x2F;\n&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;g&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&amp;lt;br&amp;gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;    },
&lt;&#x2F;span&gt;&lt;span&gt;  };
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; how to use it
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;hljs&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;addPlugin&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;brPlugin&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;hljs&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;highlightAll&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;script&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You might be wondering why there is so much custom Javascript required to make this work: by default, HubSpot puts &lt;code&gt;&amp;lt;br&amp;gt;&lt;&#x2F;code&gt; tags in your &lt;code&gt;&amp;lt;pre&amp;gt;&lt;&#x2F;code&gt; code blocks to create line breaks, rather than just leaving the normal line breaks (&lt;code&gt;\n&lt;&#x2F;code&gt;). That&#x27;s not really an ideal practice, and therefore not supported by any highlighting libraries.&lt;&#x2F;p&gt;
&lt;p&gt;Thankfully, it is easy to add some Javascript to convert those back into normal line breaks before the highlighting starts.&lt;&#x2F;p&gt;
&lt;p&gt;Once you have this in place, your code might still not be syntax highlighted.&lt;&#x2F;p&gt;
&lt;p&gt;Unfortunately, there&#x27;s a second step: all code blocks need to be wrapped like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pre&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;code&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&#x2F;&#x2F; my brilliant javascript example
&lt;&#x2F;span&gt;&lt;span&gt;	&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;code&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;pre&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The HubSpot HTML editor will by default give you only &lt;code&gt;&amp;lt;code&amp;gt;&lt;&#x2F;code&gt; tags, so you&#x27;ll need to jump into the &lt;strong&gt;Advanced&lt;&#x2F;strong&gt; → &lt;strong&gt;Source code&lt;&#x2F;strong&gt; view of the HubSpot blog editor to manually adjust this.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;zztbShm&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;If all of that seems hard and painful, I agree. HubSpot Blogs is a marketing platform, and clearly not really meant for this sort of content.&lt;&#x2F;p&gt;
&lt;p&gt;A quicker solution might be to use &lt;a href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;&quot;&gt;GitHub Gist&lt;&#x2F;a&gt; embeds, but these don&#x27;t render well in the HubSpot editor, and are not very customizable in terms of styling.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Building an Interactive Markdown Textarea</title>
          <pubDate>Sat, 29 Jun 2024 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/building-an-interactive-markdown-textarea/</link>
          <guid>https://schof.co/building-an-interactive-markdown-textarea/</guid>
          <description xml:base="https://schof.co/building-an-interactive-markdown-textarea/">&lt;p&gt;I really love the markdown editor on GitHub, especially how easy it is to drop in files and images.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s still all plain text, but it works interactively. If you drag and drop a file, it uploads that file and inserts a link into your text. If you paste an image, it uploads it and inserts an markdown image tag.&lt;&#x2F;p&gt;
&lt;p&gt;I was trying to replicate this behaviour recently, and realised it&#x27;s a lot more straightforward then you might think initially! Here&#x27;s my final end product (&lt;a href=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;5Mc5WTg&quot;&gt;video version here&lt;&#x2F;a&gt;):&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;codesandbox.io&#x2F;embed&#x2F;rxlc7l?view=preview&amp;module=%2Fsrc%2FMarkdownTextArea.js&amp;hidenavigation=1&quot; style=&quot;width:100%; height: 500px; border:0; border-radius: 4px; overflow:hidden; margin-bottom: 20px;&quot; title=&quot;markdown-upload-editor&quot; allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot; sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;Let&#x27;s go through some of the main features and how they work:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;auto-grow&quot;&gt;Auto grow&lt;&#x2F;h2&gt;
&lt;p&gt;I started doing web development in an era where you needed to write every feature twice, once for modern browsers, and once for Internet Explorer, so I didn&#x27;t quite realise it was so unbelievably straightforward to do this now:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;autoGrow &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.target;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.style.height = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;auto&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.style.height = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;scrollHeight&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}px`&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;textarea&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;addEventListener&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;input&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;autoGrow&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Setting a &lt;code&gt;min-height&lt;&#x2F;code&gt; in the CSS for the &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;&#x2F;code&gt; is advised, to give the user enough space. I also usually add &lt;code&gt;resize: none;&lt;&#x2F;code&gt; to hide the resize handle – the user won&#x27;t need to resize the text box if it auto-sizes.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;drag-and-drop-files&quot;&gt;Drag and drop files&lt;&#x2F;h2&gt;
&lt;p&gt;As you can see in the video linked below, or by trying the example, the following things happen when you drop a file into the text box:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Placeholder text is inserted in the form of &lt;code&gt;[Uploading filename.ext...]()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;The file is uploaded and the final public URL is retrieved&lt;&#x2F;li&gt;
&lt;li&gt;The placeholder text is replaced with the final link or image tag&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;To implement this, the first step is to listen to the &lt;code&gt;drop&lt;&#x2F;code&gt; event on the textbox:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;textarea&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;addEventListener&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;drop&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; only take action if a file is being dropped,
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; as we want to keep the default behaviour for
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; dropping text into the text box
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;dataTransfer&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;files&lt;&#x2F;span&gt;&lt;span&gt;.length) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;preventDefault&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; grab our File object
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;dataTransfer&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;files&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;];
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; insert some placeholder text
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;placeholder &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`![Uploading ${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.name}...]()`&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;insertAtCaret&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.target, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;placeholder&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; do what we need to upload the file
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;uploadFile&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; replace with final value
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isImage &lt;&#x2F;span&gt;&lt;span&gt;= !&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file&lt;&#x2F;span&gt;&lt;span&gt;.type || &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file&lt;&#x2F;span&gt;&lt;span&gt;.type?.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;startsWith&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;image&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;finalTag &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isImage
&lt;&#x2F;span&gt;&lt;span&gt;		? &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`![${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.name}](${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;})`
&lt;&#x2F;span&gt;&lt;span&gt;		: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`[${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.name}](${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;})`&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.target.value = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.target.value.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;replace&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;placeholder&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;finalTag&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To make sure we deal with inserting the text at the right place in the text box, based on where the user&#x27;s cursor is, let&#x27;s introduce the a little helper function:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;insertAtCaret &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;selectionStart &lt;&#x2F;span&gt;&lt;span&gt;|| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;selectionStart &lt;&#x2F;span&gt;&lt;span&gt;== &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.value =
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.value.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;substring&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;selectionStart&lt;&#x2F;span&gt;&lt;span&gt;) +
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;text &lt;&#x2F;span&gt;&lt;span&gt;+
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.value.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;substring&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;selectionEnd&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.value.length);
&lt;&#x2F;span&gt;&lt;span&gt;  } &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;else &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;el&lt;&#x2F;span&gt;&lt;span&gt;.value += &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;  }
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If we can&#x27;t figure out where the cursor is, or if the text area isn&#x27;t focussed, we&#x27;ll append the image or link at the end of the current value.&lt;&#x2F;p&gt;
&lt;p&gt;You can further optimise this by dealing with whitespaces: if you&#x27;re appending directly after a word or sentence, you might want to add a space or newline before the image or link.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pasting-images&quot;&gt;Pasting images&lt;&#x2F;h2&gt;
&lt;p&gt;Now that we have drag and drop working for files, adding the ability to add the ability to paste files and images into the text field is quite trivial!&lt;&#x2F;p&gt;
&lt;p&gt;We simply swap out listening to the &lt;code&gt;drop&lt;&#x2F;code&gt; event for listening to the &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;Element&#x2F;paste_event&quot;&gt;&lt;code&gt;paste&lt;&#x2F;code&gt; event&lt;&#x2F;a&gt;, and look at the &lt;code&gt;clipboardData&lt;&#x2F;code&gt; from the event:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;textarea&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;addEventListener&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;paste&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; only take action if a file is being pasted,
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; as we want to keep the default behaviour for
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; pasting text into the text box
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipboardData&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;files&lt;&#x2F;span&gt;&lt;span&gt;.length) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;preventDefault&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; grab our File object from the clipboard data
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;file &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;clipboardData&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;files&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;];
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; do the rest of the upload and insert like before
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;});
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Working with the modern web is such a joy!&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;
</description>
      </item>
      <item>
          <title>Creating Awesome Embeddable Scripts</title>
          <pubDate>Sun, 23 Jun 2024 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/creating-awesome-embeddable-scripts/</link>
          <guid>https://schof.co/creating-awesome-embeddable-scripts/</guid>
          <description xml:base="https://schof.co/creating-awesome-embeddable-scripts/">&lt;p&gt;I was looking at a video about the new version of Apple&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;documentation&#x2F;mapkitjs&quot;&gt;MapKit JS&lt;&#x2F;a&gt;, their web maps library, and they showed how to initialise the SDK by loading the script.&lt;&#x2F;p&gt;
&lt;p&gt;The code looks something &lt;a href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;documentation&#x2F;mapkitjs&#x2F;loading_the_latest_version_of_mapkit_js#3331749&quot;&gt;like this&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;script
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;src&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;cdn.apple-mapkit.com&#x2F;mk&#x2F;5.x.x&#x2F;mapkit.js&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;crossorigin async
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;data-callback&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;initMapKit&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;data-libraries&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;services,full-map,geojson&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;data-token&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Your MapKit JS token&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;script&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That&#x27;s very neat, being able to specify the token and other details using the &lt;code&gt;data-&lt;&#x2F;code&gt; attributes on the script tag. That made me wonder how this worked.&lt;&#x2F;p&gt;
&lt;p&gt;After searching through the minified version of the source code for a bit, I found out what their method was for making this work: &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;API&#x2F;Document&#x2F;currentScript&quot;&gt;&lt;code&gt;document.currentScript&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I did not know that property existed - but apparently it&#x27;s supported in all major browsers and allows you to get the script DOM element that invoked the currently running Javascript.&lt;&#x2F;p&gt;
&lt;p&gt;Now that we know that, we could build a similar embed script ourselves, combining &lt;code&gt;currentScript&lt;&#x2F;code&gt; with the &lt;a href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Learn&#x2F;HTML&#x2F;Howto&#x2F;Use_data_attributes#javascript_access&quot;&gt;&lt;code&gt;dataset&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; API for getting HTML &lt;code&gt;data&lt;&#x2F;code&gt; attributes.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s our example HTML:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;html&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-html &quot;&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;script 
&lt;&#x2F;span&gt;&lt;span&gt;   &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;src&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;.&#x2F;embed.js&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;   &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;data-message&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Hello world&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;script&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And our &lt;code&gt;embed.js&lt;&#x2F;code&gt; script:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(document.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;currentScript&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;   &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Invoked from an HTML page
&lt;&#x2F;span&gt;&lt;span&gt;   &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data &lt;&#x2F;span&gt;&lt;span&gt;= document.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;currentScript&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;dataset&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;   &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;message&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;alert&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`Embed script says: ${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;message&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;   }
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Hopefully you can use this the next time you are building an embed system.&lt;&#x2F;p&gt;
&lt;p&gt;I really enjoy building these things, and have gotten the opportunity to build a few of them:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;A really simple embed for &lt;a href=&quot;https:&#x2F;&#x2F;streetartcities.com&#x2F;embed&#x2F;tool&quot;&gt;Street Art Cities maps&lt;&#x2F;a&gt;, using iframes&lt;&#x2F;li&gt;
&lt;li&gt;A slightly more sophisticated &lt;a href=&quot;https:&#x2F;&#x2F;developers.near.st&#x2F;product-locator&#x2F;widget&#x2F;embed&quot;&gt;embeddable widget for NearSt&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Switching My Blog to Zola</title>
          <pubDate>Sat, 08 Jun 2024 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/switching-my-blog-to-zola/</link>
          <guid>https://schof.co/switching-my-blog-to-zola/</guid>
          <description xml:base="https://schof.co/switching-my-blog-to-zola/">&lt;p&gt;For a while now, my personal website has been built using &lt;a href=&quot;https:&#x2F;&#x2F;nextjs.org&#x2F;&quot;&gt;Next.js&lt;&#x2F;a&gt;, running on &lt;a href=&quot;https:&#x2F;&#x2F;vercel.com&quot;&gt;Vercel&lt;&#x2F;a&gt;. It was working fine, but it felt a bit slower than it should be, and quite heavy for something that is basically a static site.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve also not been super happy with Vercel&#x27;s reliability lately. Google&#x27;s indexer doesn&#x27;t seem to be super happy crawling the site, occasionally reporting network errors accessing pages in the &lt;a href=&quot;https:&#x2F;&#x2F;search.google.com&#x2F;search-console&#x2F;about&quot;&gt;Search Console&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;switching-back-to-static&quot;&gt;Switching back to static&lt;&#x2F;h2&gt;
&lt;p&gt;For a few weeks, I&#x27;ve been thinking about moving it back to something simpler, more static. Last night, I finally made the switch. I realised I didn&#x27;t need React, I didn&#x27;t need Tailwind, I didn&#x27;t need anything related to the Node&#x2F;JS ecosystem.&lt;&#x2F;p&gt;
&lt;p&gt;Initially, I wanted to just write a script to generate the pages from my own markdown files (I write my blogs in &lt;a href=&quot;https:&#x2F;&#x2F;obsidian.md&#x2F;&quot;&gt;Obsidian&lt;&#x2F;a&gt;), but then realised I should probably use a site generator. I&#x27;ve used Hugo and Gatsby in the past, but they&#x27;re both quite advanced for my simple use case. Looking at the Hugo docs made it seem quite an undertaking to switch everything over.&lt;&#x2F;p&gt;
&lt;p&gt;I then came across &lt;strong&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.getzola.org&#x2F;&quot;&gt;Zola&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;, a simple static site generator written in Rust. It is simple enough, but also does everything that is a minimal requirement for me out of the box: RSS feeds, Sitemaps, simple category pages.&lt;&#x2F;p&gt;
&lt;p&gt;I ran &lt;code&gt;zola init&lt;&#x2F;code&gt;, exported my markdown, and started rewriting my JSX and &lt;a href=&quot;https:&#x2F;&#x2F;tailwindcss.com&#x2F;&quot;&gt;Tailwind&lt;&#x2F;a&gt; component mess into simple HTML and CSS.&lt;&#x2F;p&gt;
&lt;p&gt;Within 2 hours I had something that was extremely simple and pretty clean.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;deploying-to-s3&quot;&gt;Deploying to S3&lt;&#x2F;h2&gt;
&lt;p&gt;I switched from Vercel back to just having a simple combination of AWS S3 and CloudFront to host the site.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s deployed from GitHub CI, with beautiful simplicity:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;steps&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;	- &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;uses&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;actions&#x2F;checkout@v4
&lt;&#x2F;span&gt;&lt;span&gt;	- &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;uses&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;taiki-e&#x2F;install-action@v2
&lt;&#x2F;span&gt;&lt;span&gt;	  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;with&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;	    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;tool&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;zola@0.17.2
&lt;&#x2F;span&gt;&lt;span&gt;	- &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;run&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;zola build
&lt;&#x2F;span&gt;&lt;span&gt;	- &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;run&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;aws s3 sync public s3:&#x2F;&#x2F;schof.co --acl public-read --delete
&lt;&#x2F;span&gt;&lt;span&gt;	- &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;run&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;aws cloudfront create-invalidation --distribution-id E1XOA26HO9HVF9 --paths &amp;quot;&#x2F;*&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I love it when things are just this neat and simple. No NPM or Yarn installs, no node versions to manage.&lt;&#x2F;p&gt;
&lt;p&gt;In CI, my &lt;code&gt;zola build&lt;&#x2F;code&gt; takes less than 150ms, and about 6 seconds to upload the changes to S3.&lt;&#x2F;p&gt;
&lt;p&gt;Look at the resource list that is loaded when you click on a blog post:
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;kOF9Oou&quot; alt=&quot;Image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;No client side navigation needed, it&#x27;s just plain HTML, so it&#x27;s super fast. I can probably cut it down even further if I had to, but for now this serves my purposes extremely well.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;obsidian-workflow&quot;&gt;Obsidian workflow&lt;&#x2F;h2&gt;
&lt;p&gt;The only thing left to do was to update my workflow for publishing posts.&lt;&#x2F;p&gt;
&lt;p&gt;I have my own little Obsidian plugin that publishes posts (aptly named &lt;code&gt;thomas-blog&lt;&#x2F;code&gt;). Previously, it would call an API endpoint on the old website which would store a new record in a PostgreSQL table for publishing a new post.&lt;&#x2F;p&gt;
&lt;p&gt;Now that the content lives in GitHub, I have changed it to call the GitHub API&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;docs.github.com&#x2F;en&#x2F;rest&#x2F;repos&#x2F;contents?apiVersion=2022-11-28#create-or-update-file-contents&quot;&gt;Contents endpoint&lt;&#x2F;a&gt; and create or update a file, which automatically gets committed to the &lt;code&gt;main&lt;&#x2F;code&gt; branch.&lt;&#x2F;p&gt;
&lt;p&gt;This in turn triggers the GitHub Actions workflow which deploys the new version of the site within a few seconds.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Building Composable React Components</title>
          <pubDate>Sun, 26 May 2024 12:14:30 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/building-composable-react-components/</link>
          <guid>https://schof.co/building-composable-react-components/</guid>
          <description xml:base="https://schof.co/building-composable-react-components/">&lt;p&gt;Imagine you&#x27;re building a &lt;code&gt;&amp;lt;Button&amp;gt;&lt;&#x2F;code&gt; component. You might add some props to control the appearance, like &lt;code&gt;variant&lt;&#x2F;code&gt; or &lt;code&gt;size&lt;&#x2F;code&gt;. You end up with a component that looks like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;Button &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;variant&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;size&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;children &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;className &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getCorrectStylesFor&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;variant&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;size&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;button className&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;className&lt;&#x2F;span&gt;&lt;span&gt;}&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;			{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;children&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	);
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Okay, but now you run into a problem. This always renders as a &lt;code&gt;&amp;lt;button&amp;gt;&lt;&#x2F;code&gt; HTML element, and you might need it to be an &lt;code&gt;&amp;lt;a&amp;gt;&lt;&#x2F;code&gt; tag sometimes so that you can use it as a link. Or maybe even a &lt;code&gt;Link&lt;&#x2F;code&gt; from React Router or Next, so that you can link to other pages within your SPA.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s two simple ways to take care of this:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;adding-an-as-prop&quot;&gt;Adding an &lt;code&gt;as&lt;&#x2F;code&gt; prop&lt;&#x2F;h2&gt;
&lt;p&gt;This is the classic. Adding a prop that allows you to define the actual component to render, so that you can do the following:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Button as&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;a&lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;example.com&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Link to other website
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Button&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Or maybe like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Link &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react-router-dom&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Button as&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Link&lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;to&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&#x2F;profile&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Edit profile
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Button&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;An implementation of that could look something like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;Button &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;children&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt;, ...&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;as &lt;&#x2F;span&gt;&lt;span&gt;|| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;button&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component &lt;&#x2F;span&gt;&lt;span&gt;{...&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props&lt;&#x2F;span&gt;&lt;span&gt;}&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;			{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;children&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	);
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note how the &lt;code&gt;Component&lt;&#x2F;code&gt; variable needs to start with a capital to make sure that React renders it as a React component, rather than just as a HTML element (a lowercased &lt;code&gt;component&lt;&#x2F;code&gt; would be rendered as the HTML element &lt;code&gt;&amp;lt;component&amp;gt;&lt;&#x2F;code&gt;, rather than the actual value of the &lt;code&gt;component&lt;&#x2F;code&gt; variable).&lt;&#x2F;p&gt;
&lt;p&gt;This approach has a drawback, however: you need to pass through all props you want the final element to have. This works using the normal &lt;code&gt;...props&lt;&#x2F;code&gt; spreading method, but editors will get confused because there&#x27;s no Typescript definition for what those props could be, and therefore you&#x27;re missing out on linting and validation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-better-approach-aschild&quot;&gt;A better approach: &lt;code&gt;asChild&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Because of those reasons, an alternative way has become more popular recently – adding an &lt;code&gt;asChild&lt;&#x2F;code&gt; prop. That makes our &lt;code&gt;Link&lt;&#x2F;code&gt; example from above look something like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Link &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react-router-dom&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Button asChild&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Link to&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&#x2F;profile&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;		&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Edit profile
&lt;&#x2F;span&gt;&lt;span&gt;	&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Link&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Button&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That has the advantage of not needing to pass through our props directly from &lt;code&gt;Button&lt;&#x2F;code&gt; to &lt;code&gt;Link&lt;&#x2F;code&gt;. But now we need to do the opposite: we need to pass through some of our styling props (&lt;code&gt;className&lt;&#x2F;code&gt;) in the &lt;code&gt;Button&lt;&#x2F;code&gt; component so that they&#x27;re applied to &lt;code&gt;Link&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;A nice and easy way to do so is using the &lt;a href=&quot;https:&#x2F;&#x2F;www.radix-ui.com&#x2F;primitives&#x2F;docs&#x2F;utilities&#x2F;slot&quot;&gt;&lt;code&gt;Slot&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; component from Radix:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Slot &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;@radix-ui&#x2F;react-slot&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;Button &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;variant&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;size&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;children&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;asChild &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;className &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getCorrectStylesFor&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;variant&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;size&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;asChild &lt;&#x2F;span&gt;&lt;span&gt;? &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Slot &lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;button&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component className&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;className&lt;&#x2F;span&gt;&lt;span&gt;}&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;			{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;children&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	);
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can also build your own simple &lt;code&gt;Slot&lt;&#x2F;code&gt;, if you want to understand how it works:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;Slot &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;children&lt;&#x2F;span&gt;&lt;span&gt;, ...&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;slots &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;React&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Children&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;toArray&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;children&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;slots&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;map&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;React&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;cloneElement&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span&gt;, {
&lt;&#x2F;span&gt;&lt;span&gt;      ...&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      ...&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    });
&lt;&#x2F;span&gt;&lt;span&gt;  });
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Of course that&#x27;s a very naive implementation - to make this usable, you&#x27;d want to be able to deal with non-React elements (e.g. strings), merge certain props (like &lt;code&gt;className&lt;&#x2F;code&gt; and &lt;code&gt;style&lt;&#x2F;code&gt;), and maybe allow multiple levels of passing through.&lt;&#x2F;p&gt;
&lt;p&gt;Or just use the Radix one, it &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;radix-ui&#x2F;primitives&#x2F;blob&#x2F;main&#x2F;packages&#x2F;react&#x2F;slot&#x2F;src&#x2F;Slot.tsx&quot;&gt;does all of those things&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Pro Software for the iPad</title>
          <pubDate>Sun, 26 May 2024 10:47:42 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/pro-software-for-the-ipad/</link>
          <guid>https://schof.co/pro-software-for-the-ipad/</guid>
          <description xml:base="https://schof.co/pro-software-for-the-ipad/">&lt;p&gt;I&#x27;ve complained about it before, but I still don&#x27;t understand the Apple pundits that come out every year when new iPads are released to complain about the iPad software not supporting enough pro workflows - usually with the request to have some way to dual boot or virtualise macOS. On an iPad!&lt;&#x2F;p&gt;
&lt;p&gt;I really don&#x27;t get why you&#x27;d want that. No experience of macOS is going to be better than using it on a Mac. Some small screen tablet with a suboptimal keyboard case is not the way I&#x27;d want to use macOS.&lt;&#x2F;p&gt;
&lt;p&gt;And I could imagine wanting it if Apple only made desktop PCs, but how could an iPad ever be a better way to use macOS than a MacBook? I honestly don&#x27;t understand what gap in the market that would fill.&lt;&#x2F;p&gt;
&lt;p&gt;Apple themselves are responsible for some of this confusion, however, by releasing some of their own pro software for the iPad, implying it&#x27;s more than just an amazing entertainment or work companion device. This is their classic attempt at trying to over-market their devices, and make it look like they&#x27;re all the best way to do everything (similar to them heavily promoting gaming on the Mac and iPhone).&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Writing JSDoc for React Components</title>
          <pubDate>Wed, 22 May 2024 09:02:14 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/writing-jsdoc-for-react-components/</link>
          <guid>https://schof.co/writing-jsdoc-for-react-components/</guid>
          <description xml:base="https://schof.co/writing-jsdoc-for-react-components/">&lt;p&gt;I&#x27;m starting to get into the habit of consistently writing &lt;a href=&quot;https:&#x2F;&#x2F;jsdoc.app&#x2F;&quot;&gt;JSDoc&lt;&#x2F;a&gt; comments for my front-end components, which helps with explaining usage of a component, and gets rid of VSCode’s red squiggles because it can&#x27;t figure out the type of certain props.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s some simple examples of how to use JSDoc effectively to better document your React components.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;simple-example&quot;&gt;Simple example&lt;&#x2F;h2&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;**
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * A great component that displays a shop name!
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@param &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{object} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;props
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@param &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{string} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;props.shopName&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; The name of the shop
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@returns &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{JSX.Element}
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *&#x2F;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;ShopLabel &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;shopName &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;p&amp;gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;shopName&lt;&#x2F;span&gt;&lt;span&gt;}&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;;
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;advanced-props&quot;&gt;Advanced props&lt;&#x2F;h2&gt;
&lt;p&gt;If you have more than one or two props, you might want to create a separate type definition for your component props:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Define as a separate &lt;a href=&quot;https:&#x2F;&#x2F;jsdoc.app&#x2F;tags-typedef&quot;&gt;&lt;code&gt;@typedef&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; within the same JSDoc comment&lt;&#x2F;li&gt;
&lt;li&gt;If your component name is &lt;code&gt;MyComponent&lt;&#x2F;code&gt;, this type should probably be called something like &lt;code&gt;MyComponentProps&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;**
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * A great component that displays a shop name!
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@typedef &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{object} ShopLabelProps
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@property &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{string} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;shopName&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; The name of the shop
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@property &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{string} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;[className]&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; Optional class name
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@param &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{ShopLabelProps} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;props
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@returns &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{JSX.Element}
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *&#x2F;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;ShopLabel &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;shopName&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;className &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;&amp;#39; &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;p className={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;className&lt;&#x2F;span&gt;&lt;span&gt;}&amp;gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;shopName&lt;&#x2F;span&gt;&lt;span&gt;}&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;;
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;adding-usage-examples&quot;&gt;Adding usage examples&lt;&#x2F;h2&gt;
&lt;p&gt;You can add one or more examples of how to use the component using the &lt;a href=&quot;https:&#x2F;&#x2F;jsdoc.app&#x2F;tags-example&quot;&gt;&lt;code&gt;@example&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; tag, which will be displayed when you hover over a component in VSCode.&lt;&#x2F;p&gt;
&lt;p&gt;For a single example, feel free to omit the example description after &lt;code&gt;@example&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;**
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * A great component that displays a shop name!
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@example &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;&amp;lt;caption&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#d19a66;&quot;&gt;Rendering a shop name&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;&amp;lt;&#x2F;caption&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &amp;lt;ShopLabel shopName=&amp;quot;My shop&amp;quot; &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@example &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;&amp;lt;caption&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#d19a66;&quot;&gt;Adding a class name&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;&amp;lt;&#x2F;caption&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &amp;lt;ShopLabel shopName=&amp;quot;My Shop&amp;quot; className=&amp;quot;hello&amp;quot; &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@param &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{ShopLabelProps} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;props
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@returns &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{JSX.Element}
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *&#x2F;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;ShopLabel &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;shopName&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;className &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;&amp;#39; &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;p className={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;className&lt;&#x2F;span&gt;&lt;span&gt;}&amp;gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;shopName&lt;&#x2F;span&gt;&lt;span&gt;}&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;;
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;extending-html-elements&quot;&gt;Extending HTML elements&lt;&#x2F;h2&gt;
&lt;p&gt;If you want your component to accept normal props for an HTML element in addition to your custom props, make the type of your props param &lt;a href=&quot;https:&#x2F;&#x2F;www.typescriptlang.org&#x2F;docs&#x2F;handbook&#x2F;2&#x2F;objects.html#intersection-types&quot;&gt;an intersection&lt;&#x2F;a&gt; between your props type and &lt;code&gt;React.HTMLAttributes&amp;lt;*&amp;gt;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Example for a &lt;code&gt;&amp;lt;p&amp;gt;&lt;&#x2F;code&gt; tag:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;**
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * A great component that displays a shop name!
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@typedef &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{object} ShopLabelProps
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@property &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{string} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;shopName&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; The name of the shop
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@param &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{React.HTMLAttributes&amp;lt;HTMLParagraphElement&amp;gt; &amp;amp; ShopLabelProps} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;props
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@returns &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{JSX.Element}
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *&#x2F;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;ShopLabel &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;shopName&lt;&#x2F;span&gt;&lt;span&gt;, ...&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;p {...props}&amp;gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;shopName&lt;&#x2F;span&gt;&lt;span&gt;}&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;;
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that for input type fields, you want to use &lt;code&gt;React.InputHTMLAttributes&amp;lt;*&amp;gt;&lt;&#x2F;code&gt;, to ensure the &lt;code&gt;onChange&lt;&#x2F;code&gt; prop exists and has the right type.&lt;&#x2F;p&gt;
&lt;p&gt;Example for an input:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;**
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * Text field (renders as `&amp;lt;input&amp;gt;`).
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@typedef &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;TextInputProps
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@property &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{string} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;[suffix]&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; Optional suffix to display after the input field
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@param &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{React.InputHTMLAttributes&amp;lt;HTMLInputElement&amp;gt; &amp;amp; TextInputProps} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;props
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@returns &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{JSX.Element}
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *&#x2F;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;TextInput &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;type &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;text&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;suffix &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, ...&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;div className&lt;&#x2F;span&gt;&lt;span&gt;={styles.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;inputContainer&lt;&#x2F;span&gt;&lt;span&gt;}&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;			&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;input className&lt;&#x2F;span&gt;&lt;span&gt;={styles.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;input&lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;} {...&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props&lt;&#x2F;span&gt;&lt;span&gt;} &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;			{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;suffix&lt;&#x2F;span&gt;&lt;span&gt; &amp;amp;&amp;amp; &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;span className&lt;&#x2F;span&gt;&lt;span&gt;={styles.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;inputSuffix&lt;&#x2F;span&gt;&lt;span&gt;}&amp;gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;suffix&lt;&#x2F;span&gt;&lt;span&gt;}&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;}
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	);
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;react-element-types&quot;&gt;React Element types&lt;&#x2F;h2&gt;
&lt;p&gt;There are a few similar types you might see, here’s when to use which:&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jsx-element&quot;&gt;&lt;code&gt;JSX.Element&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Usually used as the return value of a component, indicates it returns a single DOM&#x2F;JSX elements. This is basically the same as &lt;code&gt;ReactElement&lt;&#x2F;code&gt;, although &lt;code&gt;ReactElement&lt;&#x2F;code&gt; is more generic (it technically is &lt;code&gt;ReactElement&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;, meant for type expansion to define what props that element takes).&lt;&#x2F;p&gt;
&lt;p&gt;May also be used as &lt;code&gt;React.JSX.Element&lt;&#x2F;code&gt;, although both Typescript and ESLint understand what you mean if you just use &lt;code&gt;JSX.Element&lt;&#x2F;code&gt;, so that is cleaner and preferable.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;reactnode&quot;&gt;&lt;code&gt;ReactNode&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;If your component returns anything other than just a single element, you might want to consider using this. It is a union type that also includes things like &lt;code&gt;JSX.Element[]&lt;&#x2F;code&gt;, &lt;code&gt;boolean&lt;&#x2F;code&gt;, and &lt;code&gt;string&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Not super common you would need this as a return value, but might be useful as a prop. Note that it is an element, not a component (e.g. an instance of a component). Example usage:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;**
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * Only shows the children if you have access, otherwise returns the fallback.
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@typedef &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;ProtectProps
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@property &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{ReactNode} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;children&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; Rendered when you have access
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@property &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{ReactNode} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;fallback&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; Rendered when you don&amp;#39;t have access
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@param &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{ProtectProps} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;props
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@returns &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{JSX.Element}
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *&#x2F;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;Protect &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;children&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;fallback &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; ...
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; usage:
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Protect fallback&lt;&#x2F;span&gt;&lt;span&gt;={&amp;lt;p&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Boo&lt;&#x2F;span&gt;&lt;span&gt;, no access!&amp;lt;&#x2F;p&amp;gt;}&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&amp;lt;p&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Boom&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;access&lt;&#x2F;span&gt;&lt;span&gt;!&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Protect&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;react-fc&quot;&gt;&lt;code&gt;React.FC&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Stands for FunctionComponent. This is the type of the component itself, and can be useful when you want to accept a component as a prop rather than an element:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;**
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * A great component that displays a shop name!
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@typedef &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{object} ShopLabelProps
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@property &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{React.FC} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; The component to render as
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@param &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{ShopLabelProps} &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#e06c75;&quot;&gt;props
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; * &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#c678dd;&quot;&gt;@returns &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;{JSX.Element}
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt; *&#x2F;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;ShopLabel &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;props&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;as &lt;&#x2F;span&gt;&lt;span&gt;|| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;p&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;Component&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Shop name&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Component&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;;
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; usage:
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;ShopLabel as&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;h1&amp;quot; &lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;ShopLabel as&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Badge&lt;&#x2F;span&gt;&lt;span&gt;} &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Tags - From UX to Implementation</title>
          <pubDate>Mon, 20 May 2024 06:54:47 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/tags-ux-to-implementation/</link>
          <guid>https://schof.co/tags-ux-to-implementation/</guid>
          <description xml:base="https://schof.co/tags-ux-to-implementation/">&lt;p&gt;Recently, I&#x27;ve become a really massive fan of tags and attributes as ways of allowing users on the platforms I build to structure data.&lt;&#x2F;p&gt;
&lt;p&gt;My main note taking app &lt;a href=&quot;https:&#x2F;&#x2F;obsidian.md&quot;&gt;Obsidian&lt;&#x2F;a&gt; supports both of these concepts as well, and they basically eliminate the need for folders and file name structures. I&#x27;ve basically adopted a single level file structure, where all notes live in the same single folder. Tags and attributes is how I filter, which is much more powerful. Finding an in-progress project at my company means searching for notes with the tag &lt;code&gt;#business&lt;&#x2F;code&gt; and the attribute &lt;code&gt;status = in-progress&lt;&#x2F;code&gt;. No dragging of notes between folders when statuses or scopes change.&lt;&#x2F;p&gt;
&lt;p&gt;The nice thing about tags and attributes as well is that they&#x27;re very extensible and flexible. When building apps or data structures, there will always be unknowns about how users will interact with a feature. I was recently building the basics of a new platform for a customer, and was designing the database structure for the &lt;code&gt;Place&lt;&#x2F;code&gt; entity within their platform. Rather than trying to up front know which fields are important and which ones might fade away as it becomes clear that users don&#x27;t care about them, I added a simple map of attributes, which allows the platform&#x27;s admin users to determine what fields should exist, and update that taxonomy over time.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;setting-up-infrastructure-for-tags&quot;&gt;Setting up infrastructure for tags&lt;&#x2F;h2&gt;
&lt;p&gt;Adding tags to an entity is a very easy thing to do. I tend to leave the format of tags completely up to the end user, with minimal validation, often just representing them as an array of strings (&lt;code&gt;String[]&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;Even those strings probably don&#x27;t need too much validation or formatting. I do prefer to trim and lowercase them where possible, to decrease the chance of two tags being created that represent the same thing but are slightly different strings.&lt;&#x2F;p&gt;
&lt;p&gt;If you allow almost all characters in the tags, you can leave it up to the user how to use them:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;A user might use emoji as quick visual indicators: &lt;code&gt;Project stalled 💀&lt;&#x2F;code&gt; or &lt;code&gt;Low priority 🟢&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;High priority 🔴&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;It is possible to create a sense of hierarchy by using slashes: &lt;code&gt;projects&#x2F;the-one&lt;&#x2F;code&gt;, &lt;code&gt;projects&#x2F;a-second&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Or even use them as very simple key-value pairs: &lt;code&gt;blood-pressure:high&lt;&#x2F;code&gt;, &lt;code&gt;status:in-progress&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;To make it easy to show a UI to auto-fill previously used tags, or to manage tags, I tend to create a separate database entity that represents the tags themselves:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ts&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-ts &quot;&gt;&lt;code class=&quot;language-ts&quot; data-lang=&quot;ts&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;interface &lt;&#x2F;span&gt;&lt;span&gt;Tag {
&lt;&#x2F;span&gt;&lt;span&gt;   &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;: String,
&lt;&#x2F;span&gt;&lt;span&gt;   &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;numberOfUses&lt;&#x2F;span&gt;&lt;span&gt;: Number
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A background process or event-driven infrastructure can be used to update these &lt;code&gt;Tag&lt;&#x2F;code&gt; entities when new tags are added to entities, or clean up tags that are no longer in use by any entities.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;numberOfUses&lt;&#x2F;code&gt; allows you to present a management UI where you can sort the tags by most used or least used, an easy way to find tags that should be cleaned up.&lt;&#x2F;p&gt;
&lt;p&gt;Having a separate entity for tags also allows you to attach more metadata in the future, like an &lt;code&gt;icon&lt;&#x2F;code&gt; or &lt;code&gt;color&lt;&#x2F;code&gt; field per tag, similar to MacOS Finder, where you can specify a color for any file tags you create.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;building-a-ui-to-input-tags&quot;&gt;Building a UI to input tags&lt;&#x2F;h2&gt;
&lt;p&gt;I find myself using the &lt;a href=&quot;https:&#x2F;&#x2F;react-select.com&quot;&gt;react-select&lt;&#x2F;a&gt; package a lot to build tag selection UIs.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;uR_JD_u&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;It allows you to dynamically show autocomplete suggestions, but also allows arbitrary options to be added through the &lt;code&gt;AsyncCreatableSelect&lt;&#x2F;code&gt; version of the select:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;React&lt;&#x2F;span&gt;&lt;span&gt;, { &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;useMemo &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;AsyncCreatableSelect &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react-select&#x2F;async-creatable&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;useData &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;@&#x2F;lib&#x2F;api&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;normalize &lt;&#x2F;span&gt;&lt;span&gt;= (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;str&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;str
&lt;&#x2F;span&gt;&lt;span&gt;    .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;toLowerCase&lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;    .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;replace&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;[&lt;&#x2F;span&gt;&lt;span&gt;^&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;\w]&lt;&#x2F;span&gt;&lt;span&gt;+&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;g&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;    .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;trim&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;TagsSelect &lt;&#x2F;span&gt;&lt;span&gt;= ({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;onChange&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;allTags &lt;&#x2F;span&gt;&lt;span&gt;} = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;useData&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;tags&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;memoizedTags &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;useMemo&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    () &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;      (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;allTags &lt;&#x2F;span&gt;&lt;span&gt;|| []).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;map&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;tag&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;        filterValue: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;normalize&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;tag&lt;&#x2F;span&gt;&lt;span&gt;),
&lt;&#x2F;span&gt;&lt;span&gt;        label: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;tag&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        value: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;tag&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      })),
&lt;&#x2F;span&gt;&lt;span&gt;    [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;allTags&lt;&#x2F;span&gt;&lt;span&gt;],
&lt;&#x2F;span&gt;&lt;span&gt;  );
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;promiseOptions &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;inputValue&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;normalizedInput &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;normalize&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;inputValue&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;filterOptions &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;memoizedTags &lt;&#x2F;span&gt;&lt;span&gt;|| [];
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; Filter the options based on the search input
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;finalOptions &lt;&#x2F;span&gt;&lt;span&gt;= !&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;normalizedInput
&lt;&#x2F;span&gt;&lt;span&gt;      ? &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;filterOptions
&lt;&#x2F;span&gt;&lt;span&gt;      : &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;filterOptions&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;filter&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;artist&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;          &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;artist&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;filterValue&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;includes&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;normalizedInput&lt;&#x2F;span&gt;&lt;span&gt;),
&lt;&#x2F;span&gt;&lt;span&gt;        );
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;finalOptions
&lt;&#x2F;span&gt;&lt;span&gt;      .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;sort&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;a&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;b&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;a&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;filterValue&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;indexOf&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;normalizedInput&lt;&#x2F;span&gt;&lt;span&gt;) &amp;lt;
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;b&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;filterValue&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;indexOf&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;normalizedInput&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;          ? -&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1
&lt;&#x2F;span&gt;&lt;span&gt;          : &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      )
&lt;&#x2F;span&gt;&lt;span&gt;      .&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;slice&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;15&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;  };
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;AsyncCreatableSelect
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cacheOptions
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isMulti&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;allowMultiple&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;allowCreateWhileLoading
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;isValidNewOption&lt;&#x2F;span&gt;&lt;span&gt;={(inputValue) =&amp;gt; !!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;inputValue&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;placeholder&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Add tags...&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;defaultOptions
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;loadOptions&lt;&#x2F;span&gt;&lt;span&gt;={&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;promiseOptions&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;formatCreateLabel&lt;&#x2F;span&gt;&lt;span&gt;={(inputValue) =&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`Add new tag &amp;quot;${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;inputValue&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}&amp;quot;`&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;onChange&lt;&#x2F;span&gt;&lt;span&gt;={(value) =&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;onChange&lt;&#x2F;span&gt;&lt;span&gt;(value.map(({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value &lt;&#x2F;span&gt;&lt;span&gt;}) =&amp;gt; value))}
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;value&lt;&#x2F;span&gt;&lt;span&gt;={(value || []).map((tag) =&amp;gt; ({ value: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;tag&lt;&#x2F;span&gt;&lt;span&gt;, label: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;tag &lt;&#x2F;span&gt;&lt;span&gt;}))}
&lt;&#x2F;span&gt;&lt;span&gt;    &#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;  );
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export default &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;TagsSelect&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>My Second Day on the California Zephyr</title>
          <pubDate>Tue, 23 Apr 2024 15:01:49 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/california-zephyr-2/</link>
          <guid>https://schof.co/california-zephyr-2/</guid>
          <description xml:base="https://schof.co/california-zephyr-2/">&lt;p&gt;I woke up at around 6am after &lt;a href=&quot;https:&#x2F;&#x2F;schof.co&#x2F;california-zephyr&quot;&gt;my first night&#x27;s sleep&lt;&#x2F;a&gt; on the train. It took me a few hours to get to sleep, but once my body was used to movement of the train, I slept soundly for a good few hours.&lt;&#x2F;p&gt;
&lt;p&gt;After freshening up a bit, I went to the dining car for an early breakfast at 6:30am. I was the first person there, but was quickly joined by a couple from Oregon. We spent a good two hours talking and revelling in the landscape of the Utah desert, with its bold and beautiful mountains, whilst having some amazing french toast with strawberries and a lot of coffee.&lt;&#x2F;p&gt;
&lt;style&gt;@media (min-width: 700px) { .collage { display: flex; gap: 1.3rem; flex-wrap: wrap; margin: 2rem 0; } .collage &gt; * { flex: 1; width: 40%; height: auto; margin: 0; } }&lt;&#x2F;style&gt;&lt;div class=&quot;collage&quot;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;JdK_56S&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;0ijmObS&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;By the time I got back to my room, we were almost moving into the state of Colorado. Later today, we&#x27;d go past the town of Boulder, where I&#x27;ve been a few times before, and a stop in Denver.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;rS7EyEv&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;As we proceeded into Colorado, the landscape became more lush and green, with more farming and commercial activity than in Utah&#x27;s barren valleys.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;XOI1BpY&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;It is a beautiful state, also &lt;a href=&quot;https:&#x2F;&#x2F;www.coloradoinfo.com&#x2F;blog_post&#x2F;palisade-peaches-delicious-colorado-history&#x2F;&quot;&gt;known for its peaches&lt;&#x2F;a&gt;, which mostly come from a place called Palisade, that we passed by. Taking good pictures today is difficult, because the sun is shining directly at us, and the train is going quite quickly through this area.&lt;&#x2F;p&gt;
&lt;style&gt;@media (min-width: 700px) { .collage { display: flex; gap: 1.3rem; flex-wrap: wrap; margin: 2rem 0; } .collage &gt; * { flex: 1; width: 40%; height: auto; margin: 0; } }&lt;&#x2F;style&gt;&lt;div class=&quot;collage&quot;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;zwbfc_J&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;kRKFjdY&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;bmlq-wh&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;Getting further into Colorado, the weather became a bit more cloudy, and the hills more red - we&#x27;re in Rocky Mountains territory now!&lt;&#x2F;p&gt;
&lt;p&gt;The train conductor on this part of the trip is amazing, really funny announcements! He talked about how a &#x27;certain&#x27; train conductor once upon a time a few decades ago proposed to a woman at the top of one of these mountains, and the awkward hike back down the mountain that followed... 😁&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;5FfG4GW&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;Before arriving in Denver, we passed through Gore Canyon, one of the undeniably most beautiful parts of the route. The tracks here have many twists and turns, allowing you to see the front part of the train pass through some of the tunnels in the Canyon before you&#x27;re in them.&lt;&#x2F;p&gt;
&lt;p&gt;This canyon leads into to a set of rapids that is part of the Colorado River.&lt;&#x2F;p&gt;
&lt;style&gt;@media (min-width: 700px) { .collage { display: flex; gap: 1.3rem; flex-wrap: wrap; margin: 2rem 0; } .collage &gt; * { flex: 1; width: 40%; height: auto; margin: 0; } }&lt;&#x2F;style&gt;&lt;div class=&quot;collage&quot;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;IC6OKN_&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;PW7x0WO&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;flKVNqK&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;e4b6lP8&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;After Denver, we slowly made our way towards the border with Kansas and Nebraska, and by midnight we made it midway through Nebraska.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;AqwSASN&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;Tomorrow, on the last leg of the journey, we&#x27;ll make our way through Iowa, before finally reaching Illinois and arriving at Chicago Union Station around lunch time.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Sleeping the second night definitely was a lot easier, even though the tracks in Nebraska are in a worse shape, leading to a bit more of a bumpy ride.&lt;&#x2F;p&gt;
&lt;p&gt;This was definitely the best train trip I&#x27;ve ever taken. It&#x27;s hard to express in pictures (and I didn&#x27;t try too hard to do it in words either) how many unique landscapes you see during this 2.5 day trip. I&#x27;ve seen 7 states from a unique angle in just over 50 hours, there&#x27;s no other way I think you can do that.&lt;&#x2F;p&gt;
&lt;p&gt;In addition, the food and the staff was amazing, and it was great to meet such diverse people and have conversations over breakfast, lunch and dinner with people I&#x27;d never usually strike up a conversation with. There&#x27;s a lot of retirees taking this train, who all have fascinating life stories and really gave colour to this journey. Great way to spend a few vacation days.&lt;&#x2F;p&gt;
&lt;style&gt;img { border-radius: 0.5rem; background: #f2f3f4; } @media (min-width: 600px) { .collage:has(img:nth-child(3)):not(:has(img:nth-child(4))) img:first-child { width: 100%; } }&lt;&#x2F;style&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>My First Day on the California Zephyr</title>
          <pubDate>Mon, 22 Apr 2024 20:20:50 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/california-zephyr/</link>
          <guid>https://schof.co/california-zephyr/</guid>
          <description xml:base="https://schof.co/california-zephyr/">&lt;p&gt;Our &lt;a href=&quot;https:&#x2F;&#x2F;www.amtrak.com&#x2F;california-zephyr-train&quot;&gt;California Zephyr&lt;&#x2F;a&gt; train left Emeryville (just outside San Francisco, a 15 minute Uber from my hotel) a few minutes late. The train attendant, Mike, that will look after our train car for the entire journey was extremely friendly and personable.&lt;&#x2F;p&gt;
&lt;p&gt;Within 10 minutes of leaving the station, there were already beautiful views. The train wasn&#x27;t just going along the coast, it was on the coast, just a few feet away from the water of the bay.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;w_GFpCZ&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;I immediately made my way to the observation deck car. It was around 9am, and I hadn&#x27;t really had breakfast yet, so I got a tasty egg and cheese sandwich from the cafe, which is just below the observation deck. I was already enjoying this so much, and this was just hour one of 52!&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;sl3yXGy&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;After passing the bay, we went through some marshes and hills, all whilst the sun was shining down on us, a lovely 22℃, as we made our way up to upstate California.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;ilyi7Hm&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;As we came closer to Sacramento, the state capitol, the fact that California has a homelessness problem was very visible, with several tent camps near the tracks. On a positive note, Sacramento seems like it has a lot of street art, with some really nice murals visible from the train.&lt;&#x2F;p&gt;
&lt;p&gt;Lunch was the first time going into the dining car of the train, where I had a lovely chat with some people I was seated together with. Whilst having some grilled cheese, we started to see some (melting) snow on the hills we rode past.&lt;&#x2F;p&gt;
&lt;style&gt;@media (min-width: 700px) { .collage { display: flex; gap: 1.3rem; flex-wrap: wrap; margin: 2rem 0; } .collage &gt; * { flex: 1; width: 40%; height: auto; margin: 0; } }&lt;&#x2F;style&gt;&lt;div class=&quot;collage&quot;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;JqsYIz0&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;UVFSJt9&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;5jk6Nku&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;It was still 20℃ outside, but you could feel the cold radiate from the melting snow. Water was flowing down from the hills and forming into little waterfalls in many places.&lt;&#x2F;p&gt;
&lt;p&gt;A few hours and a lot of snowy mountains later (including a pass through the Donner ski resort area, with a ski lift that went over the train tracks), we were close to making our way into the state of Nevada.&lt;&#x2F;p&gt;
&lt;p&gt;The train conductor mentioned the area being named after George Donner, and dropped the phrase &#x27;Donner dinner party&#x27;. A quick Google:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.britannica.com&#x2F;topic&#x2F;Donner-party&quot;&gt;The party&lt;&#x2F;a&gt; (Donner and the other settlers) was trapped by exceptionally heavy snow in the Sierra Nevada, and, when food ran out, some members of the group reportedly resorted to cannibalism of those already dead.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;I can see how being a settler here must have been very hard, it looks like a harsh environment. Bizarre how less than 200 years later there&#x27;s a daily train full of pensioners and tourists in comfortable sleeping bunks going through this area.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;zK4LBi0&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;At around 4pm we arrived in Reno, Nevada, one of a few &#x27;fresh air stops&#x27;, where you can get out for a few minutes to stretch your legs. Reno station is below street level, and the train arrives in what is known as &#x27;the trench&#x27;.&lt;&#x2F;p&gt;
&lt;style&gt;@media (min-width: 700px) { .collage { display: flex; gap: 1.3rem; flex-wrap: wrap; margin: 2rem 0; } .collage &gt; * { flex: 1; width: 40%; height: auto; margin: 0; } }&lt;&#x2F;style&gt;&lt;div class=&quot;collage&quot;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;BKzNZ6C&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;Dbua1VV&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;This is the last stop in a major city for today, with the next one being tomorrow morning at 3am in Salt Lake City.&lt;&#x2F;p&gt;
&lt;p&gt;As we got deeper into Nevada, the landscape became flatter and increasingly desert-like, with the Rocky Mountains just barely visible at the horizon. Even late in the afternoon, the temperature outside here was around 27℃.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;VBUqKpG&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;Ending the day with this beautiful view:&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;ijXb38A&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;By the time I wake up tomorrow morning, we&#x27;ll have completed about a third of our trip, starting the journey into the Rockies.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;WuHn_lX&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;Continues here: &lt;a href=&quot;https:&#x2F;&#x2F;schof.co&#x2F;california-zephyr-2&quot;&gt;My Second Day on the California Zephyr&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;style&gt;img { border-radius: 0.5rem; background: #f2f3f4; }&lt;&#x2F;style&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Upgrading Your AWS Lambdas to Node 20</title>
          <pubDate>Mon, 19 Feb 2024 10:50:24 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/upgrading-your-aws-lambdas-to-node-20/</link>
          <guid>https://schof.co/upgrading-your-aws-lambdas-to-node-20/</guid>
          <description xml:base="https://schof.co/upgrading-your-aws-lambdas-to-node-20/">&lt;p&gt;The Node 16 runtime on Lambda will be &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;lambda&#x2F;latest&#x2F;dg&#x2F;lambda-runtimes.html&quot;&gt;deprecated in June&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Upgrading to Node 18 or 20 is relatively easy for most applications, with few breaking changes from Node 16.&lt;&#x2F;p&gt;
&lt;p&gt;Some notable additions are the native support for &lt;a href=&quot;https:&#x2F;&#x2F;nodejs.org&#x2F;dist&#x2F;latest-v18.x&#x2F;docs&#x2F;api&#x2F;globals.html&quot;&gt;&lt;code&gt;fetch()&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, which you might be familiar with from browser environments, and a &lt;a href=&quot;https:&#x2F;&#x2F;nodejs.org&#x2F;dist&#x2F;latest-v18.x&#x2F;docs&#x2F;api&#x2F;test.html&quot;&gt;built-in test runner&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;What is more difficult, is the lack of the AWS SDK v2, which is included out of the box in the &lt;code&gt;node16.x&lt;&#x2F;code&gt; Lambda runtime, but not in the more recent runtime versions.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;upgrading-to-aws-sdk-v3&quot;&gt;Upgrading to AWS SDK v3&lt;&#x2F;h2&gt;
&lt;p&gt;Ideally, you&#x27;d upgrade to v3 of the SDK, but this might involve rewriting a lot of your code. If you want to go this route, the &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;sdk-for-javascript&#x2F;v3&#x2F;developer-guide&#x2F;migrating-to-v3.html#using-codemod&quot;&gt;codemod&lt;&#x2F;a&gt; is a good starting point.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;including-v2-in-your-bundle&quot;&gt;Including v2 in your bundle&lt;&#x2F;h2&gt;
&lt;p&gt;For those of us not ready to move on to v3 yet, you need to include v2 in your bundle.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;with-serverless&quot;&gt;With serverless&lt;&#x2F;h3&gt;
&lt;p&gt;If you use the Serverless framework, you might need to move the &lt;code&gt;aws-sdk&lt;&#x2F;code&gt; package from &lt;code&gt;devDependencies&lt;&#x2F;code&gt; to &lt;code&gt;dependencies&lt;&#x2F;code&gt; in your &lt;code&gt;package.json&lt;&#x2F;code&gt;. This will tell the framework &lt;a href=&quot;https:&#x2F;&#x2F;www.serverless.com&#x2F;framework&#x2F;docs&#x2F;providers&#x2F;aws&#x2F;guide&#x2F;packaging#development-dependencies&quot;&gt;to not exclude it&lt;&#x2F;a&gt; from the bundle.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;with-serverless-esbuild&quot;&gt;With serverless-esbuild&lt;&#x2F;h3&gt;
&lt;p&gt;The &lt;code&gt;serverless-esbuild&lt;&#x2F;code&gt; plugin by default will exclude the &lt;code&gt;aws-sdk&lt;&#x2F;code&gt; package as well, so you need to specifically pass an empty array to override this behaviour:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;provider&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;   &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;runtime&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;nodejs20.x
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;custom&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;   &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;serverless-esbuild&lt;&#x2F;span&gt;&lt;span&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;exclude&lt;&#x2F;span&gt;&lt;span&gt;: []
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will tell &lt;code&gt;esbuild&lt;&#x2F;code&gt; to bundle the &lt;code&gt;aws-sdk&lt;&#x2F;code&gt;. The AWS SDK v2 is massive though, so this might result in large bundles, or even hang &lt;code&gt;esbuild&lt;&#x2F;code&gt; if it can&#x27;t use enough memory.&lt;&#x2F;p&gt;
&lt;p&gt;This is a problem we&#x27;ve experienced with several of our larger services, and there&#x27;s two good strategies for getting around it:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Manually including the SDK in the package by adding &lt;code&gt;node_modules&#x2F;aws-sdk&#x2F;**&lt;&#x2F;code&gt; into your &lt;a href=&quot;https:&#x2F;&#x2F;www.serverless.com&#x2F;framework&#x2F;docs&#x2F;providers&#x2F;aws&#x2F;guide&#x2F;packaging#development-dependencies&quot;&gt;package patterns&lt;&#x2F;a&gt;. This leads to a relatively large package (multiple MBs), but is pretty quick. You can further optimise this by excluding files in the SDK folder that you don&#x27;t need, like the &lt;code&gt;dist&lt;&#x2F;code&gt; folder or all of the &lt;code&gt;*.d.ts&lt;&#x2F;code&gt; definition files.&lt;&#x2F;li&gt;
&lt;li&gt;If you want to keep your lambda package very small, an alternative solution is including the SDK as a Lambda Layer. You can create this layer by zipping op the SDK (ensure to keep the folder structure, so that it ends up in node_modules).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Showing a Manage Subscriptions Button in React Native</title>
          <pubDate>Fri, 29 Dec 2023 12:46:45 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/showing-a-manage-subscriptions-button-in-react-native/</link>
          <guid>https://schof.co/showing-a-manage-subscriptions-button-in-react-native/</guid>
          <description xml:base="https://schof.co/showing-a-manage-subscriptions-button-in-react-native/">&lt;p&gt;If you implement &lt;a href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;in-app-purchase&#x2F;&quot;&gt;in-app purchases&lt;&#x2F;a&gt; in your app, you might want to show users a way to manage that subscription by showing links to the App Store or Google Play app.&lt;&#x2F;p&gt;
&lt;p&gt;In native Swift, on the iOS side, there is &lt;a href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;storekit&#x2F;&quot;&gt;StoreKit&lt;&#x2F;a&gt; to do this, and Android has similar APIs. If you&#x27;re using &lt;a href=&quot;https:&#x2F;&#x2F;reactnative.dev&quot;&gt;React Native&lt;&#x2F;a&gt;, however, you might prefer to just &lt;a href=&quot;https:&#x2F;&#x2F;reactnative.dev&#x2F;docs&#x2F;linking&quot;&gt;deep link&lt;&#x2F;a&gt; to the right URL.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;code-example&quot;&gt;Code example&lt;&#x2F;h2&gt;
&lt;p&gt;A very simple approach to doing this looks as follows:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;React &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Button&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Linking&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Platform &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;react-native&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;APP_ID &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;com.example.myapp&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;IAP_PRODUCT_ID &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;yearly-subscription&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;ManageSubscriptionButton &lt;&#x2F;span&gt;&lt;span&gt;= () &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;manageSubscriptionUrl &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Platform&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;select&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;		android: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`https:&#x2F;&#x2F;play.google.com&#x2F;store&#x2F;account&#x2F;subscriptions?sku=${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;IAP_PRODUCT_ID&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}&amp;amp;package=${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;APP_ID&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		ios: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;apps.apple.com&#x2F;account&#x2F;subscriptions&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	});
&lt;&#x2F;span&gt;&lt;span&gt;	
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Button
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;title&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Manage subscription&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;			&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;onPress&lt;&#x2F;span&gt;&lt;span&gt;={() =&amp;gt; Linking.openURL(manageSubscriptionUrl)}
&lt;&#x2F;span&gt;&lt;span&gt;		&#x2F;&amp;gt;
&lt;&#x2F;span&gt;&lt;span&gt;	);
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;developer.android.com&#x2F;google&#x2F;play&#x2F;billing&#x2F;subscriptions#use-deep&quot;&gt;Android: Use deep links to allow users to manage a subscription&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;developer.apple.com&#x2F;documentation&#x2F;storekit&#x2F;in-app_purchase&#x2F;original_api_for_in-app_purchase&#x2F;subscriptions_and_offers&#x2F;handling_subscriptions_billing#3221913&quot;&gt;Apple: Enable users to manage subscriptions&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Launching Flexible Feed</title>
          <pubDate>Wed, 20 Dec 2023 16:30:48 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/launching-flexible-feed/</link>
          <guid>https://schof.co/launching-flexible-feed/</guid>
          <description xml:base="https://schof.co/launching-flexible-feed/">&lt;p&gt;I released my first Shopify app a few years ago. It’s called &lt;a href=&quot;https:&#x2F;&#x2F;apps.shopify.com&#x2F;marketplace-fulfillment&quot;&gt;Flexible Fulfillment&lt;&#x2F;a&gt;, and it helps store owners distribute orders to t
hird-party vendors, often used for dropshipping.&lt;&#x2F;p&gt;
&lt;p&gt;Today, I’ve released my second app: &lt;a href=&quot;https:&#x2F;&#x2F;apps.shopify.com&#x2F;inventory-feed&quot;&gt;&lt;strong&gt;Flexible Feed&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt;. It’s a pretty simple app that makes it easier to create product and inventory feeds from your Shopify stock. This allows you to connect NearSt easily, or set up feeds for other platforms.&lt;&#x2F;p&gt;
&lt;p&gt;It’s pretty simple right now, but I look forward to extending it into something more powerful based on feedback from users.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;building-for-shopify&quot;&gt;Building for Shopify&lt;&#x2F;h2&gt;
&lt;p&gt;I really admire the ecosystem that Shopify has created. They are obviously very large as a company, but a lot of the value of their product comes from their App Store, which allows third party developers to build all sorts of new functionality to extend your store with.&lt;&#x2F;p&gt;
&lt;p&gt;They could have gone the route most other platforms choose - expose an API, and allow other developers to build their own, separate apps using some sort of OAuth for login. Instead, Shopify locked this down a bit more: you can’t just sign in with your Shopify account in other apps - those apps need to be approved by Shopify and exist within their App Store. This allows them to keep quality high through a review process reminiscent of Apple’s App Store Review.&lt;&#x2F;p&gt;
&lt;p&gt;In addition, these apps often live within Shopify rather than outside of it: they’re displayed within the Shopify admin panel, and can hook into extending the retailer’s e-commerce website, checkout process or point-of-sale tablet. This creates one coherent experience, with&lt;a href=&quot;https:&#x2F;&#x2F;polaris.shopify.com&quot;&gt; a shared design language&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This has some disadvantages as well (payments need to go through Shopify’s very minimal payments API), but overall it makes building Shopify apps quite fun!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;taking-it-serverless&quot;&gt;Taking it serverless&lt;&#x2F;h2&gt;
&lt;p&gt;If you read anything else I write, you know that I love building on AWS Lambda and DynamoDB. That’s also what I do for these Shopify apps.&lt;&#x2F;p&gt;
&lt;p&gt;It took a while to adapt &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Shopify&#x2F;shopify-app-js&quot;&gt;Shopify’s default app starter template&lt;&#x2F;a&gt; into a version that could easily be deployed on AWS Lambda, but I now have a starter template of my own for this, which allows me to whip up a new app in a day or two.&lt;&#x2F;p&gt;
&lt;p&gt;I plan on releasing this starter template if I find time to clean it up and add documentation, and might even open source Flexible Feed, again, if time allows for doing the ‘hard’ bits to get it up to a standard I’m happy to release publicly.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>A Mental Framework for Debugging</title>
          <pubDate>Mon, 11 Dec 2023 18:47:19 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/a-mental-framework-for-debugging/</link>
          <guid>https://schof.co/a-mental-framework-for-debugging/</guid>
          <description xml:base="https://schof.co/a-mental-framework-for-debugging/">&lt;p&gt;Engineers spend a lot of their time debugging issues. It&#x27;s a skill you acquire over time, and something that each engineer does in a slightly different way - a personal style.&lt;&#x2F;p&gt;
&lt;p&gt;Over time you get better at spotting patterns and finding the tools that help you trace down problems or mistakes. In addition, you&#x27;ll develop a (explicit, or more likely implicit) mental model or process you follow to debug something.&lt;&#x2F;p&gt;
&lt;p&gt;Thinking about my personal process, it breaks down into three parts:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Verifying the symptoms&lt;&#x2F;li&gt;
&lt;li&gt;Shortening the cycle&lt;&#x2F;li&gt;
&lt;li&gt;Revealing assumptions&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Let&#x27;s break these down:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;verifying-the-symptoms&quot;&gt;Verifying the symptoms&lt;&#x2F;h2&gt;
&lt;p&gt;Something I&#x27;ve done way too often is assumed a bug was actually a bug, and not user error or a side effect of something outside the control of my code.&lt;&#x2F;p&gt;
&lt;p&gt;Rather than wasting time trying to get to the bottom - first try to verify issue exists in the form it has been reported.&lt;&#x2F;p&gt;
&lt;p&gt;This might mean trying to reproduce the issue, or simply looking at contextual data, like application or database logs. Tools like Hotjar are worth their weight in gold here for front-end issues, showing you exactly how the user interacted with your app and what they got to see after encountering the bug.&lt;&#x2F;p&gt;
&lt;p&gt;Trying to collect as much as possible of this contextual data will make the rest of the debugging process much easier, and will give you more confidence in your solution.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shortening-the-cycle&quot;&gt;Shortening the cycle&lt;&#x2F;h2&gt;
&lt;p&gt;Once you&#x27;ve verified the issue exists, you want to make it as easy as possible for yourself to do the actual debugging - making changes to the code and verifying the result.&lt;&#x2F;p&gt;
&lt;p&gt;This might sound like an obvious statement, but I feel like this is a step of the process that junior devs will sometimes not spend the appropriate amount of time on.&lt;&#x2F;p&gt;
&lt;p&gt;Spending a little more time setting up your test environment pays back in a big way in saving you time in the actual fixing. Over the years, I&#x27;ve wasted a lot of time by feeling like something was too simple or straightforward to write an automated test for it, ultimately at the end of the process coming back to realising that if I&#x27;d done the from the start, I&#x27;d have saved myself a bunch of time.&lt;&#x2F;p&gt;
&lt;p&gt;The question you want to ask in this step: how can I make it take as little time as possible to see the result of my code changes? And a unit test (if applicable) is almost always faster to run than clicking around on a web page!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;revealing-assumptions&quot;&gt;Revealing assumptions&lt;&#x2F;h2&gt;
&lt;p&gt;This then leads into the more straightforward part of the debugging process: make changes to your code until the expected result is achieved.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s tempting to jump around, but try to follow a linear approach to your problem solving, actually reading each line of the relevant code and asking yourself what assumptions you are making about how the code behaves.&lt;&#x2F;p&gt;
&lt;p&gt;Testing those assumptions comes in a bunch of flavours:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Simple &lt;code&gt;print&lt;&#x2F;code&gt; and &lt;code&gt;console.log&lt;&#x2F;code&gt; statements - they&#x27;re really cheap and fast! My colleagues all have added a shortcut in their IDE where &lt;kbd&gt;Cmd + L&lt;&#x2F;kbd&gt; will insert a &lt;code&gt;console.log({variable})&lt;&#x2F;code&gt; statement.&lt;&#x2F;li&gt;
&lt;li&gt;Debuggers and debugging tools might seem overkill in many situations, but things like React Devtools, and the built in debugger in VSCode are good things to be aware of.&lt;&#x2F;li&gt;
&lt;li&gt;Sometimes it helps to test assumptions in a &#x27;clean&#x27; environment. Apps like CodeRunner, Jupyter Notebooks (which are built into VSCode!) and tools like JSBin or &lt;a href=&quot;https:&#x2F;&#x2F;jsnotebook.dev&quot;&gt;JSNotebook&lt;&#x2F;a&gt; are a good way to quickly validate assumptions on things like the behaviour of built-in functions and language features.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I hope some of these tips were useful, happy bug fixing!&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Building a Serverless Search Engine Using Fuse.js</title>
          <pubDate>Sun, 03 Dec 2023 11:23:53 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/building-a-serverless-search-engine-using-fusejs/</link>
          <guid>https://schof.co/building-a-serverless-search-engine-using-fusejs/</guid>
          <description xml:base="https://schof.co/building-a-serverless-search-engine-using-fusejs/">&lt;p&gt;One of my side projects that gets considerable traffic finally used up all its free &lt;a href=&quot;https:&#x2F;&#x2F;www.algolia.com&quot;&gt;Algolia&lt;&#x2F;a&gt; credits.&lt;&#x2F;p&gt;
&lt;p&gt;I love Algolia because of how easy it is to set up, with great SDKs, both for managing your search index, and its ready-made front-end UI components for React and many other front-end environments.&lt;&#x2F;p&gt;
&lt;p&gt;My use case in this project is really simple though - a data set of about 50,000 items, and only need fuzzy search on a single &#x27;title&#x27; field. It currently gets around 100,000 searches per month. This costs around $100 per month, which isn&#x27;t much, but is relatively much compared to the total hosting costs for this particular project, especially considering it is a secondary functionality.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;serverless-solutions&quot;&gt;Serverless solutions&lt;&#x2F;h2&gt;
&lt;p&gt;In looking for alternatives, I looked at AWS OpenSearch Serverless, which despite the name, still isn&#x27;t a full serverless solution in the same way as Lambda or S3. You pay per unit, where the number of units per hour is dependent on usage. Looking at the costs, it&#x27;s about triple what I&#x27;m currently paying for Algolia. Not an option.&lt;&#x2F;p&gt;
&lt;p&gt;Something that I realised whilst looking at the data set I want to search though, is that it&#x27;s relatively small. I can easily store it in a single file in S3.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;In which case – why not do that, and then use something like &lt;a href=&quot;https:&#x2F;&#x2F;lunrjs.com&quot;&gt;Lunr&lt;&#x2F;a&gt; or &lt;a href=&quot;https:&#x2F;&#x2F;www.fusejs.io&quot;&gt;Fuse&lt;&#x2F;a&gt; to do the actual search?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;To start prototyping this and seeing how fast it was, I first updated the way my indexing logic worked:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;rEfWQjX&quot; alt=&quot;Drawing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Rather than listening to changes in the DynamoDB stream, I now have a cron that will query all the relevant records on a set interval, format them in a way that makes them usable in my front-ends, and store them as a big JSON file in S3.&lt;&#x2F;p&gt;
&lt;p&gt;This query is very fast, taking about 10 seconds to complete, and the resulting file in S3 is about 14 MB. Big, but fast to download for a Lambda running in the same region.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-actual-search&quot;&gt;The actual search&lt;&#x2F;h2&gt;
&lt;p&gt;I chose to go with  &lt;a href=&quot;https:&#x2F;&#x2F;www.fusejs.io&quot;&gt;Fuse.js&lt;&#x2F;a&gt; to implement the fuzzy search. A separate Lambda is used to expose a search API endpoint to my frontends, and it does the following:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Downloads the latest version of the search index from S3 and caches it outside the handler in memory, so that we only need to download once during the &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;lambda&#x2F;latest&#x2F;dg&#x2F;lambda-runtime-environment.html#runtimes-lifecycle&quot;&gt;lifetime of the Lambda container&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Load the data into Fuse, then run a simple search using the query passed through as the &lt;code&gt;query&lt;&#x2F;code&gt; query string parameter from API Gateway.&lt;&#x2F;li&gt;
&lt;li&gt;Do some reshuffling of the results and return a neat little array with the top 10 matching items.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The code for that looks something like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;S3 &lt;&#x2F;span&gt;&lt;span&gt;} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;aws-sdk&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Fuse &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;fuse.js&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;s3 &lt;&#x2F;span&gt;&lt;span&gt;= new S3();
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;initializeSearchIndex &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;() &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Body &lt;&#x2F;span&gt;&lt;span&gt;} = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;s3&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;getObject&lt;&#x2F;span&gt;&lt;span&gt;({
&lt;&#x2F;span&gt;&lt;span&gt;		Bucket: process.env.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;SEARCH_INDEX_BUCKET&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;		Key: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`search-index.json`&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	}).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;promise&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data &lt;&#x2F;span&gt;&lt;span&gt;= JSON.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;parse&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;Body&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;toString&lt;&#x2F;span&gt;&lt;span&gt;());
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span&gt;new Fuse(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;, { keys: [&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;] });
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;searchIndex &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;; &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; allows us to cache between invocations
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;export const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;handler &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;async &lt;&#x2F;span&gt;&lt;span&gt;({ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;queryStringParameters &lt;&#x2F;span&gt;&lt;span&gt;}) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;=&amp;gt; &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span&gt;{ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;query &lt;&#x2F;span&gt;&lt;span&gt;} = &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;queryStringParameters&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;query&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;	&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;throw &lt;&#x2F;span&gt;&lt;span&gt;new Error(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;Search query parameter required.&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;  }
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;if &lt;&#x2F;span&gt;&lt;span&gt;(!&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;searchIndex&lt;&#x2F;span&gt;&lt;span&gt;) {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;searchIndex &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;await &lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;initializeSearchIndex&lt;&#x2F;span&gt;&lt;span&gt;();
&lt;&#x2F;span&gt;&lt;span&gt;  }
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;results &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;searchIndex&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;search&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;query&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;results&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;slice&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;20&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;end-result&quot;&gt;End result&lt;&#x2F;h2&gt;
&lt;p&gt;The end result is a search that is somewhat slower than Algolia - taking around 200ms per request, rather than the 10ms for Algolia, but this difference is barely noticeable on the front-end.&lt;&#x2F;p&gt;
&lt;p&gt;In terms of costs, the whole solution &lt;a href=&quot;https:&#x2F;&#x2F;calculator.aws&#x2F;#&#x2F;estimate?id=8cf5b5f8f614a20533e9dd0b4856b73de85e04aa&quot;&gt;costs about $2.71 per month&lt;&#x2F;a&gt; to run, which I would say is quite a good in terms of cost savings for minimal work.&lt;&#x2F;p&gt;
&lt;p&gt;If you have a small dataset you&#x27;d like to search over and don&#x27;t want to spend money on a &#x27;real&#x27; search engine, I think using serverless tools in this way is a really great solution.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>About Me</title>
          <pubDate>Sun, 03 Dec 2023 00:00:00 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/about/</link>
          <guid>https://schof.co/about/</guid>
          <description xml:base="https://schof.co/about/">&lt;p&gt;I&#x27;m a software engineer and startup founder from The Netherlands, currently living in London. I love creating well-designed user interfaces for real people and crafting tools for developers to build better software.&lt;&#x2F;p&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;WvaOxkvJ9&quot; alt=&quot;Image&quot; &#x2F;&gt;
&lt;p&gt;Looking for something fun to do while waiting for my mom, a primary school teacher, to finish grading her students’ homework, led me to start programming at age 11.&lt;&#x2F;p&gt;
&lt;p&gt;At age 16, I co-founded my first startup &lt;a href=&quot;https:&#x2F;&#x2F;infowijs.nl&#x2F;?utm_source=schof.co&quot;&gt;Scholica&lt;&#x2F;a&gt;, which primarily focussed on created learning apps for K-12 education in The Netherlands, and later became part of &lt;a href=&quot;https:&#x2F;&#x2F;infowijs.nl&#x2F;?utm_source=schof.co&quot;&gt;Infowijs&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;After high school, I moved to London to co-found &lt;a href=&quot;https:&#x2F;&#x2F;near.st&#x2F;?utm_source=schof.co&quot;&gt;NearSt&lt;&#x2F;a&gt;, a company providing modern commerce tools for thousands of physical retailers worldwide.&lt;&#x2F;p&gt;
&lt;p&gt;I’m currently spending my time building &lt;a href=&quot;https:&#x2F;&#x2F;examplary.ai&#x2F;?utm_source=schof.co&quot;&gt;Examplary&lt;&#x2F;a&gt;, in addition to my work with &lt;a href=&quot;https:&#x2F;&#x2F;streetartcities.com&#x2F;?utm_source=schof.co&quot;&gt;Street Art Cities&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;ministryofurbanculture.com&#x2F;?utm_source=schof.co&quot;&gt;Ministry of Urban Culture&lt;&#x2F;a&gt;, as well as writing about tech.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m available for consultancy and speaking engagements for topics like serverless architecture design, product strategy, developing for open source, and more.&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Going back to Obsidian</title>
          <pubDate>Sat, 02 Dec 2023 19:40:26 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/going-back-to-obsidian/</link>
          <guid>https://schof.co/going-back-to-obsidian/</guid>
          <description xml:base="https://schof.co/going-back-to-obsidian/">&lt;p&gt;Back in 2020 and 2021, I was a heavy user of personal knowledge management (PKMS) tools. Initially &lt;a href=&quot;https:&#x2F;&#x2F;roamresearch.com&quot;&gt;Roam Research&lt;&#x2F;a&gt;, where I built up a graph of around 1000 notes, and next Obsidian, when I wanted more control and ownership over my notes.&lt;&#x2F;p&gt;
&lt;p&gt;Last year, at some point, I stopped using Obsidian, in an attempt to go back to a simpler setup for my note taking.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;drafts-where-text-starts&quot;&gt;Drafts: where text starts&lt;&#x2F;h2&gt;
&lt;p&gt;That journey lead me to &lt;a href=&quot;https:&#x2F;&#x2F;getdrafts.com&quot;&gt;Drafts&lt;&#x2F;a&gt;, a tool whose tagline &quot;Where text starts&quot; accurately describes its powerful and unique position in the crowded note-taking app space. Drafts is not the final resting place for your typed words, but just the jumping off point, with complex system of &#x27;actions&#x27; that allow you to take, push and wrangle those notes to go wherever you want them to go, in whatever format.&lt;&#x2F;p&gt;
&lt;p&gt;Drafts has been the tool I&#x27;ve been using to publish blog posts, take meeting notes, and keep a movie watchlist for the last year.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;obsidian-super-powered-markdown-notes&quot;&gt;Obsidian: super-powered markdown notes&lt;&#x2F;h2&gt;
&lt;p&gt;Now I&#x27;m back to &lt;a href=&quot;https:&#x2F;&#x2F;obsidian.md&quot;&gt;Obsidian&lt;&#x2F;a&gt;. If you haven&#x27;t heard of it before - it&#x27;s a massively flexible tool that boils down to a markdown editor, with interlinked notes and plugins that turn it into a knowledge management system.&lt;&#x2F;p&gt;
&lt;p&gt;One of the main things that I love about Obsidian is the &lt;a href=&quot;https:&#x2F;&#x2F;help.obsidian.md&#x2F;Plugins&#x2F;Daily+notes&quot;&gt;Daily notes&lt;&#x2F;a&gt; functionality. It is a nice hook to think about and structure your day, and to build in habits around journaling and keeping track of media consumption.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;iYXsSah&quot; alt=&quot;Image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m now working to find a balance between permanent notes in Obsidian, and using Drafts as an entry point for notes to make it into Obsidian if they aren&#x27;t fully ephemeral. Importantly, also - trying not to get too lost in the fun process of setting up these tools, and actually using them to enhance my thinking. &lt;strong&gt;WIP&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Advent of Code 2023</title>
          <pubDate>Sat, 02 Dec 2023 19:22:28 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/advent-of-code-2023/</link>
          <guid>https://schof.co/advent-of-code-2023/</guid>
          <description xml:base="https://schof.co/advent-of-code-2023/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;adventofcode.com&#x2F;&quot;&gt;Advent of Code&lt;&#x2F;a&gt; has started again!&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s always a question around what language or tool you use to complete the challenges. This year, I decided to keep it simple, and keep it to what I know best: Javascript.&lt;&#x2F;p&gt;
&lt;p&gt;I did want to try a new way of running Javascript though, something I&#x27;ve thought about before but never had a chance to try: Jupyter Notebooks.&lt;&#x2F;p&gt;
&lt;p&gt;If you&#x27;re not familiar with &lt;a href=&quot;https:&#x2F;&#x2F;jupyter.org&quot;&gt;Jupyter&lt;&#x2F;a&gt;, it&#x27;s an interactive notebook platform, mostly used in the Python and data science community to quickly evaluate code on data and see instant results.&lt;&#x2F;p&gt;
&lt;p&gt;It has a really nice &lt;a href=&quot;https:&#x2F;&#x2F;code.visualstudio.com&#x2F;docs&#x2F;datascience&#x2F;jupyter-notebooks&quot;&gt;built-in Visual Studio Code interface&lt;&#x2F;a&gt;, which Microsoft ships pre-installed with their build of VSCode.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;running-javascript-in-jupyter&quot;&gt;Running Javascript in Jupyter&lt;&#x2F;h2&gt;
&lt;p&gt;Jupyter consists of two parts - the UI, and the underlying &#x27;kernel&#x27; that runs your code. Usually this is a Python environment, but you can install and build your own kernels to be available to Jupyter. One of those is &lt;a href=&quot;https:&#x2F;&#x2F;n-riesco.github.io&#x2F;ijavascript&#x2F;index.html&quot;&gt;IJavascript&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Installing it is pretty simple. These are the commands I ran on my Mac:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sh&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-sh &quot;&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;brew&lt;&#x2F;span&gt;&lt;span&gt; install jupyter
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;npm&lt;&#x2F;span&gt;&lt;span&gt; install&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt; -g&lt;&#x2F;span&gt;&lt;span&gt; ijavascript
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;ijsinstall
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;After running those, I was able to run &#x27;Create: New Jupyter Notebook&#x27; in VSCode, and able to select &#x27;javascript&#x27; as the kernel in the the notebook.&lt;&#x2F;p&gt;
&lt;p&gt;There we go, ready to save some elves:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;mirri.link&#x2F;0nDG3tf&quot; alt=&quot;Image&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s my repo with my solutions so far: 🔖 &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tschoffelen&#x2F;aoc-2023&quot;&gt;tschoffelen&#x2F;aoc-2023&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>The iPad as a Platform</title>
          <pubDate>Mon, 27 Nov 2023 19:56:48 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/the-ipad-as-a-platform/</link>
          <guid>https://schof.co/the-ipad-as-a-platform/</guid>
          <description xml:base="https://schof.co/the-ipad-as-a-platform/">&lt;p&gt;I&#x27;ve used an iPad since the very first year it was introduced. Me and my co-founder Dante managed to convince our school&#x27;s headmaster that our class should really get iPads to use the e-learning platform we had built, and weirdly the school agreed.&lt;&#x2F;p&gt;
&lt;p&gt;I didn&#x27;t have an iPad for a while after high school, until I bought one a little over a year ago in an attempt to stop bringing my MacBook home with me from work and still have a screen to watch hours of YouTube videos on at home.&lt;&#x2F;p&gt;
&lt;p&gt;Relatively quickly, I caved and bought a Magic Keyboard folio for it, which turns the iPad into a full PC with a keyboard and trackpad. It&#x27;s impressive how much this levels up the experience to make it a very pleasant blogging device.&lt;&#x2F;p&gt;
&lt;p&gt;Then, with the introduction of Stage Manager, it became a full platform. I currently have it hooked up to a monitor that is showing my &lt;a href=&quot;https:&#x2F;&#x2F;obsidian.md&quot;&gt;Obsidian&lt;&#x2F;a&gt; vault, a Safari window, and a YouTube video, whilst writing this post in Drafts. Unbelievable that you can do this on a device that&#x27;s about half a centimetre thick.&lt;&#x2F;p&gt;
&lt;p&gt;And whelp, there goes my good intention of spending less time behind a computer by not bringing my MacBook home from work 😁&lt;&#x2F;p&gt;
&lt;style&gt;a[href=&quot;#internal-link&quot;] { color: #9b9b9b; text-decoration: none !important; }&lt;&#x2F;style&gt;</description>
      </item>
      <item>
          <title>Comparing Cypress and Playwright</title>
          <pubDate>Sun, 22 Oct 2023 16:50:47 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/comparing-cypress-and-playwright/</link>
          <guid>https://schof.co/comparing-cypress-and-playwright/</guid>
          <description xml:base="https://schof.co/comparing-cypress-and-playwright/">&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.cypress.io&quot;&gt;Cypress&lt;&#x2F;a&gt; and &lt;a href=&quot;https:&#x2F;&#x2F;playwright.dev&quot;&gt;Playwright&lt;&#x2F;a&gt; are both end-to-end testing frameworks, usually used to simulate user behaviour in front-end applications.&lt;&#x2F;p&gt;
&lt;p&gt;Both are similarly popular, with around 50k stars on GitHub. Cypress is a bit older than Playwright, which is relatively new, but overall they&#x27;re largely similar. Each has its own strengths though:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cypress&quot;&gt;Cypress&lt;&#x2F;h2&gt;
&lt;p&gt;✅ &lt;strong&gt;Same API and framework for end-to-end and component tests.&lt;&#x2F;strong&gt; By combining end-to-end testing and component testing in the same framework, you get the advantage of being able to use the same APIs (multiple APIs for selecting and clicking is horrible for developer efficiency!) and the same development and debugging tools.&lt;&#x2F;p&gt;
&lt;p&gt;✅ &lt;strong&gt;Polished UI for test debugging.&lt;&#x2F;strong&gt; The Cypress UI for debugging tests feels a lot more polished than the Playwright built-in UI, with live reloading, a singular timeline of events, and dashboard pages that help you set up your project. The same UI is also available as a paid cloud service that lets you watch back recordings of your CI runs.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;playwright&quot;&gt;Playwright&lt;&#x2F;h2&gt;
&lt;p&gt;✅ &lt;strong&gt;Multi-browser support.&lt;&#x2F;strong&gt; Out of the box, Playwright will run your tests in multiple browsers (the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;playwright&#x2F;blob&#x2F;main&#x2F;examples&#x2F;todomvc&#x2F;playwright.config.ts#L50&quot;&gt;default config file&lt;&#x2F;a&gt; will run your app in Firefox, Safari and Chromium). That makes it very easy to feel confident you can catch errors if your app doesn&#x27;t behave the same across the major browsers.&lt;&#x2F;p&gt;
&lt;p&gt;✅ &lt;strong&gt;Code generator.&lt;&#x2F;strong&gt; With the &lt;a href=&quot;https:&#x2F;&#x2F;playwright.dev&#x2F;docs&#x2F;codegen&quot;&gt;Playwright VS Code Extension&lt;&#x2F;a&gt;, you can generate a starting point for your test code simply by navigating to your app in the browser and clicking around. This will create code for your test, which you can then polish and finish, massively reducing the time to get a first green checkmark.&lt;&#x2F;p&gt;
&lt;p&gt;✅ &lt;strong&gt;Integrated dev server control.&lt;&#x2F;strong&gt; Whereas with Cypress you need to ensure yourself that your app is running before starting your test (my preferred way of doing so is with the aptly named &lt;a href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;package&#x2F;start-server-and-test&quot;&gt;&lt;code&gt;start-server-and-test&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; package), Playwright has a built-in way to start a local dev server using their &lt;a href=&quot;https:&#x2F;&#x2F;playwright.dev&#x2F;docs&#x2F;test-webserver&quot;&gt;&lt;code&gt;webserver&lt;&#x2F;code&gt; config option&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;Either framework works fine for building end-to-end tests, and with a relatively large community, they&#x27;re both easy to use and find information about.&lt;&#x2F;p&gt;
&lt;p&gt;I tend to use Cypress for most work projects because it feels robust and I like the experience of debugging using their UI, but I&#x27;ll usually choose Playwright for small side projects, because it&#x27;s slightly faster to get started with and feels more lightweight.&lt;&#x2F;p&gt;
&lt;p&gt;Either way, if you&#x27;re doing end-to-end testing, you&#x27;re on the right track!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Hybrid Encryption in Node.js</title>
          <pubDate>Sat, 21 Oct 2023 18:59:10 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/hybrid-encryption-in-nodejs/</link>
          <guid>https://schof.co/hybrid-encryption-in-nodejs/</guid>
          <description xml:base="https://schof.co/hybrid-encryption-in-nodejs/">&lt;p&gt;I don&#x27;t get much exposure to encryption in my day-to-day engineering work, but learned recently how to implement what is called &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Hybrid_cryptosystem&quot;&gt;hybrid encryption&lt;&#x2F;a&gt; in Node.js, and wanted to jot down my learnings.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;asymmetric-encryption&quot;&gt;Asymmetric encryption&lt;&#x2F;h2&gt;
&lt;p&gt;Asymmetric encryption, where you have a &lt;em&gt;public key&lt;&#x2F;em&gt; used to encrypt data, and a separate, &lt;em&gt;private key&lt;&#x2F;em&gt; to decrypt the data, has a lot of really great advantages. It doesn&#x27;t require you to share the decryption key with the other party, whilst the public key on its own can be freely shared.&lt;&#x2F;p&gt;
&lt;p&gt;One of the easy ways of doing this in Node.js is by using RSA:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;crypto&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; encrypy
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Hello, world!&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;encrypted &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;publicEncrypt&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;publicKey&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;toString&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;hex&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; decrypt
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decrypted &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;privateDecrypt&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;privateKey&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Buffer&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;encrypted&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;#39;hex&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;))
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;However, this method has a big drawback: you can only encrypt as much data as the modulus length of your key allows, which usually is quite small (256 - 1024 chars).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;symmetric-encryption&quot;&gt;Symmetric encryption&lt;&#x2F;h2&gt;
&lt;p&gt;Symmetric encryption algorithms don&#x27;t have this limitation, by implementing all sorts of &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Advanced_Encryption_Standard&quot;&gt;mixing and shifting&lt;&#x2F;a&gt; operations to ensure there are less repeated blocks of bits as the amount of encrypted data increases.&lt;&#x2F;p&gt;
&lt;p&gt;They - as the name implies - operate with a single key, however.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;import &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto &lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;crypto&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; create a key and an &amp;#39;initialisation vector&amp;#39;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;randomBytes&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;32&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iv &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;randomBytes&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;16&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; encrypt
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Hello, world!&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cipher &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;createCipheriv&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;aes-256-cbc&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iv&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;encrypted &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cipher&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;utf8&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;encrypted &lt;&#x2F;span&gt;&lt;span&gt;+= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cipher&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;final&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; decrypt
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decipher &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;createDecipheriv&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;aes-256-cbc&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;kk&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iv2&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decrypted &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decipher&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;encrypted&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;utf8&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decrypted &lt;&#x2F;span&gt;&lt;span&gt;+= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decipher&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;final&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;utf8&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;hybrid-encryption&quot;&gt;Hybrid encryption&lt;&#x2F;h2&gt;
&lt;p&gt;To get the best of both worlds, we can combine these methods:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Create a random key for our AES&#x2F;symmetric encryption&lt;&#x2F;li&gt;
&lt;li&gt;Encrypt this key using RSA&#x2F;assymetric encryption with the public key&lt;&#x2F;li&gt;
&lt;li&gt;Send the RSA encrypted key along with our AES encrypted data to the third party&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The third party can then use their RSA private key to decrypt the AES key and use it to decrypt the actual data.&lt;&#x2F;p&gt;
&lt;p&gt;In practice, that looks something like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;js&quot; style=&quot;background-color:#282c34;color:#abb2bf;&quot; class=&quot;language-js &quot;&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;body &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;Lorem ipsum dolor sit amet...&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; encrypt (given a publicKey):
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;randomBytes&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;32&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iv &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;randomBytes&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#d19a66;&quot;&gt;16&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cipher &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;createCipheriv&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;aes-256-cbc&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iv&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;encrypted &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cipher&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;body&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;utf8&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;encrypted &lt;&#x2F;span&gt;&lt;span&gt;+= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;cipher&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;final&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data &lt;&#x2F;span&gt;&lt;span&gt;= {
&lt;&#x2F;span&gt;&lt;span&gt;  d: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;encrypted&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;  k: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;publicEncrypt&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;publicKey&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;toString&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;),
&lt;&#x2F;span&gt;&lt;span&gt;  i: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iv&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;toString&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;),
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;console&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;log&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`Encrypted: ${JSON.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;stringify&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;)}`&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6370;&quot;&gt;&#x2F;&#x2F; decrypt (given a privateKey):
&lt;&#x2F;span&gt;&lt;span&gt; 
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decryptedKey &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;privateDecrypt&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;privateKey&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Buffer&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;k&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;));
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iv2 &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;Buffer&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;i&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;const &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decipher &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;crypto&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;createDecipheriv&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;aes-256-cbc&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decryptedKey&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;iv2&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#c678dd;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decrypted &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decipher&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;data&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;d&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;hex&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;utf8&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decrypted &lt;&#x2F;span&gt;&lt;span&gt;+= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decipher&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#61afef;&quot;&gt;final&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;&amp;quot;utf8&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e5c07b;&quot;&gt;console&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#56b6c2;&quot;&gt;log&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;`Decrypted: ${&lt;&#x2F;span&gt;&lt;span style=&quot;color:#e06c75;&quot;&gt;decrypted&lt;&#x2F;span&gt;&lt;span style=&quot;color:#98c379;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now go forth and lock up that data! 😁&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Finding a Direct Link to a Google Business Profile</title>
          <pubDate>Fri, 06 Oct 2023 17:13:34 +0000</pubDate>
          <author>Thomas Schoffelen</author>
          <link>https://schof.co/finding-a-direct-link-to-a-google-business-profile/</link>
          <guid>https://schof.co/finding-a-direct-link-to-a-google-business-profile/</guid>
          <description xml:base="https://schof.co/finding-a-direct-link-to-a-google-business-profile/">&lt;p&gt;The other day, I was trying to find a way to link directly to a business listing in Google. There&#x27;s a lot of information available about this online, but it&#x27;s all quite disjointed. Here&#x27;s an overview of what I found.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;local-reviews&quot;&gt;Local reviews&lt;&#x2F;h2&gt;
&lt;p&gt;Google really wants to help business owners get more reviews on Google, so they share a few different links to encourage this:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;Write a review&quot; URL&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;Format: &lt;code&gt;https:&#x2F;&#x2F;search.google.com&#x2F;local&#x2F;writereview?placeid={googlePlaceId}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Requires the Google Place ID to be known&lt;&#x2F;li&gt;
&lt;li&gt;Will ask the user to sign in if they aren&#x27;t already&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;search.google.com&#x2F;local&#x2F;writereview?placeid=ChIJN1t_tDeuEmsRUsoyG83frY4&quot;&gt;Example&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Local reviews&quot; URL&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;Format: &lt;code&gt;https:&#x2F;&#x2F;search.google.com&#x2F;local&#x2F;reviews?placeid={googlePlaceId}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Requires the Google Place ID to be known&lt;&#x2F;li&gt;
&lt;li&gt;Does not require login, but brings up a separate popup&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;search.google.com&#x2F;local&#x2F;reviews?placeid=ChIJN1t_tDeuEmsRUsoyG83frY4&quot;&gt;Example&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;urls-using-cid&quot;&gt;URLs using CID&lt;&#x2F;h2&gt;
&lt;p&gt;Opening up a location directly in Google Maps and Google Search is possible if you know the &quot;CID&quot; (also known as &quot;ludocid&quot;) of the location. You can get this through the &lt;code&gt;url&lt;&#x2F;code&gt; field in the Google Places details API endpoint.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Maps CID URL&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;Format: &lt;code&gt;https:&#x2F;&#x2F;maps.google.com&#x2F;?cid={cid}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Requires the CID to be known&lt;&#x2F;li&gt;
&lt;li&gt;Will open in the Maps app on mobile if installed&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;maps.google.com&#x2F;?cid=10281119596374313554&quot;&gt;Example&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Google Search CID URL&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;Format: &lt;code&gt;https:&#x2F;&#x2F;local.google.com&#x2F;place?id={cid}&amp;amp;use=srp&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Requires the CID to be known&lt;&#x2F;li&gt;
&lt;li&gt;Only URL that opens the location in Google Search without popups&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;local.google.com&#x2F;place?id=10281119596374313554&amp;amp;use=srp&quot;&gt;Example&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;helpful-tools&quot;&gt;Helpful tools&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.brightlocal.com&#x2F;free-local-seo-tools&#x2F;google-id-and-review-link-generator&quot;&gt;Brightlocal Review Link Generator&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</description>
      </item>
    </channel>
</rss>
