<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Bryce Dorn</title>
    <description>The latest articles on Forem by Bryce Dorn (@bryce).</description>
    <link>https://forem.com/bryce</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F92818%2F2752117b-9960-4228-b41b-182db3dca947.jpg</url>
      <title>Forem: Bryce Dorn</title>
      <link>https://forem.com/bryce</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/bryce"/>
    <language>en</language>
    <item>
      <title>Mini site for recommending songs using Svelte &amp; Deno</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Sat, 03 Feb 2024 16:31:29 +0000</pubDate>
      <link>https://forem.com/bryce/mini-site-for-recommending-songs-using-svelte-deno-1aoh</link>
      <guid>https://forem.com/bryce/mini-site-for-recommending-songs-using-svelte-deno-1aoh</guid>
      <description>&lt;p&gt;Introducing a new way to recommend a song to your friends: &lt;a href="https://listn.fyi/"&gt;listn.fyi&lt;/a&gt;! Simply create a public Spotify playlist and name it &lt;strong&gt;"listn.fyi"&lt;/strong&gt; (without quotes) then head to the URL for your Spotify username (e.g. &lt;a href="https://listn.fyi/combatfetus"&gt;listn.fyi/combatfetus&lt;/a&gt;). Et voilà! The link is shareable and any changes to your playlist will reflect immediately on the website.&lt;/p&gt;

&lt;p&gt;I figure this could be a fun and easy way to share what you're currently into and see what others are recommending as well. Maybe a cheeky addition to a Twitter bio, etc.&lt;/p&gt;

&lt;p&gt;Behind the scenes is a simple &lt;a href="https://kit.svelte.dev/"&gt;Sveltekit&lt;/a&gt;-powered server function to fetch a Spotify client token then find a user's recommendation playlist and its track information. A &lt;a href="https://deno.com/"&gt;Deno&lt;/a&gt; edge function to performs this data fetch and renders server-side Svelte.&lt;/p&gt;

&lt;p&gt;And shoutout to Rob for the beautiful &lt;a href="https://codepen.io/robrehrig/pen/AooLxK"&gt;CSS record player&lt;/a&gt;, which I borrowed (with credits) and made some modifications to. Long live skeuomorphism!&lt;/p&gt;

&lt;p&gt;I must admit, the Sveltekit developer experience in 2024 is a lot nicer to work with than what I'm used to in the React/Vue/Next world. The filesystem-based router seemed like it would be a pain but was pleasantly surprised by how simple and clean it is to actually use. Looking forward to using it more for projects like this.&lt;/p&gt;

&lt;p&gt;Check out the source below &amp;amp; let me know what you think!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/brycedorn"&gt;
        brycedorn
      &lt;/a&gt; / &lt;a href="https://github.com/brycedorn/listn.fyi"&gt;
        listn.fyi
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Recommend a song to your friends via a playlist! Try it out: https://listn.fyi/combatfetus
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>svelte</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building APOD color search part III: Deno Search API 🔎</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Fri, 21 Apr 2023 16:27:29 +0000</pubDate>
      <link>https://forem.com/bryce/building-apod-color-search-part-iii-deno-search-api-3464</link>
      <guid>https://forem.com/bryce/building-apod-color-search-part-iii-deno-search-api-3464</guid>
      <description>&lt;p&gt;If you're following along, this is the third part in a series about how I built &lt;a href="https://bryce.io/apod-color-search/"&gt;APOD color search&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple search API 🔎
&lt;/h2&gt;

&lt;p&gt;This is the layer that powers a &lt;code&gt;/search&lt;/code&gt; endpoint to check the cache for results from previous searches, and if there are none then query the prepopulated database and store in cache. It's written in &lt;a href="https://deno.land/"&gt;Deno&lt;/a&gt; and continuously deployed using &lt;a href="https://deno.com/deploy"&gt;Deno Deploy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Deno? 🦕
&lt;/h2&gt;

&lt;p&gt;I like Deno for a variety of reasons, but first and foremost: it's &lt;em&gt;fast&lt;/em&gt;. Native TS support is also nice, and Deno Deploy is a quick (and free) hosting service. Overall, it feels great to work with and is perfect for smaller projects.&lt;/p&gt;

&lt;p&gt;Although it's somewhat overkill for the project I decided to use &lt;a href="https://fresh.deno.dev/"&gt;fresh&lt;/a&gt; as it provides some basic routing and scaffolding. There's a helpful &lt;a href="https://deno.land/x/supabase@1.3.1"&gt;supabase Deno package&lt;/a&gt; as well that I used to query the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding color matches 🎨
&lt;/h2&gt;

&lt;p&gt;To simplify the &lt;code&gt;/search&lt;/code&gt; endpoint, the query is included in the URL as a route parameter. This is practical because the only relevant query is a six character string, being a hex color (e.g. &lt;code&gt;/search/:hex&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;In fresh, a dynamic route like this is created using the filename &lt;code&gt;[param].ts&lt;/code&gt; under the directory that the route expects, and in a request the value will be added to context with this name. So in my case, this means a directory named &lt;code&gt;search&lt;/code&gt; containing a file named &lt;code&gt;[hex].ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now let's add a simple handler to get the &lt;code&gt;hex&lt;/code&gt; param from context and return an empty response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;_req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HandlerContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;...&lt;/span&gt;

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

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great, now to return results generated via the &lt;a href="https://dev.to/bryce/building-apod-color-search-part-i-image-analysis-in-rust-24a5"&gt;image analysis from before&lt;/a&gt;! (The data structures are described there and will make this part more clear.) &lt;/p&gt;

&lt;p&gt;First, we'll need a connection to the database. Assuming these credentials are stored in an &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://deno.land/x/dotenv@v3.2.0/load.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://esm.sh/@supabase/supabase-js@1.35.4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SUPABASE_PUBLIC_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, once we convert the hex value back to RGB we can then perform a query stringing multiple filters on the &lt;code&gt;colors&lt;/code&gt; table together to get colors within a certain threshold of the input color:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hexToRgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;colors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;r&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;r&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will give us a (long) list of colors, but this enables fetching a list of &lt;code&gt;clusters&lt;/code&gt; from these colors to be sorted by raw distance to the original search color by comparing red, green and blue color values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clusters&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clusters&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;color_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;minimizeDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source for the &lt;code&gt;minimizeDistance&lt;/code&gt; function above can be found in &lt;a href="https://github.com/brycedorn/apod-color-search/blob/main/api/routes/utils.ts"&gt;utils.ts&lt;/a&gt;. Finally, once the most relevant clusters are found we can return the actual APODs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;days&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;day_id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;day_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Success! This API is deployed &lt;a href="https://acs-api.deno.dev/search/ff00dd"&gt;here&lt;/a&gt; if you want to play with it. This is the project on Deno Deploy and is used by the deployed FE.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching results 🗂️
&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, there's also a caching layer to prevent unnecessary/repeated requests from forcing expensive database queries. I added simple calls to &lt;a href="https://deno.land/x/redis@v0.29.2"&gt;redis&lt;/a&gt; before each hex and &lt;code&gt;color&lt;/code&gt;/&lt;code&gt;cluster&lt;/code&gt; query, using &lt;a href="https://redis.com/redis-enterprise-cloud/overview/"&gt;Redis Enterprise Cloud&lt;/a&gt;'s free tier.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>deno</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Generate placeholder images at edge with thumbhash</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Thu, 13 Apr 2023 18:40:51 +0000</pubDate>
      <link>https://forem.com/bryce/generate-thumbhash-at-edge-for-tiny-progressive-images-282h</link>
      <guid>https://forem.com/bryce/generate-thumbhash-at-edge-for-tiny-progressive-images-282h</guid>
      <description>&lt;p&gt;Continuing my journey to build a performant edge-cached blog! It's built on top of &lt;a href="https://workers.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt;, here was my last update on it:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/bryce" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F92818%2F2752117b-9960-4228-b41b-182db3dca947.jpg" alt="bryce"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/bryce/using-workers-kv-to-build-an-edge-cached-blog-23fo" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Using Workers KV to build an edge cached blog 🌍&lt;/h2&gt;
      &lt;h3&gt;Bryce Dorn ・ Jun 3 '22&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#serverless&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#showdev&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;It's been a fun sandbox to play with edge functions, and after coming across an interesting algorithm for minimizing images I was inspired to augment this project with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pageload bottleneck 🌁
&lt;/h2&gt;

&lt;p&gt;While using KV enables globally distributed data and shorter request latency, there's still one aspect of page performance that's a major bottleneck: loading images. &lt;/p&gt;

&lt;p&gt;This tends to have the &lt;a href="https://web.dev/learn/images/performance-issues/" rel="noopener noreferrer"&gt;largest impact on performance&lt;/a&gt; regardless of server location, and in my case even though the data associated with each post is stored at the edge the image behind each URL must still be downloaded by each client.&lt;/p&gt;

&lt;p&gt;I'm not looking to solve distributed image hosting (yet) so in the meantime, I built a simple image processing solution using &lt;a href="https://github.com/evanw/thumbhash" rel="noopener noreferrer"&gt;thumbhash&lt;/a&gt; to store a tiny, base64-encoded version of the image to display while the full image is downloaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter: thumbhash 👍
&lt;/h2&gt;

&lt;p&gt;I came across this &lt;a href="https://news.ycombinator.com/item?id=35265752" rel="noopener noreferrer"&gt;HN post&lt;/a&gt; from last month that caught my interest with some impressive performance stats. Here is the repository on GitHub:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/evanw" rel="noopener noreferrer"&gt;
        evanw
      &lt;/a&gt; / &lt;a href="https://github.com/evanw/thumbhash" rel="noopener noreferrer"&gt;
        thumbhash
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A very compact representation of an image placeholder
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;While it's similar to &lt;a href="https://github.com/woltapp/blurhash" rel="noopener noreferrer"&gt;BlurHash&lt;/a&gt;, the color performance is much better for the same filesize. Here's a a demonstration of this from the &lt;a href="https://evanw.github.io/thumbhash/" rel="noopener noreferrer"&gt;demo page&lt;/a&gt; (with ThumbHash in the middle and BlurHash on the right):&lt;/p&gt;

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

&lt;p&gt;Most importantly though, the blurred images are &lt;em&gt;tiny&lt;/em&gt; (&amp;lt;1% original image size!). This makes it practical to consider storing them as encoded strings, which I intend to do. The project features an implementation in JS &amp;amp; I was confident it would be a great candidate for adding placeholder images to the site!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The implementation details below are specific to my project but the principles can be applied to anywhere there's server-side JS.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating thumbhash as part of edge caching process 🎞️
&lt;/h2&gt;

&lt;p&gt;Building on the KV caching approach from earlier, a base64 representation of the cover image can be generated and included with post data. As stated before I don't want to worry about hosting images so storing the encoded image as a string avoids this.&lt;/p&gt;

&lt;p&gt;Image manipulation is easy to do with client-side JS and &lt;code&gt;canvas&lt;/code&gt;, but as this is on the server some additional packages make it possible. I used &lt;a href="https://www.npmjs.com/package/jpeg-js" rel="noopener noreferrer"&gt;jpeg-js&lt;/a&gt; to decode the image:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;decode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jpeg-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pica&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pica&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arrayBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arrayBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;useTArray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then &lt;a href="https://www.npmjs.com/package/pica" rel="noopener noreferrer"&gt;pica&lt;/a&gt; to resize it and a simple &lt;a href="https://github.com/brycedorn/blog/blob/master/src/utils.ts#L125" rel="noopener noreferrer"&gt;function&lt;/a&gt; I wrote to crop it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pica&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;resizeBuffer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;toWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;toHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cropped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cropMid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then all that's needed is to generate the thumbhash from this &lt;code&gt;Uint8Array&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;thumbHashToDataURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rgbaToThumbHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thumbhash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thumbhash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rgbaToThumbHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cropped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;thumbHashToDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;thumbhash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Nice! So now that this tiny blurred version of the image is on-hand the last step for a good user experience is to display it initially then replace it with the actual image once it's loaded in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading the full-sized image 😴
&lt;/h2&gt;

&lt;p&gt;Normally I would write some client-side JS to do this but as this project is solely server-rendered I opted to use a simple tried-and-true library for this: &lt;a href="https://github.com/aFarkas/lazysizes" rel="noopener noreferrer"&gt;lazysizes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is as simple as adding a &lt;code&gt;data-src&lt;/code&gt; attribute and &lt;code&gt;lazyload&lt;/code&gt; class, with the original &lt;code&gt;src&lt;/code&gt; set to the base64 thumbhash generated earlier:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; 
  &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazyload blur-up"&lt;/span&gt; 
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumbhash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
  &lt;span class="na"&gt;data-src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cover_image&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then after including the script tag in the renderer it Just Works™️! I followed the steps to add the &lt;a href="https://github.com/aFarkas/lazysizes#lqipblurry-image-placeholderblur-up-image-technique" rel="noopener noreferrer"&gt;blur/unblur effect&lt;/a&gt; as well, as you can see from the &lt;code&gt;blur-up&lt;/code&gt; class.&lt;/p&gt;




&lt;p&gt;This is now active on &lt;a href="https://blog.bryce.io/" rel="noopener noreferrer"&gt;blog.bryce.io&lt;/a&gt;, go check it out! And if your internet is too fast to notice it, try throttling to 'Slow 3G' via dev tools. Thanks for reading &amp;amp; stay tuned for the next thing I do with this pet project 😇&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Building APOD color search part II: Computing in cloud ☁️</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Sun, 12 Mar 2023 12:25:58 +0000</pubDate>
      <link>https://forem.com/bryce/building-apod-color-search-part-ii-computing-in-cloud-1437</link>
      <guid>https://forem.com/bryce/building-apod-color-search-part-ii-computing-in-cloud-1437</guid>
      <description>&lt;p&gt;If you're following along, this is the second part in a series about how I built &lt;a href="https://bryce.io/apod-color-search/" rel="noopener noreferrer"&gt;APOD color search&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this post I'll cover how everything was built with the intent to run via a hosted service instead of a local environment/database. Making things cloud-first puts more effort up front but saves a tremendous amount of time when deploying and running remotely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything via GitHub Actions 🛰️
&lt;/h2&gt;

&lt;p&gt;Since processing images for this project involves iterating over a large dataset, it was clear early on that the amount of computation was going to be immense. This would require processing things over a long period of time and ensuring consistency when there are any errors.&lt;/p&gt;

&lt;p&gt;I already had some experience with &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; (their native CI/CD framework) and after finding &lt;a href="https://github.com/dtolnay/rust-toolchain" rel="noopener noreferrer"&gt;rust-toolchain&lt;/a&gt; I wanted to experiment with whether it'd be feasible to use Actions as a compute service.&lt;/p&gt;

&lt;p&gt;Turns out: works great! ✨ The hardware is more than sufficient to do CPU-bound image processing, and the only issue that I encountered related to builds stopping due to hitting my account limit of &lt;a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions" rel="noopener noreferrer"&gt;50 compute hours/month&lt;/a&gt;. But with the Actions UI I was able to trigger workflows and process batches of APODs one year at a time over the course of a few months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rust in action(s) ⚡️
&lt;/h2&gt;

&lt;p&gt;The Rust-based utility to process images from the &lt;a href="https://blog.bryce.io/building-apod-color-search-part-i-image-analysis-in-rust" rel="noopener noreferrer"&gt;previous article&lt;/a&gt; can be invoked via CLI. To be able to do this in an Action, the environment must have the right dependencies installed and appropriate environment variables.&lt;/p&gt;

&lt;p&gt;A safe way to handle environment variables is via GitHub's &lt;a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets" rel="noopener noreferrer"&gt;encrypted secrets&lt;/a&gt; - it requires manually copying values from a local &lt;code&gt;.env&lt;/code&gt; file used for development but it's better than storing unencrypted values in your repository. For this project, the Rust script fetches from apod-api and saves to a supabase Postgres instance, hence the keys for &lt;code&gt;SUPABASE_*&lt;/code&gt; and &lt;code&gt;APOD_API_*&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;For the environment to have Rust installed, including the &lt;code&gt;dtolnay/rust-toolchain@stable&lt;/code&gt; toolchain will ensure it can be used in the action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Process images for month&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;SUPABASE_REST_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_REST_URL }}&lt;/span&gt;
  &lt;span class="na"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_URL }}&lt;/span&gt;
  &lt;span class="na"&gt;SUPABASE_PUBLIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SUPABASE_PUBLIC_API_KEY }}&lt;/span&gt;
  &lt;span class="na"&gt;APOD_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APOD_API_URL }}&lt;/span&gt;
  &lt;span class="na"&gt;APOD_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APOD_API_KEY }}&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;process-images-for-month&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout 🛎️&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Rust 🦀&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtolnay/rust-toolchain@stable&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rustfmt, clippy&lt;/span&gt;
          &lt;span class="na"&gt;toolchain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Dispatching via workflow UI 🛎️
&lt;/h2&gt;

&lt;p&gt;The only remaining piece is to have a way to trigger the workflow to process images. The &lt;code&gt;on&lt;/code&gt; field is the &lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows" rel="noopener noreferrer"&gt;entrypoint&lt;/a&gt; for when a workflow runs, and thankfully there's a way to trigger directly from GitHub's UI.&lt;/p&gt;

&lt;p&gt;Adding an &lt;code&gt;on: workflow_dispatch&lt;/code&gt; will provide an entrypoint to &lt;a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch" rel="noopener noreferrer"&gt;run the workflow via the Action UI&lt;/a&gt; with given inputs. These arguments can then be referenced via &lt;code&gt;github.event.inputs&lt;/code&gt; in the &lt;code&gt;run&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Year'&lt;/span&gt;     
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Month'&lt;/span&gt;     
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="s"&gt;- name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run 🤖&lt;/span&gt;
      &lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo run -- ${{ github.event.inputs.year }} ${{ github.event.inputs.month }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that is pushed to the main branch for the repository, the option to dispatch should be available. Simply navigate to the  workflow under the 'Actions' tab:&lt;/p&gt;

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

&lt;p&gt;And on the right side of the table of workflow runs will be a 'Run workflow' option (will inform you that this is because the "workflow has a &lt;code&gt;workflow_dispatch&lt;/code&gt; event trigger"):&lt;/p&gt;

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

&lt;p&gt;Assuming all secrets are in order, triggering a run will kick of a build to process the images! &lt;/p&gt;

&lt;p&gt;Lastly, I added a couple additional jobs to avoid formatting errors or regressions when the workflow runs with new commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;...&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="s"&gt;...&lt;/span&gt;

      &lt;span class="s"&gt;- name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint 🧹&lt;/span&gt;
        &lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cargo fmt --all -- --check&lt;/span&gt;
          &lt;span class="s"&gt;cargo clippy -- -D warnings&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test 🔨&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo test&lt;/span&gt;
    &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;This made it super easy to trigger a bunch of workflows in parallel to process images over time. Would highly recommend using them for small projects like this, assuming your requirements aren't too resource-intensive.&lt;/p&gt;

</description>
      <category>memes</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>Blobby image animation in CSS 🦠</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Fri, 10 Mar 2023 17:19:17 +0000</pubDate>
      <link>https://forem.com/bryce/blobby-image-animation-in-css-5d3o</link>
      <guid>https://forem.com/bryce/blobby-image-animation-in-css-5d3o</guid>
      <description>&lt;p&gt;Was bored on a Friday and whipped up a fun little animated image using CSS:&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/blobby-image-animation-es8n3c"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It overlays multiple elements with the same background image rotated at different intervals. To align the images to the same plane and stitch them together each element is first rotated back (using the &lt;code&gt;:before&lt;/code&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements"&gt;pseudo-element&lt;/a&gt;) then resized and positioned accordingly. &lt;/p&gt;

&lt;p&gt;Then, by adding &lt;code&gt;overflow: hidden;&lt;/code&gt; to the parent element, the image is cropped to the &lt;code&gt;border-radius&lt;/code&gt; value of the pseudo-element and enables it to take a shape!&lt;/p&gt;

&lt;p&gt;Since each image is technically in the same position but with a different parent the values for dimensions and positioning must be identical. This was slightly tricky as the height &amp;amp; width vary slightly with the different rotations (thanks to &lt;a href="https://en.wikipedia.org/wiki/Pythagorean_theorem"&gt;geometry&lt;/a&gt;), so the image needs to be stretched a bit. &lt;/p&gt;

&lt;p&gt;You can see how this is handled here:&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://codesandbox.io/embed/blobby-image-animation-es8n3c?view=editor&amp;amp;module=styles.css"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;For the animation, it uses a simple &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes"&gt;&lt;code&gt;keyframes&lt;/code&gt;&lt;/a&gt; rule to smoothly transition &lt;code&gt;border-radius&lt;/code&gt; values. Each div has an &lt;code&gt;animation-delay&lt;/code&gt; value to offset and create a directional effect.&lt;/p&gt;

&lt;p&gt;Hope this was a good showcase of how easy it is to easily create complex animations using basic CSS/HTML!&lt;/p&gt;

</description>
      <category>css</category>
      <category>tutorial</category>
      <category>beginners</category>
      <category>codenewbie</category>
    </item>
    <item>
      <title>Server-render your SPA in CI at deploy time 📸</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Thu, 02 Mar 2023 10:47:36 +0000</pubDate>
      <link>https://forem.com/bryce/server-render-your-spa-in-ci-at-deploy-time-2798</link>
      <guid>https://forem.com/bryce/server-render-your-spa-in-ci-at-deploy-time-2798</guid>
      <description>&lt;p&gt;If you deploy your SPA using &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt; you can add this new &lt;a href="https://github.com/marketplace/actions/server-side-render-ssr-with-react-snap"&gt;action&lt;/a&gt; to your workflow to have it build server-rendered HTML!&lt;/p&gt;

&lt;p&gt;Server-side rendering (SSR) is &lt;a href="https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8"&gt;great for SEO and performance&lt;/a&gt;. I use it for projects that have an expensive initial render or have links that I want to be discoverable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/stereobooster/react-snap"&gt;react-snap&lt;/a&gt; is a tool to help with SSR; a while ago I wrote about it:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/bryce" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--daK1Iyh9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://res.cloudinary.com/practicaldev/image/fetch/s--U1IffX-j--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/92818/2752117b-9960-4228-b41b-182db3dca947.jpg" alt="bryce"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/bryce/perform-a-react-disappearing-act-with-react-snap-1eo3" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Perform a React disappearing act with react-snap ✨🧙💨&lt;/h2&gt;
      &lt;h3&gt;Bryce Dorn ・ Jan 23 '20 ・ 3 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#react&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;I've been using it as a &lt;code&gt;postbuild&lt;/code&gt; script but it recently broke in CI. The fix for it became rather complex, so rather than include this in each project that I use it for I decided to bundle everything into a standalone action. This also significantly reduced the number of per-project dependencies as it prevents installing big ones like &lt;code&gt;puppeteer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Though &lt;code&gt;react&lt;/code&gt; is in the name, this will work for any framework that supports hydration. In &lt;a href="https://svelte.dev/docs"&gt;Svelte&lt;/a&gt; for example, this just means switching the &lt;code&gt;hydrate&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./App.svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#server-rendered-html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;hydrate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once your app is hydratable, replacing your &lt;code&gt;build&lt;/code&gt; step with this &lt;a href="https://github.com/marketplace/actions/server-side-render-ssr-with-react-snap"&gt;action&lt;/a&gt; will run &lt;code&gt;npm build&lt;/code&gt; followed by &lt;code&gt;react-snap&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prerender&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout 🛎️&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="s"&gt;...&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Server-side render&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;brycedorn/react-snap-action@v1.0.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can then deploy this to GitHub Pages or wherever. Give it a try and let me know if it helps simplify your workflow!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/brycedorn"&gt;
        brycedorn
      &lt;/a&gt; / &lt;a href="https://github.com/brycedorn/react-snap-action"&gt;
        react-snap-action
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Github Action for pre-rendering via react-snap.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>github</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Building APOD color search part I: Image analysis in Rust</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Thu, 23 Feb 2023 17:45:19 +0000</pubDate>
      <link>https://forem.com/bryce/building-apod-color-search-part-i-image-analysis-in-rust-24a5</link>
      <guid>https://forem.com/bryce/building-apod-color-search-part-i-image-analysis-in-rust-24a5</guid>
      <description>&lt;p&gt;Kicking off the first of a series about how I built &lt;a href="https://bryce.io/apod-color-search" rel="noopener noreferrer"&gt;APOD color search&lt;/a&gt;! For an introduction to the project (and some background on &lt;a href="https://apod.nasa.gov/apod/astropix.html" rel="noopener noreferrer"&gt;Astronomy Picture of the Day&lt;/a&gt;) go here:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/bryce" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F92818%2F2752117b-9960-4228-b41b-182db3dca947.jpg" alt="bryce"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/bryce/building-an-app-to-search-the-apod-archive-by-color-23bb" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Building an app to search the APOD archive by color 🪐&lt;/h2&gt;
      &lt;h3&gt;Bryce Dorn ・ Feb 18 '23&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#showdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#rust&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;From a high level, a search like this cannot be done on-the-fly as it requires a static index of image and color information to return results. So, the first step here is to devise a way to extract relevant color information from each image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gathering the data 🚜
&lt;/h2&gt;

&lt;p&gt;On to populating the dataset - image analysis is a low-level operation. Given that there's nearly &lt;a href="https://apod.nasa.gov/apod/calendar/allyears.html" rel="noopener noreferrer"&gt;two decades worth&lt;/a&gt; of images to process, this needed to be fast and performant.&lt;/p&gt;

&lt;p&gt;The full source code can be found on &lt;a href="https://github.com/brycedorn/apod-color-search/tree/main/src" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, but I'll go through some of the essential bits here. &lt;/p&gt;

&lt;h3&gt;
  
  
  Processing data for an APOD 🌌
&lt;/h3&gt;

&lt;p&gt;To populate the searchable color-based index of each APOD, three things must be done:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch APOD information via &lt;a href="https://github.com/nasa/apod-api" rel="noopener noreferrer"&gt;apod-api&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Extract color information from image.&lt;/li&gt;
&lt;li&gt;Store color metadata in database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of my goals for this project was to use cloud-first (and free) resources whenever possible to save headaches later on with deployments &amp;amp; environments. For the database above I created a &lt;a href="https://www.postgresql.org/" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt; instance using &lt;a href="https://supabase.com/pricing" rel="noopener noreferrer"&gt;supabase&lt;/a&gt;'s free tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting a day's APOD information
&lt;/h3&gt;

&lt;p&gt;Fetching this data is easy enough using &lt;a href="https://docs.rs/reqwest/latest/reqwest/" rel="noopener noreferrer"&gt;reqwest&lt;/a&gt; and the &lt;a href="https://github.com/nasa/apod-api" rel="noopener noreferrer"&gt;apod-api&lt;/a&gt; (just need an API key):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;api_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.nasa.gov/planetary/apod"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"APOD_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;request_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;"{}?api_key={}&amp;amp;start_date={}&amp;amp;end_date={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;api_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;reqwest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To do more with this data however, Rust requires that it be properly typed. &lt;a href="https://serde.rs/" rel="noopener noreferrer"&gt;serde&lt;/a&gt; streamlines this with built-in JSON serialization; it only requires a static type and can handle the rest. Here's the type I added to correspond to the API response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;Serialize)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Day&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;copyright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;explanation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;hdurl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;service_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then calling &lt;code&gt;serde_json::from_str&lt;/code&gt; will deserialize it to the typed data structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Day&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, once we have a &lt;code&gt;Day&lt;/code&gt; object to work with, we need to fetch the actual image bytes to do pixel-based analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;image_utils&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;img_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;reqwest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;image&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;load_from_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Processing the colors 🔬
&lt;/h3&gt;

&lt;p&gt;Now all that's left is some low-level pixel processing. This isn't the most efficient algorithm as I'm still a novice Rustacean so it's the best I could do. 😇&lt;/p&gt;

&lt;p&gt;Because these images tend to be massive, the most important pieces are around removing noisy data to avoid unnecessary computation. In this case, only analyzing significant pixels and then counting, ranking and grouping them together.&lt;/p&gt;

&lt;p&gt;Many images have a minimal amount of color information, being either grayscale or mostly black due to the vast emptiness of space. As "color" is in the name, the project is more intended to enable finding colorful pictures, not specific hues of black/gray. To not waste computation time on these, I filtered out the non-colored pixels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;gray_pixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="nf"&gt;.grayscale&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.pixels&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="na"&gt;.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;all_pixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="nf"&gt;.pixels&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="na"&gt;.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;colored_pixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;all_pixels&lt;/span&gt;
  &lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;gray_pixels&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, using a &lt;a href="https://en.wikipedia.org/wiki/Relative_luminance" rel="noopener noreferrer"&gt;relative luminance function&lt;/a&gt;, only included the most luminous pixels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;luminous_pixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colored_pixels&lt;/span&gt;
  &lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;get_luminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;50.&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we're left with a cleaner dataset to work on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate frequency array
&lt;/h3&gt;

&lt;p&gt;To get the most frequent colors of the image, the primary goal of this analysis, a &lt;a href="https://en.wikipedia.org/wiki/Hash_function" rel="noopener noreferrer"&gt;frequency hash&lt;/a&gt; can be used. Put simply, a string/number map of color values to how many times they occur. For easier typing, each pixel is converted from &lt;code&gt;RGB&lt;/code&gt; to &lt;code&gt;String&lt;/code&gt; (hex value):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;colorsys&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Rgb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;generate_hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pixel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nn"&gt;Rgb&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;
    &lt;span class="n"&gt;pixel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;pixel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;pixel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.to_hex_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generate_hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="py"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then generate a &lt;a href="https://doc.rust-lang.org/stable/std/collections/struct.BTreeMap.html" rel="noopener noreferrer"&gt;&lt;code&gt;BTreeMap&lt;/code&gt;&lt;/a&gt; of type &lt;code&gt;{ [hex: string]: frequency: number }&lt;/code&gt; by iterating over this list and incrementing when the same hex value is found. &lt;/p&gt;

&lt;p&gt;To optimize for performance, the function splits the image into chunks and spawns multiple threads to run in parallel before joining them together once complete. I experimented with different values for &lt;code&gt;worker_count&lt;/code&gt; and landed on &lt;code&gt;5&lt;/code&gt; as most optimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;BTreeMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_frequency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;worker_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BTreeMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Mutex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;BTreeMap&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

  &lt;span class="n"&gt;input&lt;/span&gt;
    &lt;span class="nf"&gt;.chunks&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;worker_count&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ceil&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="py"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rresult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="nn"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.for_each&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;rresult&lt;/span&gt;
            &lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.and_modify&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.or_insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;.for_each&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;try_unwrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.into_inner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the most frequent color values are found, similar ones can be grouped together. I refer to these as &lt;code&gt;Clusters&lt;/code&gt;; if a color has &lt;code&gt;R&lt;/code&gt;, &lt;code&gt;G&lt;/code&gt;, &amp;amp; &lt;code&gt;B&lt;/code&gt; values within a certain threshold of each other they are combined into the same &lt;code&gt;Cluster&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The threshold algorithm is a simple series of conditions that only checks for the green value if the value for red is within the threshold, and so on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;within_threshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;color1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;color2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;color2&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;color2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;color2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;color2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;color1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;color1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;assign_clusters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HashMap&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;s_r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rgba&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
      &lt;span class="nf"&gt;.into_keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;within_threshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The closest color value matches are then added to the &lt;code&gt;Cluster&lt;/code&gt;. Once the clusters are finalized, the last step here is to only return the most popular ones. This is done with a simple &lt;code&gt;sort_by&lt;/code&gt; call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;sorted_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;sorted_result&lt;/span&gt;&lt;span class="nf"&gt;.sort_by&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_b&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="n"&gt;f_b&lt;/span&gt;&lt;span class="nf"&gt;.partial_cmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f_a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_clusters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sorted_result&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;sorted_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.to_vec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we have the most significant clusters! This makes it possible to search for a color and map to images that contain many pixels with that color.&lt;/p&gt;

&lt;h3&gt;
  
  
  One month at a time 🗓️
&lt;/h3&gt;

&lt;p&gt;Processing a single APOD is one thing, but the end goal is to process all of them. The cleanest way to group batches of days was by month. As the &lt;a href="https://github.com/nasa/apod-api" rel="noopener noreferrer"&gt;apod-api&lt;/a&gt; supports &lt;code&gt;start_date&lt;/code&gt; and &lt;code&gt;end_date&lt;/code&gt; &lt;a href="https://github.com/nasa/apod-api#endpoint-versionapod" rel="noopener noreferrer"&gt;parameters&lt;/a&gt; to support this, I just used the first and last days of the month for these parameters.&lt;/p&gt;

&lt;p&gt;Since I knew I'd be running this via command line I first checked the arguments provided (&lt;code&gt;year&lt;/code&gt; and &lt;code&gt;month&lt;/code&gt;) and if they correlated to a valid date for an APOD. Since this will map from raw numbers to &lt;code&gt;chrono::Date&lt;/code&gt; objects, some serialization is needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;first_apod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="nf"&gt;.ymd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1995&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Utc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.flat_map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="nf"&gt;.ymd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;first_apod&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"Out of range, date must be between {} and {}."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;first_apod&lt;/span&gt;&lt;span class="nf"&gt;.format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%b %e, %Y"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="nf"&gt;.format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%b %e, %Y"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, given that it's a valid month we can iterate over each day. I added a &lt;code&gt;fetch_month&lt;/code&gt; function to generate a list of &lt;code&gt;Day&lt;/code&gt;s for a month given the first day, as a &lt;code&gt;chrono::Date&lt;/code&gt; generated by the above arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;fetch_month&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;first_day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;chrono&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;api&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Day&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;first_day_formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_day&lt;/span&gt;&lt;span class="nf"&gt;.format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%Y-%m-%d"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Utc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;last_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;first_day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.with_day&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_day&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;last_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;last_day_formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_day&lt;/span&gt;&lt;span class="nf"&gt;.format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%Y-%m-%d"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;apods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;api&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;first_day_formatted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;last_day_formatted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apods&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After getting the data for an entire month it's as simple as iterating over each day and processing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;apods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_month&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;apod&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;apods&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;process_apod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't include this in the code snippets, but things are saved via &lt;a href="https://postgrest.org/en/stable/" rel="noopener noreferrer"&gt;Postgrest&lt;/a&gt; along the way - most importantly &lt;code&gt;Colors&lt;/code&gt; and &lt;code&gt;Clusters&lt;/code&gt; which are used to perform searches. Feel free to have a look at the full &lt;a href="https://github.com/brycedorn/apod-color-search/tree/main/src" rel="noopener noreferrer"&gt;source&lt;/a&gt; to see these.&lt;/p&gt;




&lt;p&gt;Thanks for reading &amp;amp; stay tuned for the next part: using &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; as a free provider to run it in parallel remotely!&lt;/p&gt;

</description>
      <category>motivation</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>Building an app to search the APOD archive by color 🪐</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Sat, 18 Feb 2023 21:04:49 +0000</pubDate>
      <link>https://forem.com/bryce/building-an-app-to-search-the-apod-archive-by-color-23bb</link>
      <guid>https://forem.com/bryce/building-an-app-to-search-the-apod-archive-by-color-23bb</guid>
      <description>&lt;center&gt;&lt;em&gt;tl;dr, I built &lt;a href="https://bryce.io/apod-color-search" rel="noopener noreferrer"&gt;apod-color-search&lt;/a&gt;!&lt;/em&gt;&lt;/center&gt;
&lt;br&gt;

&lt;p&gt;The news of the James Webb Space Telescope (JWST) being successfully deployed in orbit reminded me how fascinating outer space is. Hubble has already showed us how spectacular and unfathomably large the visible universe is, and now with JWST it's like Earth just received a much better glasses prescription! &lt;/p&gt;

&lt;p&gt;If you haven't been following, here's a compelling side-by-side comparison of the same nebula as seen by both telescopes posted to NASA's Instagram:&lt;/p&gt;


&lt;div class="instagram-position"&gt;
  &lt;iframe id="instagram-liquid-tag" src="https://www.instagram.com/p/Cf6pxtbOkPH/embed/captioned/"&gt;
  &lt;/iframe&gt;
  
&lt;/div&gt;


&lt;p&gt;Long before these pictures started being shared via social media however, &lt;a href="https://apod.nasa.gov/apod/astropix.html" rel="noopener noreferrer"&gt;Astronomy Picture of the Day&lt;/a&gt; (APOD) established itself as one of the first websites for this purpose. Every day since July 16 1995, a new picture (or video) with some accompanying information has been posted and it has cemented itself as the OG source of space-related content.&lt;/p&gt;

&lt;p&gt;The archive is a trove of absolutely stunning images with otherworldly color palettes. The sheer amount of them and their easy accessibility, combined with my personal desire for a new side-project led to the thought: why not make the APOD image database searchable by color? 🎨&lt;/p&gt;

&lt;h2&gt;
  
  
  The finished product 👨‍🚀
&lt;/h2&gt;

&lt;p&gt;With more images from JWST continuing to be transmitted back to us, I've been wanting to build something to do just that. Feeling inspired, I set out to build &lt;a href="https://bryce.io/apod-color-search" rel="noopener noreferrer"&gt;apod-color-search&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Do give it a try and let me know what you think. I've been putting time into it on &amp;amp; off over the past couple months but finally feel ready to release it to the public. &lt;/p&gt;

&lt;p&gt;Part of this was to maximize the various free tiers of cloud services that it utilizes - only given so many hours for GitHub Actions to run to process almost two decades of images! Happy to say that I didn't spend a single cent on any cloud infrastructure, though that came at the cost of some additional complexity which I'll go into detail about later.&lt;/p&gt;

&lt;p&gt;Here's the source if you want to poke around in the meantime:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/brycedorn" rel="noopener noreferrer"&gt;
        brycedorn
      &lt;/a&gt; / &lt;a href="https://github.com/brycedorn/apod-color-search" rel="noopener noreferrer"&gt;
        apod-color-search
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Search for Astronomy Picture of the Day images by color 🎨
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;apod color search 🪐&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;Search for &lt;a href="https://apod.nasa.gov/apod/astropix.html" rel="nofollow noopener noreferrer"&gt;APOD&lt;/a&gt; photos by color! Consists of three parts:&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;web&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;A &lt;a href="https://svelte.dev/" rel="nofollow noopener noreferrer"&gt;Svelte&lt;/a&gt; app to provide search interface. Uses &lt;a href="https://github.com/web-padawan/vanilla-colorful" rel="noopener noreferrer"&gt;vanilla-colorful&lt;/a&gt; for color picker.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;api&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a href="https://deno.land/" rel="nofollow noopener noreferrer"&gt;Deno&lt;/a&gt;-based API that retrieves APOD information from database for images that match the given hex value. Uses Redis to store &amp;amp; retrieve query results.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;src&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a href="https://www.rust-lang.org/" rel="nofollow noopener noreferrer"&gt;Rust&lt;/a&gt; utility to analyze and process images. Used to populate database with historical data, now scheduled to run monthly to process new APODs.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fetches APOD metatada via &lt;a href="https://github.com/nasa/apod-api" rel="noopener noreferrer"&gt;apod-api&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Analyzes image to get highest-frequency non-grayscale colors.&lt;/li&gt;
&lt;li&gt;Saves result to Postgres.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Runs via GitHub Actions UI to then trigger additional workflows. This was used to leverage GitHub Actions to batch process a large amount of images concurrently &amp;amp; remotely.&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/brycedorn/apod-color-search" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;





&lt;p&gt;This will be the beginning of a series of posts about how I built it consisting of multiple parts, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building a request handler and image processor in Rust&lt;/li&gt;
&lt;li&gt;Using GitHub Actions as a free cloud utility to batch process images&lt;/li&gt;
&lt;li&gt;Building an API to retrieve and cache search results&lt;/li&gt;
&lt;li&gt;Building a web application to display color-indexed images in a searchable interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned! 🔭&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>rust</category>
    </item>
    <item>
      <title>When not to use Deno Deploy ☄️</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Sun, 05 Feb 2023 11:47:30 +0000</pubDate>
      <link>https://forem.com/bryce/when-not-to-use-deno-5bnm</link>
      <guid>https://forem.com/bryce/when-not-to-use-deno-5bnm</guid>
      <description>&lt;p&gt;I've been really enjoying &lt;a href="https://deno.land/" rel="noopener noreferrer"&gt;Deno&lt;/a&gt; for personal projects! The developer experience is excellent and &lt;a href="https://deno.com/deploy" rel="noopener noreferrer"&gt;Deno Deploy&lt;/a&gt; makes it easy to quickly &amp;amp; easily get things deployed. &lt;/p&gt;

&lt;p&gt;While its on-demand design is efficient and excellent for many use cases, there are some where it performs poorly due to its nature of creating a new instance for each request. Unfortunately, I ran into one of these when building a caching layer for a project I've been working on. In this article I'll go through these issues and explain the alternative route I went to avoid them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: Redis overload 👯👯👯
&lt;/h2&gt;

&lt;p&gt;Database reads are expensive and slow. There's a &lt;a href="https://aws.amazon.com/caching" rel="noopener noreferrer"&gt;multitude of other reasons&lt;/a&gt; why caching is essential, but for the project I'm working on I'm using &lt;a href="https://supabase.com/" rel="noopener noreferrer"&gt;supabase&lt;/a&gt; and their free tier supports only a limited amount of bandwidth. To avoid any rate-limiting I added a &lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt; layer in front of it to reduce the number of queries that hit the database directly.&lt;/p&gt;

&lt;p&gt;Conveniently, Deno has a &lt;a href="https://deno.land/x/redis@v0.29.0" rel="noopener noreferrer"&gt;deno-redis&lt;/a&gt; (experimental) plugin which I used to add a simple caching layer to my project:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://deno.land/x/redis/mod.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cachedResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cachedResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cachedResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;With this in place, the full request lifecycle looks like this:&lt;/p&gt;

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

&lt;p&gt;And this works just fine! But, after enough requests start pouring in Redis will complain that there are too many connections and not allow new ones (&lt;code&gt;redis.connect&lt;/code&gt; results in: &lt;code&gt;ERR max number of clients reached&lt;/code&gt;). Granted, I am also using &lt;em&gt;their&lt;/em&gt; free tier which restricts to 30 clients at a time. But this problem will also arise in high-volume applications as &lt;a href="https://cloud.ibm.com/docs/databases-for-redis?topic=databases-for-redis-managing-redis-connections" rel="noopener noreferrer"&gt;hosted integrations&lt;/a&gt; generally have a hard cap on the number of connections.&lt;/p&gt;

&lt;p&gt;Why does this happen? It's due to a fundamental aspect of Deno Deploy: each time a request is made a completely new context is created and in this scenario, a new connection to Redis must be made. To visualize this, here is what happens when &lt;code&gt;n&lt;/code&gt; users each make a request:&lt;/p&gt;

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

&lt;p&gt;To get around this we'll need a way to cut down on the number of connections and keep it under the limit, but as we'll see there's also a performance benefit to doing this.&lt;/p&gt;
&lt;h2&gt;
  
  
  A single, persistent connection 🛰️
&lt;/h2&gt;

&lt;p&gt;Though any traditional server environment will enable this, I used Node as it's close to Deno and simple to use. With a few lines of code, an &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;express&lt;/a&gt; server that creates a single client on start will enable any incoming request to reuse the same connection. Adding some simple routing to detect request params and respond with what Redis returns then lets allows the cache requests in Deno to be swapped with &lt;code&gt;GET&lt;/code&gt; requests to this server.&lt;/p&gt;

&lt;p&gt;This is known as a &lt;a href="https://en.wikipedia.org/wiki/Reverse_proxy" rel="noopener noreferrer"&gt;reverse proxy&lt;/a&gt; and is common in any web stack. With this in place, the request lifecycle now looks like this:&lt;/p&gt;

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

&lt;p&gt;With this in place, not only are there no errors due to the number of connections but performance is significantly improved with duration cut by over 50%:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;benchmark&lt;/th&gt;
&lt;th&gt;time (avg)&lt;/th&gt;
&lt;th&gt;(min … max)&lt;/th&gt;
&lt;th&gt;p75&lt;/th&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deno -&amp;gt; Redis&lt;/td&gt;
&lt;td&gt;65.06 ms/iter&lt;/td&gt;
&lt;td&gt;(792 ns … 114.68 ms)&lt;/td&gt;
&lt;td&gt;111.41 ms&lt;/td&gt;
&lt;td&gt;114.68 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deno -&amp;gt; Node -&amp;gt; Redis&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34.79 ms/iter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(28.4 ms … 41.7 ms)&lt;/td&gt;
&lt;td&gt;36.53 ms&lt;/td&gt;
&lt;td&gt;41.7 m&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  But there's a twist! 🦹‍♂️
&lt;/h2&gt;

&lt;p&gt;My main objective was to identify a solution for the Redis errors, but I figured why not also test whether a persistent Postgres connection behaves similarly. And to my surprise, the results were the opposite!&lt;/p&gt;

&lt;p&gt;Perplexingly, when benchmarking the same as above (Deno vs Deno + Node layer for Postgres) there was a surprising result: Deno outperformed Node by a factor of ~2x! So despite recreating a supabase client for each request, Deno still outperforms Node: &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;benchmark&lt;/th&gt;
&lt;th&gt;time (avg)&lt;/th&gt;
&lt;th&gt;(min … max)&lt;/th&gt;
&lt;th&gt;p75&lt;/th&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deno -&amp;gt; Postgres&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;42.98 ms/iter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(916 ns … 89.29 ms)&lt;/td&gt;
&lt;td&gt;79.8 ms&lt;/td&gt;
&lt;td&gt;89.29 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deno -&amp;gt; Node -&amp;gt; Postgres&lt;/td&gt;
&lt;td&gt;88.24 ms/iter&lt;/td&gt;
&lt;td&gt;(667 ns … 205.44 ms)&lt;/td&gt;
&lt;td&gt;160.8 ms&lt;/td&gt;
&lt;td&gt;205.44 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Whether this is due to limitations in the implementation of &lt;code&gt;@supabase/supabase-js&lt;/code&gt; that aren't present in Deno's or just that Deno performs better I am not completely sure. And as far as I'm aware there isn't a limitation on the number of clients connecting to a single supabase instance. But based on these numbers I'll keep Node for querying Redis and Deno for querying Postgres. &lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;



&lt;p&gt;Here's the source used to run each of these benchmarks if you'd like to try them yourself:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/brycedorn" rel="noopener noreferrer"&gt;
        brycedorn
      &lt;/a&gt; / &lt;a href="https://github.com/brycedorn/deno-node-redis-postgres-benchmarks" rel="noopener noreferrer"&gt;
        deno-node-redis-postgres-benchmarks
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Benchmarking Redis and Postgres performance for Deno vs a combination of Deno and Node.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Deno vs Node Redis/Postgres benchmarking&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;As part of a dev.to &lt;a href="https://dev.to/bryce/when-not-to-use-deno-5bnm" rel="nofollow"&gt;article&lt;/a&gt;.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Results&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Results from running on Apple M1 Max 64GB Ventura 13.1 from Amsterdam with both hosted services in &lt;code&gt;eu-west-1&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;br&gt;
&lt;thead&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;th&gt;Uses node?&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Time (avg)&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;(min … max)&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;p75&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/thead&gt;
&lt;br&gt;
&lt;tbody&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Deno -&amp;gt; Redis&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;65.06 ms/iter&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;(792 ns … 114.68 ms)&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;111.41 ms&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;114.68 ms&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Deno -&amp;gt; Postgres&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;&lt;strong&gt;42.98 ms/iter&lt;/strong&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;(916 ns … 89.29 ms)&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;79.8 ms&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;89.29 ms&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Deno -&amp;gt; Node -&amp;gt; Redis&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;&lt;strong&gt;34.79 ms/iter&lt;/strong&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;(28.4 ms … 41.7 ms)&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;36.53 ms&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;41.7 ms&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Deno -&amp;gt; Node -&amp;gt; Postgres&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;88.24 ms/iter&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;(667 ns … 205.44 ms)&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;160.8 ms&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;205.44 ms&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/tbody&gt;
&lt;br&gt;
&lt;/table&gt;&lt;/div&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Running locally&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h4 class="heading-element"&gt;Prerequisites&lt;/h4&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Deno &amp;gt;= v1.30.2.&lt;/li&gt;
&lt;li&gt;V8 &amp;gt;= v10.9.194.5.&lt;/li&gt;
&lt;li&gt;TS &amp;gt;= v4.9.4.&lt;/li&gt;
&lt;li&gt;Node &amp;gt;= v16.15.0.&lt;/li&gt;
&lt;li&gt;Redis instance, I'm using &lt;a href="https://redis.com/redis-enterprise-cloud/overview/" rel="nofollow noopener noreferrer"&gt;Redis Enterprise Cloud&lt;/a&gt; (has free tier).&lt;/li&gt;
&lt;li&gt;Postgres instance, I'm using &lt;a href="https://supabase.com/" rel="nofollow noopener noreferrer"&gt;supabase&lt;/a&gt; (has free tier)
&lt;ul&gt;
&lt;li&gt;This example uses &lt;code&gt;hex&lt;/code&gt; as the lookup key but this is…&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/brycedorn/deno-node-redis-postgres-benchmarks" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>deno</category>
      <category>node</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I made an app to index my liked TikTok recipes 👨‍🍳</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Tue, 16 Aug 2022 15:50:00 +0000</pubDate>
      <link>https://forem.com/bryce/i-made-an-app-to-index-my-liked-tiktok-recipes-366a</link>
      <guid>https://forem.com/bryce/i-made-an-app-to-index-my-liked-tiktok-recipes-366a</guid>
      <description>&lt;p&gt;While TikTok started as an app for dancing and music, anyone who's used the app knows that it's grown into something much bigger. I've learned so many things from random accounts: history lessons, travel advice, "life hacks that have become unconscious standard practices", you name it!&lt;/p&gt;

&lt;p&gt;Cooking is a big topic for me. I come across lots of &lt;em&gt;delicious&lt;/em&gt; looking recipes and "like" them to revisit later. But TikTok's interface for searching through liked videos is half-baked and lacks basic features for searching through a large list of videos.&lt;/p&gt;

&lt;p&gt;So even though I &lt;em&gt;want&lt;/em&gt; to try all of these recipes, I get stuck trying to find them (and then end up liking more videos to continue to bury them in my pile of likes).&lt;/p&gt;

&lt;p&gt;But I figured, TikTok has a public API, how hard would it be to build a project to solve this?&lt;/p&gt;

&lt;p&gt;The answer: not that hard 😄 though with some hacks. Here's what I ended up building: &lt;a href="https://tiktok-recipes.deno.dev"&gt;tiktok-recipes.deno.dev&lt;/a&gt;. Will walk through how I did it below!&lt;/p&gt;

&lt;h2&gt;
  
  
  TikTok's &lt;code&gt;oembed&lt;/code&gt; API 📟
&lt;/h2&gt;

&lt;p&gt;TikTok provides a &lt;a href="https://developers.tiktok.com/doc/embed-videos/"&gt;public &lt;code&gt;oembed&lt;/code&gt; API&lt;/a&gt; that will retrieve metadata for a video. It's as simple as a &lt;code&gt;GET&lt;/code&gt; to &lt;code&gt;https://www.tiktok.com/oembed?url=${url}&lt;/code&gt;, where &lt;code&gt;url&lt;/code&gt; is the full path to the video on TikTok.&lt;/p&gt;

&lt;p&gt;With a simple script around this, a list of videos can be iterated upon to fetch and save metadata to a static file. All that's needed is the list of recipes...&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiling list of videos 📺
&lt;/h2&gt;

&lt;p&gt;This step was boring 🥱 there's no getting around needing to sift through all of the videos I've liked to save the URLs for the ones that are recipes. Ended up scrolling for a while and opening a bunch of tabs then doing a &lt;a href="https://superuser.com/questions/276321/how-to-export-opened-tabs-in-chrome"&gt;trick&lt;/a&gt; to bookmark/export those tabs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Displaying the list 📃
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://fresh.deno.dev/"&gt;fresh&lt;/a&gt; as it's been my framework of choice lately. It includes preact, typescript &amp;amp; twind by default. The interface is a simple two-pane component that renders a scrollable list of video titles on the left and a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element on the right. But while it'd be possible to just embed the TikTok entirely in an iFrame, I'd much rather only load the content I want.&lt;/p&gt;

&lt;p&gt;Ideally I could just link the &lt;code&gt;src&lt;/code&gt; of the video from metadata, but that would be too easy. The source URL isn't included in this blob so unfortunately it requires a hackier approach to obtain. But I came up with a solution for this that requires minimal code (&amp;lt;20 lines total)!&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieving video source 🚧
&lt;/h2&gt;

&lt;p&gt;I covered this in an earlier post, you can read more about it here:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/bryce" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eHWvlpX0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://media.dev.to/cdn-cgi/image/width%3D150%2Cheight%3D150%2Cfit%3Dcover%2Cgravity%3Dauto%2Cformat%3Dauto/https%253A%252F%252Fdev-to-uploads.s3.amazonaws.com%252Fuploads%252Fuser%252Fprofile_image%252F92818%252F2752117b-9960-4228-b41b-182db3dca947.jpg" alt="bryce"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/bryce/building-a-simple-tiktok-scraping-api-4mcl" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Building a simple TikTok scraping API 🤖&lt;/h2&gt;
      &lt;h3&gt;Bryce Dorn ・ Aug 14 '22&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#showdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#api&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;tl;dr: scrape the page for the media URL inside the server-generated client-side blob. Unfortunately there doesn't appear to be a simpler way to do this at the moment (let me know otherwise).&lt;/p&gt;

&lt;p&gt;This is easily to implement with a fresh &lt;a href="https://fresh.deno.dev/docs/getting-started/fetching-data"&gt;handler&lt;/a&gt; though! And since this happens on the server it gets around CSP restrictions that prevent loading content from another website. Here's the full &lt;a href="https://github.com/brycedorn/tiktok-recipes/blob/main/routes/api/fetch.ts"&gt;source&lt;/a&gt; for the handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together 🪛
&lt;/h2&gt;

&lt;p&gt;Now that all the pieces are in place, all that's left is to connect the request flow and ensure client state works correctly! With some simple logic to handle basic actions and loading states (showing video thumbnail initially &amp;amp; adding a play indicator to load video), it's looking pretty usable. Definitely nothing to write home about but gets the job done. &lt;/p&gt;

&lt;p&gt;The list ordering is randomized on each pageload for a different experience each time I visit. And, importantly, I added a simple text search as well to filter results - a basic feature TikTok's liked videos page still doesn't support.&lt;/p&gt;

&lt;p&gt;If you didn't catch the link earlier here it is in action: &lt;a href="https://tiktok-recipes.deno.dev"&gt;tiktok-recipes.deno.dev&lt;/a&gt;. Feel free to poke through the recipes I find interesting or read through the source (below) to see the full project (or fork it &amp;amp; create your own page for any category of TikToks you'd like easier access to). Thanks for reading!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/brycedorn"&gt;
        brycedorn
      &lt;/a&gt; / &lt;a href="https://github.com/brycedorn/tiktok-recipes"&gt;
        tiktok-recipes
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Indexing recipes I've liked on TikTok for easier access.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;h1&gt;
tiktok recipes&lt;/h1&gt;
&lt;p&gt;Indexing recipes I've liked on TikTok for easier access.&lt;/p&gt;
&lt;h3&gt;
Usage&lt;/h3&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;deno task start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will watch the project directory and restart as necessary.&lt;/p&gt;
&lt;h3&gt;
Fetching data&lt;/h3&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;deno task fetch:tiktoks
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will fetch metadata for videos in
&lt;a href="https://github.com/brycedorn/tiktok-recipes/tree/main/data"&gt;&lt;code&gt;data/tiktoks.txt&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/brycedorn/tiktok-recipes"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>deno</category>
    </item>
    <item>
      <title>Building a simple TikTok scraping API 🤖</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Sun, 14 Aug 2022 18:26:40 +0000</pubDate>
      <link>https://forem.com/bryce/building-a-simple-tiktok-scraping-api-4mcl</link>
      <guid>https://forem.com/bryce/building-a-simple-tiktok-scraping-api-4mcl</guid>
      <description>&lt;p&gt;&lt;strong&gt;Note: this no longer works as TikTok now blocks cross-origin media requests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As part of a project centered around TikTok (coming soon!) I needed a way to get a video's media. The closest thing that TikTok officially provides is their &lt;a href="https://developers.tiktok.com/doc/embed-videos/"&gt;&lt;code&gt;oembed&lt;/code&gt; API&lt;/a&gt;, but unfortunately this only includes basic information like &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;thumbnail_url&lt;/code&gt; etc. And other unofficial APIs &lt;a href="https://tikapi.io/#pricing"&gt;cost money&lt;/a&gt; or require &lt;a href="https://github.com/davidteather/TikTok-Api"&gt;python&lt;/a&gt;, which I don't want to do. So I needed another way to retrieve this info.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leveraging SSR patterns 📦
&lt;/h2&gt;

&lt;p&gt;As a website that handles large volumes of traffic I knew they had some sort of SSR/client-side hydration going on. This generally involves is shipping data to the client in an object inlined inside a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, reducing the number of necessary client-side requests by directly populating components with this data. In &lt;a href="https://nextjs.org/"&gt;Next.js&lt;/a&gt; for example, this happens via &lt;a href="https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props"&gt;server-side props&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And lo and behold! There's a handy blob provided alongside the page markup for every video. You can confirm this for yourself by visiting any TikTok video's page (&lt;code&gt;https://www.tiktok.com/:user/video/:id&lt;/code&gt;) and looking at the contents of the &lt;code&gt;SIGI_STATE&lt;/code&gt; tag via console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGI_STATE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Knowing this I built a simple handler using &lt;a href="https://deno.land/"&gt;deno&lt;/a&gt; to fetch this blob and return it directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter cheerio &amp;amp; deno 🥣🦕
&lt;/h2&gt;

&lt;p&gt;If the video URL is attached to the request (via &lt;code&gt;POST&lt;/code&gt; body or params), a simple handler can fetch (scrape) the content for the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://tiktok.com/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, using &lt;a href="https://cheerio.js.org/"&gt;cheerio&lt;/a&gt;, this raw text can be turned into a queryable jQuery-like API. This provides an entrypoint to get the content of the server-generated blob within the script tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cheerio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#SIGI_STATE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, it's only a matter of parsing this as JSON and returning pertinent data in the response (&lt;code&gt;ItemModule&lt;/code&gt;). The data is currently keyed by some unique identifier, &lt;code&gt;Object.keys&lt;/code&gt; helps remove that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ItemModule&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ItemModule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! Now I can make a client-side request to get up-to-date media for a video and display it directly. This also gets around TikTok's CDN expiration which would prevent saving a reference to this media.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Side-note: comparing this to the &lt;a href="https://tikapi.io/documentation/#tag/Public/operation/public.video"&gt;output&lt;/a&gt; for one of the unofficial API options I came across, the data looks identical. So it's likely they're doing the same thing (but charging almost $200/mo for unlimited usage)!&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Anyways, this is fragile and could break if they decide to rename the script tag, rate-limit, etc. But this demonstrates how easy it is to spin up an instance for your own project. And maybe with enough bot traffic TikTok will create a public API that accomplishes this instead of making us scrape for it! 😇&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>api</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Detecting dark mode on every request 🌓</title>
      <dc:creator>Bryce Dorn</dc:creator>
      <pubDate>Mon, 08 Aug 2022 13:05:00 +0000</pubDate>
      <link>https://forem.com/bryce/detecting-dark-mode-on-every-request-21b2</link>
      <guid>https://forem.com/bryce/detecting-dark-mode-on-every-request-21b2</guid>
      <description>&lt;p&gt;I'm a huge proponent of dark color schemes! Aside from causing less eye strain due to emitting less blue light, dark pixels drain less battery life (for devices with OLED screens) and make nighttime usage much more pleasant. I immediately look for the option when opening a new app and love when it's implemented well.&lt;/p&gt;

&lt;p&gt;Recently I came across an experimental approach to help simplify dark mode adoption on the web and wanted to share with others as it resolves a related frontend issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  That annoying flicker 💡
&lt;/h2&gt;

&lt;p&gt;If you've ever implemented dark-mode in a frontend application, chances are you've encountered the "flickering" issue when loading the page. This is because the client doesn't know whether to render the light or dark version of the UI initially, causing it to flicker from light to dark if the user prefers that color scheme.&lt;/p&gt;

&lt;p&gt;With SSR it's possible to get around this by storing the user's theme preference in &lt;code&gt;document.cookie&lt;/code&gt;; enabling the server to check this preference on a request before responding with HTML. But even with this approach, the cookie can only be set &lt;em&gt;after&lt;/em&gt; the user first loads a page (detecting via &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme"&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt;&lt;/a&gt; and setting this cookie), still causing the page to flicker if they prefer things to be dark.&lt;/p&gt;

&lt;p&gt;This can also become tedious if your application spans multiple subdomains, as the cookie may need to be set for each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting on &lt;em&gt;every&lt;/em&gt; request 🪄
&lt;/h2&gt;

&lt;p&gt;Luckily there's a solution here! (For Chrome at least, more on that later.)&lt;/p&gt;

&lt;p&gt;As of Chromium 93, the &lt;a href="https://chromestatus.com/feature/5642300464037888"&gt;&lt;code&gt;Sec-CH-Prefers-Color-Scheme&lt;/code&gt;&lt;/a&gt; client hint header enables attaching this preference to HTTP requests. By specifying that your server allows this header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accept-CH&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sec-CH-Prefers-Color-Scheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Vary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sec-CH-Prefers-Color-Scheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Critical-CH&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sec-CH-Prefers-Color-Scheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then get the user's preference from the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sec-CH-Prefers-Color-Scheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// "dark" | "light"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And send the appropriate markup to the client! Possibly via a class on the &lt;code&gt;body&lt;/code&gt; tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or an inlined CSS variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;`--color-scheme: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or it can integrate with the style library you're using assuming it generates styles server-side.&lt;/p&gt;

&lt;p&gt;This is great though! No need to track color-scheme preference on the client or in your database; it can be set once on the user's device and included without any interference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some limitations 💭
&lt;/h2&gt;

&lt;p&gt;Unfortunately this is currently only supported on recent (93+) versions of Chromium-based browsers. As we know Chrome dominates browser share, so this translates to &lt;a href="https://caniuse.com/?search=client%20hint"&gt;roughly 70%&lt;/a&gt; of the web.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lists.webkit.org/pipermail/webkit-dev/2021-May/031856.html"&gt;WebKit&lt;/a&gt; and &lt;a href="https://github.com/mozilla/standards-positions/issues/526"&gt;Mozilla&lt;/a&gt; have requests pending to implement. Until support reaches 100% it can't (and shouldn't) be used in production, but I'm hopeful that since this delivers real value to the user experience it will get traction soon.&lt;/p&gt;

&lt;p&gt;More information about other client hints can be found &lt;a href="https://web.dev/user-preference-media-features-headers/"&gt;here&lt;/a&gt;, as well as links to examples. If this interests you spread the word and hopefully we can see this implemented across all browsers soon!&lt;/p&gt;

</description>
      <category>news</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
    </item>
  </channel>
</rss>
