<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Joshua Tzucker&apos;s Site</title><description>Personal portfolio and assorted projects of Joshua Tzucker.</description><link>https://joshuatz.com/rss.xml</link><item><title>What Makes Svelte So Great - Features and Comparisons</title><link>https://joshuatz.com/posts/2023/what-makes-svelte-so-great---features-and-comparisons/</link><guid isPermaLink="true">https://joshuatz.com/posts/2023/what-makes-svelte-so-great---features-and-comparisons/</guid><description>A look at my favorite features of Svelte, comparisons to React and other frameworks, and what makes it a joy to use.</description><pubDate>Thu, 07 Dec 2023 07:09:16 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;At this point, many people who have worked with me at my current job know I’m an (unashamed) &lt;a href=&quot;https://svelte.dev/&quot;&gt;Svelte&lt;/a&gt; fanatic; I spread the word about it constantly, use it on both professional and side projects, and am not afraid to throw jabs at React for falling short of what Svelte delivers.&lt;/p&gt;
&lt;p&gt;In order to help spread the word even further, as well have a place I can point people to when they ask me why I like it so much, I’m creating this post as a &lt;del&gt;love-letter to Svelte&lt;/del&gt;, sorry, I meant to say &lt;em&gt;carefully thought out list&lt;/em&gt; of reasons why I like Svelte (and I think you will too!).&lt;/p&gt;
&lt;p&gt;Also, I worry it might feel like this post is constantly bashing React or might feel like a &lt;em&gt;Svelte vs React&lt;/em&gt; showdown - while there is definitely some of that going on, I want to point out that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I’m doing it out of a passion for web development and the hope that if we are honest about deficiencies, we can collectively work on addressing them. Competition is good, and React is making some good strides!&lt;/li&gt;
&lt;li&gt;I still use React when it makes sense, both professionally and on hobby projects; it isn’t going anywhere anytime soon, and I’m OK with that.&lt;/li&gt;
&lt;li&gt;Svelte is not a panacea - if you take poorly written React and re-write it in poorly written Svelte, it is still going to be poorly written. &lt;del&gt;Garbage in, garbage out&lt;/del&gt; spaghetti code in, spaghetti code out 🍝&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OK, now onto my &lt;del&gt;&lt;em&gt;fawning over&lt;/em&gt;&lt;/del&gt; careful analysis of what makes Svelte so great.&lt;/p&gt;
&lt;h2 id=&quot;performance&quot;&gt;Performance&lt;/h2&gt;
&lt;p&gt;To get the ball rolling, I’m going to start with something objective, which is hard to argue against - Svelte performs faster and with less resources than React and many other frameworks. 🔥&lt;/p&gt;
&lt;p&gt;This mostly comes down to how Svelte handles reactivity and DOM updates, which it has described as “surgical”. And this is without any hand-tuning or special code; no &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useRef&lt;/code&gt;, or having to always be thinking about the render lifecycle - it just works.&lt;/p&gt;
&lt;p&gt;If you don’t want to take my word for it, just check out &lt;a href=&quot;https://krausest.github.io/js-framework-benchmark/&quot;&gt;these JS benchmarks&lt;/a&gt;. Right now, in December 2023, of the popular frameworks people are probably most familiar with, the rankings are generally (from best to worst): &lt;code&gt;vanillajs&lt;/code&gt; -&gt; &lt;code&gt;solid&lt;/code&gt; -&gt; &lt;code&gt;svelte&lt;/code&gt; -&gt; &lt;code&gt;vue&lt;/code&gt; -&gt; &lt;code&gt;angular&lt;/code&gt; and &lt;code&gt;react&lt;/code&gt; (pretty close together).&lt;/p&gt;
&lt;h2 id=&quot;simplicity&quot;&gt;Simplicity&lt;/h2&gt;
&lt;p&gt;This is kind of jumping the gun, as this is the unifying theme of this post and how I wrap it up at the end as well, but if you walk away from this reading experience with only one takeaway about what makes Svelte so great, I think it  should be that Svelte is simpler to use.&lt;/p&gt;
&lt;p&gt;I’ll end this section with the same text I end this entire post with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;At the end of the day, I value simple, clean code that is easy to write, read, and maintain - for me personally, Svelte is the framework in which I find this the easiest to achieve, with the least amount of time and effort.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;bi-directional-variable-bindings&quot;&gt;Bi-Directional Variable Bindings&lt;/h2&gt;
&lt;p&gt;Did you hear that sound? Its the sound of tears falling as this section is read by React devs who have to deal with form input listeners (hello &lt;code&gt;formik&lt;/code&gt; users), callback hell, painful state refactoring, and everything else that comes with trying to pass values &lt;em&gt;&lt;strong&gt;upwards&lt;/strong&gt;&lt;/em&gt; in React and makes it such a chore.&lt;/p&gt;
&lt;p&gt;Yes, you read that title right - &lt;a href=&quot;https://svelte.dev/docs/element-directives#bind-property&quot;&gt;Svelte support bi-directional data bindings / variable bindings, out of the box&lt;/a&gt;. Once again, that is data flowing back &lt;em&gt;&lt;strong&gt;up&lt;/strong&gt;&lt;/em&gt; from the child to the parent, not just down.&lt;/p&gt;
&lt;p&gt;To paint a picture of what that means and how it saves so much time, imagine you have a form input for a username, and you want to keep your local variable in sync with the input as the user types.&lt;/p&gt;
&lt;p&gt;In React, if you don’t use a 3rd party library, you are going to have to write boilerplate to listen for the input value change and then update the variable yourself; React does not support bi-directional bindings:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; React, { useState } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;react&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; AgeEditor&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;userAge&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setUserAge&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; useState&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;50&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;input&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        type&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        value&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{userAge}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        onChange&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          setUserAge&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parseInt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(evt.target.value));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Your age is {userAge}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Svelte, you can just bind the variable directly:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; userAge &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 50&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; bind:value&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;{userAge}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Your age is {userAge}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BUT - that’s not all! If you order within the next 5 minutes, you get bindings for so  much more than just &lt;code&gt;&amp;#x3C;input&gt;&lt;/code&gt;s. You get them for ANY property export from a child component. This is huge, as in other frameworks like React, you would either have to pass a state updater as a prop to the child (e.g., a &lt;code&gt;useState&lt;/code&gt; setter or your own callback) or use something like &lt;code&gt;useContext&lt;/code&gt; (perhaps with a shared reducer) to establish shared state - all this to say that it is far less code in Svelte and much simpler:&lt;/p&gt;
&lt;p&gt;Parent component:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; AgeEditor &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;./AgeEditor.svelte&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; userAge &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 50&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	$&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: userAgeDoubled &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; userAge &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;&amp;#x3C;!--&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;We could have used `bind:userAge={userAge}`, but this is&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;even _more_ concise!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;--&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#FDAEB7;font-style:italic&quot;&gt;AgeEditor&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; bind:userAge&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Your age doubled = {userAgeDoubled}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Child component (&lt;code&gt;AgeEditor.svelte&lt;/code&gt;)&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; userAge &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 50&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; bind:value&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;{userAge}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Your age is {userAge}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, you can also use bi-directional bindings with entire components, through &lt;a href=&quot;https://svelte.dev/docs/element-directives#bind-this&quot;&gt;the &lt;code&gt;bind:this&lt;/code&gt; directive&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;state-state-state-did-i-mention-state-yet-and-effectful-code&quot;&gt;State, State, State (Did I Mention State Yet?) and Effectful Code&lt;/h2&gt;
&lt;p&gt;I’m going to be very honest here - I really don’t care for React’s built-in state management solutions and how it treats reactivity in regards to state variables, and I think Svelte’s approach is &lt;em&gt;FAR&lt;/em&gt; superior.&lt;/p&gt;
&lt;p&gt;In Svelte, you don’t need to do anything special to make a variable “stateful” - it just is.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on:click&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;{()&lt;/span&gt;&lt;span style=&quot;color:#FDAEB7;font-style:italic&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; count++}&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	Count = {count}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In React (and some other frameworks), this does not work, because regular variables are re-computed every time the component re-renders. So &lt;code&gt;count&lt;/code&gt; would get reset to &lt;code&gt;0&lt;/code&gt; unless you wrap it in a special &lt;code&gt;useState&lt;/code&gt; hook.&lt;/p&gt;
&lt;p&gt;Here is the same code in React:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; React, { useState } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;react&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; App&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setCount&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; useState&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; onClick&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setCount&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)}&gt;Count = {count}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a simple example like this, using &lt;code&gt;useState&lt;/code&gt; might not look so bad. But once you have multiple pieces of state, and some pieces are derived from other pieces, and there needs to be side-effects that happen when certain pieces change, it starts to become a headache to manage. Especially because of the “recompute on re-render” piece of how React works also means that things like &lt;code&gt;const myFunc = () =&gt; {}&lt;/code&gt; are &lt;em&gt;also&lt;/em&gt; recomputed on component re-render, unless wrapped in &lt;code&gt;useCallback&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;All this to say that it is not uncommon to see a single component in React with a bunch of complex &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, and &lt;code&gt;useMemo&lt;/code&gt; hooks, for not that much resulting UX. And these hooks tend to have many easy-to-miss idiosyncrasies that make it a little too easy to write bugs with.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There is a reason why there is a joke that you could rename &lt;code&gt;useEffect&lt;/code&gt; to &lt;code&gt;useFootGun&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In comparison, again, local variables are stateful in Svelte without any sort of special syntax. And for derived variables or effectful code, it just requires using a reactive declaration or reactive statements, respectfully. There is no need for &lt;code&gt;useMemo&lt;/code&gt; or &lt;code&gt;useCallback&lt;/code&gt; entirely!&lt;/p&gt;
&lt;p&gt;This is far from my best work in either framework, but here is a slapped together example that shows how much more boilerplate and complexity is generally required in React than in Svelte for a reactive UX. Notice how not only does the simplified state management result in less code, but I’m also able to take advantage of bi-directional binding and other shortcuts in Svelte.&lt;/p&gt;
&lt;p&gt;Large example, so click to expand:&lt;/p&gt;
&lt;details&gt;
	&lt;summary&gt;Click to show / hide React Version&lt;/summary&gt;
&lt;p&gt;React:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; React, { useState, useEffect, useCallback } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;react&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Mock function, invalidates coupon because special holiday is over&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; checkCoupon&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  new&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;rej&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setTimeout&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1000&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; App&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;salesTax&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setSalesTax&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; useState&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;6.5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;basketItems&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setBasketItems&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; useState&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;([]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;couponActive&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setCouponActive&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; useState&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;subTotal&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setSubTotal&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; useState&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;total&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setTotal&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; useState&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // If we don&apos;t use `useCallback`, this function is recreated on every re-render&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; addItem&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; useCallback&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    setBasketItems&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;basket&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;basket, { cost: Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;random&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }, [setBasketItems]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  useEffect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; preTaxSum &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; basketItems.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;reduce&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;running&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;current&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; running &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; current.cost,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    preTaxSum &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; preTaxSum &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (couponActive &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0.8&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Note the extra indirection here ^ and below;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // we can&apos;t just assign directly to `subTotal` and reuse for total&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    setSubTotal&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(preTaxSum);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    setTotal&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(preTaxSum &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (salesTax &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; preTaxSum);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }, [couponActive, salesTax, basketItems, setTotal, setSubTotal]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  useEffect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // useEffect does not support making the outer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // function async, so we have to do this.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Another hook foot-gun 🙃&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; verifyCoupon&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; verified&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; checkCoupon&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;verified) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        setCouponActive&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;warn&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Coupon is no longer valid&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (couponActive) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;      verifyCoupon&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }, [couponActive, setCouponActive, checkCoupon]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Coupon?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;input&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          type&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;checkbox&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          checked&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{couponActive}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          onChange&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setCouponActive&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(evt.target.checked)}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Sales Tax ({salesTax.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toFixed&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)}%)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;input&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          type&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;range&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          value&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{salesTax}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          min&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          max&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;50&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          step&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;0.5&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          onChange&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setSalesTax&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parseFloat&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(evt.target.value))}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;button&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; onClick&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{addItem}&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Add Item&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Item count = {basketItems.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Subtotal = ${subTotal.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toFixed&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Total = ${total.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toFixed&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
	&lt;summary&gt;Click to Show / Hide Svelte Version&lt;/summary&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Mock function, invalidates coupon because special holiday is over&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; checkCoupon&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    new&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;rej&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setTimeout&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1000&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Easy state variables!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; salesTax &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6.5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; basketItems &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; couponActive &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; subTotal &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; addItem&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  basketItems &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;basketItems, { cost: Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;random&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Reactive / derived / side-effects&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  $&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;     subTotal &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; basketItems.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;reduce&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;running&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;current&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; running &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; current.cost,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subTotal &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; subTotal &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (couponActive &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0.8&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  $&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: total &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; subTotal &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (salesTax &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; subTotal;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  $&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (couponActive) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; verified&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; checkCoupon&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;verified) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        couponActive &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;warn&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Coupon is no longer valid&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  })();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  Coupon?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;input&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;checkbox&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    bind:checked&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;{couponActive}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  Sales Tax ({salesTax.toFixed(2)}%)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;input&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;range&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    bind:value&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;{salesTax}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    min&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    max&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;50&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    step&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;0.5&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;button&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on:click&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;{addItem}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  Add Item&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Item count = {basketItems.length}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Subtotal = ${subTotal.toFixed(2)}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Total = ${total.toFixed(2)}&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;h2 id=&quot;sfcs-are-sfc-so-frequently-cool-and-template-syntax-vs-jsx&quot;&gt;SFCs are SFC (so-frequently-cool) and Template Syntax vs JSX&lt;/h2&gt;
&lt;p&gt;For standard Svelte projects, each of your components are going to be built as a separate &lt;em&gt;SFC&lt;/em&gt; (Single File Component).&lt;/p&gt;
&lt;p&gt;These are composed of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Any amount of regular HTML&lt;/li&gt;
&lt;li&gt;Svelte templating code / syntax (not JSX)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; section (imports, exports, logic, assignments, etc.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;style&gt;&lt;/code&gt; section, for styling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It should be noted that you can omit any of the above and there are no constraints on how many root nodes a component can have.&lt;/p&gt;
&lt;p&gt;For example, here is a valid Svelte SFC:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; name &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;world&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;h1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Hello {name}!&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;h1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;style&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;	h1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;red&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;style&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But, also so is this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Hello&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a few features of SFCs that are winners in my book:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Colocation! I find that having all three things - the logic, markup, and styling - all self-contained in one location, per-component, is a huge win for reducing complexity and keeping things clean.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;style&gt;&lt;/code&gt; code is automatically scoped to just that component!
&lt;ul&gt;
&lt;li&gt;This means you can do something like &lt;code&gt;p { font-size: 30px; }&lt;/code&gt; and it &lt;em&gt;only&lt;/em&gt; changes the font-size of &lt;code&gt;&amp;#x3C;p&gt;&lt;/code&gt; tags within that file.&lt;/li&gt;
&lt;li&gt;This is a game-changer if you are coming from React or vanilla HTML + CSS&lt;/li&gt;
&lt;li&gt;For many projects, this &lt;em&gt;can&lt;/em&gt; eliminate the need for things like SASS, CSS-in-JS, Tailwind, or other libraries that try to help with CSS complexity&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A lot of restrictions that exist in JSX / React - such as only having one root node (requiring &lt;code&gt;&amp;#x3C;Fragment&gt;&lt;/code&gt; to wrap if necessary), and having &lt;code&gt;class&lt;/code&gt; be a reserved keyword, thus requiring &lt;code&gt;className=&quot;&quot;&lt;/code&gt; - none of  these exist in Svelte’s templating syntax&lt;/li&gt;
&lt;li&gt;Valid HTML is valid Svelte code - you can just drop entire blocks of HTML into a Svelte SFC and it should work. The whole thing feels very natural and a shift back towards using the actual HTML standard more 💪&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, this might be a personal preference, but I prefer Svelte’s logic blocks to JSX’s use of pure JS, as I feel it results in a flatter structure that is more readable.&lt;/p&gt;
&lt;h3 id=&quot;shorthand-syntax-everywhere&quot;&gt;Shorthand Syntax Everywhere&lt;/h3&gt;
&lt;p&gt;A powerful feature of Svelte’s templating / markup language is that it has a lot of shorthand syntax throughout different areas. Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using variable name directly, as binding
&lt;ul&gt;
&lt;li&gt;In JSX, you often see a lot of code like &lt;code&gt;&amp;#x3C;Book author={author} /&gt;&lt;/code&gt;. In Svelte, if the variable name is the same as the prop, you can use a shortcut: &lt;code&gt;&amp;#x3C;Book {author} /&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;This works for bindings too: &lt;code&gt;bind:author&lt;/code&gt; instead of &lt;code&gt;bind:author={author}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preventDefault&lt;/code&gt; and other DOM event &lt;em&gt;modifiers&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;If you have done web development, you might be familiar with &lt;code&gt;event.preventDefault()&lt;/code&gt;, to prevent the default event handler from running. Svelte has a shortcut to apply this, and other modifiers, to &lt;a href=&quot;https://svelte.dev/docs/element-directives#on-eventname&quot;&gt;event listeners&lt;/a&gt;: &lt;code&gt;on:eventname|modifiers={handler}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;&amp;#x3C;form on:submit|preventDefault={handleSubmit}&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CSS bindings
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties&quot;&gt;CSS variables / custom properties&lt;/a&gt; have become &lt;em&gt;very&lt;/em&gt; powerful and useful in modern web development, and with Svelte, you can set them, inline with your markup, either on entire components with &lt;a href=&quot;https://svelte.dev/docs/component-directives#style-props&quot;&gt;component style props&lt;/a&gt; (&lt;code&gt;--my-css-var={val}&lt;/code&gt;), or with &lt;a href=&quot;https://svelte.dev/docs/element-directives#style-property&quot;&gt;a style directive&lt;/a&gt; (&lt;code&gt;style:--my-css-var={val}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Classes can be conditionally attached to DOM nodes, with &lt;a href=&quot;https://svelte.dev/docs/element-directives#class-name&quot;&gt;&lt;code&gt;class:&lt;/code&gt; bindings&lt;/a&gt;. E.g., &lt;code&gt;&amp;#x3C;div class=&quot;expander&quot; class:open&gt;&lt;/code&gt; to conditionally combine &lt;code&gt;.open&lt;/code&gt; with &lt;code&gt;.expander&lt;/code&gt;, based on a local &lt;code&gt;open&lt;/code&gt; variable.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;And tons of other &lt;a href=&quot;https://svelte.dev/docs/element-directives&quot;&gt;&lt;em&gt;Element Directives&lt;/em&gt;&lt;/a&gt;, &lt;a href=&quot;https://svelte.dev/docs/component-directives&quot;&gt;&lt;em&gt;Component Directives&lt;/em&gt;&lt;/a&gt;, &lt;a href=&quot;https://svelte.dev/docs/special-tags&quot;&gt;&lt;em&gt;Special Tags&lt;/em&gt;&lt;/a&gt;, and more!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;batteries-included&quot;&gt;Batteries Included&lt;/h2&gt;
&lt;p&gt;One of the common concerns or complaints I hear when it comes to considering Svelte is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;But there are more libraries out there for React!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;True… and yes, sometimes this is a legitimate concern, but this also brings me to one of my favorite things about Svelte, which is that &lt;em&gt;so&lt;/em&gt; much stuff is provided out of the box for state management, reactive bindings, form logic, styling, etc., whereas with React you pretty much just get… JSX and hooks.&lt;/p&gt;
&lt;p&gt;This isn’t even a comprehensive list, but here are some random freebies that are included with Svelte that I enjoy using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Per-component automatic CSS scoping (mentioned in the SFC section above as well)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/docs/svelte-transition&quot;&gt;Fancy transitions&lt;/a&gt;, &lt;a href=&quot;https://svelte.dev/docs/svelte-animate&quot;&gt;animations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;global state management / &lt;a href=&quot;https://svelte.dev/docs/svelte-store&quot;&gt;stores&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Very strong &lt;a href=&quot;https://svelte.dev/docs/typescript&quot;&gt;TypeScript support&lt;/a&gt;, but not opinionated about it - with support for per-component opt-in if you’d like&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/docs/custom-elements-api&quot;&gt;Support for custom elements / web components&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plus, if you use the standard Svelte tooling - such as &lt;a href=&quot;https://kit.svelte.dev/&quot;&gt;SvelteKit&lt;/a&gt; - you’ll also get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hot module reloading (aka HMR)&lt;/li&gt;
&lt;li&gt;Advanced rendering options (SSG, SSR, hybrid, etc.)&lt;/li&gt;
&lt;li&gt;Routing system, optional eager pre-loading, etc.&lt;/li&gt;
&lt;li&gt;Extensible settings / configuration (does not require “ejecting”, unlike CRA, for full control)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;incredible-documentation-and-tooling&quot;&gt;Incredible Documentation and Tooling&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://svelte.dev/docs/introduction&quot;&gt;Svelte docs&lt;/a&gt; are, hands-down, some of the best I have ever seen.&lt;/p&gt;
&lt;p&gt;In my opinion, they strike the perfect balance between detail and brevity, between being low-level and high-level, and between being beginner-friendly but also not leaving out the info you need.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 I’ve written about the importance of balanced documentation before - see  &lt;a href=&quot;https://joshuatz.com/posts/2022/high-level-docs-matter/&quot;&gt;&lt;em&gt;High Level Docs Matter&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In addition, Svelte has &lt;a href=&quot;https://learn.svelte.dev/tutorial/welcome-to-svelte&quot;&gt;guided tutorials&lt;/a&gt;, &lt;a href=&quot;https://svelte.dev/examples/hello-world&quot;&gt;live examples&lt;/a&gt;, and a &lt;a href=&quot;https://svelte.dev/repl/&quot;&gt;free REPL&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;its-fun-to-use-simple-and-youll-write-less-code-with-less-bugs&quot;&gt;It’s Fun to Use, Simple, and You’ll Write Less Code with Less Bugs&lt;/h2&gt;
&lt;p&gt;I’m ending with this one, because if I put it first, I have a feeling it would turn people off from this post. Saying a framework is “fun” and “simple” is not exactly an objective statement, and honestly usually &lt;em&gt;&lt;strong&gt;I&lt;/strong&gt;&lt;/em&gt; get irked when I read something like ”___ is magical”, but… y’all… honestly Svelte &lt;em&gt;&lt;strong&gt;IS&lt;/strong&gt;&lt;/em&gt; magical 🎉&lt;/p&gt;
&lt;p&gt;Coding with Svelte, above all other UX frameworks I’ve used, is the closest I’ve felt to having my keyboard be an immediate extension of my thoughts - I can go from an idea to functional prototype so fast that it feels like I have a mind-link with my computer (OK, maybe slight exaggeration, but you get the picture).&lt;/p&gt;
&lt;p&gt;With other frameworks, like React, I find that I have to add in a step to translate my mental model into the frameworks model for doing things - e.g., I know how I want to make this UX interactive, but how do I get this working with React’s &lt;code&gt;useEffect&lt;/code&gt; hook and its other idiosyncrasies?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🐍 If you are familiar with Python and a fan of how it tends to orient itself (with &lt;em&gt;pythonic&lt;/em&gt; approaches, &lt;a href=&quot;https://legacy.python.org/dev/peps/pep-0020/&quot;&gt;the Zen of Python&lt;/a&gt;) towards what many would call “natural” or “simpler” coding, than I think you will like what Svelte has to offer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I would also add that, by allowing for less complicated code that is more readable and comprehensible, Svelte code (on average) likely results in fewer bugs. Or at least bugs that are easier to fix.&lt;/p&gt;
&lt;h2 id=&quot;nothings-perfect&quot;&gt;Nothing’s Perfect&lt;/h2&gt;
&lt;p&gt;It feels dishonest to only talk about the good things, so here are some things that are less than perfect when it comes to Svelte.&lt;/p&gt;
&lt;h3 id=&quot;reactivity-in-svelte-requires-a-mental-model-shift&quot;&gt;Reactivity in Svelte Requires a Mental Model Shift&lt;/h3&gt;
&lt;p&gt;In general, “thinking reactively” in Svelte will take some getting used to, and IMHO, will be the most challenging part of onboarding into the Svelte world for someone new to Svelte.&lt;/p&gt;
&lt;p&gt;In particular, &lt;em&gt;Reactive Statements&lt;/em&gt;, i.e. the &lt;code&gt;$: {}&lt;/code&gt; blocks in a Svelte component, can be tricky and have some “gotchas”.&lt;/p&gt;
&lt;p&gt;However, that being said, this one is really not so bad because A) it is nowhere near as bad as it is for &lt;code&gt;useEffect&lt;/code&gt; with React (sorry React, but you know its true), and B) The new &lt;a href=&quot;https://svelte.dev/blog/runes&quot;&gt;&lt;em&gt;Runes&lt;/em&gt; feature&lt;/a&gt; in Svelte 5 should solve this (the gotchas, that is).&lt;/p&gt;
&lt;h3 id=&quot;the-talent-pool-is-smaller&quot;&gt;The talent pool is smaller&lt;/h3&gt;
&lt;p&gt;Yes, there are far fewer experienced Svelte devs out there than there are React devs.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;However&lt;/em&gt;, I would argue that it takes far less time for the average developer to learn Svelte and become productive with it than it does to learn React.&lt;/p&gt;
&lt;p&gt;I would also argue that any developer that has taken the time to teach themselves Svelte probably has great taste and admires clean code (&lt;em&gt;looks in mirror&lt;/em&gt;, &lt;em&gt;does a cheesy wink to camera&lt;/em&gt; 😉).&lt;/p&gt;
&lt;h3 id=&quot;the-svelte-ecosystem-isnt-as-rich-as-reacts&quot;&gt;The Svelte ecosystem isn’t as “rich” as React’s&lt;/h3&gt;
&lt;p&gt;Sorry Svelte, but yes, this one is categorically true. You are going to find far more libraries for &lt;em&gt;“doing _ in React”&lt;/em&gt; than &lt;em&gt;“doing _ in Svelte”&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This is probably the strongest case against Svelte, which is saying something becomes it amounts to a catch-22 of &lt;em&gt;“people aren’t building libraries for your framework because it isn’t popular enough, and we don’t want to help it become popular because it doesn’t have enough 3rd party libraries”&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Sidenote: I’ve never been a fan of the “because its popular” argument. Battle-tested and robust? Sure, great arguments! “Because the popular kids are using it”? Less so.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Reminder: Facebook built React to solve &lt;em&gt;Facebook-specific&lt;/em&gt; and &lt;em&gt;Facebook-sized&lt;/em&gt; problems. The same idea applies to a lot of frameworks; always think carefully about the &lt;em&gt;actual&lt;/em&gt; problems a tool is solving before picking it to apply to your use-case.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;conclusion--wrap-up&quot;&gt;Conclusion / Wrap-Up&lt;/h2&gt;
&lt;p&gt;Conclusion: You are silly goose if you don’t use Svelte.&lt;/p&gt;
&lt;p&gt;JUST KIDDING. My go-to sayings are “use the right tool for the job” and “strong opinions, loosely held”, so ultimately I think you should use whatever framework is the best fit for &lt;em&gt;your&lt;/em&gt; &lt;em&gt;&lt;strong&gt;specific&lt;/strong&gt;&lt;/em&gt; project. Maybe it’s Svelte, maybe it’s React, maybe it’s something completely different like Vue or Angular.&lt;/p&gt;
&lt;p&gt;But I also think people are coming around and I’m not alone in this; Svelte has consistently scored highly in developer surveys for being “desired”.&lt;/p&gt;
&lt;p&gt;At the end of the day, I value simple, clean code that is easy to write, read, and maintain - for me personally, Svelte is the framework in which I find this the easiest to achieve, with the least amount of time and effort.&lt;/p&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.joshuatz.com/cheatsheets/svelte-js/&quot;&gt;My Svelte Cheatsheet&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;This also has lots of links to other resources&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/docs&quot;&gt;The official Svelte docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kit.svelte.dev/docs/introduction&quot;&gt;The official SvelteKit docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;thanks-for-reading-&quot;&gt;Thanks for Reading 👋&lt;/h3&gt;
&lt;p&gt;I know this was a lengthy post - one could say… it’s not so &lt;em&gt;svelte&lt;/em&gt; (&lt;em&gt;ducks tomatoes being thrown&lt;/em&gt;, &lt;em&gt;audible boos&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Anyways, thanks for reading and I hope even if you don’t agree with all my points, you at least got something useful out of it.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Shell Heredocs - for Fun and Profit, in Bash and Beyond</title><link>https://joshuatz.com/posts/2023/shell-heredocs---for-fun-and-profit-in-bash-and-beyond/</link><guid isPermaLink="true">https://joshuatz.com/posts/2023/shell-heredocs---for-fun-and-profit-in-bash-and-beyond/</guid><description>What are bash heredocs (Here Documents), and why they are so fun and useful to have. Covers both the basics and advanced usage and tricks.</description><pubDate>Fri, 17 Nov 2023 23:22:11 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;One of features of shell scripting (e.g. with bash or zsh) that I feel is undervalued is &lt;em&gt;heredocs&lt;/em&gt; (aka &lt;em&gt;Here Documents&lt;/em&gt;). At their most simple level, heredocs are a way to embed and escape a multi-line block of text within shell code:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;EOF&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;This text is inside a heredoc!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;I can put pretty much anything I want here, including whitespace and special characters:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;  -&gt; $&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    -&gt; 🎉&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;🔗 Docs: &lt;a href=&quot;https://www.gnu.org/software/bash/manual/bash.html#Here-Documents&quot;&gt;GNU Bash Reference - &lt;em&gt;Here Documents&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But wait - that’s not all! - bash heredocs can be &lt;em&gt;so&lt;/em&gt; much more! And I’m here to show you why (and how).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 A lot of these tips about piping and redirection apply to shell strings in general; it is just that heredocs are the easiest to use for escaping and multi-line purposes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;🤩 On a personal level, there is something that really appeals to me about heredocs, both in shell scripting, and generally across all programming languages. There is something that feels really pleasant about the ability to write self-contained modules that can inline blocks of whatever you’d like, as opposed to having to break everything out into a separate file / component. Of course, you can overdo this too…&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;the-basics-of-heredocs&quot;&gt;The Basics of heredocs&lt;/h3&gt;
&lt;p&gt;First, a quick lesson in heredocs (or a refresher, if you are already familiar). If you want to, you can also &lt;a href=&quot;#fun-and-productive-use-cases-for-heredocs&quot;&gt;skip ahead to the use-cases / examples&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The general syntax for heredocs in shell scripts is:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[COMMAND] &amp;#x3C;&amp;#x3C; &quot;DELIMITER&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;HEREDOC_CONTENTS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;DELIMITER&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;or&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[COMMAND] &amp;#x3C;&amp;#x3C;- &quot;DELIMITER&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;HEREDOC_CONTENTS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;DELIMITER&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Breaking this down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;em&gt;command&lt;/em&gt; is optional
&lt;ul&gt;
&lt;li&gt;You often see &lt;code&gt;cat&lt;/code&gt; being used, if you simply need to pipe to stdout&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Double left redirect operators (&lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt;) are always required&lt;/li&gt;
&lt;li&gt;The hyphen / minus (&lt;code&gt;-&lt;/code&gt;) after the redirect operators is optional; if you include it, it will allow for and ignore leading whitespace for the &lt;code&gt;HEREDOC_CONTENTS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;DELIMITER&lt;/code&gt; string can be anything, but the most common convention is to use the string &lt;code&gt;EOF&lt;/code&gt; (&lt;em&gt;end-of-file&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;The double (or single) quotes around the delimiter is optional; if used, it will prevent interpolation inside the heredoc
&lt;ul&gt;
&lt;li&gt;For example, if you want to print the literal string &lt;code&gt;$PWD&lt;/code&gt; without having it evaluated, you would want to quote the delimiter&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Finally, the ending delimiter has to match the starting one, should &lt;em&gt;never&lt;/em&gt; be quoted (regardless of the starting delimiter), and should never be indented&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can redirect and pipe around Heredocs just like other strings, which makes them very powerful. For example, you can mix interpolation, command substitution, and stdout redirection, all in one go:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; tee&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; system-info.txt&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Directory: &lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$PWD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;OS: $(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;uname&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -a&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And this is just scratching the surface; read on to see some fun and productive ways to use (or abuse)  heredocs:&lt;/p&gt;
&lt;h2 id=&quot;fun-and-productive-use-cases-for-heredocs&quot;&gt;Fun and Productive Use Cases for Heredocs&lt;/h2&gt;
&lt;h3 id=&quot;using-heredocs-to-escape-and-store-special-text&quot;&gt;Using heredocs to escape and store special text&lt;/h3&gt;
&lt;p&gt;One of the most common uses for heredocs, although not the most exciting (IMHO), is to use them to escape large messy text strings. Optionally also capturing as a variable.&lt;/p&gt;
&lt;p&gt;For example, we might want to produce a string that contains a dollar sign and line breaks, without these causing issues with the shell interpreter / shell expansion. We can do this with a single heredoc, by making sure we quote the leading delimiter:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Notice that we are quoting EOF to&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# prevent expansion / interpretation of contents&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;receipt&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;EOF&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Item A: $20&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Item B: $40&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;-----------&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Total: $60&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Have a great day!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$receipt&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;using-heredocs-to-store-and-execute-non-shell-code&quot;&gt;Using heredocs to store and execute non-shell code&lt;/h3&gt;
&lt;p&gt;This is probably my favorite use of heredocs, and the one that boosts my productivity the most.&lt;/p&gt;
&lt;p&gt;Imagine you are working on a project with mixed programming languages (Python + JavaScript with Nodejs, for example) and you have a set of little utility functions that you like to run, but they are personal enough to your workflow that you don’t want to commit them to the shared codebase.&lt;/p&gt;
&lt;p&gt;One option is to write your own CLI file, or set of CLI files, and &lt;code&gt;.gitignore&lt;/code&gt; them. However, with mixed programming languages in the same project, you might end up with something like &lt;code&gt;do_thing.py&lt;/code&gt;, &lt;code&gt;do_other_thing.py&lt;/code&gt;, &lt;code&gt;do_js_thing.js&lt;/code&gt;, and so on. This might be the approach that most people take, but I find it cumbersome.&lt;/p&gt;
&lt;p&gt;However, another option is to inline them - as many different languages and utilities as you’d like - in a shell script:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;EOF&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;from my_lib import do_thing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;print(&quot;hello from Python!&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;do_thing()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the surface, this might not seem that useful, but trust me, this can come in very handy. Especially when you combine it with task runners, like &lt;a href=&quot;https://github.com/go-task/task&quot;&gt;my favorite (&lt;code&gt;task&lt;/code&gt;)&lt;/a&gt;. You can then execute multiple scripts, in &lt;em&gt;multiple&lt;/em&gt; different programming languages, all from a single file / CLI ✨:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;tasks&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # Standard shell scripting&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  clean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;rm -rf ./logs/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  test&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;npm run test&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # Python heredoc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  python_util&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    python &amp;#x3C;&amp;#x3C; &quot;EOF&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    from my_lib import do_thing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    print(&quot;hello from Python!&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    do_thing()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # More advanced python heredoc, with variable from shell&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # NOTE: `{{.CLI_ARGS}}` is specific to `task` - outside of this file, you&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # would usually just use `$1`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  manage:delete_user_by_last_name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    cat &amp;#x3C;&amp;#x3C; &quot;EOF&quot; | xargs -0 python3 my_backend/manage.py shell --command&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    from my_app import models&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    print(&quot;🗑 Deleting user with last name of {{.CLI_ARGS}}&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    models.User.objects.filter(last_name=&quot;{{.CLI_ARGS}}&quot;).delete()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    echo &quot;✅ User deleted!&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # NodeJS heredoc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  node_util&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    node &amp;#x3C;&amp;#x3C; &quot;EOF&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    const { myNodeUtil } = require(&apos;./src/utils.js&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    myNodeUtil();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I can run &lt;code&gt;task python_util&lt;/code&gt; to run my python command, &lt;code&gt;task node_util&lt;/code&gt; to run my node script, and so on. Single CLI, single file to manage, and as many utilities as my yak-shaving heart desires.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The downside to this approach is that IDE syntax highlighting of code within the heredocs is generally not very good.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;taking-advantage-of-more-libraries-from-your-terminal&quot;&gt;Taking Advantage of More Libraries From Your Terminal&lt;/h3&gt;
&lt;p&gt;This is basically an extension of the above section on mixing other scripting languages into shell scripts - the idea that you can use this approach to pull in more standard libraries (and 3rd party libraries) into your terminal, more than just the regular UNIX commands you might be used to.&lt;/p&gt;
&lt;p&gt;Look, I like standard shell commands, the UNIX philosophy, and I get the importance of being comfortable with shell scripting, but at the same time, I think there is no denying that some of the core utilities might not have the best ergonomics. For example, I would argue that both NodeJS’s and Python’s standard library functions for dealing with regular expressions beat &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt; by a large margin.&lt;/p&gt;
&lt;p&gt;With heredocs, it becomes much easier to pull in various standard library functions from different scripting languages directly into your shell.&lt;/p&gt;
&lt;p&gt;For example, if we have a JSON file, &lt;code&gt;test.json&lt;/code&gt; that matches this format:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;a&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;b&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;c&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is all it takes to get the length of the nested array with Node:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;JSON_STRING&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; test.json)&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; node&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;EOF&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;console.log(JSON.parse(process.env.JSON_STRING).a.b.c.length);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even if you disagree with me on the usability of some of these commands, I think it is worth considering the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scripting standard libraries are often &lt;em&gt;more&lt;/em&gt; portable than shell commands. Many developers have at least Python and NodeJS installed, and both of these languages have stable standard libraries that largely operate the same across OSes and are backwards compatible. For comparison, lots of shell commands have identical names across platforms but act completely different on Mac vs Linux.
&lt;ul&gt;
&lt;li&gt;To put it simply, not everyone is going to have &lt;code&gt;jq&lt;/code&gt; installed, but everyone with Node has &lt;code&gt;JSON.parse()&lt;/code&gt; available.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The knowledge that you build working with a scripting language is more transferable than that of a specific shell command
&lt;ul&gt;
&lt;li&gt;Knowing how to use your terminal in general is important, but at the same time memorizing the exact flags to a specific command is going to be less useful than working on your general coding skills&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Why not take advantage of a battle-tested standard library, instead of rolling your own?&lt;/li&gt;
&lt;/ul&gt;
&lt;details&gt;
	&lt;summary&gt;More advanced example (click to expand)&lt;/summary&gt;
&lt;p&gt;Here is a more complex, but slightly contrived, example.&lt;/p&gt;
&lt;p&gt;Let’s say that I want to read in a JSON file, find any key matching the pattern of &lt;code&gt;user_image_\d+&lt;/code&gt;, and then extract out the ID from the key (the digits), and the filename and extension from the string (&lt;code&gt;filename.ext&lt;/code&gt;). Sure, I could probably get this done using a combination of &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt; or &lt;code&gt;sed&lt;/code&gt;, and &lt;a href=&quot;https://github.com/jqlang/jq&quot;&gt;&lt;code&gt;jq&lt;/code&gt;&lt;/a&gt;, but it is going to be convoluted (capture groups are a pain in both &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt;) and involve me learning a bunch of syntax that is hyper-specific to &lt;code&gt;jq&lt;/code&gt; and will not benefit me in other contexts. For comparison, I can accomplish this easily with NodeJS or Python (this example will be with Node though):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get_user_data&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	JSON_FILE_PATH&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$1&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; node&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;EOF&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;const fs = require(&apos;fs&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;const rawData = JSON.parse(fs.readFileSync(process.env.JSON_FILE_PATH));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;const extractedData = [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Object.keys(rawData).forEach((key) =&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	const userId = /user_image_(\d+)$/.exec(key)?.[1];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	if (!userId) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;		return;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	const {fileName, extension} = /^(?&amp;#x3C;fileName&gt;.+)(?&amp;#x3C;extension&gt;\.[^.]+)$/.exec(rawData[key])?.groups;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	extractedData.push({userId, fileName, extension});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;fs.writeFileSync(&apos;extracted.json&apos;, JSON.stringify(extractedData));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;h3 id=&quot;using-heredocs-as-real-files-and-virtual-files&quot;&gt;Using heredocs as real files and virtual files&lt;/h3&gt;
&lt;p&gt;We’ve already covered piping or redirecting heredocs to commands that are expecting standard input (stdin) , but what about commands that &lt;em&gt;only&lt;/em&gt; take files?&lt;/p&gt;
&lt;p&gt;The route often taken here would be to pipe the heredoc to an actual temp file that gets written out, then pass it to the command, like so:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;TEMP_PATH&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;tempfile&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $TEMP_PATH &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Hello!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Get filesize&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;du&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -sb&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $TEMP_PATH&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rm&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $TEMP_PATH&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, with my obsession with seeing how much I can inline into my Taskfiles / shell scripts, I also went to the trouble of figuring out how to do this without needing the intermediate temp file.&lt;/p&gt;
&lt;p&gt;There are two main workarounds.&lt;/p&gt;
&lt;p&gt;The first is &lt;a href=&quot;https://mywiki.wooledge.org/ProcessSubstitution&quot;&gt;&lt;em&gt;process substitution&lt;/em&gt;&lt;/a&gt;. We can use &lt;code&gt;&amp;#x3C;()&lt;/code&gt; to get something that acts like a file descriptor (or is one), and pass our heredoc into it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;diff&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#x3C;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Line 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#x3C;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Line 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Line 2 - I&apos;m new!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are concerned about the readability of the above snippet, you could separate out the heredocs as variables first - I think this makes it more legible, but also adds more lines:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;version_a&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Line 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;version_b&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Line 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Line 2 - I&apos;m new!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;diff&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#x3C;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$version_a&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;)&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#x3C;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$version_b&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, there is actually one final trick up our sleeve to pass a heredoc to a command that only accepts file paths: we can pipe the heredoc, and then pass stdin as a file descriptor!&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; wc&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -l&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /dev/fd/0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;print(&quot;hello from Python&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;print(&quot;Line 2&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# This also works&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; wc&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -l&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /dev/fd/0&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;print(&quot;hello from Python&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;print(&quot;Line 2&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;I’m using &lt;code&gt;/dev/fd/0&lt;/code&gt; instead of &lt;code&gt;/dev/stdin&lt;/code&gt;. Either should realistically work in most OSes, but &lt;code&gt;/0&lt;/code&gt; and &lt;code&gt;/1&lt;/code&gt; (for &lt;code&gt;/dev/stdout&lt;/code&gt;) might be &lt;a href=&quot;https://unix.stackexchange.com/a/36404/428258&quot;&gt;more portable&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This might fall under the “hey is Josh taking this too far?” or “maybe we should cut off his coffee intake” side of things, as this is taking the “how many things can we inline” to the extreme, perhaps beyond where it should. But hey, I get a kick out of this stuff!&lt;/p&gt;
&lt;h2 id=&quot;here-strings&quot;&gt;Here Strings&lt;/h2&gt;
&lt;p&gt;So far I’ve exclusively been talking about heredocs / here documents, but it is worth mentioning that there are also &lt;a href=&quot;https://www.gnu.org/software/bash/manual/bash.html#Here-Strings&quot;&gt;&lt;em&gt;Here Strings&lt;/em&gt;&lt;/a&gt;. They use a triple redirection operator (&lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&lt;/code&gt;), but as they can only operate on a single quoted string (aka &lt;em&gt;word&lt;/em&gt;), they have more limited utility.&lt;/p&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;/h2&gt;
&lt;p&gt;I hope you found this post useful, or at least entertaining.&lt;/p&gt;
&lt;p&gt;If you did, you might also like my &lt;a href=&quot;https://docs.joshuatz.com/cheatsheets/bash-and-shell/&quot;&gt;my bash / shell scripting cheatsheet&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And on that note,&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;✨ Happy coding!!! ✨&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;   Sincerely, &lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$USER&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;   $(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; +%F)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Writing Slack Messages with Markdown (Multiple Options)</title><link>https://joshuatz.com/posts/2023/writing-slack-messages-with-markdown-multiple-options/</link><guid isPermaLink="true">https://joshuatz.com/posts/2023/writing-slack-messages-with-markdown-multiple-options/</guid><description>How to write or paste Markdown into Slack messages and have it formatted correctly, while still keeping the format toolbar, by using HTML pasting.</description><pubDate>Tue, 14 Mar 2023 17:27:10 GMT</pubDate><content:encoded>&lt;h2 id=&quot;tldr&quot;&gt;TLDR&lt;/h2&gt;
&lt;p&gt;The short answer to “how do I write a Slack message in Markdown” is that there are two options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Option A (not very good): Turn your global setting / preference for message formatting to &lt;em&gt;“Format messages with markup”&lt;/em&gt; (documented &lt;a href=&quot;https://slack.com/help/articles/360039953113-Set-your-message-formatting-preference&quot;&gt;here&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;This is not very ideal, because it is not true Markdown, but rather their own markdown-adjacent syntax, and also requires that you globally disable the formatting toolbar / WYSIWYG (what-you-see-is-what-you-get) editing. More on this in &lt;a href=&quot;#wait-i-thought-slack-supported-pasting-markdown&quot;&gt;my section on the pitfalls of this setting below&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Option B (my favorite!): There is a cool trick, where you can author your message in full Markdown, in any editor you chose and then get it into a Slack message. You need to first turn it into an HTML clipboard buffer, and then paste &lt;em&gt;that&lt;/em&gt; into Slack. You can jump to that solution &lt;a href=&quot;#the-solution--shortcut&quot;&gt;here&lt;/a&gt;, or read on for the background on how this works and why it is necessary.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;
&lt;p&gt;I want to be very clear: this post is about turning Markdown into a slack message that you send to someone, not the other way around, or anything to deal with their API.&lt;/p&gt;
&lt;p&gt;Put another way: this is a solution for if you want to be able to copy and paste rich markdown into Slack and have it actually format the message correctly.&lt;/p&gt;
&lt;p&gt;Why? Well,&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I like writing in Markdown&lt;/li&gt;
&lt;li&gt;For really long async messages, it makes me feel better to write it in a different editor (like VS Code), so the other person isn’t seeing &lt;em&gt;“Joshua is typing…”&lt;/em&gt; for multiple minutes while I write (and sometimes rewrite) a message. I want to be able to paste from this other editor, into Slack.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;wait-i-thought-slack-supported-pasting-markdown&quot;&gt;Wait, I thought Slack Supported Pasting Markdown?&lt;/h3&gt;
&lt;p&gt;Ah, good question. Yes and no, but mostly no. Slack supports &lt;em&gt;their&lt;/em&gt; “flavor” of a markdown &lt;em&gt;like&lt;/em&gt; syntax (which they are referring to only as “markup”) which is &lt;a href=&quot;https://slack.com/help/articles/202288908-Format-your-messages&quot;&gt;a small subset&lt;/a&gt; of the typical Markdown syntax that most developers (like myself) are used to.&lt;/p&gt;
&lt;p&gt;This alone wouldn’t be a deal breaker - many of the normal things you want regular Markdown syntax for (links, italics, etc.) are supported.&lt;/p&gt;
&lt;p&gt;However - and here is my personal deal-breaker - in order to get the message editor to accept and parse Markdown (and again, not even full markdown, just their version / “markup”), &lt;a href=&quot;https://slack.com/help/articles/360039953113-Set-your-message-formatting-preference&quot;&gt;you have to flip a global setting&lt;/a&gt; that then removes the formatting toolbar and WYSIWYG editing across Slack and forces you into &lt;em&gt;&lt;strong&gt;only&lt;/strong&gt;&lt;/em&gt; using “markup”, 100% of the time.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I haven’t tested this with every subset of their special syntax, but this is &lt;a href=&quot;https://stackoverflow.com/a/68711870/11447682&quot;&gt;at least true for links&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To me, this is the worst of both worlds; I can get some markdown syntax in my editor, but it is an incomplete version of the Markdown spec, and at the same time, if I turn that on, then I can no longer do things like press &lt;code&gt;CTRL + B&lt;/code&gt; to bold a section of text directly within the editor.&lt;/p&gt;
&lt;p&gt;This post is the result of me searching for an alternative, and finding a “shortcut” that works surprisingly well!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Side note: One of the problems with Markdown itself is that the core original spec isn’t actually used directly all that much anymore; most of the time when people are talking about Markdown, they really mean a Markdown standard that is built on top of the original one - something like CommonMark, GitHub Flavored Markdown, etc.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-solution--shortcut&quot;&gt;The Solution / Shortcut&lt;/h2&gt;
&lt;p&gt;Here it is - the pro-tip - Slack doesn’t work well with pasting Markdown, but it &lt;em&gt;does&lt;/em&gt; work well with pasted HTML!&lt;/p&gt;
&lt;p&gt;By pasted HTML, I should also be clear that I don’t mean copying and pasting something like &lt;code&gt;&amp;#x3C;b&gt;Hello&amp;#x3C;/b&gt;&lt;/code&gt; with a standard copy-and-paste operation. Rather, it is pasting out of the special &lt;code&gt;text/html&lt;/code&gt; buffer that clipboards can contain.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Side note: If you weren’t aware, most operating system clipboards can contain a plain-text buffer, &lt;em&gt;plus&lt;/em&gt; additional buffers with various file types, like RTF (rich text format), HTML, and even raw binary data. This is why you can do things like copy and paste images between applications. This isn’t documented very well - I have a WIP blog post about this that I’d like to publish as some point 😀.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 You can inspect your clipboard contents with this handy online tool - &lt;a href=&quot;https://evercoder.github.io/clipboard-inspector/&quot;&gt;Clipboard Inspector&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 This trick actually works with &lt;em&gt;many&lt;/em&gt; different applications that don’t accept Markdown but do accept pasting, so it is a great one to keep in your back pocket. In fact, sometimes this even works with programs that act like they don’t support formatting at all, as a way to get things like bolded text when there isn’t a toolbar or keyboard shortcut to enable it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;converting-markdown-to-html-clipboard-content&quot;&gt;Converting Markdown to HTML Clipboard Content&lt;/h3&gt;
&lt;p&gt;For how to convert your Markdown message to HTML and put it in your clipboard, there are a few options.&lt;/p&gt;
&lt;p&gt;Personally, I often have VS Code’s &lt;a href=&quot;https://code.visualstudio.com/docs/languages/markdown#_markdown-preview&quot;&gt;Markdown Live Preview&lt;/a&gt; tab open as I author Markdown, so I can just copy and paste directly out of the preview window to get HTML to paste into Slack.&lt;/p&gt;
&lt;p&gt;For a more automated solution, you can use scripting to A) Convert the markdown to HTML with something like &lt;a href=&quot;https://github.com/markedjs/marked&quot;&gt;marked&lt;/a&gt;, &lt;a href=&quot;https://github.com/markdown-it/markdown-it&quot;&gt;markdown-it&lt;/a&gt;, or &lt;a href=&quot;https://pandoc.org/&quot;&gt;pandoc&lt;/a&gt;, and B) Put the HTML in your clipboard with &lt;a href=&quot;https://ss64.com/osx/pbpaste.html&quot;&gt;pbpaste&lt;/a&gt; (MacOS), &lt;a href=&quot;https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/clip&quot;&gt;clip&lt;/a&gt; (Window), or something like &lt;a href=&quot;https://linux.die.net/man/1/xclip&quot;&gt;xclip&lt;/a&gt; (Linux).&lt;/p&gt;
&lt;p&gt;Here is a shell script to do this on MacOS:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;markdown_to_html_clip&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	# Get markdown as arg, or fallback to clipboard contents&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	md&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;$1&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [[ &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-z&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$md&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ]]; &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		md&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pbpaste&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	html&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npx&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; marked&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --gfm&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -s&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$md&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	html_hex&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$html&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; hexdump&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -ve&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;1/1 &quot;%.2x&quot;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	osascript&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&amp;#x3C;-&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;		set the clipboard to {«class HTML»:«data HTML${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;html_hex&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}», string:&quot;${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;md&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	EOF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;🔗 Functions for markdown conversion and clipboard manipulation, such as the above, are now in &lt;a href=&quot;https://github.com/joshuatz/dotfiles&quot;&gt;my dotfiles&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;credit--acknowledgments--wrap-up&quot;&gt;Credit / Acknowledgments / Wrap Up&lt;/h3&gt;
&lt;p&gt;I’m definitely not the first person to realize this trick with Slack. Also, in particular the information about how to get HTML back into MacOS’s clipboard was hard to find, and I have &lt;a href=&quot;https://stackoverflow.com/questions/11085654/apple-script-how-can-i-copy-html-content-to-the-clipboard#comment136570431_11089226&quot;&gt;this StackOverflow comment&lt;/a&gt; and &lt;a href=&quot;https://aaron.cc/copying-the-current-safari-tab-as-a-to-the-clipboard-as-a-clickable-link/&quot;&gt;blog post&lt;/a&gt; to thank for helping with that.&lt;/p&gt;
&lt;p&gt;Also, I realize that my “shortcut” might not be the panacea that readers might be hoping for - obviously the most ideal solution would be for Slack to implement native Markdown support without requiring that it be an all-or-nothing setting; here’s hoping that they are actively working on that.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>We Should Care More About Higher Level Documentation</title><link>https://joshuatz.com/posts/2022/we-should-care-more-about-higher-level-documentation/</link><guid isPermaLink="true">https://joshuatz.com/posts/2022/we-should-care-more-about-higher-level-documentation/</guid><description>Why we should all care more about higher-level documentation, a way to view documentation as a funnel, and an explanation of how things have changed.</description><pubDate>Mon, 26 Sep 2022 20:57:26 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;I am going to reference several projects throughout this post as both positive and negative examples of following the guidance I’m laying out; these are illustrative examples and I hope it is clear that I’m not passing judgement on these projects. Documentation is hard!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Cover Image Photo by &lt;a href=&quot;https://unsplash.com/@kimberlyfarmer?utm_source=unsplash&amp;#x26;utm_medium=referral&amp;#x26;utm_content=creditCopyText&quot;&gt;Kimberly Farmer&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/?utm_source=unsplash&amp;#x26;utm_medium=referral&amp;#x26;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;I’m going to be saying some opinionated things in this post, so to help get you on my side, I want to try a thought experiment.&lt;/p&gt;
&lt;p&gt;Pretend that you suddenly need to get up-to-speed on a new framework or library, within a short amount of time and without guidance. Maybe you just got assigned to a new project at work or started a new job. This is minute zero, and you are sitting at your computer, unfamiliar with this new “thing” and looking to get started.&lt;/p&gt;
&lt;p&gt;You type “{name_of_thing} documentation” into your preferred search engine and hit enter. You end up on the official documentation website and you are presented with the following three links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Quick Start Guide&lt;/li&gt;
&lt;li&gt;Handbook / User Manual&lt;/li&gt;
&lt;li&gt;Complete API List / Low-Level Docs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which one do you click on? Keep in mind, you are short on time and the pressure is on to get productive with this new tool quickly.&lt;/p&gt;
&lt;p&gt;If you are like me, you are smashing that &lt;em&gt;Quick Start Guide&lt;/em&gt; link as fast as possible.&lt;/p&gt;
&lt;p&gt;Once I’ve started spending some time in the project, I’m probably going to be perusing that &lt;em&gt;Handbook&lt;/em&gt; and/or &lt;em&gt;User Manual&lt;/em&gt;. Finally, I might reference the full API docs from time to time.&lt;/p&gt;
&lt;p&gt;Yet…&lt;/p&gt;
&lt;p&gt;What I seem to consistently find is that too many projects focus on documentation in the reverse order. They have beautiful full API docs, generated through some bespoke automated process, they might or might not have a handbook or manual, and the “getting started” guide is often insufficient, outdated, or simply omitted entirely.&lt;/p&gt;
&lt;p&gt;This should not be the case.&lt;/p&gt;
&lt;h2 id=&quot;thinking-of-documentation-levels-as-a-funnel&quot;&gt;Thinking of Documentation Levels as a Funnel&lt;/h2&gt;
&lt;p&gt;I’m not an expert on this, but to me it seems like you can categorize most types of documentation by their technical level and degree of abstraction. E.g., on the high-level end of the spectrum, you have a step-by-step tutorial that a completely novice can follow. On the other end, you have the actual source code and any comments contained within.&lt;/p&gt;
&lt;p&gt;I would argue that these levels of documentation are also closely correlated with how long you spend with a given piece of technology, so you can view the levels of documentation not just as a pyramid of abstraction level, but also as a funnel:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/different-levels-of-documentation.png&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto; text-align:center;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/different-levels-of-documentation.png&quot; alt=&quot;Funnel showing different levels of documentation, with decreasing level of abstraction as time spent goes on&quot; style=&quot;width:100%;max-width: 960px;height:auto;&quot; loading=&quot;lazy&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I am aware that this graphic is not an exhaustive representation of every type of documentation&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;examples-of-higher-level-documentation&quot;&gt;Examples of Higher-Level Documentation&lt;/h2&gt;
&lt;p&gt;Again, I’m calling out these projects as illustrative examples, not to shame them or cast judgement in any way. Also, things change; these projects might have improved their docs since this post was published. Anyways, here are some links to documentation that, &lt;em&gt;in my opinion&lt;/em&gt;, neglect the top part of the documentation funnel:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.selenium.dev/documentation/&quot;&gt;Selenium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://markdown-it.github.io/markdown-it/&quot;&gt;markdown-it&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For an example of a project with (again, in my opinion) excellent higher-level docs, check out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/&quot;&gt;TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/docs&quot;&gt;Svelte&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-more-effort-should-be-put-on-the-early-part-of-the-funnel&quot;&gt;Why More Effort Should be Put On The Early Part of the Funnel&lt;/h2&gt;
&lt;p&gt;This post &lt;a href=&quot;#intro&quot;&gt;started with a hypothetical scenario&lt;/a&gt;, in which you are an employee looking to get started ASAP using a new framework or library. I’m hoping that this alone was pretty convincing of why high-level documentation matters, but here are some additional thoughts on the topic.&lt;/p&gt;
&lt;h3 id=&quot;jobs-have-changed&quot;&gt;Jobs Have Changed&lt;/h3&gt;
&lt;p&gt;It used to be very common for developers to stay in a given job for many years, perhaps even spanning decades. Nowadays, for a multitude of reasons, you see more and more developers only spending a few years at a given company. And for those that stay longer, often they are switching teams and/or projects more frequently than before. With these changes in position often come changes in the developer’s day-to-day tech stack, meaning there are more developers trying to learn new things all the time.&lt;/p&gt;
&lt;p&gt;On a related note, even when staying in the same role at the same company, it seems like technology stacks are still changing faster than ever (especially in the web space).&lt;/p&gt;
&lt;p&gt;All this is to say that the funnel pictured above has been flipped 180 degrees from how it used to be. With more new developers entering the industry, more projects competing for use, more switching between stacks, and an increased urgency for getting “productive” with new things faster than before, the higher-level parts of the documentation funnel have become a bottleneck and core area of focus.&lt;/p&gt;
&lt;h3 id=&quot;your-documentation-is-a-form-of-marketing&quot;&gt;Your Documentation is a Form of Marketing&lt;/h3&gt;
&lt;p&gt;Those familiar with the term “Developer Advocate” probably already know this, but documentation is actually a form of marketing for most large projects. This is particularly the case when developers are trying to decide between multiple competing options.&lt;/p&gt;
&lt;p&gt;Going once again back to the concept of a documentation funnel, if your high-level documentation is terrible, you might get developers abandoning your project before they have even tried it.&lt;/p&gt;
&lt;h3 id=&quot;source-code-is-more-accessible-than-ever&quot;&gt;Source Code Is More Accessible Than Ever&lt;/h3&gt;
&lt;p&gt;This is more an argument for spending &lt;em&gt;less&lt;/em&gt; time on the lower-level part of the tech documentation funnel, not necessarily &lt;em&gt;more&lt;/em&gt; time on the upper part (although it should free you up to do so). My argument is that access to the actual source code of a given project is easier now than ever before and this access can often mitigate the need for (exhaustive) lower-level docs.&lt;/p&gt;
&lt;p&gt;There have been many times where I have had a question that required a deeper understanding of a framework than the docs were able to provide; usually the way this went is I would spend 10+ minutes struggling to find the answer in the official documentation, give up and just open the source code on GitHub, use search + jump-to tools to navigate to the relevant code, and get my answer directly from the code.&lt;/p&gt;
&lt;p&gt;In these instances, it often takes me only minutes to find the answer via source code, whereas I could have spent half-an-hour with the documentation and still not had a complete answer to my question.&lt;/p&gt;
&lt;p&gt;I’m not saying that you should completely omit lower-level docs (such as auto-generated API docs), but that they are slightly less important than before and your time should be spent accordingly.&lt;/p&gt;
&lt;h2 id=&quot;why-are-high-level-docs-often-neglected&quot;&gt;Why Are High-Level Docs Often Neglected?&lt;/h2&gt;
&lt;p&gt;It’s easy for me to say that more quality high-level documentation should be produced, but another thing to have it actually happen. Why is higher-level accessible documentation often neglected?&lt;/p&gt;
&lt;h3 id=&quot;writing-at-all-levels-is-tough&quot;&gt;Writing at All Levels is &lt;em&gt;Tough&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;The short answer is that writing quality documentation takes time, effort, and ability.&lt;/p&gt;
&lt;p&gt;I would argue that writing high-quality high-level (abstracted) documentation tends to actually be &lt;em&gt;more&lt;/em&gt; difficult than writing low-level documentation, which is a big part of why it is often neglected. This kind of falls into the same paradigm as the fact that often experts in various fields are not the best teachers - it is one thing to understand something at a very low level, but an entirely different can of worms to be able to translate those concepts and communicate them effectively to an audience with a range of different technical abilities and background.&lt;/p&gt;
&lt;h3 id=&quot;exploiting-the-rise-of-tech-blogs-and-community-posts&quot;&gt;Exploiting The Rise of Tech Blogs and Community Posts&lt;/h3&gt;
&lt;p&gt;This idea bleeds into the next section, when I rant about React’s documentation, but my argument here is that it &lt;em&gt;seems&lt;/em&gt; like many large projects are neglecting their higher-level docs, because they know that tech bloggers (like myself) and communities like &lt;a href=&quot;https://dev.to/&quot;&gt;dev.to&lt;/a&gt; will pick up the slack and fill in the gaps. Why bother writing a “How to get started with ___” guide, if 500 different bloggers are all going to write their own version of it anyways?&lt;/p&gt;
&lt;p&gt;Although I’m generally OK with this for projects that are not very profitable, I have a few issues with it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It can lead to misinformation
&lt;ul&gt;
&lt;li&gt;I have seen a number of community-written guides on various pieces of technology which contain information that is undeniably incorrect, and at times, dangerous.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;It is using free labor
&lt;ul&gt;
&lt;li&gt;Again, for smaller projects that aren’t turning a profit, I’m OK with this. But for large projects, with millions in funding, this feels super exploitative and just wrong. Hire more tech writers!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;dont-overdo-it&quot;&gt;Don’t Overdo It&lt;/h2&gt;
&lt;p&gt;As much as I think focusing on the early part of the funnel is an important part of modern documentation development, I have also seen a fair number of instances where it has gone too far in that direction.&lt;/p&gt;
&lt;p&gt;The best example I can think of for this is &lt;a href=&quot;https://reactjs.org/docs/getting-started.html&quot;&gt;the official React docs&lt;/a&gt; (sorry Dan!). React has often sold itself as a beginner-friendly framework, so it is not that surprising that the docs reflect that (I have even heard stories about bootcamps teaching React before teaching JavaScript…). But I would argue that React has gone too far in focusing on the early stages of the funnel, to the detriment of those using the framework past the “getting started” stage of learning. The majority of their official documentation, even the “API Reference” sections, are written with a high-level of abstraction and tend to gloss over a ton of lower-level implementation details, which is great for getting started with React, but leads to too many developers not being aware of the nuances and (many) footguns that come with React.&lt;/p&gt;
&lt;p&gt;I think that part of why this is the case with the React docs brings me to another point about insufficient documentation; I have a feeling that Facebook / React are exploiting the open-source community to write their docs for them &lt;em&gt;for free&lt;/em&gt;, especially the lower-level docs. The best documentation I have read on React rendering has all been written by people &lt;em&gt;&lt;strong&gt;outside&lt;/strong&gt;&lt;/em&gt; the official React team. If you google things about React beyond the surface level, most of the results are not the official docs. I don’t want to go on too much of a rant here, but Facebook / Meta is basically a money-printing machine and it upsets me that they, and others, are exploiting free labor (although that is far from the worst practices - I’m not a fan of FB if you can’t tell 😉).&lt;/p&gt;
&lt;h2 id=&quot;throwing-stones&quot;&gt;Throwing Stones&lt;/h2&gt;
&lt;p&gt;Look, no one is perfect and writing good documentation is hard. Writing “perfect” documentation that pleases everyone is likely impossible. I’m not perfect, or anywhere close to it, so I know I haven’t always followed my own guidance here. And I don’t expect every project to have the time or resources to do so either.&lt;/p&gt;
&lt;p&gt;This post is mostly just some food-for-thought. Happy snacking.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Pytest Productivity - Tips I Wish I Had Known Sooner</title><link>https://joshuatz.com/posts/2022/pytest-productivity---tips-i-wish-i-had-known-sooner/</link><guid isPermaLink="true">https://joshuatz.com/posts/2022/pytest-productivity---tips-i-wish-i-had-known-sooner/</guid><description>A list of things I wish I had known sooner about using pytest and some general tips on usage and productivity with it.</description><pubDate>Fri, 12 Aug 2022 13:27:57 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;Pytest is a very powerful test framework / runner for Python, but if you are new to it, the depth and surface area can be a bit overwhelming; there are &lt;a href=&quot;https://docs.pytest.org/en/7.1.x/contents.html&quot;&gt;lots of pytest docs&lt;/a&gt;, but once you are past the basics, it can be unclear what features you might be missing out on. One of my favorite types of documentation is a &lt;em&gt;“getting productive with ___”&lt;/em&gt; guide or &lt;em&gt;”___ cheatsheet”&lt;/em&gt;, and I feel like pytest could use one - this is not that exactly, but at least lays out a few &lt;em&gt;“thing I wish I had known sooner about pytest”&lt;/em&gt; type tips.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;My &lt;a href=&quot;https://joshuatz.com/posts/2022/high-level-docs-matter/&quot;&gt;last blog post was about the importance of high-level documentation&lt;/a&gt; and was inspired by situations like my experience getting up-to-speed with pytest. The pytest docs are comprehensive, but not the easiest to navigate and could use some improvement “at the top of the funnel” (see linked blog post for more on what this means).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;pytest-real-time-usage&quot;&gt;Pytest Real-Time Usage&lt;/h2&gt;
&lt;p&gt;Similar to many other test frameworks, the default way that pytest acts is in sort of a black-box one-shot approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Input: tests, source code, pytest config files&lt;/li&gt;
&lt;li&gt;Black box: pytest runs&lt;/li&gt;
&lt;li&gt;Output: pytest spits out everything at once - which tests passed or failed, stdout and stderr, etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;However, this doesn’t &lt;em&gt;have&lt;/em&gt; to be how you use pytest; there are different ways you can interact with pytest as it runs and gain better visibility into how your tests are working.&lt;/p&gt;
&lt;h3 id=&quot;you-can-stream-live-logs&quot;&gt;You Can Stream Live Logs&lt;/h3&gt;
&lt;p&gt;Rather then get all your log messages at the very end of the entire pytest run, you can stream them as they run, with the &lt;a href=&quot;https://docs.pytest.org/en/7.1.x/how-to/logging.html#live-logs&quot;&gt;&lt;code&gt;log_cli&lt;/code&gt; live log feature&lt;/a&gt;: set &lt;code&gt;log_cli&lt;/code&gt; to &lt;code&gt;True&lt;/code&gt; (in the config file or via CLI).&lt;/p&gt;
&lt;p&gt;By combining this with &lt;code&gt;tee&lt;/code&gt; (in *nix shells), you can even see the output in real-time in your terminal while also saving it to a log file:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;log_cli&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; pytest&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; 2&gt;&amp;#x26;1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; tee&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pytest_output.log&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;you-can-see-and-interact-with-exceptions-as-they-happen&quot;&gt;You Can See and Interact With Exceptions As They Happen&lt;/h3&gt;
&lt;p&gt;The very nature of test runners requires that they catch errors as they happen, but that same feature makes interacting with real-time errors generated by your code, as it gets executed through the test runner, tricky.&lt;/p&gt;
&lt;p&gt;For example, if you tell your interactive debugger to break on exceptions and invoke pytest, you are going to end up looking at pytest’s own code, as it ends up handling errors in order to intercept them and determine the &lt;code&gt;pass&lt;/code&gt; / &lt;code&gt;fail&lt;/code&gt; outcome of a given test.&lt;/p&gt;
&lt;p&gt;What I wish I had discovered sooner (it’s a bit buried in the docs) is that pytest exposes some lifecycle &lt;a href=&quot;https://docs.pytest.org/en/latest/reference/reference.html#hooks&quot;&gt;hooks&lt;/a&gt; that you can “register” functions for and hook into - including one for interacting with exceptions as they happen!&lt;/p&gt;
&lt;p&gt;By hooking into &lt;a href=&quot;https://docs.pytest.org/en/latest/reference/reference.html#pytest.hookspec.pytest_exception_interact&quot;&gt;the &lt;code&gt;pytest_exception_interact&lt;/code&gt; hook&lt;/a&gt;, we can interact with an exception as pytest catches it and do whatever we want (including calling a third-party debugger, printing special messages, etc.).&lt;/p&gt;
&lt;p&gt;The easiest way to register this hook is through the &lt;code&gt;conftest.py&lt;/code&gt; file. Here is an example, complete with optional typings:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;@file conftest.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; typing &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Any&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pytest &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; CallInfo, Item, TestReport&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; pytest_exception_interact&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(node: Item, call: CallInfo[Any], report: TestReport):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	# This is just an example&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	# You could invoke an interactive debugger here, call an API, or really do anything you want&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	print&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;=====&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Intercepted pytest test failure!:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	Node: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;node.nodeid&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	Marks: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;, &apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.join([m.name &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; m &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; node.own_markers])&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	Duration: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;call.duration&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	Exception: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\n{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;report.longreprtext&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;=====&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;you-can-debug-at-any-level&quot;&gt;You Can Debug at Any Level&lt;/h3&gt;
&lt;p&gt;There is a tendency when dealing with tests to think of them as running completely detached from your normal environment, and to a certain extent this is (and should be) true. However, that doesn’t mean that you should abandon time-saving tools like interactive debuggers.&lt;/p&gt;
&lt;p&gt;Although the above section showed hooking into only exception catching in pytest, the truth is that you can invoke an interactive debugger anywhere in your tests, just like how you would normally do it in the rest of your codebase.&lt;/p&gt;
&lt;p&gt;If you use a debugger that needs to establish a connection at the start of the session, like Microsoft’s &lt;code&gt;debugpy&lt;/code&gt;, you can hook into the &lt;code&gt;pytest_sessionstart&lt;/code&gt; hook to do so before any tests run:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;@file conftest.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pytest &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Session&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; debugpy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; pytest_sessionstart&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(session: Session):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	debugpy.listen((&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;localhost&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;5678&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	print&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Waiting for client connection...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	debugpy.wait_for_client()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://code.visualstudio.com/docs/python/testing#_debug-tests&quot;&gt;Visual Studio Code (aka VSCode)&lt;/a&gt;, along with pycharm, offers a test GUI that should have a one-click “debug test” button that &lt;em&gt;&lt;strong&gt;should&lt;/strong&gt;&lt;/em&gt; eliminate the need for this boilerplate code in most situations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;If you need help getting Python debugging setup with VSCode, I have &lt;a href=&quot;https://docs.joshuatz.com/cheatsheets/vscode/vscode-python/#debugging-python-in-vs-code&quot;&gt;a guide I’ve written that covers it&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;support-for-flaky-tests&quot;&gt;Support for Flaky Tests&lt;/h2&gt;
&lt;p&gt;A flaky test is one that is inconsistent in whether or not it passes, even if no code has been changed, usually due to some subtle edge-case, accidental dependency on execution order, or timing-related quirk. If you have a flaky test, it should go without saying that the most optimal solution should be to fix it, but sometimes that is easier said than done. In those instances, it is worth knowing that pytest maintains a test cache by default, and &lt;a href=&quot;https://docs.pytest.org/en/7.1.x/how-to/cache.html&quot;&gt;has some built-in tools to help with flaky tests&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pytest --last-failed&lt;/code&gt; will re-run only the tests that failed in the last run&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pytest --failed-first&lt;/code&gt; will re-run &lt;em&gt;all&lt;/em&gt; tests, but start with the failed ones first&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pytest --exitfirst&lt;/code&gt; or &lt;code&gt;pytest -x&lt;/code&gt; will exit the entire test run on the first failed test&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pytest --stepwise&lt;/code&gt; will exit on test failure and continue from last failing test next time&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;There is also an installable plugin that does configurable re-runs all in one command: &lt;a href=&quot;https://github.com/pytest-dev/pytest-rerunfailures&quot;&gt;the &lt;code&gt;pytest-rerunfailures&lt;/code&gt; plugin&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;you-can-get-granular-with-running-tests&quot;&gt;You Can Get Granular With Running Tests&lt;/h2&gt;
&lt;p&gt;Pytest has many different ways it can be invoked, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For specific files: &lt;code&gt;pytest FILEPATH&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For single test functions or methods: &lt;code&gt;pytest &quot;MODULE::MY_TEST&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For pattern matching: &lt;code&gt;pytest -k &quot;SEARCH_STRING&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 &lt;a href=&quot;https://docs.pytest.org/en/7.1.x/how-to/usage.html#specifying-which-tests-to-run&quot;&gt;Full docs on invocation approaches&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you are trying to see which tests are matched against a given invocation command, you can use the &lt;code&gt;--collect-only&lt;/code&gt; flag to get back a list of tests that &lt;em&gt;will&lt;/em&gt; run, without actually running them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Certain IDEs, &lt;a href=&quot;https://www.jetbrains.com/help/pycharm/performing-tests.html&quot;&gt;like pycharm&lt;/a&gt; and &lt;a href=&quot;https://code.visualstudio.com/docs/python/testing#_run-tests&quot;&gt;VSCode&lt;/a&gt;, have built-in support for running tests via a granular GUI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;magical-fixtures&quot;&gt;Magical Fixtures&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Fixtures&lt;/em&gt; are a common part of many testing setups, across different programming languages and frameworks, but they are extra special with pytest. I won’t go into detail, but the basic “magic” of pytest fixtures is that they are auto-injected (as isolated copies) based on naming, so a lot of manual setup / teardown can be replaced with a decorated fixture function.&lt;/p&gt;
&lt;p&gt;For more information, &lt;a href=&quot;https://docs.pytest.org/en/6.2.x/fixture.html&quot;&gt;refer to the official docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I hope this helps someone out there! If you are a fan of to-the-point documentation focused on getting things done, you might enjoy &lt;a href=&quot;https://docs.joshuatz.com/&quot;&gt;my personal documentation / cheatsheet site&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>How to Bulk Delete Album or Track Scrobbles on Last.fm</title><link>https://joshuatz.com/posts/2022/how-to-bulk-delete-album-or-track-scrobbles-on-lastfm/</link><guid isPermaLink="true">https://joshuatz.com/posts/2022/how-to-bulk-delete-album-or-track-scrobbles-on-lastfm/</guid><description>How to delete all last.fm scrobbles of a specific track or album, with Tidal Advertisements as an example.</description><pubDate>Fri, 29 Jul 2022 13:32:54 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;The solutions in this post cover bulk deleting albums or tracks in general on Tidal, but I’ll use a specific example, outlined below:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Recently I decided to give &lt;a href=&quot;https://tidal.com/&quot;&gt;Tidal&lt;/a&gt; a try, which is an online music streaming platform. I signed up and started using a free account to give it a spin. I’ve also been a long-time &lt;a href=&quot;https://www.last.fm&quot;&gt;last.fm&lt;/a&gt; user, a service that tracks what you listen to across multiple platforms, so connecting it to my Tidal account was one of the first things I did.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you came to this page, you might already know this, but the term &lt;em&gt;“scrobble”&lt;/em&gt; is what &lt;code&gt;last.fm&lt;/code&gt; uses for a recorded play of a song. E.g., if you listen to a song once on Spotify and once on Tidal, but have both accounts connected to &lt;code&gt;last.fm&lt;/code&gt;, you would see &lt;code&gt;2&lt;/code&gt; &lt;em&gt;scrobbles&lt;/em&gt; logged.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Unfortunately, I recently noticed that Tidal was “scrobbling” all the advertisements they play when you are using a free account. In fact, it recorded so many &lt;a href=&quot;https://www.last.fm/music/Tidal/Advertisement&quot;&gt;&lt;em&gt;Tidal Advertisement&lt;/em&gt;&lt;/a&gt; (or &lt;em&gt;Advertisement&lt;/em&gt; by &lt;em&gt;Tidal&lt;/em&gt;) scrobbles that &lt;code&gt;last.fm&lt;/code&gt; started saying it was the song I have listened to the most, across all time 😂&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;/media/Last.fm-showing-Tidal-Advertisement-As-Top-Track.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;Screenshot of a last.fm profile page, showing that the top track is a song titled \&amp;#x22;Advertisement\&amp;#x22; by \&amp;#x22;Tidal\&amp;#x22;&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“What’s your taste in music?” … “It’s complicated”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;bulk-deleting-lastfm-scrobbles&quot;&gt;Bulk Deleting Last.fm Scrobbles&lt;/h2&gt;
&lt;h3 id=&quot;how-to-bulk-delete-all-scrobbles-of-a-given-album-in-lastfm&quot;&gt;How to Bulk Delete All Scrobbles of a Given Album in Last.fm&lt;/h3&gt;
&lt;p&gt;Steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to the album page, but via your profile
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Homepage&lt;/em&gt; -&gt; &lt;em&gt;Profile&lt;/em&gt; -&gt; &lt;em&gt;Albums&lt;/em&gt; -&gt; find the album in the list -&gt; click the three dots to open the extended menu -&gt; &lt;em&gt;Go to Album in Library&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;The final URL should look something like: &lt;code&gt;last.fm/user/{USERNAME}/library/music/{ARTIST}/{ALBUM}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For deleting the Tidal Advertisement plays, it should be &lt;code&gt;last.fm/user/{USERNAME}/library/music/Tidal/Advertisement&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click the link labeled &lt;em&gt;Delete album From library&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;In the confirmation modal that appears, check the box to confirm that you indeed want to delete all plays from this album, thn click &lt;em&gt;DELETE&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;You should be good to go!&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;how-to-delete-all-scrobbles-of-a-specific-track-in-lastfm&quot;&gt;How to Delete All Scrobbles of a Specific Track in Last.fm&lt;/h3&gt;
&lt;p&gt;Steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to the track, but via your profile
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Homepage&lt;/em&gt; -&gt; &lt;em&gt;Profile&lt;/em&gt; -&gt; &lt;em&gt;Tracks&lt;/em&gt; -&gt; find the track in the list -&gt; click the three dots to open the extended menu menu -&gt; &lt;em&gt;Go to Track in Library&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;The final URL should look something like &lt;code&gt;last.fm/user/{USERNAME}/library/music/{ARTIST}/_/{TRACK}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click the link labeled *Delete track from library&lt;/li&gt;
&lt;li&gt;In the confirmation modal that appears, check the box to confirm that you indeed want to delete all plays of this track, thn click &lt;em&gt;DELETE&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;You should be good to go!&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;the-easier-solution-for-tidal-advertisement-scrobbles&quot;&gt;The Easier Solution for Tidal Advertisement Scrobbles&lt;/h2&gt;
&lt;p&gt;From a practical perspective, the easiest solution here would be to just upgrade my Tidal account to a paid account 😅&lt;/p&gt;
&lt;p&gt;I probably will soon (I’m still evaluating different platforms), but this would still be something that needs to be fixed regardless.&lt;/p&gt;
&lt;p&gt;Or, Tidal could fix this on their end. Although I don’t really blame them for not prioritizing a “bug” that only affects free accounts.&lt;/p&gt;
&lt;h2 id=&quot;the-harder-solution&quot;&gt;The Harder Solution&lt;/h2&gt;
&lt;p&gt;If you need to delete hundreds of different tracks and/or albums according to some specific logic, you might want to look into scripting a solution with &lt;a href=&quot;https://www.last.fm/api&quot;&gt;the last.fm API&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>YouTube History Bulk Deletion Script</title><link>https://gist.github.com/joshuatz/8dd1c5c3714b9ab615853a0c8c163626</link><guid isPermaLink="true">https://gist.github.com/joshuatz/8dd1c5c3714b9ab615853a0c8c163626</guid><description>Simple JavaScript snippet to manage YouTube history in bulk</description><pubDate>Mon, 06 Jun 2022 07:20:17 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_code_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Nook Simple Touch - Factory Reset &amp; Bypass Network Error</title><link>https://joshuatz.com/posts/2022/nook-simple-touch---factory-reset-bypass-network-error/</link><guid isPermaLink="true">https://joshuatz.com/posts/2022/nook-simple-touch---factory-reset-bypass-network-error/</guid><description>How to bypass the network connectivity error when trying to factory reset a Nook Simple Touch ereader from Barnes &amp; Noble.</description><pubDate>Mon, 07 Feb 2022 07:59:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;🚨 &lt;em&gt;&lt;strong&gt;WARNING&lt;/strong&gt;&lt;/em&gt;: Due to the same reason why factory reset fails with a network error, if you factory reset your device, it will be unable to be re-registered after it has been wiped, and will require &lt;a href=&quot;https://www.mobileread.com/forums/showthread.php?t=295225&quot;&gt;a workaround to become usable again&lt;/a&gt;. Also, if you have remapped buttons, deactivate the remapping before factory resetting (mine seemed to stick through the reset, which screwed up the registration skip hack).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’m currently trying to declutter and came across my old Nook Simple Tough (BRNV300) in the closet. Feeling a pang of nostalgia (the Nook is, IMHO, one of the best designed e-readers, and the fact that it could run full Android was icing on the cake), I powered it up. Noticing that it had a bunch of garbage still installed on it, I figured I should do a factory reset. Unfortunately, I immediately ran into issue.&lt;/p&gt;
&lt;p&gt;The normal way to run a factory reset on these devices is to go to &lt;code&gt;Settings&lt;/code&gt;, then &lt;code&gt;Device Info&lt;/code&gt;, then click &lt;code&gt;Erase &amp;#x26; Deregister Device&lt;/code&gt;. However, when trying to actually run this, I would quickly get an error message:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Network Error&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;You are currently not connected to the network. Network connectivity is required to deregister this device.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;However, my Nook &lt;em&gt;&lt;strong&gt;is&lt;/strong&gt;&lt;/em&gt; connected to the internet, and the connection is working (I can browse webpages, download files, etc.). What gives?&lt;/p&gt;
&lt;p&gt;My guess is that Barnes and Noble used to maintain an endpoint for device registration, that did something like track serial numbers against account IDs. With how old this device is, I also think it is likely that this endpoint was likely deprecated, meaning that the device is trying to “phone home” to an address that no longer has a recipient. The developers likely did not code for this kind of thing to happen (they rarely do), and so it simply blames the network connection, when the real error message should blame the endpoint (or better yet, fallback to just erasing the device if “derigestering” fails).&lt;/p&gt;
&lt;p&gt;All that being said, here is how to work around this:&lt;/p&gt;
&lt;h2 id=&quot;solutions&quot;&gt;Solution(s)&lt;/h2&gt;
&lt;p&gt;Smash your device with a hammer. Kidding! Here is what you can do&lt;/p&gt;
&lt;h3 id=&quot;special-boot-mode&quot;&gt;Special Boot Mode&lt;/h3&gt;
&lt;p&gt;This is the most straightforward approach to factory reset:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Power off the device (press the power button on the top of the rear of the device, select the power off option)&lt;/li&gt;
&lt;li&gt;Press the power button to start it back up&lt;/li&gt;
&lt;li&gt;Once the screen changes to show the device is starting back up, immediately press and hold the two lower buttons (one on each side) until the “Factory Reset” menu comes up&lt;/li&gt;
&lt;li&gt;Press home key to continue, follow prompts&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Credit to &lt;a href=&quot;https://forum.xda-developers.com/t/img-n2t-recovery-nook-simple-touch-factory-restore-recovery-v-0-2-17-11-11.1289233/post-79670667&quot;&gt;this XDA Post&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;the-8-failed-boots-method&quot;&gt;The 8 Failed Boots Method&lt;/h3&gt;
&lt;p&gt;If the above does not work for some reason, there is another approach you can use.&lt;/p&gt;
&lt;p&gt;This workaround is so well known by the modding community it has a name you will often see: &lt;em&gt;“The 8 Failed Boots Method”&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It is outlined in multiple places (including &lt;a href=&quot;https://forum.xda-developers.com/t/img-n2t-recovery-nook-simple-touch-factory-restore-recovery-v-0-2-17-11-11.1289233/post-72380153&quot;&gt;this post on XDA&lt;/a&gt;), but I’ll summarize below:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Power the nook off by pressing the power button (top button on the back of the device) and selecting the option to power off&lt;/li&gt;
&lt;li&gt;Power the nook back on, by pressing and letting go of the power button&lt;/li&gt;
&lt;li&gt;As soon as it starts to boot (the screen changes), long press the power button to interrupt the bootup process and shut it down again&lt;/li&gt;
&lt;li&gt;Repeat steps 2-3 a total of 8 times to trigger a failsafe&lt;/li&gt;
&lt;li&gt;At this point, the device should enter an automatic recover menu, with options for a factory reset&lt;/li&gt;
&lt;/ol&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Support Me and My Content</title><link>https://joshuatz.com/support-me/</link><guid isPermaLink="true">https://joshuatz.com/support-me/</guid><description>How to support the site, myself, and the content I produce.</description><pubDate>Sun, 09 Jan 2022 03:04:54 GMT</pubDate><content:encoded>&lt;p&gt;The number one thing that anyone can do to support me and what I do is… go be kind, ethical, and caring to each other. If everyone did this, the wold would be a much nicer place.&lt;/p&gt;
&lt;p&gt;Telling me that my content helped you in some way is also a great free way to show your support - and honestly means a lot to me. You can leave a comment on a individual post, &lt;a href=&quot;#businessCardMaterializeModal&quot; class=&quot;modal-trigger&quot;&gt;email me&lt;/a&gt;, or &lt;a href=&quot;https://twitter.com/1joshuatz&quot;&gt;@ or DM me on Twitter&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;monetary-support&quot;&gt;Monetary Support&lt;/h2&gt;
&lt;p&gt;I have enjoyed a decent amount of privilege in my life, and there are a lot of people out there that need your support more than I do, so I feel really strange accepting tips or donations, even when I have poured days or even weeks into a project that others find useful.&lt;/p&gt;
&lt;p&gt;However, if you &lt;em&gt;really&lt;/em&gt; want to send me a tip or donation, I finally have gotten around to setting up a Ko-Fi account:&lt;/p&gt;
&lt;div class=&quot;textCenter&quot;&gt;
	&lt;a href=&quot;https://ko-fi.com/G2G27LYNJ&quot; target=&quot;_blank&quot;&gt;&lt;img height=&quot;36&quot; style=&quot;border:0px;height:36px;&quot; src=&quot;https://cdn.ko-fi.com/cdn/kofi2.png?v=3&quot; border=&quot;0&quot; alt=&quot;Buy Me a Coffee at ko-fi.com&quot;&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h2 id=&quot;thank-you&quot;&gt;Thank You&lt;/h2&gt;
&lt;p&gt;Finally, thank you, the reader, for visiting my site!&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Writing for DigitalOcean - Gatsby Tutorial Series</title><link>https://www.digitalocean.com/community/tutorial_series/how-to-create-static-web-sites-with-gatsby-js</link><guid isPermaLink="true">https://www.digitalocean.com/community/tutorial_series/how-to-create-static-web-sites-with-gatsby-js</guid><description>I wrote several of the posts in this series, on how to build static web sites with the Gatsby.js framework, working with a Senior Technical Editor to get articles from idea to publication.</description><pubDate>Mon, 15 Nov 2021 01:23:28 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_full_page_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Canvas Hit Detection Methods - With or Without Tolerances</title><link>https://joshuatz.com/posts/2021/canvas-hit-detection-methods---with-or-without-tolerances/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/canvas-hit-detection-methods---with-or-without-tolerances/</guid><description>How to determine point intersection or collision detection with Canvas paths, with or without tolerances / buffers / padding.</description><pubDate>Sat, 25 Sep 2021 23:45:37 GMT</pubDate><content:encoded>&lt;p&gt;The HTML Canvas element often feels like a web development super power - you can render arbitrary content, take advantage of  hardware acceleration (in some cases), and have full control over every pixel under your domain. However, all that power comes with some tradeoffs. One significant tradeoff is the inability to attach event listeners to visual content &lt;em&gt;within&lt;/em&gt; the Canvas.&lt;/p&gt;
&lt;p&gt;Unlike the regular DOM, where you can capture events at varying levels of granularity, with the Canvas you can only capture events on the Canvas element itself. For using the Canvas purely for output, this is usually fine, but if you need to add interactivity, things get more challenging.&lt;/p&gt;
&lt;p&gt;Since it might be hard to visualize what I’m talking about, let’s start with a simple example: you are drawing a cloud on the screen, and when the user hovers over the cloud and/or the edge of it, you want to add some extra effects.&lt;/p&gt;
&lt;h2 id=&quot;pure-html-approach&quot;&gt;Pure HTML Approach&lt;/h2&gt;
&lt;p&gt;A refresher on event listeners and the non-canvas approach: in a pure HTML approach, without Canvas, we have per-element event listeners to make this easy:&lt;/p&gt;
&lt;iframe height=&quot;400&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Untitled&quot; src=&quot;https://codepen.io/joshuatz/embed/abyRrpe?default-tab=js%2Cresult&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&gt;
  See the Pen &amp;#x3C;a href=&quot;https://codepen.io/joshuatz/pen/abyRrpe&quot;&gt;
  Untitled&amp;#x3C;/a&gt; by Joshua T (&amp;#x3C;a href=&quot;https://codepen.io/joshuatz&quot;&gt;@joshuatz&amp;#x3C;/a&gt;)
  on &amp;#x3C;a href=&quot;https://codepen.io&quot;&gt;CodePen&amp;#x3C;/a&gt;.
&lt;/iframe&gt;
&lt;p&gt;As you can see, we can use these native event listeners even on SVG elements, like &lt;code&gt;path&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;canvas&quot;&gt;Canvas&lt;/h2&gt;
&lt;p&gt;Pretty much all intersection calculations with Canvas start the same way. Since we can’t attach event listeners directly to areas of the canvas, we start by getting the coordinates of the mouse cursor / touch point, and  then checking if it is inside a hit-box or path.&lt;/p&gt;
&lt;p&gt;For checking if the mouse coordinates are inside our area of interest, you have a few different options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manual tracking
&lt;ul&gt;
&lt;li&gt;If you know exactly which pixels an object occupies (or can determine easily), you can check if the touch point is inside those pixels.&lt;/li&gt;
&lt;li&gt;This approach really only works well and is scalable for bounding-box situations, and does not lend itself well to curved paths, circles, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Canvas point overlap check methods (&lt;code&gt;isPointIn___&lt;/code&gt;):
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath&quot;&gt;The &lt;code&gt;context.isPointInPath&lt;/code&gt; method&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInStroke&quot;&gt;The &lt;code&gt;context.isPointInStroke&lt;/code&gt; method&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;These two methods are what you will usually want to use&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rest of this post will mostly focus on  &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInStroke&quot;&gt;&lt;code&gt;context.isPointInStroke&lt;/code&gt;&lt;/a&gt;, as our demo focuses on detecting intersection between the cursor and a line, rather than an entire shape.&lt;/p&gt;
&lt;h3 id=&quot;canvas---getting-coordinates-from-a-mouse-event&quot;&gt;Canvas - Getting Coordinates from a Mouse Event&lt;/h3&gt;
&lt;p&gt;The first complexity involved with Canvas point methods is that they want coordinates relative to the canvas element (distance from canvas top left), not to the DOM. This is an issue because rarely is it the case that your &lt;code&gt;&amp;#x3C;canvas&gt;&lt;/code&gt; element is going to be taking up 100% of the viewport, so you can’t simply use &lt;code&gt;MouseEvent.clientX, MouseEvent.clientY&lt;/code&gt; as inputs into context methods.&lt;/p&gt;
&lt;p&gt;Instead, you need to translate those coordinates each time, to adjust for the offset of the canvas element, and optionally for scaling as well.&lt;/p&gt;
&lt;p&gt;Here is what that might look like:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * For a given mouse coordinate, translate to corresponding coordinate within original canvas element&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; mouseEvent&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; {{clientX: number, clientY: number}}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; canvasElem&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; {HTMLCanvasElement}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getCanvasCoords&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;mouseEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;canvasElem&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; canvasBoundingRect&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; canvasElem.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getBoundingClientRect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; scale&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		x: canvasElem.width &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; canvasBoundingRect.width,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		y: canvasElem.height &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; canvasBoundingRect.height&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		x: (mouseEvent.clientX &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; canvasBoundingRect.left) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; scale.x,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		y: (mouseEvent.clientY &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; canvasBoundingRect.top) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; scale.y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;demo---without-padding&quot;&gt;Demo - Without Padding&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://codepen.io/joshuatz/pen/yLPBJOz&quot;&gt;Here is a demo&lt;/a&gt; without padding / increased hit-box:&lt;/p&gt;
&lt;iframe height=&quot;400&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Canvas - Point Hit Detection Without Padding&quot; src=&quot;https://codepen.io/joshuatz/embed/preview/yLPBJOz?default-tab=js%2Cresult&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&gt;
  See the Pen &amp;#x3C;a href=&quot;https://codepen.io/joshuatz/pen/yLPBJOz&quot;&gt;
  Canvas - Point Hit Detection Without Padding&amp;#x3C;/a&gt; by Joshua T (&amp;#x3C;a href=&quot;https://codepen.io/joshuatz&quot;&gt;@joshuatz&amp;#x3C;/a&gt;)
  on &amp;#x3C;a href=&quot;https://codepen.io&quot;&gt;CodePen&amp;#x3C;/a&gt;.
&lt;/iframe&gt;
&lt;p&gt;You will likely notice that it is kind of hard to get the code to detect your mouse cursor over the cloud edge - the tip of the cursor has to be pretty much exactly over the line. Read on for how to add padding to our mouse collision detection so that our tolerance doesn’t have to be so tight.&lt;/p&gt;
&lt;h3 id=&quot;canvas-hit-detection---with-a-tolerance-allowance&quot;&gt;Canvas Hit Detection - With a Tolerance Allowance&lt;/h3&gt;
&lt;p&gt;A minor issue with the demo above, and a common issue with detecting mouse interaction with a path, is that if the stroke of our path is not very wide, it can be hard for a user to actually get their mouse cursor directly over the path. In many applications, there is a tolerance involved with calculating overlap, so that you can still click on or mouseover elements that have very small regions.&lt;/p&gt;
&lt;p&gt;To implement this for canvas, many forums and answer sites (like StackOverflow) will give (in my opinion) misleading and overly-complicated recommendations. For example, they might insist that you need to stroke your paths with a larger width before render, but this changes the visual output. Or an answer might involve complex transformation matrices and custom algorithms, which seems like overkill for simple path point intersection detection.&lt;/p&gt;
&lt;p&gt;If you want to add a tolerance to &lt;code&gt;isPointIn&lt;/code&gt; detection &lt;em&gt;without&lt;/em&gt; visually changing the output, here is one of the more simple approaches:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: This works for both &lt;code&gt;isPointInStroke&lt;/code&gt; and &lt;code&gt;isPointInPath&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Step 1: For the path that you want to perform hit detection on, convert methods that operate directly on &lt;code&gt;context&lt;/code&gt;, to those that construct a &lt;code&gt;path&lt;/code&gt; instance &lt;em&gt;&lt;strong&gt;outside&lt;/strong&gt;&lt;/em&gt; of it
&lt;ul&gt;
&lt;li&gt;For example:
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Before&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; ctx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; canvas.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;2d&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stroke&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// After&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; myRectPath&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Path2D&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;myRectPath.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stroke&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(myRectPath);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// We still have access to `myRectPath` after it has been stroked&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step 2: Pass the path object to &lt;code&gt;isPointIn___&lt;/code&gt; methods, instead of relying on the current path
&lt;ul&gt;
&lt;li&gt;Example: replace &lt;code&gt;ctx.isPointInPath(10, 10)&lt;/code&gt; with &lt;code&gt;ctx.isPointInPath(myRectPath, 10, 10)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step 3: Instead of padding the path with &lt;code&gt;strokeStyle&lt;/code&gt; or &lt;code&gt;lineWidth&lt;/code&gt; before drawing it onto the canvas, only pad the path right before calling &lt;code&gt;isPointIn___&lt;/code&gt; and then reset it right after
&lt;ul&gt;
&lt;li&gt;Example:
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;* ======== Before =======&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;*/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.lineWidth &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; myRectPath&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Path2D&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;myRectPath.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.lineWidth &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Pad for bigger hit-target&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stroke&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(myRectPath);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;isPointInStroke&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(myRectPath, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;12&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;11&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;* ======== After =======&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;*/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.lineWidth &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; myRectPath&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Path2D&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;myRectPath.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rect&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stroke&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(myRectPath);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.lineWidth &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Pad for bigger hit-target&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;isPointInStroke&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(myRectPath, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;12&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;11&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ctx.lineWidth &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// reset&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is &lt;a href=&quot;https://codepen.io/joshuatz/pen/vYWBgeE&quot;&gt;an updated demo&lt;/a&gt; that uses a much larger &lt;code&gt;lineWidth&lt;/code&gt; for mouse detection, but does not visually change what is shown in the Canvas:&lt;/p&gt;
&lt;iframe height=&quot;400&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Canvas - Point Hit Detection WITH Padding&quot; src=&quot;https://codepen.io/joshuatz/embed/preview/vYWBgeE?default-tab=js%2Cresult&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&gt;
  See the Pen &amp;#x3C;a href=&quot;https://codepen.io/joshuatz/pen/vYWBgeE&quot;&gt;
  Canvas - Point Hit Detection WITH Padding&amp;#x3C;/a&gt; by Joshua T (&amp;#x3C;a href=&quot;https://codepen.io/joshuatz&quot;&gt;@joshuatz&amp;#x3C;/a&gt;)
  on &amp;#x3C;a href=&quot;https://codepen.io&quot;&gt;CodePen&amp;#x3C;/a&gt;.
&lt;/iframe&gt;
&lt;h3 id=&quot;ispointinpath-vs-ispointinstroke&quot;&gt;&lt;code&gt;isPointInPath&lt;/code&gt; vs &lt;code&gt;isPointInStroke&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In general, use &lt;code&gt;isPointInStroke&lt;/code&gt; if you are trying to determine if a point exists directly &lt;em&gt;on&lt;/em&gt; or &lt;em&gt;inside&lt;/em&gt; the path &lt;em&gt;&lt;strong&gt;itself&lt;/strong&gt;&lt;/em&gt; (e.g., on the perimeter of a shape) and use &lt;code&gt;isPointInPath&lt;/code&gt; if you are trying to determine if a point is &lt;em&gt;&lt;strong&gt;enclosed&lt;/strong&gt;&lt;/em&gt; by a given path (e.g., within a filled shape).&lt;/p&gt;
&lt;p&gt;This also explains why the &lt;code&gt;isPointInPath&lt;/code&gt; method has options for which &lt;code&gt;fillRule&lt;/code&gt; to use for determination, but &lt;code&gt;isPointInStroke&lt;/code&gt; does not.&lt;/p&gt;
&lt;p&gt;If we wanted our demos to work when the cursor is anywhere inside or over the cloud, instead of only when it overlaps with the border of the cloud, we would need to update our code to use &lt;code&gt;isPointInPath&lt;/code&gt; instead of &lt;code&gt;isPointInStroke&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;other-approaches&quot;&gt;Other Approaches&lt;/h3&gt;
&lt;p&gt;As is frequently the case with Web Development, the only real limits here are your imagination. There are other ways I’ve seen to tackle this problem that have not been enumerated above, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stacking fixed position elements on top of the canvas
&lt;ul&gt;
&lt;li&gt;Let’s say that you want to track clicks on a button that should appear as part of the UI being presented by the Canvas. Instead of rendering this as part of the Canvas, you could render it in the DOM, as a regular &lt;code&gt;&amp;#x3C;button&gt;&lt;/code&gt; element, and then position it &lt;em&gt;over&lt;/em&gt; the Canvas element, which would let you attach event listeners much more easily&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use a full-fledge Canvas library with interaction support
&lt;ul&gt;
&lt;li&gt;Some Canvas libraries, &lt;a href=&quot;https://github.com/fabricjs/fabric.js&quot;&gt;like &lt;code&gt;fabric.js&lt;/code&gt;&lt;/a&gt;, provide flexible interaction tracking and event listeners out-of-the-box&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>How to Use and Customize Lighthouse - CLI &amp; NodeJS</title><link>https://joshuatz.com/posts/2021/how-to-use-and-customize-lighthouse---cli-nodejs/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/how-to-use-and-customize-lighthouse---cli-nodejs/</guid><description>An introductory guide to the different parts of Google Lighthouse, how to use it via CLI and NodeJS, and strategies for customizing its configuration.</description><pubDate>Mon, 12 Jul 2021 01:09:27 GMT</pubDate><content:encoded>&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://developers.google.com/web/tools/lighthouse&quot;&gt;Google Lighthouse&lt;/a&gt; is a free (as in both cost, and &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse&quot;&gt;open-source&lt;/a&gt;) tool for evaluating the performance and capabilities of a website (or web app). Over time its capabilities have been expanded, and the “Lighthouse” product now encompasses multiple categories, including &lt;code&gt;accessibility&lt;/code&gt;, 20+ category “groups”, 150+ audits,  and hundreds of metrics, timings, and more.&lt;/p&gt;
&lt;p&gt;With power, in this case, comes a little bit of complexity. This post covers a bit of &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse&quot;&gt;the Lighthouse NodeJS package and CLI tool&lt;/a&gt;, getting started with using it, and some basics on customizing the output. As well tips as using it in a TypeScript project.&lt;/p&gt;
&lt;p&gt;Let’s get started!&lt;/p&gt;
&lt;h2 id=&quot;different-ways-to-use-lighthouse&quot;&gt;Different Ways to Use Lighthouse&lt;/h2&gt;
&lt;p&gt;Although this post will focus on the particulars of the CLI and a customized integration with &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse&quot;&gt;the Lighthouse NodeJS package&lt;/a&gt;, published on NPM, I wanted to mention that there are many different forms that Lighthouse takes, and systems it is integrated into. Here are some of those:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci&quot;&gt;Lighthouse CI&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;This is a wrapper around the core Lighthouse library, meant for use with a CI environment. It exposes an altered CLI tailored to CI, report uploading (and/or serving), an integrated report viewer, &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/architecture.md&quot;&gt;and more&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;web.dev has &lt;a href=&quot;https://web.dev/lighthouse-ci/&quot;&gt;a good introductory guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;You can also find &lt;a href=&quot;https://github.com/treosh/lighthouse-ci-action&quot;&gt;a pre-built GitHub Action&lt;/a&gt; for it&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lighthouse also powers some existing Google tools for measuring web performance, such as:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/speed/pagespeed/insights/&quot;&gt;PageSpeed Insights&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/measure/&quot;&gt;web.dev/measure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/web/tools/lighthouse#devtools&quot;&gt;Chrome DevTools Audits Panel&lt;/a&gt; (this is probably how many, if not most developers are first introduced to Lighthouse)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lighthouse is also integrated into dozens of 3rd party &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse#lighthouse-integrations-in-web-perf-services&quot;&gt;services&lt;/a&gt; and &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse#related-projects&quot;&gt;projects&lt;/a&gt;.
&lt;ul&gt;
&lt;li&gt;Some popular examples are &lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;WebPageTest&lt;/a&gt; and &lt;a href=&quot;https://treo.sh/&quot;&gt;Treo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In addition to using in a NodeJS project, the NPM package can also be used &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse#using-the-node-cli&quot;&gt;directly via the CLI&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;npx lighthouse https://example.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;npm-package&quot;&gt;NPM Package&lt;/h2&gt;
&lt;p&gt;The “core” of Lighthouse is the &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse&quot;&gt;&lt;code&gt;GoogleChrome/lighthouse&lt;/code&gt; repo&lt;/a&gt;, which is also distributed as &lt;a href=&quot;https://www.npmjs.com/package/lighthouse&quot;&gt;a published NPM package&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;lighthouse-package---cli-usage&quot;&gt;Lighthouse Package - CLI Usage&lt;/h3&gt;
&lt;p&gt;The Lighthouse package exposes a convenient CLI, which can be used to accomplish many standard Lighthouse related tasks, with or without customization, all from the command line.&lt;/p&gt;
&lt;p&gt;The instructions for the CLI live in a few different places, but there are two main starting points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse#using-the-node-cli&quot;&gt;subsection of the main README&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Covers &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse#cli-options&quot;&gt;CLI syntax and options&lt;/a&gt; and &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse#output-examples&quot;&gt;usage examples&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md&quot;&gt;configuration docs&lt;/a&gt; are also helpful, since the CLI can use a configuration file&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are going to be using the CLI with a NodeJS project, you should install it as a local dependency, otherwise, you can install it globally with &lt;code&gt;npm install -g lighthouse&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A minimal example that shows running Lighthouse and then viewing the results is:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;lighthouse&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; https://example.com&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --view&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;lighthouse-package---customized-usage&quot;&gt;Lighthouse Package - Customized Usage&lt;/h3&gt;
&lt;p&gt;Working with the Lighthouse CLI, there is a lot that you can do entirely through command line arguments. You can specify which audits to run, with &lt;code&gt;--only-audits&lt;/code&gt;, or categories, with &lt;code&gt;--only-categories&lt;/code&gt;, or even emulate a different screen type with &lt;code&gt;--screenEmulation&lt;/code&gt;. However, not &lt;em&gt;everything&lt;/em&gt; can be specified by argument alone, and at a certain point putting dozens of arguments into a CLI command also becomes unwieldy.&lt;/p&gt;
&lt;p&gt;If you are looking to configure Lighthouse to your exact needs, going beyond the default arguments, here are your options.&lt;/p&gt;
&lt;h4 id=&quot;lighthouse-config-file&quot;&gt;Lighthouse Config File&lt;/h4&gt;
&lt;p&gt;If you want to customize Lighthouse to your specific requirements, you should be considering &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/blob/HEAD/docs/configuration.md&quot;&gt;a Lighthouse configuration file&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can use this to customize pretty much every setting for Lighthouse, and it can be reused across both the CLI and custom code that uses the Lighthouse package.&lt;/p&gt;
&lt;p&gt;For example, here is a config file for only generating a PWA and performance report, emulating a mobile form factor:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;my-lh-config.js&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	extends: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;lighthouse:default&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	settings: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		onlyCategories: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;performance&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;pwa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		formFactor: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;mobile&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use this config with Lighthouse, you can pass the config file path via CLI:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;lighthouse&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; https://example.com&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --config-path&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ./my-lh-config.js&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, you could import the file and pass it to the Lighthouse runner in a custom NodeJS script:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; MyConfig &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;./my-lh-config.js&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; results&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lighthouse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://example.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {}, MyConfig);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;lighthouse-config-files---presets-examples-and-extending&quot;&gt;Lighthouse Config Files - Presets, Examples, and Extending&lt;/h4&gt;
&lt;p&gt;Rather than building your config file completely from scratch, you might find it helpful to start with an existing template, or even just completely reuse a preset config that ships with Lighthouse. This is not only an option with Lighthouse, but it is encouraged!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Presets&lt;/strong&gt;&lt;/em&gt;: In the Lighthouse project, the CLI parameter of &lt;code&gt;--preset&lt;/code&gt; is meant for specifying a config file by its special preset alias. As of right now, those are &lt;code&gt;perf&lt;/code&gt;, &lt;code&gt;experimental&lt;/code&gt; and &lt;code&gt;desktop&lt;/code&gt;. Each of those actually just maps to the corresponding config file, with &lt;code&gt;-config.js&lt;/code&gt; appended, such as &lt;code&gt;perf-config.js&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The pre-built config files that ship with Lighthouse can be found in &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/tree/HEAD/lighthouse-core/config&quot;&gt;the &lt;code&gt;lighthouse-core/config/&lt;/code&gt; directory&lt;/a&gt;. Because each config file follows the conventions of a JS module whose default export is a LH config object, you can use them with the CLI:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;lighthouse&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; https://example.com&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --config-path&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ./node_modules/lighthouse/lighthouse-core/config/desktop-config.js&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;… Or in custom JS:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; DesktopConfig &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;lighthouse/lighthouse-core/config/desktop-config.js&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; results&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lighthouse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://example.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {}, DesktopConfig);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Don’t forget that you can also &lt;em&gt;extend&lt;/em&gt; the default configs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🚨 Note that this is &lt;em&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;/em&gt; done through the &lt;code&gt;config.extends&lt;/code&gt; property - that is &lt;em&gt;&lt;strong&gt;only&lt;/strong&gt;&lt;/em&gt; for dictating if your config values should be applied on top of the default settings. &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md#config-extension&quot;&gt;This value should only ever be &lt;code&gt;undefined&lt;/code&gt; or &lt;code&gt;lighthouse:default&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To extend an existing config, you can import and then &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax&quot;&gt;&lt;em&gt;spread&lt;/em&gt;&lt;/a&gt; the config object into your own. For example:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; results&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lighthouse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://example.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {}, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;DesktopConfig,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	categories: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;DesktopConfig.categories,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;		&apos;installable-check&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			title: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Installable Check&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			auditRefs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;				{id: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;installable-manifest&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, weight: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, group: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;pwa-installable&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;custom-nodejs-scripting&quot;&gt;Custom NodeJS Scripting&lt;/h4&gt;
&lt;p&gt;Working with Lighthouse outside of the CLI, in a custom NodeJS script, can feel a lot different than the CLI, even though a lot of the same settings are shared. For example, in a NodeJS script, you are responsible for spawning the Chrome instance that will be used to host the Lighthouse execution, and passing the &lt;code&gt;port&lt;/code&gt; number to the Lighthouse runner / main function.&lt;/p&gt;
&lt;p&gt;There is less documentation about the NodeJS scripting side than the CLI side (part of why I’m writing this post), but I can still point to at least two documentation section to review:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/blob/HEAD/docs/readme.md#using-programmatically&quot;&gt;&lt;em&gt;“Using Programmatically”&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/tree/HEAD/docs/recipes&quot;&gt;&lt;code&gt;docs/recipes&lt;/code&gt; directory&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is an example of using the Lighthouse package in a custom NodeJS script:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Main lighthouse runner / fn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; lighthouse &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;lighthouse&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Required for launching chrome instance&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; chromeLauncher &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;chrome-launcher&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// So we can save output&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {writeFile} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;fs/promises&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Launch instance of Chrome&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; chrome&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; chromeLauncher.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;launch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Gather results and report from Lighthouse&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; results&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lighthouse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://example.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		port: chrome.port,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		output: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;html&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		extends: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;lighthouse:default&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		settings: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			onlyCategories: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;performance&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Save report to file&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; writeFile&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;./lighthouse-report.html&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, results.report);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Kill Chrome&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; chrome.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kill&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;There are examples like this, and more, in &lt;a href=&quot;https://github.com/joshuatz/lighthouse-ts-examples&quot;&gt;my TypeScript example repo&lt;/a&gt; (more on this below).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;flags-vs-config&quot;&gt;Flags vs Config&lt;/h4&gt;
&lt;p&gt;When using the &lt;code&gt;lighthouse()&lt;/code&gt; function in NodeJS, the first three arguments are &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;flags&lt;/code&gt; and &lt;code&gt;configJson&lt;/code&gt;. I’ll call &lt;code&gt;configJson&lt;/code&gt; just &lt;code&gt;config&lt;/code&gt; to make it easier. The delineation between &lt;code&gt;flags&lt;/code&gt; and &lt;code&gt;config&lt;/code&gt; can be a little murky, because they share some common properties (such as &lt;code&gt;onlyCategories&lt;/code&gt;), but the general idea is that flags:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If used, any values will override &lt;code&gt;configJson&lt;/code&gt;, and are the highest level of configuration. Similar to what you would pass via CLI&lt;/li&gt;
&lt;li&gt;Are less granular than the configuration; used for high-level settings, like report output format&lt;/li&gt;
&lt;li&gt;Should (IMHO) be used sparingly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;config&lt;/code&gt;, on the other hand, is the main way to configure Lighthouse and is where you will most commonly be passing custom settings (other than the port for Chrome and output options).&lt;/p&gt;
&lt;h4 id=&quot;slimming-down-results&quot;&gt;Slimming Down Results&lt;/h4&gt;
&lt;p&gt;One common misunderstanding that I had about Lighthouse was the linkage between scores, audits, categories, and artifacts. It’s tempting to think that if you limit the report to just a single category that the size of the data collected should shrink drastically, but that is not necessarily the case. Each category can pull in dozens of audits, and the score is not based on a single data source - rather it is the composite of multiple audits and metrics.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Related explanation &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/issues/11357#issuecomment-684936538&quot;&gt;here on #11357&lt;/a&gt;, and &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/issues/11885#issuecomment-752267970&quot;&gt;here on #11885&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you really need a smaller JSON export from the report, the key is to use &lt;code&gt;onlyCategories&lt;/code&gt; and &lt;code&gt;onlyAudits&lt;/code&gt; to slim down the collected metrics, and then go a step further and use JavaScript’s &lt;code&gt;delete&lt;/code&gt; to trim items you don’t need off the result object.&lt;/p&gt;
&lt;p&gt;For example, here is a small cleaner function I put together that focuses on collecting the standard browser Lighthouse report scores, removing a lot of data in the JSON that is no longer necessary once the score is generated:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; AUDITS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	FCP: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;first-contentful-paint&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	LCP: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;largest-contentful-paint&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	FID: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;max-potential-fid&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	CLS: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;cumulative-layout-shift&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	TTI: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;interactive&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; onlyAudits&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Object.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;keys&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;AUDITS&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; AUDITS&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[key]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {LH.RunnerResult}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@returns&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {LH.RunnerResult}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; cleanReport&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;result&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result.lhr.audits) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;onlyAudits.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(key)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;			delete&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result.lhr.audits[key];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	delete&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result.lhr.stackPacks;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	delete&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result.lhr.i18n;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	delete&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result.lhr.timing;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	delete&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result.lhr.categoryGroups;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	delete&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result.artifacts;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	delete&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result.report;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is for the core result object, but could be modified to operate directly on the &lt;code&gt;result.report&lt;/code&gt; string instead.&lt;/p&gt;
&lt;h2 id=&quot;typescript-support&quot;&gt;TypeScript Support&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;🚨 Warning: Due to some incomplete types, you might need to set &lt;code&gt;skipLibCheck&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; when running &lt;code&gt;tsc&lt;/code&gt; with some of these approaches.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;lighthouse-config-types&quot;&gt;Lighthouse Config Types&lt;/h3&gt;
&lt;p&gt;Although supporting types for the overall module is complicated (more on this in a bit), adding types for just the custom Lighthouse config object takes just a few lines of code, depending on your approach.&lt;/p&gt;
&lt;p&gt;First, you need to make the ambient types for the config object available. You can do so by using a triple slash directive:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/// &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;reference&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; path&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;./node_modules/lighthouse/types/config.d.ts&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, if you have a &lt;code&gt;tsconfig.json&lt;/code&gt; file and prefer this approach, adding that file to the &lt;code&gt;include&lt;/code&gt; array. You can add &lt;em&gt;all&lt;/em&gt; of the Lighthouse ambient types with:’&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;include&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;./node_modules/lighthouse/types/**/*.d.ts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you have those types loaded, you can refer to the &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/blob/c28425341c7a39a245986dad6a889384bdd45f2d/types/config.d.ts#L17-L34&quot;&gt;&lt;code&gt;LH.Config.Json&lt;/code&gt; interface&lt;/a&gt; when using LH config objects. For example, in a TypeScript file:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; LH&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Config&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	extends: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;lighthouse:default&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; config;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want to annotate the config type in a regular JavaScript file, you can do that with the support of an IDE like VSCode, which supports advanced type-checking via JSDoc:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// @ts-check&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/// &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;reference&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; path&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;./node_modules/lighthouse/types/config.d.ts&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/** &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {LH.Config.Json}&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	extends: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;lighthouse:default&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; config;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;lighthouse-module-types&quot;&gt;Lighthouse Module Types&lt;/h3&gt;
&lt;p&gt;Lighthouse is a huge repository, spanning &lt;em&gt;thousands&lt;/em&gt; of files, and unfortunately the TypeScript situation is &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/issues/1773&quot;&gt;currently not exactly ideal&lt;/a&gt;. Although &lt;em&gt;some&lt;/em&gt; types are explicitly exported, large parts of the codebase only use internal type annotations (JSDoc / &lt;a href=&quot;https://developers.google.com/closure/compiler/docs/js-for-compiler&quot;&gt;Closure Compiler&lt;/a&gt;), and do not publish their types as part of the NPM package. The net result of this is that, in your own code, imported methods from &lt;code&gt;lighthouse&lt;/code&gt; might appear half-typed (with lots of &lt;code&gt;any&lt;/code&gt; inferred types), or not typed at all.&lt;/p&gt;
&lt;p&gt;Luckily, between the types that &lt;em&gt;are&lt;/em&gt; exported and what TS can infer, you can pretty much fill in the gaps if you are coding a TypeScript project that uses the Lighthouse package. You can take a look at how some existing TypeScript projects did this, such as &lt;a href=&quot;https://github.com/paulirish/pwmetrics&quot;&gt;Paul Irish’s &lt;code&gt;pwmetrics&lt;/code&gt;&lt;/a&gt;, or, I’ve also put together a small sample repo to demonstrate: &lt;a href=&quot;https://github.com/joshuatz/lighthouse-ts-examples&quot;&gt;&lt;code&gt;lighthouse-ts-examples&lt;/code&gt;&lt;/a&gt;. Of particular interest is probably &lt;a href=&quot;https://github.com/joshuatz/lighthouse-ts-examples/blob/HEAD/ambients.d.ts&quot;&gt;the &lt;code&gt;ambients.d.ts&lt;/code&gt; file&lt;/a&gt;, which is what provides the types for the main &lt;code&gt;lighthouse()&lt;/code&gt; function.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Android WebView - Intercepting and Blocking Requests</title><link>https://joshuatz.com/posts/2021/android-webview---intercepting-and-blocking-requests/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/android-webview---intercepting-and-blocking-requests/</guid><description>How to use WebViewClient with Android WebView to hook into network requests, intercept them, and block or modify them if they match certain rules.</description><pubDate>Tue, 01 Jun 2021 21:40:02 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;One of the neat things about WebView on Android is the level of control that you are given over the WebView component. In addition to controlling settings and navigating to URLs, you can actually control the WebView at the &lt;em&gt;network request&lt;/em&gt; level, intercepting network calls, modifying content, or even blocking requests altogether.&lt;/p&gt;
&lt;p&gt;This post is about how to implement some network request blocking (and modification) with WebView and related classes, such as &lt;code&gt;WebViewClient&lt;/code&gt;. Examples will focus on Kotlin, but would apply to Java as well with just a little tweaking.&lt;/p&gt;
&lt;h2 id=&quot;where-to-hook-into-webview-requests&quot;&gt;Where to Hook into WebView Requests&lt;/h2&gt;
&lt;p&gt;There are a lot of different ways to customize your WebView within Android code; settings, callbacks, &lt;a href=&quot;https://developer.android.com/reference/android/webkit/WebChromeClient&quot;&gt;WebChromeClient&lt;/a&gt;, and &lt;a href=&quot;https://developer.android.com/reference/android/webkit/WebViewClient&quot;&gt;WebViewClient&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For those wondering what the difference is between &lt;code&gt;WebChromeClient&lt;/code&gt; and &lt;code&gt;WebViewClient&lt;/code&gt;, the answer is mostly in separation of concerns. The WebChromeClient class is mostly concerned with long-lived browser-level things, such as &lt;a href=&quot;https://developer.android.com/reference/android/webkit/WebChromeClient#getVisitedHistory(android.webkit.ValueCallback%3Cjava.lang.String%5B%5D%3E)&quot;&gt;the user’s visit history&lt;/a&gt;, whereas WebViewClient has more to do with the lifecycle of each individual webpage and things like hooking into &lt;a href=&quot;https://developer.android.com/reference/android/webkit/WebViewClient#onPageFinished(android.webkit.WebView,%20java.lang.String)&quot;&gt;when a page has finished loading&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For hooking into network requests, the easiest entry point is by subclassing WebViewClient and overriding two important  functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/reference/kotlin/android/webkit/WebViewClient#shouldinterceptrequest_1&quot;&gt;shouldInterceptRequest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/reference/kotlin/android/webkit/WebViewClient#shouldoverrideurlloading_1&quot;&gt;shouldOverrideUrlLoading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These methods both do similar things; they let you, as a developer, intercept and modify how each browser request is handled.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The difference is a little subtle between these methods; although AJAX requests use URLs, the &lt;code&gt;shouldOverrideUrlLoading&lt;/code&gt;  is really only called for the URL of the webpage itself - e.g. the one that the user types in. AJAX and other requests will not come through this hook, but will flow through &lt;code&gt;shouldInterceptRequest&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;writing-our-webview-request-modifiers&quot;&gt;Writing our WebView Request Modifiers&lt;/h2&gt;
&lt;p&gt;Since we will want to use both hooks (&lt;code&gt;shouldOverrideUrlLoading&lt;/code&gt; and &lt;code&gt;shouldInterceptRequest&lt;/code&gt;), and both have identical method signatures, this is an ideal situation to keep our code clean and have a single function that can be called by both:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;kotlin&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;fun&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; filterNetworkRequests&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(view: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, request: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebResourceRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;): &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Boolean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// By default, let all requests pass through&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; shouldBlock &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// ....&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// code to determine if shouldBlock should be changed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; shouldBlock&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Although we can call this function from both hooks with identical arguments, how we use the response actually changes in each.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shouldOverrideUrlLoading&lt;/code&gt;: Returning &lt;code&gt;true&lt;/code&gt; will abort / block the URL being loaded, &lt;code&gt;false&lt;/code&gt; allows it to proceed as normal&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shouldInterceptRequest&lt;/code&gt;: To block the request, we need to actually return our own custom response which should replace the actual one. Returning &lt;code&gt;null&lt;/code&gt; will allow it to proceed as normal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Knowing this, here is how we could use our common function within each hook:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;kotlin&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;override&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; fun&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; shouldInterceptRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	view: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	request: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebResourceRequest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;): &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebResourceResponse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;? {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	val&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; shouldBlock &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; filterNetworkRequests&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(webView, request);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (shouldBlock) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; WebResourceResponse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;text/javascript&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;UTF-8&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; null&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;override&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; fun&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; shouldOverrideUrlLoading&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	view: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	request: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebResourceRequest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;): &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Boolean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; filterNetworkRequests&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(view, request)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;implementing-blocking-patterns&quot;&gt;Implementing Blocking Patterns&lt;/h2&gt;
&lt;p&gt;To take this a step further, I’m going to demonstrate how you could use these hooks with blocklists to prevent any requests from being fulfilled for certain domains or URL patterns.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/road-ends-sign.jpg&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto; text-align:center;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/road-ends-sign-1024x683.jpg&quot; alt=&quot;Road with a sign next to it reading &amp;#x22;Road Ends&amp;#x22;&quot; style=&quot;width:100%;max-width: 400px;height:auto;&quot; loading=&quot;lazy&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;First, let’s declare our block lists:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;kotlin&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;val&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; BannedDomains: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Array&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; arrayOf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	&quot;facebook.com&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;	&quot;connect.facebook.net&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;val&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; BannedUrlPatterns: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Array&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Regex&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; arrayOf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	Regex&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\\&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.taboola&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\\&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we can modify our reusable filterNetworkRequests function to take these into account:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;kotlin&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;fun&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; filterNetworkRequests&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(view: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, request: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebResourceRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;): &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Boolean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// By default, let all requests pass through&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; shouldBlock &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	val&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; reqUri &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; request.url&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	val&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; reqUrl &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; reqUri.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (BannedDomains.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;contains&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(reqUri.host?.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;www.&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		shouldBlock &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (bannedPattern &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; BannedUrlPatterns) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;shouldBlock) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			shouldBlock &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; bannedPattern.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;containsMatchIn&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(reqUrl)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; shouldBlock&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;request-blocking-in-webview-a-complete-demo&quot;&gt;Request Blocking in WebView: A Complete Demo&lt;/h3&gt;
&lt;p&gt;Since sometimes it helps to see everything put together, here is a complete (although basic) working demo of request blocking with WebView, using the previous steps we built out.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot; data-jptremote=&quot;https://gist.githubusercontent.com/joshuatz/e801edbe9f545ebddb2352f2d1941db3/raw/07d928b2f5830712af20def267c46305fc58fb6d/Android_WebView_Blocking.kt&quot;&gt;Loading Github Gist: https://gist.github.com/joshuatz/e801edbe9f545ebddb2352f2d1941db3&lt;/code&gt;&lt;/pre&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Svelte Weather App</title><link>https://github.com/joshuatz/svelte-weather-app</link><guid isPermaLink="true">https://github.com/joshuatz/svelte-weather-app</guid><description>Demo weather app project, using Svelte, TypeScript, and Express to power a web interface with a location-based forecast, powered by the AccuWeather API.</description><pubDate>Sun, 30 May 2021 13:11:20 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_full_page_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Express - Selectively Using CSURF for CSRF Protection</title><link>https://joshuatz.com/posts/2021/express---selectively-using-csurf-for-csrf-protection/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/express---selectively-using-csurf-for-csrf-protection/</guid><description>Approaches for using CSURF for CSRF protection, but only on certain routes, and some with the ability to extract the generated token from the request.</description><pubDate>Fri, 28 May 2021 12:03:13 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;This post is going to be talking about using &lt;a href=&quot;https://expressjs.com/en/resources/middleware/csurf.html&quot;&gt;the &lt;code&gt;csurf&lt;/code&gt; CSRF library&lt;/a&gt; with &lt;a href=&quot;https://expressjs.com/&quot;&gt;Express&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;More specifically, it is going to be talking about approaches for selectively applying CSRF protection to only certain routes, and how to dynamically extract the generated token during a request without enforcing CSRF for that request. I’ll start with &lt;a href=&quot;#approaches-without-extraction&quot;&gt;the approaches that don’t allow for token extraction in non-protected routes&lt;/a&gt;, before getting to &lt;a href=&quot;#approaches-with-extraction&quot;&gt;those that do&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;approaches-without-extraction&quot;&gt;Approaches Without Extraction&lt;/h2&gt;
&lt;h3 id=&quot;separate-router&quot;&gt;Separate Router&lt;/h3&gt;
&lt;p&gt;Starting with the approach recommended &lt;a href=&quot;https://github.com/expressjs/csurf#ignoring-routes&quot;&gt;by the official docs&lt;/a&gt;, one way to disable CSRF protection on a subset of routes is to use a separate &lt;a href=&quot;https://expressjs.com/en/guide/routing.html#express-router&quot;&gt;Express Router&lt;/a&gt; and mount it to the main app / express instance &lt;em&gt;&lt;strong&gt;before&lt;/strong&gt;&lt;/em&gt; attaching the CSRF middleware.&lt;/p&gt;
&lt;p&gt;Again, the docs have &lt;a href=&quot;https://github.com/expressjs/csurf#ignoring-routes&quot;&gt;a full code example&lt;/a&gt;, but a quick rundown:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; csrf &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;csurf&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; express &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;express&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; express&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getNonProtectedRouter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; router&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; express.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Router&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Non-protected routes...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; router;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Mount non-csrf protected routes first&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/nonProtectedRoutes&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getNonProtectedRouter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Now add middleware&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;csrf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({cookie: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// protected routes...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: With this approach, the non-protected routes do &lt;em&gt;not&lt;/em&gt; have access to &lt;code&gt;req.csrfToken()&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;passing-the-csrf-middleware-via-route-argument&quot;&gt;Passing the CSRF Middleware Via Route Argument&lt;/h3&gt;
&lt;p&gt;Often, the CSRF middleware is applied by passing it very early on to the global express instance. Something like:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; express&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; csrfInstance&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; csrf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({cookie: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Add all middleware&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(csrfInstance);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// protected routes...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;post&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/newUser&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, you can also pass middleware as an argument to each route, instead of at the app level. This lets you apply it to only some routes:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; express&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; csrfInstance&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; csrf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ cookie: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// protected route:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;post&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/newUser&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, csrfInstance, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Protected by CSRF!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Unprotected route&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;post&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/checkPublicUser&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// No CSRF check here!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: The unprotected routes do &lt;em&gt;not&lt;/em&gt; have access to &lt;code&gt;req.csrfToken()&lt;/code&gt;, similar to the separate router instance approach. Theoretically, you could however combine this approach with two separate csurf instances (like &lt;a href=&quot;https://stackoverflow.com/a/66319515/11447682&quot;&gt;this user&lt;/a&gt;) - one that ignores all methods and one that doesn’t - and use the ignoring one in routes where you want to extract the token, but not enforce it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;selectively-calling-with-wrapper-middleware&quot;&gt;Selectively Calling with Wrapper Middleware&lt;/h3&gt;
&lt;p&gt;Since Express allows you to create your own middleware without much setup, it is relatively pain-free to create a &lt;em&gt;“wrapper”&lt;/em&gt; middleware that only calls the csurf middleware when you want it to, otherwise just passing the request along.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; express&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; csrfInstance&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; csrf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ cookie: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Custom middleware - wrapper around csurf&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((req, res, next) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (req.path === &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/checkPublicUser&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// This skips CSRF - route will be unprotected&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; next&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	csrfInstance&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;next&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;approaches-with-extraction&quot;&gt;Approaches with Extraction&lt;/h2&gt;
&lt;h3 id=&quot;using-get-and-ignoremethods&quot;&gt;Using GET and ignoreMethods&lt;/h3&gt;
&lt;p&gt;The default value for &lt;a href=&quot;https://github.com/expressjs/csurf#ignoremethods&quot;&gt;the &lt;code&gt;ignoreMethods&lt;/code&gt; option&lt;/a&gt; is &lt;code&gt;[&apos;GET&apos;, &apos;HEAD&apos;, &apos;OPTIONS&apos;]&lt;/code&gt;, which, as you might have guessed, actually means that CSRF is not enforced for any of those request types.&lt;/p&gt;
&lt;p&gt;So, unless you have modified this option, you can actually use &lt;em&gt;&lt;strong&gt;any&lt;/strong&gt;&lt;/em&gt; GET route without CSRF tokens being checked. The token is still generated during these requests and accessible via &lt;code&gt;req.csrfToken()&lt;/code&gt; - in fact, that is how you are supposed to pass the token to the front-end in the first place; your GET route passes the token to a view, which embeds it into the page (usually via hidden &lt;code&gt;meta&lt;/code&gt; or &lt;code&gt;input&lt;/code&gt; tags).&lt;/p&gt;
&lt;h3 id=&quot;hooking-into-next-and-ignoring-errors&quot;&gt;Hooking Into Next and Ignoring Errors&lt;/h3&gt;
&lt;p&gt;This method is one that I realized could be used, but didn’t find in most code examples.&lt;/p&gt;
&lt;p&gt;As csurf is a form of Express middleware, it shares the same interface as &lt;a href=&quot;https://expressjs.com/en/guide/writing-middleware.html&quot;&gt;all Express middleware&lt;/a&gt; - a function that takes arguments &lt;code&gt;req&lt;/code&gt;, &lt;code&gt;res&lt;/code&gt;, and &lt;code&gt;next&lt;/code&gt;. Normally, when you attach the middleware via &lt;code&gt;use()&lt;/code&gt;, Express will automatically be calling the middleware for you on each request, with those three arguments.&lt;/p&gt;
&lt;p&gt;However, we can also &lt;strong&gt;manually&lt;/strong&gt; call the middleware, even inside of our own middleware. And, if we pass our own function as the &lt;code&gt;next&lt;/code&gt; argument, we can intercept the result from the csurf middleware, choosing to ignore or rethrow errors it generates while checking tokens.&lt;/p&gt;
&lt;p&gt;By combining this idea with &lt;a href=&quot;#selectively-calling-with-wrapper-middleware&quot;&gt;the wrapper middleware approach&lt;/a&gt;, we have a way where we can run the CSRF middleware for every request, which means all requests will have tokens via &lt;code&gt;req.csrfToken()&lt;/code&gt;, but ignore the checking of tokens when we want to.&lt;/p&gt;
&lt;p&gt;Here it is in action:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; express&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; csrfInstance&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; csrf&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ cookie: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// CSRF protection, with excluded route that still captures token&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;next&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	csrfInstance&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(req, res, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Within this function, and now within all routes that follow,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// we have access to req.csrfToken()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (req.path &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/nonProtected&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; e.code &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;EBADCSRFTOKEN&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;			// We will ignore the specific CSRF error for this specific path&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;			// Notice how we are not passing err back to next()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;			next&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;			// Pass request along normally&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;			// If there is an error from CSRF or other middleware, it will be rethrown,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;			// instead of being ignored&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;			next&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(err);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Routes go here...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 &lt;a href=&quot;https://github.com/expressjs/csurf#custom-error-handling&quot;&gt;According to the docs&lt;/a&gt;, you can rely on ‘EBADCSRFTOKEN’ to be the error code.&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Info on WaveShare Passive NFC-Powered E-Paper Modules</title><link>https://joshuatz.com/posts/2021/info-on-waveshare-passive-nfc-powered-e-paper-modules/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/info-on-waveshare-passive-nfc-powered-e-paper-modules/</guid><description>Resources and information about the WaveShare line of NFC-Powered Passive E-Ink / E-Paper modules, complete with teardown pictures and some development tips.</description><pubDate>Thu, 20 May 2021 22:59:25 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post comprises my assorted notes related to WaveShare Passive NFC-Powered E-Paper Displays. I recently bought a 2.9” module and ended up learning a good deal about it while building an Android app to interface with it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;resources&quot;&gt;Resources&lt;/h2&gt;
&lt;style&gt;details[open] .isClosed{display:none}details[open] .isOpen{display:inline!important}&lt;/style&gt;
&lt;details&gt;
	&lt;summary&gt;&lt;span class=&quot;isClosed&quot;&gt;Show Resources&lt;/span&gt;&lt;span style=&quot;display:none;&quot; class=&quot;isOpen&quot;&gt;Hide Resources&lt;/span&gt;&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;WaveShare Store
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.waveshare.com/product/displays/e-paper/epaper-2/2.9inch-nfc-powered-e-paper.htm&quot;&gt;2.9” Passive NFC e-Paper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.waveshare.com/product/displays/e-paper/epaper-2/2.9inch-e-paper-module.htm&quot;&gt;296x128 2.9” E-Ink Display Module&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;WaveShare Wiki
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.waveshare.com/wiki/Main_Page&quot;&gt;Homepage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.waveshare.com/wiki/Special:NewFiles?like=nfc&amp;#x26;limit=50&amp;#x26;offset=&quot;&gt;Images tagged “NFC”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Developer tools:
&lt;ul&gt;
&lt;li&gt;Official
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.waveshare.com/wiki/Android_SDK_for_NFC-Powered_e-Paper&quot;&gt;Android / Java SDK&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Note: This is a bytecode / compiled JAR file, not raw source code&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.waveshare.com/w/upload/d/d7/NFCTag_EN.zip&quot;&gt;Android App (APK)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.waveshare.com/w/upload/e/e3/ST25R3911B-NFC-Demo.zip&quot;&gt;*ST25R3911B-NFC-Demo&lt;/a&gt; (C code)
&lt;ul&gt;
&lt;li&gt;Related to &lt;a href=&quot;https://www.waveshare.com/wiki/ST25R3911B_NFC_Board&quot;&gt;this WaveShare board&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Unofficial
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/RfidResearchGroup/proxmark3/blob/HEAD/client/src/cmdhfwaveshare.c&quot;&gt;Proxymark3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Examples using the JAR file:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/joshuatz/nfc-epaper-writer&quot;&gt;joshuatz/nfc-epaper-writer&lt;/a&gt; (my app! 😀)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/j796160836/EinkTagDemo&quot;&gt;j796160836/EinkTagDemo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/thomasleplus/android-nfc-timestamp&quot;&gt;thomasleplus/android-nfc-timestamp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Blog posts
&lt;ul&gt;
&lt;li&gt;The Digital Reader - &lt;a href=&quot;https://the-digital-reader.com/2020/06/04/hands-on-with-waveshares-nfc-powered-e-ink-screens/&quot;&gt;*Hands on With Waveshare’s NFC-Powered E-Ink Screens&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;General WaveShare E-Ink Projects and Libraries
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/caemor/epd-waveshare&quot;&gt;https://github.com/caemor/epd-waveshare&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ryanweal/papercards&quot;&gt;https://github.com/ryanweal/papercards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sidoh/epaper_templates&quot;&gt;https://github.com/sidoh/epaper_templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Wh1teRabbitHU/ImageToEpaperConverter&quot;&gt;https://github.com/Wh1teRabbitHU/ImageToEpaperConverter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Other
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/w3c/web-nfc/blob/gh-pages/EXPLAINER.md&quot;&gt;Web-NFC Explainer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id=&quot;what-is-powering-the-waveshare-nfc-epaper-module&quot;&gt;What is Powering the Waveshare NFC EPaper Module?&lt;/h2&gt;
&lt;p&gt;Hard to tell - probably depends on model.&lt;/p&gt;
&lt;p&gt;It looks like many use the &lt;code&gt;ST25*&lt;/code&gt; (ST Microelectronics) based hardware for the NFC detection and reading. For example, the &lt;a href=&quot;https://www.waveshare.com/wiki/ST25R3911B_NFC_Board&quot;&gt;&lt;code&gt;ST25R3911B&lt;/code&gt; board.&lt;/a&gt;. But, even more likely, they are specifically using &lt;a href=&quot;https://www.st.com/en/nfc/st25-nfc-rfid-tags-readers.html&quot;&gt;the &lt;code&gt;ST25&lt;/code&gt; &lt;em&gt;&lt;strong&gt;Dynamic&lt;/strong&gt;&lt;/em&gt; chips&lt;/a&gt;, since those can support &lt;em&gt;fast transfer mode&lt;/em&gt;, &lt;em&gt;I2c&lt;/em&gt;, and &lt;em&gt;&lt;strong&gt;energy harvesting&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Some models might also be using the &lt;a href=&quot;https://www.st.com/en/nfc/m24lr-series-dynamic-nfc-tags.html&quot;&gt;&lt;code&gt;ST M24LR&lt;/code&gt;&lt;/a&gt; for energy harvesting.&lt;/p&gt;
&lt;h3 id=&quot;my-teardown&quot;&gt;My Teardown&lt;/h3&gt;
&lt;p&gt;I was really curious on how these worked, so I took my 2.9” module apart and took some pictures of the circuit board. Disassembly actually made things more confusing, not less.&lt;/p&gt;
&lt;p&gt;To begin with, the main integrated circuit that obviously handles the bulk of the operations has had its label etched out - so you cannot discern model number, or even the manufacturer. It also is a 40-pin chip, which strikes me as odd, as 40-pin chips appear far more rarely than 44-pin chips. However, there is a package type that might match - the &lt;code&gt;HVQFN-40&lt;/code&gt; (or for NPX, &lt;code&gt;SOT618-1&lt;/code&gt;). Using this package type to narrow down options makes it seem likely that this is actually a NXP chip, because as far as I can tell, they are the only ones with NFC chips that comes in 40-pin packages - for example, the &lt;a href=&quot;https://www.nxp.com/products/rfid-nfc/nfc-hf/nfc-readers/standard-nfc-frontend:PN512&quot;&gt;&lt;code&gt;PN512&lt;/code&gt;&lt;/a&gt; or &lt;a href=&quot;https://www.nxp.com/docs/en/nxp/data-sheets/PN532_C1.pdf&quot;&gt;&lt;code&gt;PN532&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;teardown---image-gallery&quot;&gt;Teardown - Image Gallery&lt;/h4&gt;
&lt;div style=&quot;display:flex;flex-wrap: wrap;align-content: space-around;justify-content: space-evenly;&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/media/WaveShare-2.9-Inch-NFC-EPaper-Module-Left-Side-scaled.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-1299&quot; src=&quot;/media/WaveShare-2.9-Inch-NFC-EPaper-Module-Left-Side-300x201.jpg&quot; alt=&quot;WaveShare 2.9 Inch NFC EPaper Module - Left Side&quot; width=&quot;300&quot; height=&quot;201&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/WaveShare-Passive-NFC-Powered-EInk-Epaper-2.9inch-Module-Circuit-Board-Closeup-scaled.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-1298&quot; src=&quot;/media/WaveShare-Passive-NFC-Powered-EInk-Epaper-2.9inch-Module-Circuit-Board-Closeup-300x201.jpg&quot; alt=&quot;WaveShare Passive NFC Powered EInk Epaper 2.9inch Module - Circuit Board Closeup&quot; width=&quot;300&quot; height=&quot;201&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/WaveShare-Passive-NFC-Powered-EInk-Epaper-2.9inch-Module-Circuit-Board-Closeup-Angled.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-1297&quot; src=&quot;/media/WaveShare-Passive-NFC-Powered-EInk-Epaper-2.9inch-Module-Circuit-Board-Closeup-Angled-300x200.jpg&quot; alt=&quot;WaveShare Passive NFC Powered EInk Epaper 2.9inch Module - Circuit Board Closeup (Angled)&quot; width=&quot;300&quot; height=&quot;200&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/WaveShare-Passive-NFC-Powered-EInk-Epaper-2.9inch-Module-Circuit-and-Display-scaled.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-1296&quot; src=&quot;/media/WaveShare-Passive-NFC-Powered-EInk-Epaper-2.9inch-Module-Circuit-and-Display-300x169.jpg&quot; alt=&quot;WaveShare Passive NFC Powered EInk Epaper 2.9inch Module - Circuit and Display&quot; width=&quot;300&quot; height=&quot;169&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/WaveShare-NFC-EPaper-Module-Open-Case.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-1295&quot; src=&quot;/media/WaveShare-NFC-EPaper-Module-Open-Case-300x200.jpg&quot; alt=&quot;WaveShare NFC EPaper Module - Open Case&quot; width=&quot;300&quot; height=&quot;200&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/WaveShare-2.9-Inch-Passive-NFC-EInk-Module-Teardown-Back-scaled.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-1293&quot; src=&quot;/media/WaveShare-2.9-Inch-Passive-NFC-EInk-Module-Teardown-Back-300x201.jpg&quot; alt=&quot;WaveShare 2.9 Inch Passive NFC EInk Module - Teardown (Back)&quot; width=&quot;300&quot; height=&quot;201&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/WaveShare-2.9-Inch-Passive-NFC-EInk-Module-Teardown-Front-scaled.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-1294&quot; src=&quot;/media/WaveShare-2.9-Inch-Passive-NFC-EInk-Module-Teardown-Front-300x201.jpg&quot; alt=&quot;WaveShare 2.9 Inch Passive NFC EInk Module - Teardown (Front)&quot; width=&quot;300&quot; height=&quot;201&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;what-nfc-standards-does-it-use&quot;&gt;What NFC standards does it use?&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;These are based on my observations, with a 2.9” passive display&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The answer is complicated.&lt;/p&gt;
&lt;p&gt;It’s a Type-2 Tag (aka &lt;em&gt;NFC Tag Type 2&lt;/em&gt;, aka &lt;em&gt;NFC Forum Type 2&lt;/em&gt;), which is covered specficially by ISO/IEC 14443-3A (aka &lt;em&gt;ISO14443A&lt;/em&gt;). Falls under &lt;code&gt;NFC-A&lt;/code&gt; spec.&lt;/p&gt;
&lt;p&gt;However, the way to talk to this tag type is also covered by the &lt;a href=&quot;https://nfc-forum.org/product/digital-protocol-technical-specification-2-1/&quot;&gt;&lt;em&gt;NFC Forum Digital Protocol Technical Specifications&lt;/em&gt;&lt;/a&gt;, which further details things like command sets (such as &lt;code&gt;WRITE&lt;/code&gt; and &lt;code&gt;READ&lt;/code&gt;), error checking, etc.&lt;/p&gt;
&lt;p&gt;Although the data set maximum message size is reported at 46 bytes, &lt;a href=&quot;#capability-container&quot;&gt;the Capability Container (CC)&lt;/a&gt; reports a maximum NDEF data size of 2016 bytes, which would match the maximum allowed by the type-2 spec. ST25 app, which is probably more accurate, reports 2032 bytes.&lt;/p&gt;
&lt;p&gt;Although the AAR uses NDEF, the actual image memory stuff appears to use lower-level NFC stuff, so you can’t use Web-NFC or anything else that only speaks NDEF. (see &lt;a href=&quot;https://news.ycombinator.com/item?id=22613041&quot;&gt;this&lt;/a&gt;, and &lt;a href=&quot;https://news.ycombinator.com/item?id=22618012&quot;&gt;this&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id=&quot;capability-container&quot;&gt;Capability Container&lt;/h3&gt;
&lt;p&gt;AFAIK, the Capability Container (CC) is basically a very specific area of memory for NFC chips that contains metadata about the &lt;em&gt;capabilities&lt;/em&gt; of the chip itself. Thus, I believe it is usually programmed / written / burned in at the factory, and written as “read-only” memory.&lt;/p&gt;
&lt;h2 id=&quot;data-format&quot;&gt;Data Format&lt;/h2&gt;
&lt;h3 id=&quot;stored-data&quot;&gt;Stored Data&lt;/h3&gt;
&lt;p&gt;Reading the raw data off the chip, via &lt;code&gt;NFC Tools&lt;/code&gt;, &lt;code&gt;TagInfo&lt;/code&gt;, or the &lt;code&gt;ST25&lt;/code&gt; app - all of them say the same thing: that the only data on the NFC chip is some meta information, and the single NDEF AAR entry. The hex is as follows:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;57:53:44:5A:31:30:6D:FF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;FF:FF:00:00:E1:10:FC:00&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;03:27:D4:0F:15:61:6E:64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;72:6F:69:64:2E:63:6F:6D&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3A:70:6B:67:77:61:76:65&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;73:68:61:72:65:2E:66:65&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;6E:67:2E:6E:66:63:74:61&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;67:FE:00:00&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first 7 bytes (&lt;code&gt;57:53:44:5A:31:30:6D&lt;/code&gt;) are &lt;code&gt;WSDZ10m&lt;/code&gt; in ASCII, which is not random; this is the &lt;em&gt;&lt;code&gt;UID&lt;/code&gt;&lt;/em&gt; (&lt;em&gt;Unique ID&lt;/em&gt;), which belongs the specific manufacturer and/or product (I’m sure the &lt;code&gt;WS&lt;/code&gt; in the string is for &lt;em&gt;&lt;strong&gt;W&lt;/strong&gt;&lt;/em&gt;ave&lt;em&gt;&lt;strong&gt;S&lt;/strong&gt;&lt;/em&gt;hare). In this case, the UID is a &lt;em&gt;“double size UID”&lt;/em&gt;, which is 7 bytes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Keep in mind: Not all NFC tags use UIDs (some use NUIDs), and not all are the same byte length - &lt;a href=&quot;https://rfidcard.com/types-of-uid-rfid-card/&quot;&gt;it can differ&lt;/a&gt;, but &lt;a href=&quot;http://nfc-tools.org/index.php/ISO14443A&quot;&gt;some are well-known&lt;/a&gt; or &lt;a href=&quot;https://github.com/RfidResearchGroup/proxmark3/blob/0d1f8ca957c0ae6f3039237889cdabe5921afe2d/client/src/cmdhf14a.c#L1638-L1761&quot;&gt;can be determined&lt;/a&gt;. For Type 1, 2, and 4 tags, this is governed by ISO/IEC 14443-3.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;
&lt;h3 id=&quot;flashing-issues&quot;&gt;Flashing Issues&lt;/h3&gt;
&lt;p&gt;I ran into this with both the official WaveShare Android App and &lt;a href=&quot;https://github.com/joshuatz/nfc-eink-writer&quot;&gt;the one that I built with their library&lt;/a&gt; - flashing new content to the module tends to be really touch-and-go, and it often takes multiple tries of holding your phone against the module &lt;em&gt;just right&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I came to these conclusions with my specific phone (a Pixel 4a), so your experience my differ, but here is what seems to work best for me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Not holding the phone flush against the display, but with a tiny gap (about 0.25 - 0.5 inches) away.&lt;/li&gt;
&lt;li&gt;The less data you are pushing, the more likely the flash is to succeed&lt;/li&gt;
&lt;li&gt;Try to align the NFC transceiver in your phone with the circuit board behind the display of the module.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;problem-nfc-tag-contains-deeplink&quot;&gt;Problem: NFC tag contains “deeplink”&lt;/h3&gt;
&lt;p&gt;One issue I ran into very quickly is that the very first NDEF Record is an &lt;a href=&quot;https://developer.android.com/guide/topics/connectivity/nfc/nfc#aar&quot;&gt;AAR (&lt;em&gt;Android Application Record&lt;/em&gt;)&lt;/a&gt;, which Android treats as a high priority signal to open the corresponding app.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;type: &quot;android.com:pkg&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;data: &quot;waveshare.feng.nfctag&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As constructed with JS (Web-NFC), it would look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; encoder&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; TextEncoder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/** &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {NDEFRecordInit}&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; waveshareAAR&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	recordType: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;android.com:pkg&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	data: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		records: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;				recordType: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;text&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;				data: encoder.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;encode&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;waveshare.feng.nfctag&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With an AAR, the only way to prevent the default app from taking over (or opening the Play Store to locate it, if not installed) is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web-NFC: Start an active scan, and handle the &lt;code&gt;read&lt;/code&gt; event&lt;/li&gt;
&lt;li&gt;Android: By intercepting the intent with &lt;a href=&quot;https://developer.android.com/guide/topics/connectivity/nfc/advanced-nfc#foreground-dispatch&quot;&gt;the &lt;em&gt;foreground dispatch system&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Removing the AAR&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>NFC E-Paper Writer Android Application</title><link>https://github.com/joshuatz/nfc-epaper-writer</link><guid isPermaLink="true">https://github.com/joshuatz/nfc-epaper-writer</guid><description>Native Android application (Kotlin and Java) to update WaveShare Passively Powered NFC E-Paper Modules over NFC, and generate bitmaps for them.</description><pubDate>Thu, 20 May 2021 22:54:31 GMT</pubDate><content:encoded/><dc:creator>Joshua Tzucker</dc:creator></item><item><title>WebViewAssetLoader and WebViewClient with Kotlin</title><link>https://joshuatz.com/posts/2021/webviewassetloader-and-webviewclient-with-kotlin/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/webviewassetloader-and-webviewclient-with-kotlin/</guid><description>How to use WebViewAssetLoader and WebViewClient, with Kotlin code and syntax, to load local HTML files from asset folders into a WebView component.</description><pubDate>Mon, 10 May 2021 16:10:33 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;For fun, I’m trying to hack together a little Android app for myself. Because it potentially will use low-level system calls, I wanted to try my hand at building the app with a native Android approach - Kotlin, Java, and Android APIs - as opposed to React Native or Flutter. This is a considerable challenge to take on, given that up to this point I have had zero experience with Java, Kotlin, or native Android development.&lt;/p&gt;
&lt;p&gt;This post is about a particular challenge I ran into - trying to use &lt;code&gt;WebViewAssetLoader&lt;/code&gt; and &lt;code&gt;WebViewClient&lt;/code&gt; to get around the &lt;code&gt;ERR_ACCESS_DENIED&lt;/code&gt; issue with local HTML files.&lt;/p&gt;
&lt;h2 id=&quot;issue&quot;&gt;Issue&lt;/h2&gt;
&lt;p&gt;My scenario is fairly standard - I have some local HTML files that I want to load into a &lt;code&gt;WebView&lt;/code&gt;. The first attempt, loading via &lt;code&gt;file:///android_asset/my-file.html&lt;/code&gt; failed with &lt;code&gt;ERR_ACCESS_DENIED&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At first glance, the easy way to solve this seems to be through one or more of the following (depending on API level) (based on &lt;a href=&quot;https://stackoverflow.com/q/57131662/11447682&quot;&gt;these responses&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/reference/android/webkit/WebSettings#setAllowFileAccess(boolean)&quot;&gt;&lt;code&gt;setAllowFileAccess(true)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;android:usesCleartextTraffic=&quot;true&quot;&lt;/code&gt; to the manifest&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, after further digging, it became clear that both of the above options are not best practice, and actually reduce the security of our app. The better approach, which is mentioned in the &lt;code&gt;setAllowFileAccess&lt;/code&gt; docs, is to use &lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/webkit/WebViewAssetLoader&quot;&gt;&lt;code&gt;androidx.webkit.WebViewAssetLoader&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, &lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/webkit/WebViewAssetLoader&quot;&gt;the docs for that class&lt;/a&gt; only show examples in Java, even when you select &lt;code&gt;Kotlin&lt;/code&gt; as the language. Given that I had no experience with Java, and they were using more advanced syntax, this is where I got stuck for a tiny bit (and is the reason for this post).&lt;/p&gt;
&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;/h2&gt;
&lt;h3 id=&quot;installing-the-webviewassetloader-dependency&quot;&gt;Installing the WebViewAssetLoader Dependency&lt;/h3&gt;
&lt;p&gt;First, before even working on the Kotlin code, something that I had to figure out was that WebViewAssetLoader is &lt;a href=&quot;https://maven.google.com/web/index.html?q=webkit#androidx.webkit:webkit&quot;&gt;an extra dependency&lt;/a&gt; - it is part of &lt;code&gt;androidx&lt;/code&gt; not &lt;code&gt;android&lt;/code&gt;. Note the import path:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;kotlin&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; androidx.webkit.WebViewAssetLoader&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, before moving on, go ahead and install the required dependency. You can do this in one of two ways.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Recommend&lt;/strong&gt;: Use Android Studio: &lt;code&gt;Project Structure -&gt; Dependencies -&gt; Add Dependency (+) -&gt; Library Dependency&lt;/code&gt; and then search for and add &lt;code&gt;androidx.webkit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Alternatively, you can add the dependency directly to your &lt;code&gt;gradle&lt;/code&gt; file. For example:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;implementation &apos;androidx.webkit:webkit:1.4.0&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;webviewassetloader--webviewclient&quot;&gt;WebViewAssetLoader + WebViewClient&lt;/h3&gt;
&lt;p&gt;OK, so now that we have the dependency installed, how do we go about actually using &lt;code&gt;WebViewAssetLoader&lt;/code&gt; with a webview? In trying to re-build the example from the docs, I got tripped up on some fancy syntax and how to emulate it in Kotlin:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Java code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;webView.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setWebViewClient&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; WebViewClient&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Override&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	public&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; WebResourceResponse &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;shouldInterceptRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(WebView &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;view&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, WebResourceRequest &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; assetLoader.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;shouldInterceptRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getUrl&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Coming from web development, that syntax for both extending and initializing a class instance in the same section (and as an argument!) is something I had rarely ever seen before. In Java, this is known as &lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/javaOO/anonymousclasses.html#syntax-of-anonymous-classes&quot;&gt;&lt;code&gt;Anonymous Class&lt;/code&gt; syntax&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To replicate this same syntax in Kotlin, &lt;a href=&quot;https://stackoverflow.com/a/17516998/11447682&quot;&gt;there are several options&lt;/a&gt;, but the important part is mostly &lt;a href=&quot;https://kotlinlang.org/docs/object-declarations.html#inheriting-anonymous-objects-from-supertypes&quot;&gt;the &lt;code&gt;object : MyClass() { }&lt;/code&gt; syntax.&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here is a consolidated example, showing an activity that uses WebView with a local HTML file, via WebViewAssetLoader:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MyActivity.kt&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;kotlin&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;package&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; com.joshuatz.demo&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; androidx.appcompat.app.AppCompatActivity&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; android.os.Bundle&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; android.webkit.WebResourceRequest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; android.webkit.WebResourceResponse&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; android.webkit.WebView&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; android.webkit.WebViewClient&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; androidx.webkit.WebViewAssetLoader&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; MyActivity&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; : &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;AppCompatActivity&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	override&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; fun&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; onCreate&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(savedInstanceState: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Bundle&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;?) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		super&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;onCreate&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(savedInstanceState);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;		setContentView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(R.layout.activity_my_activity);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		val&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; webView: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebView&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; findViewById&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(R.id.myWebView);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Setup asset loader to handle local asset paths&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		val&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; assetLoader &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; WebViewAssetLoader.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Builder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;addPathHandler&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/assets/&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, WebViewAssetLoader.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;AssetsPathHandler&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;build&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Override WebView client, and if request is to local file, intercept and serve local&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		webView.webViewClient &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; object&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; : &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebViewClient&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;			override&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; fun&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; shouldInterceptRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;				view: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;				request: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebResourceRequest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			): &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;WebResourceResponse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;? {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;				return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; assetLoader.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;shouldInterceptRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request.url);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		webView.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;loadUrl&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://appassets.androidplatform.net/assets/my-file.html&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 There is another important change between using WebViewAssetLoader and standard WebView - notice how the path changed from&lt;code&gt;file:///android_asset/...&lt;/code&gt; to &lt;code&gt;https://appassets.androidplatform.net/assets/...&lt;/code&gt;. The new domain is less important (it’s a non-existent subdomain), but the &lt;code&gt;/assets/&lt;/code&gt; is very important for our WebViewAssetLoader path handler - it is how it matches the URL.&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Globlist Packer - NodeJS Powered File Packing Tool</title><link>https://github.com/joshuatz/globlist-packer</link><guid isPermaLink="true">https://github.com/joshuatz/globlist-packer</guid><description>CLI and module for packing files into directories or archives, based on ignorelist files and glob patterns. Built with TypeScript and NodeJS, and highly configurable.</description><pubDate>Thu, 29 Apr 2021 01:54:52 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_full_page_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Using TypeScript with WordPress Gutenberg Blocks</title><link>https://joshuatz.com/posts/2021/using-typescript-with-wordpress-gutenberg-blocks/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/using-typescript-with-wordpress-gutenberg-blocks/</guid><description>How to use TypeScript with WordPress Blocks and the Gutenberg environment. Covers config changes, necessary dependencies, and some possible issues.</description><pubDate>Sat, 17 Apr 2021 10:42:55 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;For those unfamiliar, Gutenberg Blocks, or now known simply as &lt;a href=&quot;https://wordpress.org/support/article/blocks/&quot;&gt;&lt;em&gt;Blocks&lt;/em&gt;&lt;/a&gt;, are the backbone behind the new reusable component-driven paradigm for WordPress.&lt;/p&gt;
&lt;p&gt;For more info on Gutenberg, you can check out &lt;a href=&quot;https://wordpress.org/gutenberg/&quot;&gt;WordPress’s landing page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, if you are reading this, there is a good chance that you are a WordPress developer, maybe even already familiar with Blocks and how they work. There are lots of dev guides out there on how to use Blocks (including my own notes, &lt;a href=&quot;https://docs.joshuatz.com/cheatsheets/wordpress/wp-gutenberg-blocks/&quot;&gt;here&lt;/a&gt;), but I was recently having some issues using Blocks with &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;the TypeScript programming language&lt;/a&gt; (which gets converted into JavaScript) and found much less out there to guide my way.&lt;/p&gt;
&lt;p&gt;This post covers some steps I had to take to get TypeScript working with WP Gutenberg Blocks, as well as some issues that I ran into.&lt;/p&gt;
&lt;h2 id=&quot;steps&quot;&gt;Steps&lt;/h2&gt;
&lt;p&gt;The first step, whether or not you end up using TypeScript, I would recommend to be to install and start using &lt;code&gt;wp-scripts&lt;/code&gt; (&lt;a href=&quot;https://github.com/WordPress/gutenberg/tree/trunk/packages/scripts&quot;&gt;source code&lt;/a&gt;, &lt;a href=&quot;https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/&quot;&gt;docs&lt;/a&gt;) for orchestrating your build setup. You don’t &lt;em&gt;&lt;strong&gt;have&lt;/strong&gt;&lt;/em&gt; to do this, but it makes things a lot easier while still being extensible via configuration files and CLI arguments.&lt;/p&gt;
&lt;p&gt;Now, assuming that you are using the &lt;code&gt;wp-scripts&lt;/code&gt;, or something similar, here are the general steps I had to take to get TypeScript working&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install TypeScript and &lt;code&gt;ts-loader&lt;/code&gt; locally
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm i -D ts-loader&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm i -D typescript&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;tsconfig.json&lt;/code&gt; file and adjust it (&lt;a href=&quot;#tsconfig&quot;&gt;see below&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Modify the WebPack config to use &lt;code&gt;ts-loader&lt;/code&gt; (&lt;a href=&quot;#modifying-the-webpack-config&quot;&gt;see below&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Install types for packages that distribute them separately (i.e. through the &lt;code&gt;@types&lt;/code&gt; monorepo)&lt;/li&gt;
&lt;li&gt;Ambient type-declare for packages that are missing them (&lt;a href=&quot;#untyped-packages&quot;&gt;see below&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;tsconfig&quot;&gt;TSConfig&lt;/h2&gt;
&lt;p&gt;Since your WebPack config will override a lot of settings of your TSConfig, there is less to worry about, but still some settings that really matter.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;compilerOptions&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Note: Webpack config overrides this anyways&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;outDir&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;./dist&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// React compatibility&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;jsx&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;react-jsx&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// We want ESM supported in the output, because WebPack is going to bundle this anyways,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// and we want the tree-shaking support and some other benefits&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;module&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ESNext&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;target&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ES5&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// == Now for some settings that are not *strictly* required, but I recommend:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Embrace strong-typing!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;strict&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Not strictly necessary, but nice&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;allowSyntheticDefaultImports&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Recommended&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;moduleResolution&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;node&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;modifying-the-webpack-config&quot;&gt;Modifying the WebPack Config&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 WebPack has &lt;a href=&quot;https://webpack.js.org/guides/typescript/&quot;&gt;a guide on using TypeScript with WebPack&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For actually generating our final code that will be distributed, we are still going to be using WebPack, not the TypeScript compiler (&lt;code&gt;tsc&lt;/code&gt;). However, we have to tell WebPack about the TypeScript code we have added and give it a tool to load it - that is why we added &lt;code&gt;ts-loader&lt;/code&gt; as a devDependency.&lt;/p&gt;
&lt;p&gt;Luckily for us, &lt;code&gt;wp-scripts&lt;/code&gt; not only allows us to use a custom WebPack config, but also &lt;a href=&quot;https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/config/webpack.config.js&quot;&gt;exports the base config&lt;/a&gt; so that we can &lt;em&gt;extend&lt;/em&gt; it instead of &lt;em&gt;replacing&lt;/em&gt; it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;webpack.config.js&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// This comes from https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/config/webpack.config.js&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; wpConfig&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;( &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;@wordpress/scripts/config/webpack.config&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Modified config&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Since whatever object we export from this file will *replace* the default config, we need to&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// make sure to constantly pull in the default values from WordPress&apos;s config file&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;wpConfig,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Point to your main file, wherever it is, which should now be TS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	entry: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`./src/index.ts`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// We need to add a new rule to process `.ts` and `.tsx` files with `ts-loader&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	module: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;defaultConfig.module,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		rules: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;			...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;defaultConfig.module.rules,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;				// Notice that this regex matches both `.ts` and `.tsx`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;				test:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#85E89D;font-weight:bold&quot;&gt;\.&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;tsx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?$&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;				use: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;					{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;						loader: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ts-loader&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;						options: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;							// You can specify any custom config&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;							configFile: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;tsconfig.json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;							// See note under &quot;issues&quot; for details&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;							// Speeds up by skipping type-checking. You can still use TSC for that.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;							transpileOnly: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;						},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;					},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;				],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// This merges the extensions setting with whatever WP has in their config, but also adds TS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// extensions, and puts them first, to indicate preferred resolving over .js files&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// @see https://webpack.js.org/configuration/resolve/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	resolve: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		extensions: [ &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;.ts&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;.tsx&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(defaultConfig.resolve &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; defaultConfig.resolve.extensions &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;.js&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;.jsx&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [])],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;issues&quot;&gt;Issues&lt;/h2&gt;
&lt;p&gt;Other than getting the configuration setup, there were a few issues that I ran into that I feel would be helpful to share.&lt;/p&gt;
&lt;h3 id=&quot;untyped-packages&quot;&gt;Untyped packages&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Some WordPress packages offer type definitions (either bundled directly, or, very frequently, distributed via &lt;code&gt;@types&lt;/code&gt;, such as &lt;a href=&quot;https://www.npmjs.com/package/@types/wordpress__blocks&quot;&gt;&lt;code&gt;@types/wordpress__blocks&lt;/code&gt;&lt;/a&gt;). However, there are still a bunch that are completely “untyped”, which will throw all kinds of errors in TypeScript, especially if you have strict rules on.&lt;/li&gt;
&lt;li&gt;To address this, you either need to wait for types to be added upstream, do it yourself and make a PR to merge them (help the open-source community!), or, if you are in a rush, you can augment the types by using ambient declaration files.
&lt;ul&gt;
&lt;li&gt;I’m not going to go into detail on how to do that here, since it is complicated, but I &lt;em&gt;do&lt;/em&gt; have a &lt;a href=&quot;https://docs.joshuatz.com/cheatsheets/typescript/#dealing-with-legacy-js-ambient-declarations-errors-oh-my&quot;&gt;comprehensive section on this in my TypeScript cheatsheet&lt;/a&gt;!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;ts-loader-refuses-to-stop-type-checking-node_modules&quot;&gt;TS-Loader: Refuses to Stop Type-Checking node_modules&lt;/h3&gt;
&lt;p&gt;One frustrating issue that I ran into was that I simply could not get my build command to stop type-checking &lt;code&gt;node_modules&lt;/code&gt; - this is a big deal because A) it slows everything down, and B) it actually finds errors and will throw an exception. But it’s not my code!&lt;/p&gt;
&lt;p&gt;Trying all the common settings had no effect, such as explicitly excluding node_modules through &lt;code&gt;tsconfig&lt;/code&gt;, excluding through the WebPack config, and passing explicit inputs.&lt;/p&gt;
&lt;p&gt;However, I did eventually find the culprit, as well as two solutions.&lt;/p&gt;
&lt;p&gt;The culprit for me was that I was using &lt;code&gt;&quot;checkJs&quot;: true&lt;/code&gt; in my &lt;code&gt;tsconfig.json&lt;/code&gt; file, which apparently causes &lt;code&gt;ts-loader&lt;/code&gt; to ignore the exclude rules and start checking &lt;code&gt;node_modules&lt;/code&gt;. Although &lt;a href=&quot;https://github.com/TypeStrong/ts-loader/issues/702&quot;&gt;this issue is marked “Closed” on Github&lt;/a&gt;, I can assure you it still exists (even with TypeScript version &lt;code&gt;4.2+&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Solutions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Recommendation&lt;/strong&gt;: Run &lt;code&gt;ts-loader&lt;/code&gt; with &lt;a href=&quot;https://github.com/TypeStrong/ts-loader#transpileonly&quot;&gt;the &lt;code&gt;transpileOnly&lt;/code&gt; setting&lt;/a&gt; changed to &lt;code&gt;true&lt;/code&gt;.
&lt;ul&gt;
&lt;li&gt;This stops WebPack from type-checking your code &lt;strong&gt;entirely&lt;/strong&gt;, which means that you should run &lt;code&gt;tsc&lt;/code&gt; separately with &lt;code&gt;tsc --noEmit&lt;/code&gt; when you want to type-check&lt;/li&gt;
&lt;li&gt;This also speeds up build and reload times!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;You can create a separate TS config file just to use with &lt;code&gt;ts-loader&lt;/code&gt;, such as &lt;code&gt;tsconfig.webpack.json&lt;/code&gt;, which has &lt;code&gt;&quot;checkJs&quot;: false&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Instead of copying and pasting the other settings, you can use TSC’s support for &lt;a href=&quot;https://www.typescriptlang.org/tsconfig#extends&quot;&gt;&lt;code&gt;extends&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pass the selected config to &lt;code&gt;ts-loader&lt;/code&gt; via &lt;a href=&quot;https://github.com/TypeStrong/ts-loader#configfile&quot;&gt;the &lt;code&gt;options.configFile&lt;/code&gt; setting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;using-typescript-changed-the-output-filenames&quot;&gt;Using TypeScript Changed The Output Filenames&lt;/h3&gt;
&lt;p&gt;The last interesting issue that I encountered was that changing the entry point of my WebPack config to point to a TypeScript file changed all the output filenames from &lt;code&gt;index.*&lt;/code&gt; to &lt;code&gt;main.*&lt;/code&gt;. I’m not sure exactly what caused this internally (my guess would be something hidden in the WordPress Webpack base config that handles chunk names), but the fix was easy enough.&lt;/p&gt;
&lt;p&gt;Just make sure that in the PHP side, when you register and enqueue assets for your block, you use the updated filenames that show up in the build output.&lt;/p&gt;
&lt;p&gt;If you really want to keep the original filenames, you can override the output settings in the WebPack config: see how &lt;a href=&quot;https://github.com/mkaz/tsblock/blob/267fe41786e34959852ae6d09131055c451d0863/webpack.config.js#L24-L28&quot;&gt;this sample repo solved it&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you are looking for more resources on TypeScript + Blocks, there aren’t many guides out there, but &lt;a href=&quot;https://mkaz.blog/wordpress/create-a-block-in-typescript/&quot;&gt;this blog post by Kazmierczak&lt;/a&gt; is a nice succinct explanation, and they also have a &lt;a href=&quot;https://github.com/mkaz/tsblock&quot;&gt;sample repo&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Feel free to let me know if I’ve missed anything or you have tips of your own to share! Hope this helps someone!&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>HTML Canvas - Filled Quarter and Partial Circles with Arc</title><link>https://joshuatz.com/posts/2021/html-canvas---filled-quarter-and-partial-circles-with-arc/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/html-canvas---filled-quarter-and-partial-circles-with-arc/</guid><description>How to use the Arc Canvas Web API method to draw partial filled circle segments, including quarter circles, and even with added rotation.</description><pubDate>Thu, 25 Mar 2021 07:14:55 GMT</pubDate><content:encoded>&lt;p&gt;Quick post today - I thought I would share some tips on drawing rotated partial circles (e.g. quarter-circles, or slices) with HTML Canvas and JavaScript. It is actually slightly harder than one might think, as there are some “gotchas”.&lt;/p&gt;
&lt;h2 id=&quot;things-to-note&quot;&gt;Things to Note&lt;/h2&gt;
&lt;p&gt;Here are the two things that I ran into that made me pause when trying to do this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API&quot;&gt;Canvas Web API&lt;/a&gt; uses &lt;em&gt;&lt;strong&gt;radians&lt;/strong&gt;&lt;/em&gt; for angle measurement, &lt;em&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;/em&gt; &lt;em&gt;degrees&lt;/em&gt;.
&lt;ul&gt;
&lt;li&gt;If you are new working with Canvas, this might be easy to miss, as the function arguments are often just named things like &lt;code&gt;angle&lt;/code&gt; or &lt;code&gt;startAngle&lt;/code&gt;, not indicating units unless you read through the docs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If you are trying to create a &lt;strong&gt;filled&lt;/strong&gt; segment of a circle (a.k.a. a &lt;em&gt;slice&lt;/em&gt;), you need to start an explicit path before drawing your arc, and you need to make sure to set the starting point at the arc origin, &lt;em&gt;even though&lt;/em&gt; you also pass that origin to the &lt;code&gt;.arc()&lt;/code&gt; method.
&lt;ul&gt;
&lt;li&gt;Use&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/beginPath&quot;&gt; &lt;code&gt;ctx.beginPath()&lt;/code&gt; method&lt;/a&gt; to start the path&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/moveTo&quot;&gt;&lt;code&gt;ctx.moveTo()&lt;/code&gt; method&lt;/a&gt; to move “cursor” / starting point, and explicitly start sub-path&lt;/li&gt;
&lt;li&gt;Technically, I believe you could just use &lt;code&gt;moveTo()&lt;/code&gt; without &lt;code&gt;beginPath()&lt;/code&gt;, since &lt;code&gt;moveTo()&lt;/code&gt; starts a new sub-path automatically&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;wrapper-function---quarter-circles&quot;&gt;Wrapper Function - Quarter Circles&lt;/h2&gt;
&lt;p&gt;Putting these important pieces together, I created a simle wrapper function that I can call whenever I want to draw a quarter circle:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; drawQuarterCircle&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;origin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;radius&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;startAngleDeg&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; startAngle&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; startAngleDeg &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (Math.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;PI&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 180&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; endAngle&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; startAngle &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Math.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;PI&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0.5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;beginPath&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;moveTo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(origin.x, origin.y);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Draw actual arc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;arc&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(origin.x, origin.y, radius, startAngle, endAngle, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	ctx.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;fill&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;startAngleDeg&lt;/code&gt; is in normal degrees, to make conceptualizing what you are drawing a little easier.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;startAngleDeg&lt;/code&gt; is also what controls the rotation of the rendered quarter circle. See demo below for a clear use-case&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;And here is an embedded demo, showing how to use it to draw rotated quarter circles:&lt;/p&gt;
&lt;iframe height=&quot;265&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Canvas - Fill Quarter Circles&quot; src=&quot;https://codepen.io/joshuatz/embed/OJWymdm?height=265&amp;#x26;theme-id=light&amp;#x26;default-tab=js,result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&gt;
  See the Pen &amp;#x3C;a href=&apos;https://codepen.io/joshuatz/pen/OJWymdm&apos;&gt;Canvas - Fill Quarter Circles&amp;#x3C;/a&gt; by Joshua T
  (&amp;#x3C;a href=&apos;https://codepen.io/joshuatz&apos;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&apos;https://codepen.io&apos;&gt;CodePen&amp;#x3C;/a&gt;.
&lt;/iframe&gt;
&lt;h2 id=&quot;arbitrary-partial-circles&quot;&gt;Arbitrary Partial Circles&lt;/h2&gt;
&lt;p&gt;To draw arbitrary slices of a circle, you can adapt the above code; &lt;code&gt;startAngle&lt;/code&gt; and &lt;code&gt;endAngle&lt;/code&gt; are the arguments of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc&quot;&gt;the &lt;code&gt;.arc()&lt;/code&gt; method&lt;/a&gt; that you want to use to control the slice of the circle that gets rendered.&lt;/p&gt;
&lt;h2 id=&quot;importance-of-setting-the-first-point-on-the-path&quot;&gt;Importance of Setting The First Point on the Path&lt;/h2&gt;
&lt;p&gt;Just for fun, here is what happens if you forget to explicitly set the first point of your path as the origin point of the arc:&lt;/p&gt;
&lt;iframe height=&quot;265&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Canvas - Arc With and Without moveTo()&quot; src=&quot;https://codepen.io/joshuatz/embed/yLgeoOB?height=265&amp;#x26;theme-id=light&amp;#x26;default-tab=js,result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&gt;
  See the Pen &amp;#x3C;a href=&apos;https://codepen.io/joshuatz/pen/yLgeoOB&apos;&gt;Canvas - Arc With and Without moveTo()&amp;#x3C;/a&gt; by Joshua T
  (&amp;#x3C;a href=&apos;https://codepen.io/joshuatz&apos;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&apos;https://codepen.io&apos;&gt;CodePen&amp;#x3C;/a&gt;.
&lt;/iframe&gt;
&lt;p&gt;As you can see, without the first point on the path being the origin of the arc (or center point), the &lt;code&gt;.fill()&lt;/code&gt; method only fills the area between the two end points of the arc itself, drawing a direct line across the curve.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Why Every Developer Should Care About Documentation</title><link>https://joshuatz.com/posts/2021/why-every-developer-should-care-about-documentation/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/why-every-developer-should-care-about-documentation/</guid><description>Reasons why every developer should care about documentation. Personal, business, and practical reasons why writing documentation is worth it.</description><pubDate>Mon, 01 Mar 2021 13:57:10 GMT</pubDate><content:encoded>&lt;p&gt;I’ve wanted to write this post for a while now, as with every day that passes, I believe its message more and more: truly, every developer should care about documentation. And I really do mean &lt;em&gt;&lt;strong&gt;every&lt;/strong&gt;&lt;/em&gt; developer.&lt;/p&gt;
&lt;p&gt;Personally, documentation is something that I care about and is closely related to my passion for the sharing of knowledge, self-education, and the belief that coding should be accessible to all.&lt;/p&gt;
&lt;p&gt;However, I realize that not everyone shares these passions, so I’ve set out to compile a list of &lt;em&gt;&lt;strong&gt;practical&lt;/strong&gt;&lt;/em&gt; reasons why developers should care about documentation (&lt;em&gt;spoiler&lt;/em&gt;: documenting your code is often as much a benefit to yourself as it is to others!).&lt;/p&gt;
&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#clarification&quot;&gt;Clarification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part-a-its-for-your-own-good&quot;&gt;Part A: It’s For Your Own Good&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#documenting-code-uncovers-bugs-vulnerabilities-and-issues&quot;&gt;Documenting Code Uncovers Bugs, Vulnerabilities, and Issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#writing-while-dogfooding&quot;&gt;Writing While “Dogfooding”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#improve-productivity&quot;&gt;Improve Productivity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#documentation-driven-development&quot;&gt;Documentation Driven Development&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#part-b-for-the-good-of-the-team-users-and-everyone&quot;&gt;Part B: For the Good of the Team, Users, and Everyone&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#developer-experience-and-relations&quot;&gt;Developer Experience and Relations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#as-a-form-of-backup&quot;&gt;As a Form of Backup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#final-thoughts&quot;&gt;Final Thoughts:&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#refuting-common-arguments&quot;&gt;Refuting Common Arguments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#preventing-out-of-date-docs&quot;&gt;Preventing Out-of-Date Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#im-not-perfect&quot;&gt;I’m Not Perfect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#further-reading&quot;&gt;Further Reading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;clarification&quot;&gt;Clarification&lt;/h2&gt;
&lt;p&gt;Before getting started, I should clarify for whom and how this post might apply. Starting with the term &lt;em&gt;documentation&lt;/em&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The term &lt;em&gt;documentation&lt;/em&gt; can refer to a lot of different things; README files, Wikis (e.g. &lt;em&gt;Confluence&lt;/em&gt;), inline code comments, or even documents that are auto-generated directly from the source code!&lt;/p&gt;
&lt;p&gt;Within the scope of this post, I’ll be talking about documentation in general, although the focus will be somewhat on more verbose forms of documentation, such as README files.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And for &lt;em&gt;“whom”&lt;/em&gt; do the points in this post apply to? Well, I stand by my article title - yes, &lt;em&gt;every&lt;/em&gt; developer should care about docs - but, at the same time, I acknowledge that some specific points (such as using doc writing as motivation) might apply less to those in highly regulated industries or working in a role that is already driven by tight requirements and things like &lt;a href=&quot;https://en.wikipedia.org/wiki/Behavior-driven_development&quot;&gt;BDD&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-a-its-for-your-own-good&quot;&gt;Part A: It’s For Your Own Good&lt;/h2&gt;
&lt;p&gt;Since I often see that it is difficult to get some developers to care about documentation, since they claim they don’t see any &lt;em&gt;&lt;strong&gt;personal&lt;/strong&gt;&lt;/em&gt; benefit from spending time on it - I’m going to start by challenging that fallacy. Even if you are a team of one, only working on your own solo project that &lt;em&gt;only you use&lt;/em&gt;, there are &lt;strong&gt;still&lt;/strong&gt; very important reasons to care about documentation.&lt;/p&gt;
&lt;h3 id=&quot;documenting-code-uncovers-bugs-vulnerabilities-and-issues&quot;&gt;Documenting Code Uncovers Bugs, Vulnerabilities, and Issues&lt;/h3&gt;
&lt;p&gt;This goes hand-in-hand with my point about “dogfooding” (more on this later), but often a underappreciated way to find bugs, security concerns, and issues that need to be raised is to write documentation, and / or working on keeping it up-to-date.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I’m not going to go deep into discussing things like requirements and specification languages, but they are an obvious tangent to this point, and can have a similar effect.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The reason why I say that writing docs can help a developer uncover bugs and issues is because the act of writing them often challenges the assumptions they might have about how their code works, and forces them to see their system in a new context.&lt;/p&gt;
&lt;p&gt;When you are writing and viewing code, unless your code base is extremely tiny (or your monitor is extremely large), you are never seeing the entire thing at one time. Your brain conveniently fills in the gaps - you know that when you call method X, it is going to do thing Y and Z.&lt;/p&gt;
&lt;p&gt;The same thing generally goes for thinking about the user experience (or developer experience) - the model of what the experience looks like is too large to “see” at a single moment, so you either consider a tiny part (“What does it look like for a user to request an authentication token from us?”), or for the macro level, you brain goes back to its old tricks of filling in the gaps and abstracting into broader levels.&lt;/p&gt;
&lt;p&gt;Reading the above, you might see how this could lead to issues. Assumptions are dangerous in software development, and although tests might catch bugs, or even visual regressions, they don’t generally check &lt;em&gt;experience&lt;/em&gt; or &lt;em&gt;usability&lt;/em&gt;, they check that something works and meets preset expectations. An application might compile and run without crashing, but fail to be functional in a practical sense.&lt;/p&gt;
&lt;p&gt;Writing documentation encourages you to slow down, see your code in a new context, and challenges assumptions you have about how your own codebase works.&lt;/p&gt;
&lt;h3 id=&quot;writing-while-dogfooding&quot;&gt;Writing While “Dogfooding”&lt;/h3&gt;
&lt;p&gt;One of my favorite “side-effects” of writing documentation is that it often forces a developer to “dogfood” their own product (or area of the product that they are working on).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For those unfamiliar, the term &lt;a href=&quot;https://en.wikipedia.org/wiki/Eating_your_own_dog_food&quot;&gt;&lt;em&gt;“Dogfooding”&lt;/em&gt;&lt;/a&gt; usually refers to the practice of using your own product or service.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is often a disconnect that happens with software development, where people end up working on features that they never use themselves. In my opinion, this is also a large contributing factor to how we end up with scenarios where a feature is released or updated in a way that makes the user experience &lt;em&gt;&lt;strong&gt;worse&lt;/strong&gt;&lt;/em&gt; for many users, and they are left wondering: &lt;em&gt;“Why did no one realize how unusable this is?”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/joshuatz-posts/cant-be-usability-issues-orly.png&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto; text-align:center;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/c_scale,w_360/joshuatz-posts/cant-be-usability-issues-orly.png&quot; alt=&quot;Spoof O&amp;#x27;Reilly book cover that reads &amp;#x27;There Can&amp;#x27;t Be Usability Issues If I Never Use It&quot; style=&quot;width:100%;max-width: 360px;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Writing documentation usually &lt;em&gt;&lt;strong&gt;forces&lt;/strong&gt;&lt;/em&gt; the person writing it to, at the very least, run through the basic steps of using the thing they are writing about. This, in turn, has a couple huge benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The author might discover &lt;em&gt;&lt;strong&gt;new&lt;/strong&gt;&lt;/em&gt; features that can be added in the future. This might come about in several different forms of realizations:
&lt;ul&gt;
&lt;li&gt;“Wait, our product doesn’t do ____? We should add that!”&lt;/li&gt;
&lt;li&gt;“Huh, what I’m documenting sounds useful, but I have a feeling it could be better if ___”&lt;/li&gt;
&lt;li&gt;“This can’t be all there is to ____!”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The author might discover shortcomings in existing features
&lt;ul&gt;
&lt;li&gt;“Wow, this should not take this many steps to do! Our users put up with this?”&lt;/li&gt;
&lt;li&gt;“This can’t be right…”&lt;/li&gt;
&lt;li&gt;“Wait, how does this work again?”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The author might realize ways in which their mental model of the application has &lt;em&gt;diverged&lt;/em&gt; from what actually ends up getting delivered to the end-user
&lt;ul&gt;
&lt;li&gt;“I could have sworn it worked this way!”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;improve-productivity&quot;&gt;Improve Productivity&lt;/h3&gt;
&lt;p&gt;Comprehensive documentation can have a &lt;em&gt;massive&lt;/em&gt; impact on developer productivity, when you consider that it can have all the following effects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduce employee onboarding time
&lt;ul&gt;
&lt;li&gt;“Getting started” docs, even if only internally used, can greatly cut down on time to get a new dev setup&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Reduce redundant questions / issues
&lt;ul&gt;
&lt;li&gt;I love people asking questions - it is a necessary part of growth. However, if you have multiple people all asking the same questions over and over again, that is a good sign that the knowledge they are seeking is important enough to belong in shared documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Reduces delays / “knowledge latency”
&lt;ul&gt;
&lt;li&gt;No one wants a project held up because everyone is waiting on one person with secret knowledge to reply to a long email chain&lt;/li&gt;
&lt;li&gt;Docs don’t take vacations, they aren’t going to burn out from everyone pestering them, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;documentation-driven-development&quot;&gt;Documentation Driven Development&lt;/h3&gt;
&lt;!-- omit in toc --&gt;
&lt;h4 id=&quot;starting-with-docs-is-motivating&quot;&gt;Starting with Docs is Motivating&lt;/h4&gt;
&lt;p&gt;This point is especially true if you are a “indie hacker”, solo coder, entrepreneur, etc. - &lt;em&gt;&lt;strong&gt;writing documentation can be extremely motivating if you approach it with the right perspective(s)&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In explaining this point, I’ll start off with what is probably the wrong approach if your goal is self-motivation: don’t leave writing documentation as the last thing to do, as a final step when all the code has already been written.&lt;/p&gt;
&lt;p&gt;Instead, to use documentation as motivation I would suggest writing and updating docs actively as you work, or even writing ahead - describing features that you &lt;em&gt;aspire&lt;/em&gt; to one day implement (making sure to either keep these out of public-facing docs, or clearly marking them as “roadmap” items).&lt;/p&gt;
&lt;p&gt;This might not work for everyone, but I often find that if I have already written down the basics of a feature that I’m personally excited about, having that documentation in front of me is highly motivating; even if it is just a single bullet point, having it written down helps me visualize the end-goal and focus on my desire to make it become reality.&lt;/p&gt;
&lt;!-- omit in toc --&gt;
&lt;h4 id=&quot;starting-with-docs-is-a-legitimate-pattern&quot;&gt;Starting with Docs Is a Legitimate Pattern&lt;/h4&gt;
&lt;p&gt;I’m far from the first person to think that starting with the docs is an approach worth exploring. Those who have been in software for some time might already be familiar with a lot of related topics:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;a href=&quot;https://en.wikipedia.org/wiki/Waterfall_model&quot;&gt;&lt;em&gt;“Waterfall Model”&lt;/em&gt; of Software Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Behavior-driven_development&quot;&gt;&lt;em&gt;“Behavior-driven development”&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-b-for-the-good-of-the-team-users-and-everyone&quot;&gt;Part B: For the Good of the Team, Users, and Everyone&lt;/h2&gt;
&lt;h3 id=&quot;developer-experience-and-relations&quot;&gt;Developer Experience and Relations&lt;/h3&gt;
&lt;!-- omit in toc --&gt;
&lt;h4 id=&quot;developer-experience--dx&quot;&gt;Developer Experience / DX&lt;/h4&gt;
&lt;p&gt;If the product that you are working is something that is developer focused (e.g. an API, SDK, Framework, PaaS, etc.), then you might already be aware of how critical documentation is in eliciting a positive developer experience (&lt;a href=&quot;https://css-tricks.com/what-is-developer-experience-dx/&quot;&gt;DX&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;However, something that often gets overlooked is that, in the same way that having a lot of tests is not &lt;em&gt;necessarily&lt;/em&gt; the same as having high test coverage, having a lot of docs is not necessarily the same as having documentation that properly covers the developer experience of your product.&lt;/p&gt;
&lt;p&gt;To be fair, there is a very fine balance between too much documentation (which can overwhelm the user / dev), and too little or sparse documentation. But at the end of the day, there is a truth which is hard to ignore - you can build a dev product that beats every competitor out of the water with features, stability, and performance - but without docs, without a way for developers to know &lt;em&gt;&lt;strong&gt;how&lt;/strong&gt;&lt;/em&gt; to use your product - you can’t expect people to jump on board.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/q_90,w_300/joshuatz-posts/lets-do-it-postit.png&quot; alt=&quot;Hand-written Post-It Note reading &amp;#x27;Let&amp;#x27;s Do It!&amp;#x27;&quot; style=&quot;margin: auto; display:block; max-width: 300px; width: 95%; height: auto;&quot;&gt;
&lt;figcaption&gt;&quot;You&apos;re saying this inspirational post-it note isn&apos;t enough instruction for potential users?&quot;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;!-- omit in toc --&gt;
&lt;h4 id=&quot;developer-relations-devrel-advocacy-evangelism&quot;&gt;Developer Relations (DevRel), Advocacy, Evangelism&lt;/h4&gt;
&lt;p&gt;The role of a &lt;a href=&quot;https://medium.com/@ashleymcnamara/what-is-developer-advocacy-3a92442b627c&quot;&gt;developer advocate&lt;/a&gt;, if you want to boil it down into the core purpose, usually comes down to “helping developers (use your product)”. In the context of this definition, I don’t think anyone will argue with me that docs are a big part of helping developers, but sometimes there are more subtle influences that documentation has on developer relations aka &lt;em&gt;DevRel&lt;/em&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Evangelism and driving product growth
&lt;ul&gt;
&lt;li&gt;Usually, under the umbrella of DevRel, one of the objectives is spreading word about the product to developers and helping to drive growth. (“Hey, it’s a business after all!”)&lt;/li&gt;
&lt;li&gt;People often focus on blog posts, viral tweets, and landing pages as the content that drives further adoption, while neglecting to talk about docs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Good docs can help drive growth&lt;/strong&gt;, especially if attention is paid to proper SEO (Search-Engine Optimization) and organization
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;There are a number of products that I have found, and started using, only because I ended up on their documentation while researching something related.&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Conversely, there are also some products that I have avoided entirely, or hesitated trying, due to what I perceived as sub-par documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Some products are renowned for the quality of their docs, and that itself can become a driving force behind “word-of-mouth” growth&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;For open source: driving contributions and community interest
&lt;ul&gt;
&lt;li&gt;If your product is open-source, and you are looking to get the community more involved and grow the number of contributors, documentation is great way to do so&lt;/li&gt;
&lt;li&gt;Documentation is often the entry-point for a developer, before pretty much anything else, so it becomes an easy place for developers to contribute back to the project by addressing shortcomings in the documentation as they on-board themselves
&lt;ul&gt;
&lt;li&gt;Example: Developer opens a Pull-Request (PR) to add a section to the “Getting Started” section, with added instructions for Windows (current docs only have instructions for Mac or *Nix)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 Further reading:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/kimmaida/the-developer-relations-explainer-431o&quot;&gt;&lt;em&gt;“The Developer Relations Explainer”&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@ashleymcnamara/what-is-developer-advocacy-3a92442b627c&quot;&gt;&lt;em&gt;“What is Developer Advocacy”&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;!-- omit in toc --&gt;
&lt;h4 id=&quot;empathy&quot;&gt;Empathy&lt;/h4&gt;
&lt;p&gt;The best documentation is inclusive and meant for as many people as possible. Writing documentation with this outcome as the goal is a great way to challenge yourself to approach your codebase with greater empathy for other developers.&lt;/p&gt;
&lt;h3 id=&quot;as-a-form-of-backup&quot;&gt;As a Form of Backup&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Just like code and data, knowledge needs protecting and backing up too! A bunch of code without the information on how to run or update it does not hold much value in practice.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you mention the words “backup”, “recovery”, or “redundancy” in a room full of software industry professionals, you are likely to get swarmed by multiple people who are all too enthusiastic to explain how &lt;em&gt;&lt;strong&gt;their&lt;/strong&gt;&lt;/em&gt; company’s systems are built to handle any sort of catastrophe or incident.&lt;/p&gt;
&lt;figure&gt;
&lt;a href=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/v1616010992/joshuatz-posts/line-of-people-walking-up-steps.jpg&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto; text-align:center;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/v1616010992/joshuatz-posts/line-of-people-walking-up-steps.jpg&quot; alt=&quot;Line of people walking up steps&quot; style=&quot;width:100%;max-width: 800px;height:auto;&quot;&gt;&lt;/a&gt;
&lt;figcaption&gt;&quot;Is this the line to talk that person&apos;s ear off about backup systems?&quot;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;You’ll hear things like how they use multiple redundant backups, perhaps even replicating onto physical long-term storage, like tapes. They might backup every version-controlled repository daily, refusing to rely entirely on a third-party host like Github or Perforce, in case of a failure. They might boast about how fast they can rollback the data itself - reverting a &lt;code&gt;DROP TABLE&lt;/code&gt; or &lt;code&gt;rm -rf&lt;/code&gt; type incident with lightning speed.&lt;/p&gt;
&lt;p&gt;You might also get an ear-full about how easy deployment and scaling are for them. Perhaps they have reached less than a minute of downtime per year (aka &lt;em&gt;Six Nines&lt;/em&gt;, 99.9999% uptime).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;However&lt;/em&gt;… try asking this same group of individuals how &lt;em&gt;knowledge&lt;/em&gt; is backed up within their organization. Often, the answer is either “it isn’t”, or the efforts to organize, safeguard, and streamline documentation is miniscule in comparison to the systems it belongs to.&lt;/p&gt;
&lt;!-- omit in toc --&gt;
&lt;h4 id=&quot;why-does-knowledge-need-backing-up&quot;&gt;Why Does Knowledge Need Backing Up?&lt;/h4&gt;
&lt;p&gt;One way to think about this, which is unfortunately morbid, is “what would happen if ____ employee suddenly passed away”.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Or, if it makes you feel better, they were suddenly “made redundant” after they spilled coffee all over the server racks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In many companies, information tends to get &lt;em&gt;siloed&lt;/em&gt; - within a team, or even limited to a single person. You might ask someone “How do I change X in system Y” and get a response like “Oh! Morgan, the architect of system Y is the only one that can answer that.”. Or maybe you’ve been a participant on a message chain that goes something like: “I’m not sure about that… I’ll have to CC ___ in on this…”, until it turns out you need Morgan again.&lt;/p&gt;
&lt;figure&gt;
&lt;a href=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/v1616009747/joshuatz-posts/finger-pointing-unsplash_jGsaTYMJJm0.jpg&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto; text-align:center;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/v1616009747/joshuatz-posts/finger-pointing-unsplash_jGsaTYMJJm0.jpg&quot; alt=&quot;Someone pointing a finger off towards the distance / empty space&quot; style=&quot;width:100%;max-width: 762px;height:auto;&quot;&gt;&lt;/a&gt;
&lt;figcaption&gt;Hmm... I think you *actually* want to talk to Morgan... I think they went that way...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;blockquote&gt;
&lt;p&gt;Notice how these examples are also instances in which developer productivity is reduced, because we are now waiting on knowledge to be available (something &lt;a href=&quot;#improve-productivity&quot;&gt;I touched on earlier&lt;/a&gt;)?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Going back to our morbid question: what if we suddenly lose Morgan? Did they ever document how &lt;code&gt;system Y&lt;/code&gt; worked, &lt;em&gt;&lt;strong&gt;in a way that others can understand&lt;/strong&gt;&lt;/em&gt;? If not, we might still survive - but it might take quadruple the resources - in time and money - to decipher their code and make sense of everything. Even if we can hire someone of exactly the same skill level, they aren’t going to be able to hit the ground running without having that system properly documented and the knowledge accessible.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🚍 The software industry actually has a term for the idea of how many people it would take suddenly dying (e.g. &lt;em&gt;getting hit by a bus&lt;/em&gt;) before it brings down a project - the &lt;a href=&quot;https://en.wikipedia.org/wiki/Bus_factor&quot;&gt;&lt;em&gt;“Bus Factor”&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Also, nothing against anyone named Morgan!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts&quot;&gt;Final Thoughts:&lt;/h2&gt;
&lt;h3 id=&quot;refuting-common-arguments&quot;&gt;Refuting Common Arguments&lt;/h3&gt;
&lt;p&gt;There seems to be a certain subset of developers that, the second someone brings up the importance of documentation, will (loudly) interject with objections and various claims of why they don’t need to write docs.&lt;/p&gt;
&lt;p&gt;This is already a very long post, so I don’t want to spend too much time digging into these (maybe in a future post?). But I’ll very quickly give a response in list format&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“My code is &lt;em&gt;self-documenting&lt;/em&gt;!” (or &lt;em&gt;self-explanatory&lt;/em&gt;, or something like that)
&lt;ul&gt;
&lt;li&gt;Nice try, but no. Even if you are using a strongly-typed language, your code can answer the question of &lt;em&gt;how&lt;/em&gt;, but not &lt;em&gt;why&lt;/em&gt; (that is what comments are great for). Well-name variables and readable code do great things, but they need to be part of a larger picture.&lt;/li&gt;
&lt;li&gt;ALSO: &lt;code&gt;readability !== maintainability&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;“It’s just me! I know my own code!”
&lt;ul&gt;
&lt;li&gt;If I we had time travel, there are a non-trivial number of developers that would all have bruises from when their future self went back in time to punch their past self for saying this. Yes, you know your own code &lt;em&gt;&lt;strong&gt;now&lt;/strong&gt;&lt;/em&gt;, but will you really feel the exact same way in a year? 5 years? 10?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;“Too many comments in code are a form of &lt;em&gt;code-smell&lt;/em&gt;”
&lt;ul&gt;
&lt;li&gt;People love to throw this argument around. They will use examples where a code comment will basically be identical to the code - but this is a contrived example, and &lt;em&gt;not&lt;/em&gt; how comments should be written in the first place&lt;/li&gt;
&lt;li&gt;Again, comments should be mostly about the &lt;em&gt;why&lt;/em&gt;, not the &lt;em&gt;how&lt;/em&gt;. Docs such as READMEs can be all of the above (+ &lt;em&gt;what&lt;/em&gt;, &lt;em&gt;when&lt;/em&gt;, etc.)&lt;/li&gt;
&lt;li&gt;See responses in &lt;a href=&quot;https://softwareengineering.stackexchange.com/q/1/340952&quot;&gt;this discussion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;“We are already using auto-generated docs from [methods, JSDoc, etc.]”
&lt;ul&gt;
&lt;li&gt;Auto-generated docs serve a purpose, and are good for things like describing API endpoints, but they often don’t add that much value on-top of the code they are generated from.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;“Docs get easily outdated. Code does not.”
&lt;ul&gt;
&lt;li&gt;This is actually a reasonable point. See &lt;a href=&quot;#preventing-out-of-date-docs&quot;&gt;below&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;preventing-out-of-date-docs&quot;&gt;Preventing Out-of-Date Docs&lt;/h3&gt;
&lt;p&gt;The &lt;em&gt;one&lt;/em&gt; argument against spending much time on documentation that I feel actually has some merit is the tendency for docs to get out of date (or &lt;em&gt;out-of-sync&lt;/em&gt;) with the code they belong to. Critics will claim that bad docs are worse than no docs at all, which I don’t  agree 100% with, but as someone who has been burned by bad documentation, can attest to the frustration that is felt when you waste time and effort because no one bothered to update materials.&lt;/p&gt;
&lt;p&gt;This is a complex problem, worthy of its own in-depth discussion, so I’ll just provide what I find as the best counter-measure; &lt;em&gt;&lt;strong&gt;the closer your docs live to the source-code the more likely they are to stay up to date&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is where systems like Confluence, Notion, or shared Google Docs can potentially cause issues. In my honest opinion, those should be used for &lt;em&gt;knowledge base&lt;/em&gt; (e.g. &lt;em&gt;wiki&lt;/em&gt;) stuff, and &lt;em&gt;documentation&lt;/em&gt; is better suited to things like Markdown files that live near the actual code (aka &lt;em&gt;&lt;a href=&quot;https://kentcdodds.com/blog/colocation&quot;&gt;colocation&lt;/a&gt;&lt;/em&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Another important aspect is how easy is it, from a procedural standpoint, for new people to edit the docs? Can any new employee or contributor submit an update? Or does it require some lengthy approval and merge process, that is likely to discourage frequent edits?&lt;/p&gt;
&lt;h3 id=&quot;im-not-perfect&quot;&gt;I’m Not Perfect&lt;/h3&gt;
&lt;p&gt;Finally, I’m not asking that you follow every tip I’ve laid out here, or buy into the points I’m making without any objections. I’m not perfect, and some of the realizations I’m sharing here I have only because of painful learning experiences.&lt;/p&gt;
&lt;p&gt;I’m only asking for a little more mindfulness around documentation, for everyone’s benefit.&lt;/p&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Holman: &lt;a href=&quot;https://zachholman.com/posts/documentation/&quot;&gt;&lt;em&gt;“The Most Important Code Isn’t Code”&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cinger: &lt;a href=&quot;https://dev.to/nebojsac/-fixing-your-documentation-problem-oll&quot;&gt;&lt;em&gt;“Fixing your Documentation Problem&lt;/em&gt;”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Preston-Werner: &lt;a href=&quot;https://tom.preston-werner.com/2010/08/23/readme-driven-development.html&quot;&gt;&lt;em&gt;“Readme Driven Development”&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Knuth: &lt;a href=&quot;https://en.wikipedia.org/wiki/Literate_programming&quot;&gt;&lt;em&gt;“Literate Programming”&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Also, it would be remiss of me not to mention my extensive personal documentation project - &lt;a href=&quot;https://docs.joshuatz.com/&quot;&gt;docs.joshuatz.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://unsplash.com/photos/uvdhVGaeem4&quot;&gt;Cover image&lt;/a&gt; by &lt;a href=&quot;https://unsplash.com/@jubalkenneth?utm_source=unsplash&amp;#x26;utm_medium=referral&amp;#x26;utm_content=creditCopyText&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Jubal Kenneth Bernal&lt;/a&gt; on &lt;a href=&quot;/?utm_source=unsplash&amp;#x26;utm_medium=referral&amp;#x26;utm_content=creditCopyText&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Strongly Typed Web and Service Workers with TypeScript</title><link>https://joshuatz.com/posts/2021/strongly-typed-web-and-service-workers-with-typescript/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/strongly-typed-web-and-service-workers-with-typescript/</guid><description>How to use TypeScript, or JSDoc-powered JavaScript, with Service Workers, as well as some important caveats for current issues.</description><pubDate>Tue, 09 Feb 2021 06:10:48 GMT</pubDate><content:encoded>&lt;p&gt;I’m writing this post because I had trouble finding a straightforward answer to this question: what are the &lt;em&gt;current&lt;/em&gt; ways to use TypeScript with &lt;a href=&quot;https://developers.google.com/web/fundamentals/primers/service-workers&quot;&gt;Service Worker&lt;/a&gt; files? And, to take it a step further, how to use those types in JavaScript, with JSDoc comments?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The examples in this post are specifically for a Service Worker script, but in general the solutions and issues in this post apply to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API&quot;&gt;web workers&lt;/a&gt; in general as well.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-basics-of-where-things-are-at&quot;&gt;The Basics of Where Things are At&lt;/h2&gt;
&lt;p&gt;First, before getting into the solutions, I want to bring up a few things that I found in my research that are important to note. Keep in mind that these things might change, so this is kind of a “here things are &lt;em&gt;now&lt;/em&gt;” section:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You no longer need external type definitions for web workers / service workers&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;A lot of older posts and StackOverflow answers will suggest things like copying and pasting code out of a Gist, using &lt;code&gt;@types&lt;/code&gt; (&lt;em&gt;DefinitelyTyped&lt;/em&gt;), or some other external type defs. These used to be necessary, but Web Worker types are now shipped by default with TypeScript, in the lib file &lt;code&gt;lib.webworker.d.ts&lt;/code&gt;. How to use this file will be discussed in the solutions below.
&lt;ul&gt;
&lt;li&gt;For relevant issues / PRs on how these made their way in:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/439&quot;&gt;TS DOM-lib-generator #439&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/TypeScript/issues/11781&quot;&gt;TS #11781&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;There are some big &lt;strong&gt;open issues&lt;/strong&gt; with Web Worker types that are &lt;em&gt;&lt;strong&gt;blocking&lt;/strong&gt;&lt;/em&gt; easier solutions for strong-typing service worker files.
&lt;ul&gt;
&lt;li&gt;These will be discussed again, throughout this post, but the major ones are:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/TypeScript/issues/14877&quot;&gt;#14877&lt;/a&gt;: The definition for &lt;code&gt;self&lt;/code&gt; is not resolved correctly&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Microsoft/TypeScript/issues/20595&quot;&gt;#20595&lt;/a&gt;: Conflicts with DOM vs WebWorker
&lt;ul&gt;
&lt;li&gt;Related - &lt;a href=&quot;https://github.com/microsoft/TypeScript/issues/11093&quot;&gt;#11093&lt;/a&gt;: Intersection of libs&lt;/li&gt;
&lt;li&gt;If this issue is resolved, depending on how it is fixed, the solution to using TS with web workers might become greatly simplified&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;solution-a-triple-slash-directives&quot;&gt;Solution A: Triple Slash Directives&lt;/h3&gt;
&lt;p&gt;Since a Service Worker often lives in a single file, some users prefer to pull in its lib types via a triple slash directives instead of modifying the &lt;code&gt;tsconfig&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/// &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;reference&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; no-default-lib&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/// &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;reference&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lib&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ES2015&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/// &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;reference&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lib&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;webworker&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Default type of `self` is `WorkerGlobalScope &amp;#x26; typeof globalThis`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// https://github.com/microsoft/TypeScript/issues/14877&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;declare&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ServiceWorkerGlobalScope&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Example:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;self.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;addEventListener&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;install&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	//&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// We need an export to force this file to act like a module, so TS will let us re-type `self`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; null&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;lib=ES2015&lt;/code&gt; should be changed to whatever you are using, but it should be an es5+ choice&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div style=&quot;text-align:center; font-size:1.4rem;&quot;&gt;
🚨 Uh-oh! Triple-slash directives don&apos;t play nice with other files that use the regular DOM!
&lt;/div&gt;
&lt;p&gt;If you have noticed that using triple-slash directives to bring in Web Worker types in your service worker file also messed up your regular TS files, you are not imagining things. For a pure TS solution, skip to &lt;a href=&quot;#solution-b-tsconfig-libs&quot;&gt;solution B&lt;/a&gt;, which explores multiple scoped TSConfigs.&lt;/p&gt;
&lt;p&gt;But, for JS users, we can bypass this tricky issue; read on below!&lt;/p&gt;
&lt;h4 id=&quot;triple-slash-directives-with-javascript-and-jsdoc&quot;&gt;Triple-Slash Directives with JavaScript and JSDoc&lt;/h4&gt;
&lt;p&gt;Here is the same approach as above, but carried out with JavaScript, using JSDoc for types:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// @ts-check&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/// &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;reference&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; no-default-lib&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;false&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/// &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;reference&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lib&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ES2015&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/// &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;reference&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lib&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;webworker&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Using IIFE to provide closure to redefine `self`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// This is a little messy, but necessary to force type assertion&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Same issue as in TS -&gt; https://github.com/microsoft/TypeScript/issues/14877&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// prettier-ignore&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; /** &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {ServiceWorkerGlobalScope}&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/** &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {unknown}&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (globalThis.self));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	self.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;addEventListener&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;install&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💡 Ah-ha! In implementing this solution I have found a trick around the issue of triple-slash directives polluting other files.&lt;/p&gt;
&lt;p&gt;The trick is to exclude the JS file from your &lt;code&gt;tsconfig&lt;/code&gt; and then &lt;em&gt;&lt;strong&gt;add back&lt;/strong&gt;&lt;/em&gt; checking by using &lt;code&gt;//@ts-check&lt;/code&gt; at the top. This prevents the triple slash directives in the file from &lt;em&gt;“bleeding”&lt;/em&gt; into the other files that are &lt;em&gt;not&lt;/em&gt; excluded by the tsconfig. This will prevent things like &lt;code&gt;window&lt;/code&gt; becoming undefined or all instances of &lt;code&gt;globalThis.self&lt;/code&gt; getting changed to the Service Worker Type!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;lib=ES2015&lt;/code&gt; should be changed to whatever you are using, but it should be an es5+ choice&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;solution-b-tsconfig-libs&quot;&gt;Solution B: TSConfig Libs&lt;/h3&gt;
&lt;p&gt;Triple slash directives are not always the best approach, especially since they can “bleed” into other files anyways when you are using them with libs (they essentially act like global lib options - &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-lib-&quot;&gt;see docs&lt;/a&gt;). So, there is not much benefit over using the &lt;code&gt;--lib&lt;/code&gt; flag or &lt;code&gt;lib&lt;/code&gt; option in &lt;code&gt;tsconfig&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Relevant issue: &lt;a href=&quot;https://github.com/Microsoft/TypeScript/issues/20595&quot;&gt;#20595&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In fact, the best option might be to have a different &lt;code&gt;tsconfig&lt;/code&gt; for your service worker, versus for all other files. For example:&lt;/p&gt;
&lt;p&gt;Base &lt;code&gt;tsconfig.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;compilerOptions&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;target&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ES6&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;exclude&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;service-worker.js&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Service-Worker &lt;code&gt;tsconfig.service-worker.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;extends&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;./tsconfig&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;compilerOptions&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;lib&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;WebWorker&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ES2015&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;files&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;service-worker.js&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are looking for a more detailed breakdown of this approach, check out &lt;a href=&quot;https://stackoverflow.com/a/56374158/11447682&quot;&gt;this StackOverflow answer&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;nested-service-worker-folder&quot;&gt;Nested Service Worker Folder&lt;/h4&gt;
&lt;p&gt;The above option works if you are only using TypeScript via CLI, and not for Intellisense, as VSCode &lt;a href=&quot;https://github.com/microsoft/vscode/issues/3772&quot;&gt;does not resolve more than one tsconfig per folder&lt;/a&gt;. If you wanted to use the above solution with VSCode and get full in-IDE type-checking, you would need to move &lt;code&gt;service-worker.js&lt;/code&gt; to a sub-directory, with its &lt;code&gt;tsconfig&lt;/code&gt; file located alongside it.&lt;/p&gt;
&lt;p&gt;However, it should be noted that this causes its own issue, and in fact, a big one. Service workers by default are granted a scope based on file location. So a worker located in the root directory:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── index.html&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── tsconfig.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── service-worker.{ts|js}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Can access any request. However, if we move to a subfolder:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── index.html&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── tsconfig.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── sw/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	├── service-worker.{ts|js}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	└── tsconfig.json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;… now our service worker has a default scope of &lt;code&gt;/sw&lt;/code&gt;, and declaring a higher scope actually &lt;a href=&quot;https://developers.google.com/web/ilt/pwa/introduction-to-service-worker#registration_and_scope&quot;&gt;requires configuring&lt;/a&gt; our server to send a unique HTTP header with any request for our worker script!&lt;/p&gt;
&lt;p&gt;The easiest fix is &lt;em&gt;probably&lt;/em&gt; to use &lt;code&gt;outDir&lt;/code&gt; or &lt;code&gt;outFile&lt;/code&gt; to make sure that the transpiled SW script ends up in the &lt;em&gt;hosted&lt;/em&gt; project &lt;strong&gt;root&lt;/strong&gt;, regardless of the source code directory structure.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Auto Compiled Svelte Tutorial Docs - Single Page Version</title><link>https://svelte-tuts-one-pager.netlify.app/</link><guid isPermaLink="true">https://svelte-tuts-one-pager.netlify.app/</guid><description>NodeJS-powered automation that extracts all the tutorial docs out of the Svelte repo and combines them into a single formatted HTML document.</description><pubDate>Thu, 04 Feb 2021 03:36:07 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_full_page_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Light Screen Display - Virtual Fill Light</title><link>https://joshuatz.com/projects/web-stuff/light-screen-display---virtual-fill-light/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/light-screen-display---virtual-fill-light/</guid><description>A fun browser-based ring light / fill light app, that lets you control a virtual light on your screen.</description><pubDate>Tue, 02 Feb 2021 16:34:38 GMT</pubDate><content:encoded>&lt;p&gt;This was a fun project that I built the first version of in a few hours, on a whim. I have multiple extra monitors pointing down at my face that are empty real-estate while I participate in video calls, and I had been noticing that the light they cast actually has a subtle effect on the image captured by my webcam.&lt;/p&gt;
&lt;p&gt;I was curious how much a difference it would make if I filled up my extra monitors with solid colors, so I built this app as a quick way to test out different fill styles and colors.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Spoiler: I made this for fun, and the end result was that, although it makes a small subtle difference, using OBS filters or buying a dedicated external light will make 100x the difference that this does. But it is still a fun experiment!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;where-to-get-it&quot;&gt;Where to Get It&lt;/h2&gt;
&lt;p&gt;🔗 App: &lt;a href=&quot;https://displaylight.netlify.app/&quot;&gt;displaylight.netlify.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💾 Source code: &lt;a href=&quot;https://github.com/joshuatz/light-screen&quot;&gt;github.com/joshuatz/light-screen&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;demo&quot;&gt;Demo&lt;/h2&gt;
&lt;video loop muted controls src=&quot;/media/Light_Screen_Display-Demo.mp4&quot; style=&quot;width: 90%; margin:auto; max-width:1280px; display:block;&quot;&gt;
	Sorry, your browser doesn&apos;t support embedded videos.
&lt;/video&gt;
&lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Multiple modes
&lt;ul&gt;
&lt;li&gt;Ring Light
&lt;ul&gt;
&lt;li&gt;LEDs&lt;/li&gt;
&lt;li&gt;Diffused LEDs&lt;/li&gt;
&lt;li&gt;Solid Ring&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Solid Full-Screen Fill&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Camera preview&lt;/li&gt;
&lt;li&gt;Auto-hiding settings panel (can be turned  off)&lt;/li&gt;
&lt;li&gt;Keeps screen on while in use&lt;/li&gt;
&lt;li&gt;Custom color picker, or use presets&lt;/li&gt;
&lt;li&gt;Works offline, and as a PWA&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-i-used&quot;&gt;What I Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JavaScript (Vanilla)&lt;/li&gt;
&lt;li&gt;HTML, CSS&lt;/li&gt;
&lt;li&gt;Service Workers&lt;/li&gt;
&lt;li&gt;TypeScript (with JSDoc)&lt;/li&gt;
&lt;li&gt;HTML Canvas&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Sheets - How To Split a String Into Characters</title><link>https://joshuatz.com/posts/2021/google-sheets---how-to-split-a-string-into-characters/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/google-sheets---how-to-split-a-string-into-characters/</guid><description>How to split a string into individual characters in Google Sheets, using a  pure formula approach. Even handles line breaks and special characters.</description><pubDate>Sun, 31 Jan 2021 13:53:46 GMT</pubDate><content:encoded>&lt;p&gt;I’m writing up this post because this is now the second time I have run into this issue, and the solution I finally ended up with is not as simple as one might guess.&lt;/p&gt;
&lt;p&gt;For splitting text into columns in Google Sheets, or even with a known delimiter, the approach is not that difficult - &lt;a href=&quot;https://www.howtogeek.com/415512/how-to-split-text-in-google-sheets/&quot;&gt;“How-To Geek” has a good step-by-step guide&lt;/a&gt; for those situations.&lt;/p&gt;
&lt;p&gt;What this post is about is splitting text in Google Sheets &lt;em&gt;by character&lt;/em&gt;, with an unknown input length and text content. For example:&lt;/p&gt;













&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Input&lt;/th&gt;&lt;th&gt;Expected Output&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;| hello |&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;| h | e | l | l | o |&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;This is harder than it might seem for several reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;a href=&quot;https://support.google.com/docs/answer/3094136&quot;&gt;&lt;code&gt;SPLIT()&lt;/code&gt; function&lt;/a&gt; in Google Sheets does not allow splitting by an empty delimiter (e.g. &lt;code&gt;&quot;&quot;&lt;/code&gt;)
&lt;ul&gt;
&lt;li&gt;This might be surprising to you if you are a JavaScript developer, since &lt;code&gt;const charArr = &apos;hello&apos;.split(&apos;&apos;);&lt;/code&gt; is not only permissable in JavaScript, but it does exactly what we want
&lt;ul&gt;
&lt;li&gt;In fact, one solution to our issue could be to simply create a custom function with Google Apps Script, called &lt;code&gt;JS_SPLIT()&lt;/code&gt;, and allow it to be called from Sheets&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Inserting a delimiter into the input string, between each input character, to workaround the above limitation works, but has its own set of nuances&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;/h2&gt;
&lt;p&gt;First, I’ll provide the solution, and then break down how it works, and why it might seem more complicated than necessary.&lt;/p&gt;
&lt;p&gt;Formula:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;=SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Easier to read:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;=SPLIT(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	REGEXREPLACE(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;		REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	,&quot;&apos;&quot;,&quot;&apos;&apos;&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;,CHAR(127))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Breaking this down section by section, from inside to out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127))&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;What: This replaces every character in the input string (contained in cell &lt;code&gt;A2&lt;/code&gt;) with itself, plus a special delimiter character&lt;/li&gt;
&lt;li&gt;You’ll notice that I use &lt;code&gt;CHAR(127)&lt;/code&gt; as my inserted delimiter, whereas many answers on StackOverflow use a plain text character, like &lt;code&gt;~&lt;/code&gt;. There are two reasons why I use &lt;code&gt;CHAR(127)&lt;/code&gt; instead of &lt;code&gt;~&lt;/code&gt;.
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CHAR(127)&lt;/code&gt; is the &lt;a href=&quot;https://en.wikipedia.org/wiki/Delete_character&quot;&gt;Delete Character&lt;/a&gt;, which is an ASCII control character (invisible in this case) that is highly unlikely to ever show up in arbitrary input text. The Tilde character (&lt;code&gt;~&lt;/code&gt;), on the other hand, is much more likely to show up in text input, which would cause issues with a formula that uses it as a delimiter&lt;/li&gt;
&lt;li&gt;The delete character is one of the very few control characters that Google Sheets does not strip out of input / output&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Another really important part of this step that a lot of StackOverflow answers miss is the &lt;code&gt;(?s)&lt;/code&gt; part. This is a RegEx flag (in &lt;a href=&quot;https://github.com/google/re2/wiki/Syntax&quot;&gt;the RE2 flavor&lt;/a&gt;) that allows &lt;code&gt;.&lt;/code&gt; to match any character &lt;em&gt;&lt;strong&gt;as well as new line (&lt;code&gt;\n&lt;/code&gt;)&lt;/strong&gt;&lt;/em&gt;. If we were to leave off this flag, then our formula would fail to work properly with input cells that contained multiple lines of input&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REGEXREPLACE(..., &lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;&quot;&apos;&quot;, &quot;&apos;&apos;&quot;&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;code&gt;)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Here I am escaping any single quotes in our input string, by replacing them with &lt;code&gt;&apos;&apos;&lt;/code&gt; (two single quotes).&lt;/li&gt;
&lt;li&gt;This is important because if we &lt;em&gt;don’t&lt;/em&gt; do this, then any single quotes in the input string will turn into empty cells when we split by the delimiter in the next step. &lt;code&gt;&apos;&lt;/code&gt; is a special leading escape character in Sheets.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;=SPLIT(..., CHAR(127))&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Finally, I am splitting the string with delimiters inserted, by the delimiter itself (&lt;code&gt;CHAR(127)&lt;/code&gt;). The result will be an array, which gets expanded out, starting in the cell you place this formula in&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;emoji-issue&quot;&gt;Emoji Issue&lt;/h2&gt;
&lt;p&gt;Despite this formula working with line breaks, symbols, special characters, and even quotes, there is one instance I have found where it might still produce &lt;em&gt;unexpected&lt;/em&gt; results: emoji input. I’m reluctant to call this a problem or “bad” behavior, because you could also view it as the formula working correctly.&lt;/p&gt;
&lt;p&gt;Here is the issue: certain “emoji” are displayed as one character, but actually comprised of multiple emoji joined together, by a &lt;a href=&quot;https://en.wikipedia.org/wiki/Zero-width_joiner&quot;&gt;“zero-width joiner”&lt;/a&gt; (ZWJ), into a &lt;em&gt;ZWJ Sequence&lt;/em&gt;. These tend to be emoji that have a common base (such as a face), but are a variation on that base (maybe a different skin color, hair color, etc.), and especially for gendered emoji combinations. A list of some of these can be found &lt;a href=&quot;https://emojipedia.org/emoji-zwj-sequence/&quot;&gt;on emojipedia&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Without getting too much into the details, here is an example and how it plays out with my formula.&lt;/p&gt;
&lt;p&gt;This emoji - 👩‍🦰 - is &lt;code&gt;Woman: Red Hair&lt;/code&gt;, and as long as you have an up-to-date operating system, you should see it as a single character on your screen. However, look at how the Google Sheets formula splits up that character when it appears in our input string:&lt;/p&gt;
&lt;img src=&quot;/media/ZWJ-Emoji-Sequence-in-Google-Sheets-Woman-with-Red-Hair.png&quot; alt=&quot;ZWJ Emoji Sequence in Google Sheets - Woman with Red Hair&quot; style=&quot;margin: auto; display:block; max-width: 1565px; width: 95%; height: auto;&quot;&gt;
&lt;p&gt;What has happened is that the formula has treated 👩‍🦰 as three characters, not one, which is technically correct since it is a ZWJ Sequence comprised of &lt;code&gt;👩 (Woman, U+1F469) + ZWJ (U+200D) + 🦰 (Red Hair, U+1F9B0)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The 👋 (waving hand emoji) is split as a single character, since it is &lt;em&gt;not&lt;/em&gt; a ZWJ sequence, but rather a regular single glyph / emoji.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: This same behavior can be seen in JavaScript - try copying and pasting this into your console for a fun experiment: &lt;code&gt;console.log([...&apos;👩‍🦰&apos;])&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;ZWJ Sequences also completely break formula that rely on using &lt;em&gt;length&lt;/em&gt; tricks, such as &lt;a href=&quot;https://www.reddit.com/r/sheets/comments/cx7vhd/split_text_every_character_in_cell_into_its_own/&quot;&gt;this alternative solution for splitting by character&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Brain Breaks Browser - Online Web App for Fun Activities</title><link>https://joshuatz.com/projects/web-stuff/brain-breaks-browser---online-web-app-for-fun-activities/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/brain-breaks-browser---online-web-app-for-fun-activities/</guid><description>Project write-up for Brain Breaks Browser - a simple Preact-based web app to manage fun break exercises and activities, with a few added bells and whistles.</description><pubDate>Wed, 20 Jan 2021 00:08:44 GMT</pubDate><content:encoded>&lt;p&gt;Brain Breaks Browser is a web app that lets you create, store, track, and organize “brain break” activities. These are fun tasks and exercises that you can use to split up work time into more manageable chunks, and exercise your brain while enjoying a break.&lt;/p&gt;
&lt;h2 id=&quot;where-to-get-it&quot;&gt;Where to Get It&lt;/h2&gt;
&lt;p&gt;🔗 &lt;a href=&quot;https://brainbreaksbrowser.netlify.app/&quot;&gt;brainbreaksbrowser.netlify.app&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;demo&quot;&gt;Demo&lt;/h2&gt;
&lt;video controls preload=&quot;metadata&quot; style=&quot;display:block; width:94%; margin:auto;&quot; title=&quot;Brain Breaks Demo - Standup Board&quot;&gt;
	&lt;source src=&quot;/media/Brain-Breaks-Browser-Standups-Board-Demo.mp4&quot; type=&quot;video/mp4&quot;&gt;
	Sorry, your browser doesn&apos;t support embedded videos.
&lt;/video&gt;
&lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Create multiple boards, share activities between boards&lt;/li&gt;
&lt;li&gt;Create custom activities with an easy-to-use form&lt;/li&gt;
&lt;li&gt;Use activity types for extra automatic features
&lt;ul&gt;
&lt;li&gt;Videos: Task automatically marked as complete and popup closed when video reaches the end of its duration&lt;/li&gt;
&lt;li&gt;Website / URL: Embed an entire webpage, securely, into your activity&lt;/li&gt;
&lt;li&gt;Timed: Multiple activity types, even the completely customized one, supports an integrated timer component&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Track activity completion, reset progress, or disable repeat limits entirely&lt;/li&gt;
&lt;li&gt;Randomize activities, or even mask activity icons&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why&quot;&gt;Why?&lt;/h2&gt;
&lt;p&gt;These types of activities used to be common to do in person, and were often the type of thing to be written on flashcards or popsicle sticks and picked out of a cup. With the combination of COVID, advancement of technology, and renewed focus on remote learning and professional work, these offline systems simply are not feasible for many users.&lt;/p&gt;
&lt;p&gt;I thought it would be a fun project to create a simple web portal to organize these activities, and provide extra features like built-in timers and progress tracking, so that not only will users not miss their stacks of activity flashcards, but they might even prefer the new system.&lt;/p&gt;
&lt;p&gt;Although the focus is on remote learning, there are many use-cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Classrooms, tutors, remote education settings&lt;/li&gt;
&lt;li&gt;Business Meetings / Standups&lt;/li&gt;
&lt;li&gt;Web conferences&lt;/li&gt;
&lt;li&gt;Yourself! Control your break time with preset activities and auto-closing tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-i-used&quot;&gt;What I Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://preactjs.com/&quot;&gt;Preact&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Preact / React Hooks&lt;/li&gt;
&lt;li&gt;React Player&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chakra-ui.com/&quot;&gt;Chakra UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dexie.org/&quot;&gt;Dexie.js&lt;/a&gt; (IndexedDB wrapper)&lt;/li&gt;
&lt;li&gt;Javascript, HTML, CSS&lt;/li&gt;
&lt;li&gt;And more!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also published a writeup around a specific issue I ran into with this app - &lt;a href=&quot;https://joshuatz.com/posts/2021/deploy-preact-from-subdirectory/&quot;&gt;“How to Deploy Preact from a Subdirectory”&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>How to Embed, Inline, and Reference SVG Files in Svelte</title><link>https://joshuatz.com/posts/2021/how-to-embed-inline-and-reference-svg-files-in-svelte/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/how-to-embed-inline-and-reference-svg-files-in-svelte/</guid><description>A guide on using SVG files with Svelte, and how to embed, inline-directly, or reference them, with or without rollup plugins.</description><pubDate>Tue, 19 Jan 2021 14:40:39 GMT</pubDate><content:encoded>&lt;p&gt;I recently needed to work with SVGs in Svelte, and had trouble finding comprehensive guides on solutions, especially those that allow for the SVG to be completely inlined, so that you can use things like &lt;code&gt;fill: currentColor&lt;/code&gt;, or CSS rules. This post tries to cover some of the most useful solutions I found during my research, complete with step-by-step instructions and code snippets.&lt;/p&gt;
&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#placing-in-public-and-avoiding-bundling&quot;&gt;Placing in Public and Avoiding Bundling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#using-a-bundler-plugin&quot;&gt;Using a Bundler Plugin&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#rollup-plugin-svg-or-rollup-plugin-inline-svg&quot;&gt;rollup-plugin-svg [or] rollup-plugin-inline-svg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rollupplugin-image&quot;&gt;@rollup/plugin-image&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#rollupplugin-image-inline-svg-workaround&quot;&gt;@rollup/plugin-image: Inline SVG Workaround&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#svite--vite&quot;&gt;Svite / Vite&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#directly-inlining&quot;&gt;Directly Inlining&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#saving-as-svelte-files&quot;&gt;Saving as Svelte Files&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#side-note-dealing-with-typescript&quot;&gt;Side-note: Dealing with TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#svelte-inline-svg-and-other-ajax-based-solutions&quot;&gt;Svelte-Inline-SVG and other AJAX Based Solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#other-solutions&quot;&gt;Other Solutions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;placing-in-public-and-avoiding-bundling&quot;&gt;Placing in Public and Avoiding Bundling&lt;/h2&gt;
&lt;p&gt;If you don’t care about inlining the SVG, and are OK with an external embed, the easiest way to get an SVG into a Svelte component is to simply bypass the bundler and use the &lt;code&gt;/public&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;For example, you could place your file at: &lt;code&gt;/public/assets/dog.svg&lt;/code&gt;, and then your Svelte file could be as simple as:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;img&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/assets/dog.svg&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In general, there are some important drawbacks to this approach however:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paths are not checked by the compiler / bundler, or even the IDE
&lt;ul&gt;
&lt;li&gt;This makes typos a little too easy, or situations where a file gets renamed and no one catches the change&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Assets are not optimized
&lt;ul&gt;
&lt;li&gt;Any file you stick in &lt;code&gt;/public&lt;/code&gt; is going to end up in the build output. By contrast, if you use the bundler (see other approaches I outline), assets that are not used will not get &lt;em&gt;bundled&lt;/em&gt; into the build output&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;You cannot inline SVGs this way
&lt;ul&gt;
&lt;li&gt;This method allows you to embed SVGs via &lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt; tags, but it does not allow you to inline the SVG contents directly into the DOM&lt;/li&gt;
&lt;li&gt;This prevents a lot of direct manipulation of the SVG from working, which might be a deal-breaker if you need to animate parts of it, change nested colors, or pass through CSS variables&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;using-a-bundler-plugin&quot;&gt;Using a Bundler Plugin&lt;/h2&gt;
&lt;p&gt;As is common with many frameworks, one way to solve this problem is to have the bundler take care of reading in the image files, transforming them, and letting them be imported into components. Since the Svelte starter template ships with Rollup by default, that is the bundler I’ll be discussing.&lt;/p&gt;
&lt;p&gt;To solve this with Rollup, there are three plugins I’ll suggest as solutions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/rollup-plugin-svg&quot;&gt;rollup-plugin-svg&lt;/a&gt; or &lt;a href=&quot;https://www.npmjs.com/package/rollup-plugin-inline-svg&quot;&gt;rollup-plugin-inline-svg&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;These are both similar, and both are good if you need to inline a lot of SVGs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@rollup/plugin-image&quot;&gt;@rollup/plugin-image&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;This supports more image formats, but is less friendly for inlining&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I’m not going to cover it in-depth in this post, but for webpack, check out their docs on &lt;a href=&quot;https://webpack.js.org/guides/asset-modules/&quot;&gt;asset modules&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;rollup-plugin-svg-or-rollup-plugin-inline-svg&quot;&gt;rollup-plugin-svg [or] rollup-plugin-inline-svg&lt;/h3&gt;
&lt;p&gt;These two plugins are similar in how they work, although &lt;a href=&quot;https://www.npmjs.com/package/rollup-plugin-inline-svg&quot;&gt;rollup-plugin-inline-svg&lt;/a&gt; has more configuration options.&lt;/p&gt;
&lt;p&gt;Once you have installed the plugin you have chosen and updated your rollup config, for actually importing and using the SVG, both require similar code.&lt;/p&gt;
&lt;p&gt;Inline SVG embedding (this code works for either plugin):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lang&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; dogSvgDecoded &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;../assets/dog.svg&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	{@html dogSvgDecoded}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Embedding as external embed, via image tag + data URI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For embedding via data URI:
&lt;ul&gt;
&lt;li&gt;rollup-plugin-svg
&lt;ul&gt;
&lt;li&gt;Change &lt;code&gt;base64&lt;/code&gt; config option to be &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;In component, import and then bind as &lt;code&gt;&amp;#x3C;img src={importedSvgStr}&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;rollup-plugin-inline-svg
&lt;ul&gt;
&lt;li&gt;Since the plugin is focused on inlining, there is no config option for escaping / base64 encoding.&lt;/li&gt;
&lt;li&gt;The workaround is to decode the string inside the component - this is essentially the reverse of the &lt;a href=&quot;#rollupplugin-image-inline-svg-workaround&quot;&gt;@rollup/plugin-image workaround&lt;/a&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lang&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; dogSvgDecoded &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;../assets/dog.svg&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; dogSvgEncoded&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; `data:image/svg+xml,${&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;encodeURIComponent&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;dogSvgDecoded&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;img&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;{dogSvgEncoded}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;rollupplugin-image&quot;&gt;@rollup/plugin-image&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.npmjs.com/package/@rollup/plugin-image&quot;&gt;@rollup/plugin-image&lt;/a&gt; is the most popular of the plugins, and can support more formats than just SVGs (also supports JPG, PNG, etc.), but also comes with an important limitation.&lt;/p&gt;
&lt;p&gt;The limitation is that it converts the contents to a base64 string (or HTML encoded entities string, in the case of SVGs) and then expects you to use it as a Data URI - for most image formats this is no problem (and actually preferred), but for SVGs, this also prevents them from being directly inlined, which subsequently prevents CSS rules from working. If you are fine with that limitation, or want to use the workaround (more on this in a bit), it is &lt;a href=&quot;https://github.com/rollup/plugins/tree/master/packages/image/#usage&quot;&gt;fairly simple to set up&lt;/a&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Modify your rollup config to pull in the the new plugin&lt;/li&gt;
&lt;li&gt;Activate the plugin within the config (&lt;code&gt;plugins: [image()]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Now, in any Svelte component where you want to use an SVG:
&lt;ol&gt;
&lt;li&gt;Import the file, while assigning it to a variable&lt;/li&gt;
&lt;li&gt;Bind the variable you assign it to, as &lt;code&gt;src={myVar}&lt;/code&gt; on an image element&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example component:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lang&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; DogSvgEncoded &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;../assets/dog.svg&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;img&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;{DogSvgEncoded}&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; alt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Cute dog graphic&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;rollupplugin-image-inline-svg-workaround&quot;&gt;@rollup/plugin-image: Inline SVG Workaround&lt;/h4&gt;
&lt;p&gt;As noted above, a limitation with &lt;code&gt;@rollup/plugin-image&lt;/code&gt; is that it is designed to be used with the &lt;code&gt;src&lt;/code&gt; attribute on image tags, not for embedding the raw text of a SVG into the DOM. This is why it Base64 encodes the value of binary image files, and escapes / encodes HTML entities in the value of SVG files.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;However&lt;/em&gt;, with some extra work we can turn the encoded value it gives us back into the original raw SVG string:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; lang&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; DogSvgEncoded &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;../assets/dog.svg&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; dogSvgDecoded&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; decodeURIComponent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(DogSvgEncoded).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;data:image/svg+xml,&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	{@html dogSvgDecoded}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works, but should be avoided (in favor of any of the alternatives outlined in this post), for three reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You are shifting computational work from a build-server onto the users’ browser, which is… not great.&lt;/li&gt;
&lt;li&gt;This is adding extra steps - why escape characters only to decode immediately?&lt;/li&gt;
&lt;li&gt;It actually increases bandwidth used; it takes extra characters (longer string, more bytes) to escape HTML entities
&lt;ul&gt;
&lt;li&gt;For a tiny test SVG, the original version used 3,988 characters, whereas the encoded one grew to 4,285.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;svite--vite&quot;&gt;Svite / Vite&lt;/h3&gt;
&lt;p&gt;Currently, using SVG bundling plugins with Vite or Svite is a little tricky.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;If you are using Svite, I would recommend just using &lt;a href=&quot;https://www.npmjs.com/package/svelte-inline-svg&quot;&gt;svelte-inline-svg&lt;/a&gt; components, or directly inlining SVGs into Svelte SFC. If Svite is updated to internally use Vite 2.0+, instead of &lt;code&gt;v1&lt;/code&gt;, then the below will apply.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you are using Vite (2.0+), SVG inlining should &lt;em&gt;eventually&lt;/em&gt; work out of the box; it currently has an open issue against it (&lt;a href=&quot;https://github.com/vitejs/vite/issues/1204&quot;&gt;#1204&lt;/a&gt;), but there is a PR in progress that should address it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;directly-inlining&quot;&gt;Directly Inlining&lt;/h2&gt;
&lt;p&gt;If the SVG file is small enough and it makes sense for your needs, you can actually just copy and paste the raw SVG contents into a Svelte SFC. This is similar to the another solution in this post, &lt;a href=&quot;#saving-as-svelte-files&quot;&gt;renaming entire SVG files to &lt;code&gt;.svelte&lt;/code&gt; files&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Make sure you remove any declarations, like &lt;code&gt;&amp;#x3C;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;/code&gt;, before inlining&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;saving-as-svelte-files&quot;&gt;Saving as Svelte Files&lt;/h2&gt;
&lt;p&gt;As &lt;a href=&quot;https://stackoverflow.com/q/61452073/11447682&quot;&gt;suggested in the comments on this S/O question&lt;/a&gt;, one option for importing SVGs and rendering them inline is by renaming the &lt;code&gt;.svg&lt;/code&gt; file to &lt;code&gt;.svelte&lt;/code&gt; and then importing and using as a normal Svelte component. This works because of how flexible Svelte is with its templating / SFC system.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;However&lt;/strong&gt;&lt;/em&gt;, there are some catches. The main one is that if you try this with some SVG files, you might see this error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ParseError: Expected valid tag name&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This might happen because your &lt;code&gt;.svg&lt;/code&gt; file contains (at the top) something like:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;?&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;xml&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;1.0&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; encoding&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;UTF-8&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;?&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an XML declaration (a subset of an &lt;em&gt;XML Prolog&lt;/em&gt;). The short summary of what is going on here is that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Svelte doesn’t like the prolog&lt;/li&gt;
&lt;li&gt;It shouldn’t be allowed anyways if the SVG is going to be inlined into the DOM - HTML spec allows / recommends only &lt;em&gt;one&lt;/em&gt; declaration per doc, at the top of the page&lt;/li&gt;
&lt;li&gt;It is not even &lt;em&gt;necessary&lt;/em&gt; to include in the SVG, even if the file is going to be used as a normal SVG external embed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As &lt;a href=&quot;https://stackoverflow.com/a/38172170/11447682&quot;&gt;the declaration is not even necessary for standalone SVG files&lt;/a&gt;, the easy fix is to simply remove the declaration line from your file before or after changing the extension to &lt;code&gt;.svelte&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The other solutions outlined in this post deal with omitting the declaration &lt;em&gt;automatically&lt;/em&gt;, which is another reason to prefer them over converting your SVGs to Svelte files&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;side-note-dealing-with-typescript&quot;&gt;Side-note: Dealing with TypeScript&lt;/h2&gt;
&lt;p&gt;If you are using TypeScript with Svelte, you might see the following error when trying to import an SVG, even if you have properly configured your bundler:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cannot find module ’../dog.svg’ or its corresponding type declarations.ts(2307)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If our bundler is going to give us a string when we import the SVG path, we can tell TS that this is the case by creating an ambient declaration file and typing the file pattern.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ambients.d.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;declare&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; module&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;*.svg&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; content&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; content;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;svelte-inline-svg-and-other-ajax-based-solutions&quot;&gt;Svelte-Inline-SVG and other AJAX Based Solutions&lt;/h2&gt;
&lt;p&gt;The plugin &lt;a href=&quot;https://www.npmjs.com/package/svelte-inline-svg&quot;&gt;svelte-inline-svg plugin&lt;/a&gt;, and some similar plugins, solve the inlining issue while avoiding bundlers, by taking a somewhat unusual approach. The package is essentially a Svelte component that requests your SVG file as a network request, then parses the response and inlines the nodes.&lt;/p&gt;
&lt;p&gt;This can work for a lot of situations, but also comes with an important caveat; the SVG files must be able to be &lt;em&gt;fetched&lt;/em&gt; by the component at run-time, or in the case of SSR, at build-time. In practicality, this means that SVG assets would need to be placed in &lt;code&gt;/public&lt;/code&gt;, and not &lt;code&gt;/src&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;other-solutions&quot;&gt;Other Solutions&lt;/h2&gt;
&lt;p&gt;There are other solutions out there, that I either have missed or have not taken the time to discuss in this post.&lt;/p&gt;
&lt;p&gt;If you have found a solution that you think would be helpful to share, please do so below in the comments (or ping me via message, and I’ll update the post).&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Guide: How To Deploy Preact from a Subdirectory</title><link>https://joshuatz.com/posts/2021/guide-how-to-deploy-preact-from-a-subdirectory/</link><guid isPermaLink="true">https://joshuatz.com/posts/2021/guide-how-to-deploy-preact-from-a-subdirectory/</guid><description>A step-by-step guide on how to deploy a Preact app from a nested subdirectory, with instructions on updating Webpack, preact-router, and more.</description><pubDate>Sat, 02 Jan 2021 09:54:45 GMT</pubDate><content:encoded>&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#intro&quot;&gt;Intro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-to-deploy-preact-from-a-subdirectory&quot;&gt;How to Deploy Preact from a Subdirectory&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#prepare-for-changes-setup-shared-base-path&quot;&gt;Prepare for Changes: Setup Shared Base Path&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#shared-base-path-setting-environment-variable&quot;&gt;Shared Base Path: Setting Environment Variable&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#configure-bundler&quot;&gt;Configure Bundler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#fixing-router&quot;&gt;Fixing Router&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#patching-preact-router&quot;&gt;Patching Preact-Router&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#fix-hard-coded-links&quot;&gt;Fix Hard-Coded Links&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#pwa-support-double-check-manifestjson&quot;&gt;PWA Support: Double-Check manifest.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#update-deploy-config&quot;&gt;Update Deploy Config&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#summarizing-a-checklist&quot;&gt;Summarizing: A Checklist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#alternate-approaches&quot;&gt;Alternate Approaches&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;I’ve been working on a Preact project recently, which is a single-page-application, and I decided that I wanted to host and deploy the landing page from the same project repository, on the same domain. This means that instead of serving my generated Preact files from server root (by deploying &lt;code&gt;/build&lt;/code&gt;), I want to serve them from a subdirectory:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;___&lt;/th&gt;&lt;th&gt;Old&lt;/th&gt;&lt;th&gt;New&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Build output:&lt;/td&gt;&lt;td&gt;&lt;code&gt;/build&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/static-root/app&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Homepage (example.com) gives you:&lt;/td&gt;&lt;td&gt;The app&lt;/td&gt;&lt;td&gt;Custom static LP (&lt;code&gt;/static-root/index.html&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;App URL Path:&lt;/td&gt;&lt;td&gt;&lt;code&gt;/&lt;/code&gt; (homepage)&lt;/td&gt;&lt;td&gt;&lt;code&gt;/app&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;My first thought was simply to change the destination of the build output to the nested folder and change my deploy settings to host the parent folder, but, without changing anything else, this basically breaks the entire app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Breaks assets, script loading, etc.&lt;/li&gt;
&lt;li&gt;Breaks Preact router&lt;/li&gt;
&lt;li&gt;Breaks webpack bundle output, ESM chunk loading, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When I searched for &lt;em&gt;the right way&lt;/em&gt; on how to deploy a Preact app from a subdirectory, a lot of the results I got were not very straightforward, and not necessarily even the right things to do.&lt;/p&gt;
&lt;p&gt;I’m making this guide as a reference point for myself, and anyone else that wants to see the basic steps that are necessary to get this working.&lt;/p&gt;
&lt;p&gt;It can be done! 😄&lt;/p&gt;
&lt;h2 id=&quot;how-to-deploy-preact-from-a-subdirectory&quot;&gt;How to Deploy Preact from a Subdirectory&lt;/h2&gt;
&lt;p&gt;Most of these steps are not in any particular order, and they are somewhat specific to my setup; if you are using a different bundler, router, etc. - you will need to slightly tweak things. But, still a good starting point for the right things to adjust.&lt;/p&gt;
&lt;p&gt;However, this first step should probably be done first, since it will help later:&lt;/p&gt;
&lt;h3 id=&quot;prepare-for-changes-setup-shared-base-path&quot;&gt;Prepare for Changes: Setup Shared Base Path&lt;/h3&gt;
&lt;p&gt;My first step, which I’m glad I had the foresight to think through, was setting up a shared base URL / path value, which can intelligently change based on the environment the app is launched in. For example, if I launch my app in dev mode, with hot-reloading, I &lt;em&gt;&lt;strong&gt;don’t&lt;/strong&gt;&lt;/em&gt; want the app to try and use &lt;code&gt;/app/*&lt;/code&gt; URLs, because the dev server launches the Preact app directly - not in a subdirectory.&lt;/p&gt;
&lt;p&gt;You can set this up however, you please, but the way I did it was first adding a value to my &lt;code&gt;package.json&lt;/code&gt; for easy configuration changes:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;publicPath&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/app/&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is important, because the &lt;code&gt;package.json&lt;/code&gt; is an easy root-level spot to share values, and &lt;code&gt;publicPath&lt;/code&gt; matches the name used by webpack (more on this later).&lt;/p&gt;
&lt;p&gt;Next, in a common export point (I used &lt;code&gt;src/constants.ts&lt;/code&gt;), I export a wrapper around this value:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pkgInfo &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;../package.json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * The base URL where this app lives. Used for routes, assets, etc.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; *  - Needed for dealing with serving out of subdirectory&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; APP_BASE_URL_NO_SLASH_END&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	process.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;NODE_ENV&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; !==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;production&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		?&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pkgInfo.publicPath.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;endsWith&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		?&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pkgInfo.publicPath.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;substring&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, pkgInfo.publicPath.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pkgInfo.publicPath;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; APP_BASE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; APP_BASE_URL_NO_SLASH_END&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above code uses the &lt;code&gt;package.json&lt;/code&gt; value, which means that right now it either returns &lt;code&gt;&apos;&apos;&lt;/code&gt; (empty string) if running in dev mode, or &lt;code&gt;/app&lt;/code&gt; if actually running in production, with the nested subdirectory.&lt;/p&gt;
&lt;p&gt;Now, I can import &lt;code&gt;APP_BASE&lt;/code&gt; in various parts of my app, and automatically prefix links with the correct base, by doing something like &lt;code&gt;${APP_BASE}/user/edit/&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;shared-base-path-setting-environment-variable&quot;&gt;Shared Base Path: Setting Environment Variable&lt;/h4&gt;
&lt;p&gt;One issue that I quickly ran into with the above code is that, unlike most &lt;em&gt;other&lt;/em&gt; frameworks I’ve used, Preact CLI does &lt;em&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;/em&gt; actually change the value of &lt;code&gt;process.env.NODE_ENV&lt;/code&gt; based on the command that is run. In order to have &lt;code&gt;process.env.NODE_ENV = &apos;production&apos;&lt;/code&gt; when &lt;code&gt;preact build&lt;/code&gt; is ran, and &lt;code&gt;= &apos;development&lt;/code&gt; when &lt;code&gt;dev&lt;/code&gt; is ran, I had to install &lt;a href=&quot;https://www.npmjs.com/package/cross-env&quot;&gt;&lt;code&gt;cross-env&lt;/code&gt; as a dependency&lt;/a&gt;, and then manually add those values to my &lt;code&gt;package.json&lt;/code&gt; scripts.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;scripts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;dev&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;cross-env NODE_ENV=development preact watch&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;build&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;cross-env NODE_ENV=production preact build&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;configure-bundler&quot;&gt;Configure Bundler&lt;/h3&gt;
&lt;p&gt;This is one of the most important steps; you need to update the configuration of the JavaScript bundler (e.g. Webpack) so that all the &lt;code&gt;distribution&lt;/code&gt; files end up in the right places, with the right files pulling them in.&lt;/p&gt;
&lt;p&gt;I won’t provide instructions for every bundler out there, but I will show how to do it for Webpack.&lt;/p&gt;
&lt;p&gt;If you are using standard Preact template, there is a good chance you are using Webpack, with &lt;a href=&quot;https://github.com/preactjs/preact-cli#webpack&quot;&gt;configuration controlled via the &lt;code&gt;preact.config.js&lt;/code&gt; file&lt;/a&gt;. To update this config to handle a subdirectory deploy, you want to mutate the config to add a &lt;code&gt;publicPath&lt;/code&gt; option - this tells webpack where to place generated files. Here is how I modified my &lt;code&gt;preact.config.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// We need the shared path value!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pkgInfo &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;./package.json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	webpack&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;config&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;env&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;helpers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Rest of my config omitted&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		config.output &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; config.output &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (process.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;NODE_ENV&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;production&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			config.output.publicPath &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pkgInfo.publicPath;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://webpack.js.org/configuration/output/#outputpublicpath&quot;&gt;https://webpack.js.org/configuration/output/#outputpublicpath&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://webpack.js.org/guides/public-path/&quot;&gt;https://webpack.js.org/guides/public-path/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/preactjs/preact-cli#webpack&quot;&gt;https://github.com/preactjs/preact-cli#webpack&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;fixing-router&quot;&gt;Fixing Router&lt;/h3&gt;
&lt;p&gt;This step can take the most work, especially if you are using &lt;code&gt;preact-router&lt;/code&gt; and have lots of routes and links.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I highly suggest using an IDE that supports advanced Find-and-Replace searching (such as VSCode), so you can bulk update links and paths&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;patching-preact-router&quot;&gt;Patching Preact-Router&lt;/h4&gt;
&lt;p&gt;Preact-Router does not currently support a base path option, although &lt;a href=&quot;https://github.com/preactjs/preact-router/issues/279&quot;&gt;there is an issue and open PR to add it&lt;/a&gt;. In the meantime, it is feasible to patch the functionality in-place.&lt;/p&gt;
&lt;p&gt;First, I created a wrapper method around the normal &lt;code&gt;route()&lt;/code&gt; function. This is in TypeScript, which makes it little more verbose in dealing with function overloading signatures (&lt;code&gt;route()&lt;/code&gt; is overloaded in its exported definition):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Notice: I need the automatic sub-dir path from where it is exported&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { APP_BASE_URL_NO_SLASH_END } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;../constants&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { route } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;preact-router&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Wrapper around preact-router `route()`, since they have not implemented a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * base URL option, needed for subdirectories, or custom base paths&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; appRoute&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; RouteFuncSignatures&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	input&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	replace&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; boolean&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; inputUrl&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; typeof&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;string&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input.url;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; patchedUrl &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; inputUrl;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;patchedUrl.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;startsWith&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;APP_BASE_URL_NO_SLASH_END&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; sep&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;patchedUrl.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;startsWith&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		patchedUrl &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; `${&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;APP_BASE_URL_NO_SLASH_END&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sep&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;patchedUrl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;typeof&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;string&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; route&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(patchedUrl, replace);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; route&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		url: patchedUrl,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		replace: input.replace,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;interface&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; RouteFuncSignatures&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;options&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; })&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, I can simply find all uses of &lt;code&gt;route()&lt;/code&gt; and replace them with my &lt;code&gt;appRoute()&lt;/code&gt; wrapper function (which takes identical arguments, so I can literally find and replace all instances).&lt;/p&gt;
&lt;p&gt;I also need to fix how I have defined my route matches. Luckily, this is a lot easier; I just need to prefix all the paths with my shared prefix:&lt;/p&gt;
&lt;p&gt;Diff:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;diff&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;Router onChange={handleRoute}&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	&amp;#x3C;Route&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FDAEB7&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;-&lt;/span&gt;		path=&quot;/user/create&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;+&lt;/span&gt;		path={`${APP_BASE}/user/create`}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		component={User}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/Router&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;There are other ways you could patch this - e.g., you could create a wrapper around &lt;code&gt;&amp;#x3C;Router&gt;&amp;#x3C;/Router&gt;&lt;/code&gt; instead.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;fix-hard-coded-links&quot;&gt;Fix Hard-Coded Links&lt;/h3&gt;
&lt;p&gt;Anywhere that you have hard-coded links, that are not relative, you need to update them to either be relative (which should work with the subdirectory move), or prefix with the shared subdirectory base.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;diff&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FDAEB7&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;-&lt;/span&gt; &amp;#x3C;a href=&quot;/dashboard&quot;&gt; Dashboard &amp;#x3C;/a&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;+&lt;/span&gt; &amp;#x3C;a href={`${APP_BASE}/dashboard`}&gt; Dashboard &amp;#x3C;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;diff&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FDAEB7&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;-&lt;/span&gt; &amp;#x3C;Link href=&quot;/dashboard&quot;&gt; Dashboard &amp;#x3C;/Link&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;+&lt;/span&gt; &amp;#x3C;Link href={`${APP_BASE}/dashboard`}&gt; Dashboard &amp;#x3C;/Link&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;pwa-support-double-check-manifestjson&quot;&gt;PWA Support: Double-Check manifest.json&lt;/h3&gt;
&lt;p&gt;If you are deploying your app as a PWA, you will probably want to check your &lt;a href=&quot;https://web.dev/add-manifest/&quot;&gt;&lt;code&gt;manifest.json&lt;/code&gt; file&lt;/a&gt;, and make sure that the URLs within it are relative and not absolute, as well as update the &lt;code&gt;start_url&lt;/code&gt; value to point to the subdirectory and not the root path (&lt;code&gt;/&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id=&quot;update-deploy-config&quot;&gt;Update Deploy Config&lt;/h3&gt;
&lt;p&gt;This step is unrelated to Preact, and would be required for any project where you have changed what folders should become the root of the host / deployment server.&lt;/p&gt;
&lt;p&gt;This step is also going to be unique to your deployment situation. For example, if you using an automated system like Netlify, it should just require changing some values through an Admin UI (Example: &lt;a href=&quot;https://docs.netlify.com/configure-builds/get-started/#basic-build-settings&quot;&gt;Netlify - change the &lt;em&gt;publish directory&lt;/em&gt; option&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id=&quot;summarizing-a-checklist&quot;&gt;Summarizing: A Checklist&lt;/h2&gt;
&lt;p&gt;Here are all the steps I’ve listed, as a checklist that you can scan through if you are still having issues getting things working.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot;&gt;&lt;span&gt;&lt;a href=&quot;#prepare-for-changes-setup-shared-base-path&quot;&gt;Setting up a shared base path variable&lt;/a&gt;&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
&lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot;&gt;&lt;span&gt;&lt;a href=&quot;#configure-bundler&quot;&gt;Configure your Bundler&lt;/a&gt;&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
&lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot;&gt;&lt;span&gt;&lt;a href=&quot;#fixing-router&quot;&gt;Fixing Router&lt;/a&gt;&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
&lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot;&gt;&lt;span&gt;&lt;a href=&quot;#fix-hard-coded-links&quot;&gt;Fixing Links&lt;/a&gt;&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
&lt;li&gt;&lt;label&gt;&lt;input type=&quot;checkbox&quot;&gt;&lt;span&gt;&lt;a href=&quot;#pwa-support-double-check-manifestjson&quot;&gt;PWA Support: Checking manifest.json&lt;/a&gt;&lt;/span&gt;&lt;/label&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;alternate-approaches&quot;&gt;Alternate Approaches&lt;/h2&gt;
&lt;p&gt;Through all the steps I listed, my approach was to store the subdirectory path string in my &lt;code&gt;package.json&lt;/code&gt;, but that is not the only option. If we wanted an even more flexible system, where the same repo could be deployed to &lt;em&gt;multiple&lt;/em&gt; different subdirectories, we could pull the subdirectory path from an environmental variable instead of hard-coding it into the package file. Then, in each deployment, we would just need to make sure the build server had that environmental variable populated with the correct value for its target.&lt;/p&gt;
&lt;p&gt;I’m sure there are many more approaches I haven’t discussed; as always, use what works best for you and your requirements. And happy coding! 👩‍💻&lt;/p&gt;
&lt;img loading=&quot;lazy&quot; src=&quot;https://media.giphy.com/media/11BbGyhVmk4iLS/giphy.gif&quot; alt=&quot;Animated GIF showing Dee Dee from Dexter&amp;#x27;s laboratory typing frantically&quot; style=&quot;margin: auto; display:block; max-width: 400px; width: 95%; height: auto;&quot;&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Sheets - Faster Data Exports Options, Including CSVs</title><link>https://joshuatz.com/posts/2020/google-sheets---faster-data-exports-options-including-csvs/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/google-sheets---faster-data-exports-options-including-csvs/</guid><description>Options for getting data feeds out of Google Sheets, avoiding slow-to-update published CSV feed URLs, and custom scripting with Google Apps Scripts.</description><pubDate>Wed, 30 Dec 2020 06:04:29 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;I recently came across &lt;a href=&quot;https://twitter.com/jlongster/status/1343405253882998787&quot;&gt;this thread on Twitter&lt;/a&gt;, which captured my interest:&lt;/p&gt;
&lt;iframe style=&quot;border:none;&quot; width=&quot;550&quot; height=&quot;400&quot; data-tweet-url=&quot;https://twitter.com/jlongster/status/1343405253882998787&quot; src=&quot;data:text/html;charset=utf-8,%3Cblockquote%20class%3D%22twitter-tweet%22%3E%3Cp%20lang%3D%22en%22%20dir%3D%22ltr%22%3EWhen%20you%20add%20new%20data%2C%20it%20seems%20to%20be%20eventually%20consistent%2C%20and%20super%20slow%3F%20New%20data%20will%20show%20up%20after%20a%20few%20minutes%2C%20and%20then%20disappear%21%20It%20takes%20_forever_%20for%20new%20data%20to%20consistently%20show%20up%20in%20the%20curl%3C/p%3E%26mdash%3B%20James%20Long%20%28@jlongster%29%20%3Ca%20href%3D%22https%3A//twitter.com/jlongster/status/1343405253882998787%3Fref_src%3Dtwsrc%255Etfw%22%3EDecember%2028%2C%202020%3C/a%3E%3C/blockquote%3E%0A%3Cscript%20async%20src%3D%22https%3A//platform.twitter.com/widgets.js%22%20charset%3D%22utf-8%22%3E%3C/script%3E%0A&quot;&gt;&lt;/iframe&gt;
&lt;blockquote&gt;
&lt;p&gt;If you have Twitter embeds blocked, the Tweet thread is a discussion of an issue with Google Sheets, where the public “published” CSV feed URL does not return up-to-date data when fetched via curl, or at least not consistently and quickly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I fired up a demo sheet, and lo-and-behold, I could easily reproduce the same issue they were seeing; Google Sheets was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Taking a decent amount of time to update the published CSV&lt;/li&gt;
&lt;li&gt;The published data would sometimes fluctuate, and rows would randomly disappear from the CSV feed&lt;/li&gt;
&lt;li&gt;Both of the above issues were occurring randomly; I could not determine a consistent pattern&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, I used to use the published CSV/TSV feature all the time and rarely ever saw this, but was also not relying on &lt;em&gt;real-time&lt;/em&gt; data feeds, so my guess is that it is a replication issue that goes away with time; James is looking for data ASAP, and the Google servers are likely still passing around the data as it is added (could be multiple layers, like CDN + replication + sharding).&lt;/p&gt;
&lt;p&gt;But, if you are in James position, and you are looking for the ability to have a CSV feed that &lt;em&gt;always&lt;/em&gt; returns an up-to-date fresh export from your Google Sheet, is there an option for you? Yes! Several!&lt;/p&gt;
&lt;h3 id=&quot;option-a---use-the-official-sheet-apis-v4&quot;&gt;Option A - Use the Official Sheet APIs (v4)&lt;/h3&gt;
&lt;p&gt;This (using &lt;a href=&quot;https://developers.google.com/sheets/api/reference/rest&quot;&gt;the Official v4 Sheets API&lt;/a&gt;) was suggested pretty quickly in the Twitter thread:&lt;/p&gt;
&lt;iframe style=&quot;border:none;&quot; width=&quot;550&quot; height=&quot;500&quot; data-tweet-url=&quot;https://twitter.com/RayGesualdo/status/1343587267512823809&quot; src=&quot;data:text/html;charset=utf-8,%3Cblockquote%20class%3D%22twitter-tweet%22%3E%3Cp%20lang%3D%22en%22%20dir%3D%22ltr%22%3EI%20followed%20this%20SO%20answer%3A%20%3Ca%20href%3D%22https%3A//t.co/spCuJqImba%22%3Ehttps%3A//t.co/spCuJqImba%3C/a%3E%3Cbr%3E%3Cbr%3EImplemented%20in%20a%20plain%20Node%20script%20here%20where%20%60DATASOURCE_URL%60%20is%20the%20URL%20to%20the%20sheet%3A%20%3Ca%20href%3D%22https%3A//t.co/0nuzQzH77r%22%3Ehttps%3A//t.co/0nuzQzH77r%3C/a%3E%3C/p%3E%26mdash%3B%20Ray%20Gesualdo%20%28@RayGesualdo%29%20%3Ca%20href%3D%22https%3A//twitter.com/RayGesualdo/status/1343587267512823809%3Fref_src%3Dtwsrc%255Etfw%22%3EDecember%2028%2C%202020%3C/a%3E%3C/blockquote%3E%0A%3Cscript%20async%20src%3D%22https%3A//platform.twitter.com/widgets.js%22%20charset%3D%22utf-8%22%3E%3C/script%3E%0A&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;But, if you follow &lt;a href=&quot;https://stackoverflow.com/a/44479726/11447682&quot;&gt;the linked StackOverflow answer&lt;/a&gt;, you’ll see that there are some limitations with the new &lt;code&gt;v4&lt;/code&gt; API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need an API key (which requires using Google Cloud Platform)&lt;/li&gt;
&lt;li&gt;Data is always returned as JSON&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, this is your best bet for a more robust, best-practices, “future-proof” choice, as &lt;code&gt;v3&lt;/code&gt; of the API is being deprecated very soon.&lt;/p&gt;
&lt;h3 id=&quot;option-b---google-sheets-public-feed-urls-v3&quot;&gt;Option B - Google Sheets Public Feed URLs (v3)&lt;/h3&gt;
&lt;p&gt;To start with, if you have a Google Sheet and want a public CSV feed, you can use the &lt;code&gt;Publish to the Web&lt;/code&gt; option, and get a feed URL that looks like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;https://docs.google.com/spreadsheets/d/e/{UNIQUE_ID}/pub?gid={SHEET_ID}&amp;#x26;single=true&amp;#x26;output=csv&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;… where &lt;code&gt;{UNIQUE_ID}&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; the normal document ID, but instead a unique random publication ID. &lt;code&gt;{SHEET_ID}&lt;/code&gt; does however, correspond to normal sheet (pages of the spreadsheet) IDs.&lt;/p&gt;
&lt;p&gt;As already noted, an issue with the above feed can be a delay in accurate data being reflected after the source is updated. However, that is not the &lt;em&gt;only&lt;/em&gt; feed URL that is available to us without an API key. Until Google deprecates &lt;code&gt;v3&lt;/code&gt; of the Sheets API, there are still &lt;code&gt;v3&lt;/code&gt; API endpoints that you can use to get fresh data, without needing an API key:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🚨 WARNING 🚨: All these URLs will likely stop working on June 8, 2021, which is when Google is planning on shutting down the &lt;code&gt;v3&lt;/code&gt; version of the API. They are not likely to extend the deadline, as &lt;a href=&quot;https://developers.google.com/sheets/api/v3/data&quot;&gt;they already did so once already&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;All values on a sheet
&lt;ul&gt;
&lt;li&gt;XML: &lt;code&gt;https://spreadsheets.google.com/feeds/cells/{DOC_ID}/{PAGE_NUM}/public/full&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;JSON: &lt;code&gt;https://spreadsheets.google.com/feeds/cells/{DOC_ID}/{PAGE_NUM}/public/full?alt=json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Specific range of values
&lt;ul&gt;
&lt;li&gt;Use the same endpoints as above, but control range with:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;min-row=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max-row=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min-col=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max-col=&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are a lot of other advanced feeds you can get with the &lt;code&gt;v3&lt;/code&gt; API - see Docs: &lt;a href=&quot;https://developers.google.com/sheets/api/v3/data&quot;&gt;“Manage List-based and Cell-based Feeds”&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📄 You can find a more “step-by-step” guide &lt;a href=&quot;https://www.freecodecamp.org/news/cjn-google-sheets-as-json-endpoint/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;option-c---google-apps-scripting&quot;&gt;Option C - Google Apps Scripting&lt;/h3&gt;
&lt;p&gt;All this research into Google Sheets got me thinking about Google Apps Scripts (aka &lt;em&gt;GAS&lt;/em&gt;), which I have used many times before, and wondering if it could be fitted to this task. GAS can live with a spreadsheet document, access cell values, and can even be published as a web app and respond to web requests!&lt;/p&gt;
&lt;p&gt;I spent a little time and threw together a fun mini-API that can export just the last X number of rows of data, or the entire document, as CSV, TSV, or JSON! If you don’t want to require a Google login, but also don’t want it 100% public, you can even lock it down with an authentication key, entered into a named range of your document.&lt;/p&gt;
&lt;!-- Github Gist --&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-jptremote=&quot;https://gist.githubusercontent.com/joshuatz/1d98be6e801c00b0b2ceb85ec111578a/raw/google-sheets-data-export-with-gas.js&quot;&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course, after finishing my script, I found that someone had already put together a similar solution. Feel free to check that out as well: &lt;a href=&quot;https://gist.github.com/ronaldsmartin/47f5239ab1834c47088e&quot;&gt;gist.github.com/ronaldsmartin/47f5239ab1834c47088e&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>MedTimer - Shared Medication Timer and Tracker Web App</title><link>https://joshuatz.com/projects/web-stuff/medtimer---shared-medication-timer-and-tracker-web-app/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/medtimer---shared-medication-timer-and-tracker-web-app/</guid><description>A small web app with a shared login system to track the status of timed medication doses, as well as scheduled medications and therapy durations.</description><pubDate>Tue, 29 Dec 2020 09:44:28 GMT</pubDate><content:encoded>&lt;!-- This project post was created in 2020, but is for a project from early 2018 --&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;I’m writing this project post in late 2020, but I actually coded and built this out in early 2018. It was one of my earliest PHP projects, and also an experiment in starting from scratch (no frameworks, no starter package). I’m making a writeup for it years later, because, looking back I actually really like what I built and feel it deserves one. I kind of wish I had done more with this project, and perhaps built it into a larger-scale SaaS. As it is though, it served its purpose for a long time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This project came about at the request of a friend of the family. Her son, who has a chronic health condition, was taking special medication to help his body with ingestion. He would take a pill, and then have 90 minutes to eat. Once the 90 minutes were up, he would need to take another pill if he wanted to eat more, even for tiny snacks. The part that I was asked if I could help with was how to track this; at the time he was really young, in elementary school, and because he was often moving between environments (home, school, friend’s houses) it was hard for the adults in his life to have a central place to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A) Had he recently taken a pill?&lt;/li&gt;
&lt;li&gt;B) And, if so, were the 90 minutes up yet?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I thought this sounded like a great opportunity for me to learn some more full-stack development, and build a tiny web app that could have a shared login and “virtual” timer.&lt;/p&gt;
&lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;
&lt;h3 id=&quot;core-functionality&quot;&gt;Core Functionality&lt;/h3&gt;
&lt;p&gt;The core functionality of the app is fairly simple:&lt;/p&gt;
&lt;p&gt;Each adult is given their own “passphrase” to log into the system. This is so that, if the timer is started, everyone can see which adult was the one to administer the pill and start the timer.&lt;/p&gt;
&lt;p&gt;The main admin account has the ability to add and delete these passphrases whenever they want. New school nurse? Give them their own login!&lt;/p&gt;
&lt;details&gt;
	&lt;summary&gt;Show / Hide Passphrase Demo&lt;/summary&gt;
&lt;video title=&quot;MedTimer - Using the Passphrase System&quot; loop muted preload=&quot;metadata&quot; style=&quot;display:block; width:94%; margin:auto;&quot;&gt;
	&lt;source src=&quot;/media/MedTimer-Using-the-Passphrase-System.mp4&quot; type=&quot;video/mp4&quot;&gt;
	Sorry, your browser doesn&apos;t support embedded videos.
&lt;/video&gt;
&lt;/details&gt;
&lt;p&gt;After logging in, there is a giant status indicator area, which tells everyone:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the timer is running, and he can currently eat&lt;/li&gt;
&lt;li&gt;If he can eat, for how much longer (how much time is left of the 90 minutes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/media/MedTimer-Main-Timer-Homepage.png&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto; text-align:center;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/MedTimer-Main-Timer-Homepage.png&quot; alt=&quot;MedTimer - App Homepage&quot; style=&quot;width:100%;max-width: 1280px;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;video title=&quot;MedTimer - Timer Start&quot; controls loop muted preload=&quot;auto&quot; style=&quot;display:block; width:94%; margin:auto;&quot;&gt;
	&lt;source src=&quot;/media/MedTimer-Timer-Start.mp4&quot; type=&quot;video/mp4&quot;&gt;
	Sorry, your browser doesn&apos;t support embedded videos.
&lt;/video&gt;
&lt;p&gt;After the 90 minutes are up, the countdown automatically ends and the status changes. Additionally, if the timer was started on accident, it can be manually cancelled (which still gets saved to history). Or, if you forgot to start it and the pill has already been taken recently, you can offset the start time manually.&lt;/p&gt;
&lt;p&gt;All entries are automatically recorded and summarized, under the “History” page:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/MedTimer-Timer-History-Page.png&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto; text-align:center;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/MedTimer-Timer-History-Page.png&quot; alt=&quot;MedTimer - History Entries Page&quot; style=&quot;width:100%;max-width: 1280px;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;details&gt;
	&lt;summary&gt;Show / Hide History Demo Video&lt;/summary&gt;
&lt;video title=&quot;MedTimer - Timer Start, and Show New Entry&quot; controls loop muted preload=&quot;metadata&quot; style=&quot;display:block; width:94%; margin:auto;&quot;&gt;
	&lt;source src=&quot;/media/MedTimer-Timer-Start-and-Show-New-Entry.mp4&quot; type=&quot;video/mp4&quot;&gt;
	Sorry, your browser doesn&apos;t support embedded videos.
&lt;/video&gt;
&lt;/details&gt;
&lt;h3 id=&quot;extra-feature---medication-scheduling&quot;&gt;Extra Feature - Medication Scheduling&lt;/h3&gt;
&lt;p&gt;The person I built this for was so pleased with the timer feature, they asked if I could add on another feature; something to replace their paper-and-pen solution for tracking medication and therapy that was being administered. I said yes, and quickly put together a “schedule” feature.&lt;/p&gt;
&lt;p&gt;You can configure medications and therapies, specify time of day, and for therapies, even specify and track the required duration (e.g. &lt;code&gt;30 minutes of hand grips&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;👇 Tracking Medications
&lt;video controls loop muted preload=&quot;metadata&quot; style=&quot;display:block; width:94%; margin:auto;&quot; title=&quot;MedTimer - Scheduler - Tracking Medications&quot;&gt;
&lt;source src=&quot;/media/MedTimer-Scheduler-Tracking-Medications.mp4&quot; type=&quot;video/mp4&quot;&gt;
Sorry, your browser doesn’t support embedded videos.
&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;👇 Tracking Timed Therapies
&lt;video title=&quot;MedTimer - Scheduler - Using Timed Therapies&quot; controls loop muted preload=&quot;metadata&quot; style=&quot;display:block; width:94%; margin:auto;&quot;&gt;
&lt;source src=&quot;/media/MedTimer-Scheduler-Using-Timed-Therapies.mp4&quot; type=&quot;video/mp4&quot;&gt;
Sorry, your browser doesn’t support embedded videos.
&lt;/video&gt;&lt;/p&gt;
&lt;h2 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h2&gt;
&lt;p&gt;At the time, I wanted to get a bit more familiar with PHP and general full-stack basics, so I used this project as an exercise in using some PHP, HTML, CSS, JS, and SQL fundamentals.&lt;/p&gt;
&lt;p&gt;I built it with zero frameworks, and only a tiny bit of third party for front-end stuff; backend is all vanilla PHP. For tying together the front and backend, I wrote a tiny API in PHP to handle the requests.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backend
&lt;ul&gt;
&lt;li&gt;PHP&lt;/li&gt;
&lt;li&gt;PDO + SQLite&lt;/li&gt;
&lt;li&gt;Apache&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Frontend
&lt;ul&gt;
&lt;li&gt;HTML&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;JQuery (just a little 😅)&lt;/li&gt;
&lt;li&gt;DataTables&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;outcome--wrap-up&quot;&gt;Outcome / Wrap-Up&lt;/h2&gt;
&lt;p&gt;I received very positive feedback from the family that this was built for, and it served its function hundreds of times; to me, that is what matters most.&lt;/p&gt;
&lt;p&gt;Overall, I’m happy with what I built, although I might revisit this at some point in the future as a fun project to rebuild at-scale.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Common CSS Color Variables and Properties in Theming</title><link>https://joshuatz.com/posts/2020/common-css-color-variables-and-properties-in-theming/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/common-css-color-variables-and-properties-in-theming/</guid><description>An effort to collect, categorize, and summarize some of the common CSS custom properties that are being used to maintain colors in modern theming approaches.</description><pubDate>Fri, 25 Dec 2020 01:23:27 GMT</pubDate><content:encoded>&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#collected-framework-results&quot;&gt;Collected Framework Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#examples-of-custom-variable-sets&quot;&gt;Examples of Custom Variable Sets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#some-common-patterns&quot;&gt;Some Common Patterns&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#color-wheel-based-names&quot;&gt;Color Wheel based names&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#named-scaled-color-palettes--swatches&quot;&gt;Named scaled color palettes / swatches&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#contrast-variations&quot;&gt;Contrast Variations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#semantic-named-colors&quot;&gt;Semantic Named Colors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#brand-colors&quot;&gt;Brand Colors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#color-parts--calc-helpers&quot;&gt;Color Parts / Calc Helpers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#differentiating-variable-types&quot;&gt;Differentiating Variable Types&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#summary&quot;&gt;Summary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#more-resources&quot;&gt;More Resources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;As I’ve dipped in and out of different front-end theming frameworks, something that I have noticed is a lack of standardization when it comes to global theme color variables.&lt;/p&gt;
&lt;p&gt;For the unfamiliar, many front-end frameworks provide global (e.i. &lt;em&gt;shared&lt;/em&gt;, or &lt;em&gt;root&lt;/em&gt;) color variables that can be easily re-used across your app, and often support customization and hot-swapping of values for instantaneous theme switching (e.g. between “dark mode” and “light mode”).&lt;/p&gt;
&lt;p&gt;This blog post is the result of a small research and classification attempt by myself, to see if I can identify some common trends and tools that might be useful within this area of theme development.&lt;/p&gt;
&lt;h2 id=&quot;collected-framework-results&quot;&gt;Collected Framework Results&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ionicframework.com/&quot;&gt;Ionic Framework&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ionicframework.com/docs/theming/basics&quot;&gt;Variables&lt;/a&gt;:
&lt;ul&gt;
&lt;li&gt;Primary&lt;/li&gt;
&lt;li&gt;Secondary&lt;/li&gt;
&lt;li&gt;Tertiary&lt;/li&gt;
&lt;li&gt;Success&lt;/li&gt;
&lt;li&gt;Warning&lt;/li&gt;
&lt;li&gt;Danger&lt;/li&gt;
&lt;li&gt;Dark&lt;/li&gt;
&lt;li&gt;Medium&lt;/li&gt;
&lt;li&gt;Light&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Tools:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ionicframework.com/docs/theming/color-generator&quot;&gt;Color Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ionicframework.com/docs/theming/colors#new-color-creator&quot;&gt;New Color Creator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://getbootstrap.com/docs/4.0/getting-started/introduction/&quot;&gt;Boostrap 4&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://getbootstrap.com/docs/4.1/getting-started/theming/#theme-colors&quot;&gt;Variables&lt;/a&gt;:
&lt;ul&gt;
&lt;li&gt;Primary&lt;/li&gt;
&lt;li&gt;Secondary&lt;/li&gt;
&lt;li&gt;Success&lt;/li&gt;
&lt;li&gt;Danger&lt;/li&gt;
&lt;li&gt;Warning&lt;/li&gt;
&lt;li&gt;Info&lt;/li&gt;
&lt;li&gt;Light&lt;/li&gt;
&lt;li&gt;Dark&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Tools:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://themestr.app/&quot;&gt;Theme builder and customization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://material.io/&quot;&gt;Material Design&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://material.io/design/material-theming/implementing-your-theme.html#color&quot;&gt;Variables&lt;/a&gt;:
&lt;ul&gt;
&lt;li&gt;Primary&lt;/li&gt;
&lt;li&gt;Primary Variant&lt;/li&gt;
&lt;li&gt;Secondary&lt;/li&gt;
&lt;li&gt;Secondary Variant&lt;/li&gt;
&lt;li&gt;Background&lt;/li&gt;
&lt;li&gt;Surface&lt;/li&gt;
&lt;li&gt;Error&lt;/li&gt;
&lt;li&gt;On Primary&lt;/li&gt;
&lt;li&gt;On Secondary&lt;/li&gt;
&lt;li&gt;On Background&lt;/li&gt;
&lt;li&gt;On Surface&lt;/li&gt;
&lt;li&gt;On Error&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Tools:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://material.io/resources/color/#!/?view.left=0&amp;#x26;view.right=0&quot;&gt;Color Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://material.io/resources/build-a-material-theme#how-to-using-glitch&quot;&gt;Theme Builder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;&quot;&gt;Vuetify&lt;/a&gt; (Material framework for Vue)
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vuetifyjs.com/en/features/theme/#customizing&quot;&gt;Variables&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Primary&lt;/li&gt;
&lt;li&gt;Secondary&lt;/li&gt;
&lt;li&gt;Accent&lt;/li&gt;
&lt;li&gt;Error&lt;/li&gt;
&lt;li&gt;Info&lt;/li&gt;
&lt;li&gt;Success&lt;/li&gt;
&lt;li&gt;Warning&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Tools:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://theme-generator.vuetifyjs.com/&quot;&gt;Color Palette Generator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;examples-of-custom-variable-sets&quot;&gt;Examples of Custom Variable Sets&lt;/h2&gt;
&lt;p&gt;As noted in the intro, the whole reason behind this post was because I noticed how many different combinations of global CSS variables I was seeing. In addition to noting the variance between design frameworks (see &lt;a href=&quot;#collected-framework-results&quot;&gt;above&lt;/a&gt;), I also wanted to callout some interesting examples of highly customized sets of global CSS variables.&lt;/p&gt;
&lt;details&gt;
	&lt;summary&gt;Twitch (&lt;a href=&quot;https://www.twitch.tv&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;twitch.tv&lt;/a&gt;)&lt;/summary&gt;
&lt;p&gt;Twitch has, literally, &lt;em&gt;&lt;strong&gt;hundreds&lt;/strong&gt;&lt;/em&gt; of &lt;code&gt;:root&lt;/code&gt; variables, some of which are pretty standard, such as &lt;code&gt;--color-accent&lt;/code&gt;, while others are pretty specific to their use-case, such as &lt;code&gt;--color-fill-channel-status-text-indicator-offline&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There is a built-in theme switcher, to switch between dark and light. Switching between them swaps a class in the top level element (e.g. &lt;code&gt;tw-root--theme-light&lt;/code&gt;), which in turn changes which values are assigned to the root-level variables. This is a common CSS-variable-based theming approach.&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
	&lt;summary&gt;StackOverflow (&lt;a href=&quot;https://stackoverflow.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;stackoverflow.com&lt;/a&gt;)&lt;/summary&gt;
&lt;p&gt;StackOverflow makes use of a lot of patterns covered in this post, as well as some advanced patterns that shift more theme processing towards native CSS and away from pre-processors.&lt;/p&gt;
&lt;p&gt;For example, they define the primary color in separate chunks (both &lt;em&gt;HSL&lt;/em&gt; and &lt;em&gt;RGB&lt;/em&gt;), and then dynamically compute derived colors by combining those values and optionally applying calculations. Here is how they compute a derived color, which is a shade of the primary color darkened by 30%:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;:root&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--theme-primary-color-h&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;26.53846154&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--theme-primary-color-s&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;90.43478261&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--theme-primary-color-l&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;54.90196078&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--theme-primary-color-darken-30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;hsl&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;--theme-primary-color-h&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;--theme-primary-color-s&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;calc&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;--theme-primary-color-l&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 30&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;h2 id=&quot;some-common-patterns&quot;&gt;Some Common Patterns&lt;/h2&gt;
&lt;p&gt;Here is a breakdown of some common recurring patterns I saw:&lt;/p&gt;
&lt;h3 id=&quot;color-wheel-based-names&quot;&gt;Color Wheel based names&lt;/h3&gt;
&lt;p&gt;Although the values people pick might not always correspond with it, these variable names seem to be based on the standard color wheel.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Examples:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--primary: red&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--secondary: orange&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--tertiary: orangered&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;named-scaled-color-palettes--swatches&quot;&gt;Named scaled color palettes / swatches&lt;/h3&gt;
&lt;p&gt;Since there is no such thing as just “&lt;code&gt;red&lt;/code&gt;”, many themes maintain their own palette of colors, with different weights / intensities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Examples:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--orange-400: #f9872c;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--orange-500: #f4740d;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--yellow-400: #d8ba45;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Notes:
&lt;ul&gt;
&lt;li&gt;This does not appear to be standardized either. Many sites and frameworks use a scale that seems to correlate with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight&quot;&gt;font weights&lt;/a&gt; (100-900), where others might use &lt;code&gt;0-10&lt;/code&gt;, or &lt;code&gt;-a#&lt;/code&gt; for properties using alpha values.&lt;/li&gt;
&lt;li&gt;I think part of the reason why this is so common is because creating “shades” in CSS is a lot more complicated than in SASS (where you have access to native functions like &lt;code&gt;lighten()&lt;/code&gt; and &lt;code&gt;darken()&lt;/code&gt;). Another consideration is performance; having these values pre-populated is probably has significantly better performance than a bunch of calls to &lt;code&gt;calc()&lt;/code&gt;, especially if you have dozens of colors and derivative shades / variations.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;contrast-variations&quot;&gt;Contrast Variations&lt;/h3&gt;
&lt;p&gt;When specifying a color, you have to consider all the different ways it will be used, and that includes how it contrasts with the colors around it. Thus, thus some systems like to explicitly declare contrasting color values.&lt;/p&gt;
&lt;p&gt;Often, the contrast value is used for text or icons; for example, the Material Design system has &lt;a href=&quot;https://material.io/design/color/the-color-system.html#color-theme-creation#:~:text=%22On%22%20colors&quot;&gt;an entire section&lt;/a&gt; devoted to what they call “On Colors” - this is a color that should be used for text or iconography when it appears “on” another surface.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;:root&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--primary&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;#261ff2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--secondary&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;#ea80fc&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--on-primary&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;#fafafa&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--on-secondary&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;#000504&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main concern with these values should be of proper contrast and accessibility; you don’t want yellow text on a yellow background.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In some systems, these contrasting colors might be automatically derived based on calculated minimums (e.g. &lt;a href=&quot;https://web.dev/color-and-contrast-accessibility/&quot;&gt;WCAG / WebAIM&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also see these variables reflected in &lt;a href=&quot;https://material.io/develop/web/theming/color&quot;&gt;Material’s custom theme color docs&lt;/a&gt;. And the Ionic Framework &lt;a href=&quot;https://ionicframework.com/docs/theming/colors#layered-colors&quot;&gt;places particular importance on contrast in color variables&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;semantic-named-colors&quot;&gt;Semantic Named Colors&lt;/h3&gt;
&lt;p&gt;Although some might call &lt;code&gt;primary&lt;/code&gt; and &lt;code&gt;secondary&lt;/code&gt; semantic variables, I think the best examples of what one might classify as semantic color variables are things like:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;:root&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--success&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;#28a745&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;#17a2b8&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--warning&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;#ffc107&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--danger&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;#dc3545&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;These come directly from &lt;a href=&quot;https://getbootstrap.com/docs/4.1/getting-started/theming/#available-variables&quot;&gt;Bootstrap 4.1’s root variables&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For those unfamiliar, the term &lt;em&gt;semantic&lt;/em&gt;, within the context of variable naming, usually means that the name itself conveys the meaning and/or application of the value held, rather then relying on said value.&lt;/p&gt;
&lt;p&gt;The benefit to these types of values is largely in readability and ease of refactoring. For example, imagine that you have a bunch of components that show errors. You might improve refactor-ability and ease of reading by changing:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;background-color: var(--red-600);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;background-color: var(--danger);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;brand-colors&quot;&gt;Brand Colors&lt;/h3&gt;
&lt;p&gt;Although &lt;code&gt;primary&lt;/code&gt; and &lt;code&gt;secondary&lt;/code&gt; colors usually are the main brand colors, a brand will often have a large palette that makes up their branding guidelines, and you might see this reflected in CSS variables.&lt;/p&gt;
&lt;p&gt;For example, if you open up a new empty Chrome in tab, you are likely to see variables like &lt;code&gt;--google-blue-refresh-700&lt;/code&gt;. Or, on &lt;em&gt;twitch.tv&lt;/em&gt;, you might see a smattering of &lt;code&gt;--color-amazon&lt;/code&gt; throughout the site, as well as things like &lt;code&gt;--color-brand-accent-cherry&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you have a really massive application, and especially if it has third party assets or different themed areas, it might be helpful to clearly differentiate between variables that are part of the application theme, vs part of your actual overall brand (and stricter brand guidelines).&lt;/p&gt;
&lt;h3 id=&quot;color-parts--calc-helpers&quot;&gt;Color Parts / Calc Helpers&lt;/h3&gt;
&lt;p&gt;I’m not trying to diss CSS (I love CSS!), but the truth is that there are not a whole &lt;em&gt;lot&lt;/em&gt; of built-in functions for manipulating hex based color values (especially compared with things like SASS).&lt;/p&gt;
&lt;p&gt;One really neat trick to work around this fact is to store color values as their individual parts (either &lt;code&gt;R-G-B&lt;/code&gt; or &lt;code&gt;H-S-L&lt;/code&gt;), and then put them together and manipulate them in the process.&lt;/p&gt;
&lt;p&gt;Here is an example of this in action:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;:root&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--theme-primary-color-h&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;242&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;deg&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--theme-primary-color-s&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;56&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--theme-primary-color-l&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;62&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	--theme-primary-color-darken-30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;hsl&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;--theme-primary-color-h&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;--theme-primary-color-s&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;calc&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;--theme-primary-color-l&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 30&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: The &lt;code&gt;hue&lt;/code&gt; value in CSS can be written as either a number (&lt;code&gt;242&lt;/code&gt;) with &lt;code&gt;deg&lt;/code&gt; unit implied, or with angle unit (e.g. &lt;code&gt;242deg&lt;/code&gt;). &lt;a href=&quot;https://drafts.csswg.org/css-color/#typedef-hue&quot;&gt;See docs&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://una.im/css-color-theming/&quot;&gt;This writeup by Una&lt;/a&gt; is amazing guide to using this approach to dynamically generate derived colors from your CSS variables.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Before you dismiss this approach, I’ll note that several high-volume production sites (such as StackOverflow) use this technique in CSS&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;differentiating-variable-types&quot;&gt;Differentiating Variable Types&lt;/h3&gt;
&lt;p&gt;You might notice that some applications / sites are very careful to distinguish color properties, and they will always use things like &lt;code&gt;--border-color:&lt;/code&gt; instead of just &lt;code&gt;--border:&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A good reason to do so is if your app stores (or could in the future) non color values in CSS custom properties. Not everyone always remembers this, but CSS custom properties can actually store all types of values beyond just colors;  you can store strings, numbers, or any CSS value.&lt;/p&gt;
&lt;p&gt;Thus, it might be wise to differentiate color values from the start, since that will make refactoring and readability easier (&lt;em&gt;“Is &lt;code&gt;--border&lt;/code&gt; for border color or border value, or border px?”&lt;/em&gt;).&lt;/p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;Although I enjoy consistency and standardization, I think seeing a moderate level of variance in how CSS custom properties are getting used and their naming conventions is actually a good thing. It speaks to the flexibility of CSS and what I believe is important part of web development, which is to use what works best for the situation and your users, not necessarily what the most popular framework dictates.&lt;/p&gt;
&lt;h2 id=&quot;more-resources&quot;&gt;More Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Prototypr.io - Anna Molly: &lt;a href=&quot;https://blog.prototypr.io/basic-ui-color-guide-7612075cc71a&quot;&gt;Basic UI color guide&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Excellent breakdown of the different types of colors in a color system&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Sebastiano Guerriero: &lt;a href=&quot;https://medium.com/codyhouse/create-your-design-system-part-3-colors-798e4729921f&quot;&gt;Create your design system, part 3 - colors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Generic color palette creators:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://coolors.co&quot;&gt;Coolors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://colormind.io/&quot;&gt;Colormind&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;More listed &lt;a href=&quot;https://docs.joshuatz.com/elevator-pitches/#css--styling&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Material Design Docs - &lt;a href=&quot;https://material.io/design/color/the-color-system.html&quot;&gt;“The Color System”&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;The Material Design docs are a fairly comprehensive resource&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Examples of theme variable sets
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/DiscordStyles/Slate/wiki/Hidden-Variables&quot;&gt;DiscordStyles/Slate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Materialize CSS: &lt;a href=&quot;https://github.com/Dogfalo/materialize/blob/v1-dev/sass/components/_variables.scss&quot;&gt;_variables.scss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Vuetify: &lt;a href=&quot;https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/presets/default/index.ts&quot;&gt;Default theme&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Bootstrap: &lt;a href=&quot;https://github.com/twbs/bootstrap/blob/main/scss/_variables.scss&quot;&gt;_variables.scss&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Cover image background - &lt;span&gt;Photo by &lt;a href=&quot;https://unsplash.com/@keilahoetzel?utm_source=unsplash&amp;#x26;utm_medium=referral&amp;#x26;utm_content=creditCopyText&quot;&gt;Keila Hötzel&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/?utm_source=unsplash&amp;#x26;utm_medium=referral&amp;#x26;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Git SHA Badges for Deploys - Know Which Commit is Live</title><link>https://joshuatz.com/posts/2020/git-sha-badges-for-deploys---know-which-commit-is-live/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/git-sha-badges-for-deploys---know-which-commit-is-live/</guid><description>Want a clear visual indicator in your app or documentation so you know *exactly* which commit was pushed and successfully deployed? Then this post is for you.</description><pubDate>Wed, 09 Dec 2020 06:00:10 GMT</pubDate><content:encoded>&lt;h2 id=&quot;demo-repo&quot;&gt;Demo Repo&lt;/h2&gt;
&lt;p&gt;I’ve taken the approaches covered in this post and created a fully-functional demo repo to demonstrate them in action:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;💾 &lt;a href=&quot;https://github.com/joshuatz/git-sha-badges&quot;&gt;github.com/joshuatz/git-sha-badges&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;When building a project that lives in a Git repository and auto-deploys elsewhere, a common concern is how to be 100% absolutely sure that the live “deployed” version is up-to-date. If I go to the live, production application, how can I trust that what I am seeing is the latest pushed code in my repository?&lt;/p&gt;
&lt;p&gt;Just because my CI/CD pipeline passed does not &lt;em&gt;necessarily&lt;/em&gt; mean that the latest commit is live yet. Furthermore, if something goes bad, I want to be able to know at a glance &lt;em&gt;&lt;strong&gt;exactly&lt;/strong&gt;&lt;/em&gt; what code is live in production. A simple “pass / fail” badge cannot tell me that.&lt;/p&gt;
&lt;h3 id=&quot;close-but-not-good-enough&quot;&gt;Close, but Not Good Enough&lt;/h3&gt;
&lt;p&gt;Many automated deploy systems provide a build status badge, but this doesn’t tell us much:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Standard Netlify Status Badge:&lt;/p&gt;
&lt;img src=&quot;https://api.netlify.com/api/v1/badges/4b298306-7d69-4214-b790-b37a56e59ec4/deploy-status&quot; alt=&quot;Netlify Status&quot;&gt;
&lt;/blockquote&gt;
&lt;p&gt;Many apps also display in the deployed UI the current version string of the app (e.g. &lt;code&gt;1.8.2-beta.1.10&lt;/code&gt;), but this actually &lt;em&gt;&lt;strong&gt;still&lt;/strong&gt;&lt;/em&gt; leaves a lot of room for ambiguity; what if a developer used &lt;code&gt;--force&lt;/code&gt; to override the head of the main branch? What if there have been commits added to the main branch, but no new release authored? Etc.&lt;/p&gt;
&lt;p&gt;Instead of a version string, a nice alternative (or better yet, an addition) is to use the actual SHA of the latest commit as a status indicator. This makes it very easy to compare against the latest commit in version control, and verify that the correct set of changes are live.&lt;/p&gt;
&lt;p&gt;Furthermore, no two commits can have the same SHA, even if &lt;code&gt;--force&lt;/code&gt; has been used, so if I know the SHA that is “live”, I know &lt;em&gt;&lt;strong&gt;exactly&lt;/strong&gt;&lt;/em&gt; what code is running, and can even quickly “checkout” that exact version of code in my local environment.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Similar to how you can use &lt;code&gt;git checkout {branch_name}&lt;/code&gt;, you can also use &lt;code&gt;git checkout {SHA}&lt;/code&gt; to checkout your entire repo at that commit. It kind of feels like time travelling!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;methods-for-creating-sha-badges&quot;&gt;Methods for Creating SHA Badges&lt;/h2&gt;
&lt;p&gt;The general concept that we are going to employ is that our build step, which runs on every deploy, is now going to save the current SHA of the branch (aka the &lt;em&gt;tip&lt;/em&gt;) in an accessible format that can be used for visual indicators.&lt;/p&gt;
&lt;p&gt;Two easy approaches that build off this concept, and which I’ll explore here, are JSON files and raw SVG files.&lt;/p&gt;
&lt;h3 id=&quot;json-file&quot;&gt;JSON File&lt;/h3&gt;
&lt;p&gt;The basic idea with the JSON file approach is that, at build time, we stash the value of the latest SHA in a shared JSON file. This makes it easy to reuse across our codebase, possibly even at runtime, and in a widely compatible format.&lt;/p&gt;
&lt;p&gt;The JSON file approach is my favorite, for a couple of reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is easier than dealing with SVG string building, but can be combined with that approach easily (more details further below)&lt;/li&gt;
&lt;li&gt;The JSON file can be re-used, and even imported into runtime code (assuming they share access)!&lt;/li&gt;
&lt;li&gt;Usable by APIs and 3rd party services (including Shields.io for badges)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;json-file---getting-and-saving-the-sha&quot;&gt;JSON File - Getting and Saving the SHA&lt;/h4&gt;
&lt;p&gt;The implementation of this task could be accomplished in an endless number of ways, and the best option probably depends on the specifics of your build environment, project type, and operating system.&lt;/p&gt;
&lt;p&gt;For a generic example, here is how I how I have integrated this approach into a NodeJS based project, within a &lt;code&gt;package.json&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;scripts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;build-badge&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;$NETLIFY &amp;#x26;&amp;#x26; git rev-parse --short HEAD | xargs -I % printf &apos;{&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;sha&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}&apos; &gt; ./static/build-info.json&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;build&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;(yarn run build-badge || true) &amp;#x26;&amp;#x26; gatsby build&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The most important thing about the above command is that it modifies our build command, which used to be just &lt;code&gt;gatsby build&lt;/code&gt;, to include a step that saves the current SHA to a JSON file (&lt;code&gt;./static/build-info.json&lt;/code&gt;). If you wanted to, you could replace a lot of the bash stuff with a NodeJS or python script that does the same thing.&lt;/p&gt;
&lt;p&gt;If you want a breakdown of the above command, you can view it below&lt;/p&gt;
&lt;style&gt;details[open] .isClosed{display:none}details[open] .isOpen{display:inline!important}&lt;/style&gt;
&lt;details&gt;
	&lt;summary&gt;&lt;span class=&quot;isClosed&quot;&gt;Open Command Explanation&lt;/span&gt;&lt;span style=&quot;display:none;&quot; class=&quot;isOpen&quot;&gt;Close Command Explanation&lt;/span&gt;&lt;/summary&gt;
&lt;p&gt;To break this down further, the &lt;code&gt;build-badge&lt;/code&gt; command is composed of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$NETLIFY&lt;/code&gt; is an environmental variable that &lt;em&gt;only&lt;/em&gt; exists within my build environment (&lt;a href=&quot;https://docs.netlify.com/configure-builds/environment-variables/&quot;&gt;Netlify servers&lt;/a&gt;).
&lt;ul&gt;
&lt;li&gt;You could use any value that evaluates to true in your build system&lt;/li&gt;
&lt;li&gt;Or, if you always want this file generated regardless of local vs production, you could omit the whole &lt;code&gt;$VARIABLE &amp;#x26;&amp;#x26;&lt;/code&gt; part&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git rev-parse --short HEAD&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;This is a git command to get the &lt;em&gt;short&lt;/em&gt; version of the last commit SHA&lt;/li&gt;
&lt;li&gt;You could use the full SHA instead; just omit &lt;code&gt;--short&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;If you are looking for a lot of nifty git commands like this one, here is a shameless plug to check out &lt;a href=&quot;https://docs.joshuatz.com/cheatsheets/git/&quot;&gt;my Git Cheat Sheet&lt;/a&gt; (this command is on it!)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xargs -I % printf &apos;{\&quot;sha\&quot;:\&quot;%\&quot;}&apos;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Not going to get too much into this, but it is a fancy way of passing the SHA from the last section into a stringified JSON representation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&gt; ./static/build-info.json&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Save the generated JSON string to a file. This could be any file, and it doesn’t even necessarily need the &lt;code&gt;.json&lt;/code&gt; extension&lt;/li&gt;
&lt;li&gt;You should make sure that whatever directory you save it to is going to be accessible after the build is complete, either internally and/or externally (hosted)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And I’ve changed my build command from &lt;code&gt;gatsby build&lt;/code&gt; to &lt;code&gt;(yarn run build-badge || true) &amp;#x26;&amp;#x26; gatsby build&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is an easy way to make sure that &lt;code&gt;gatsby build&lt;/code&gt;, the true build command, always runs regardless of the success or failure of the &lt;code&gt;build-badge&lt;/code&gt; command.&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;p&gt;Again, as an alternative to a long bash command you could alternatively extract and save the SHA entirely in your favorite scripting language of choice (NodeJS, Python, etc.).&lt;/p&gt;
&lt;h4 id=&quot;json-file---using-the-stored-sha&quot;&gt;JSON File - Using the Stored SHA&lt;/h4&gt;
&lt;p&gt;As-is, once we have our JSON file, that alone is enough to be able to add badges or pull the SHA value into front-end code. For example, we can use &lt;a href=&quot;https://shields.io/#dynamic-badge&quot;&gt;the dynamic badge feature of Shields.io&lt;/a&gt; to easily get a SVG URL that displays our SHA. Something like:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;img src=&quot;https://img.shields.io/badge/dynamic/json?color=blue&amp;#x26;label=Deployed%20SHA&amp;#x26;query=sha&amp;#x26;url=https%3A%2F%2Fgit-sha-badges.netlify.app%2Fbuild-info.json&quot; alt=&quot;Deployed SHA&quot; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Notice the importance of:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt; - query=sha&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt; - url=HOSTED_SITE/build-info.json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://img.shields.io/badge/dynamic/json?color=blue&amp;#x26;label=Deployed%20SHA&amp;#x26;query=sha&amp;#x26;url=https%3A%2F%2Fgit-sha-badges.netlify.app%2Fbuild-info.json&quot; alt=&quot;Deployed SHA&quot;&gt;
&lt;p&gt;If you wanted to pull it into your code, in CommonJS, that is as easy as:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; buildInfo&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;BUILD_DIR&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/build-info.json`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(buildInfo.sha);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;svg-file&quot;&gt;SVG File&lt;/h3&gt;
&lt;p&gt;Rather than displaying your SHA in front-end code by pulling it in via AJAX or inside framework components (e.g. a React component), you could also pre-generate SVG badges at build time, with just a little bit of code.&lt;/p&gt;
&lt;p&gt;The easiest approach is to have 99% of the SVG badge ready-to-go, stored as a string. You can use whatever method you prefer for designing your badge. The main thing to do is leave room for the SHA to be inserted at build time, as a &lt;code&gt;&amp;#x3C;text&gt;&amp;#x3C;/text&gt;&lt;/code&gt; element inside the SVG.&lt;/p&gt;
&lt;p&gt;At build time, you combine your SVG template string with the latest SHA (this is where it is handy to have that &lt;code&gt;build-info.json&lt;/code&gt; already generated), and then write out the resulting string to an actual &lt;code&gt;.svg&lt;/code&gt; file, which can be served.&lt;/p&gt;
&lt;p&gt;You can checkout how I put this all together &lt;a href=&quot;https://github.com/joshuatz/git-sha-badges/blob/main/build.js&quot;&gt;in &lt;code&gt;build.js&lt;/code&gt; in my demo repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In pseudo code, this approach might look something like:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Compose full SVG string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; svgStr&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&amp;#x3C;svg&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; svgTemplateStr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&amp;#x3C;text&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sha &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&amp;#x3C;/text&gt;&amp;#x3C;/svg&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Save to SVG&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;saveFile&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(svgStr, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;my-badge.svg&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And don’t forget, you can get as fancy as you want with your generated SVGs 😄&lt;/p&gt;
&lt;img src=&quot;https://git-sha-badges.netlify.app/badge-fancy.svg&quot; style=&quot;width:95%;margin:auto;display:block;max-width:400px;&quot;&gt;
&lt;h2 id=&quot;bonus-github-commit-badges&quot;&gt;Bonus: Github Commit Badges&lt;/h2&gt;
&lt;p&gt;If all you want is a badge that shows the current HEAD of a particular branch in Github (i.e. the most recent &lt;em&gt;pushed&lt;/em&gt; commit) - you can actually do this entirely with just Shields.io and the public Github API (no credentials required).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Caveat: this requires that your repo is published as public&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;First, you need get the API endpoint that returns HEAD info, for your specific repo and branch combo. The syntax follows:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;https://api.github.com/repos/{USER}/{REPO}/git/refs/heads/{BRANCH}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, for example, to get the currently pushed tip for my &lt;code&gt;main&lt;/code&gt; branch in my demo repo for this post (&lt;code&gt;joshuatz/git-sha-badges&lt;/code&gt;), I can use &lt;a href=&quot;https://api.github.com/repos/joshuatz/git-sha-badges/git/refs/heads/main&quot;&gt;api.github.com/repos/joshuatz/git-sha-badges/git/refs/heads/main&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you try that URL, you will get a JSON response, including:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Other stuff&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;sha&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;____&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;commit&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we can plug that endpoint directly into Shield’s amazing &lt;a href=&quot;https://shields.io/#dynamic-badge&quot;&gt;dynamic badge&lt;/a&gt; generator, which accepts a JSON endpoint and lets us control badge settings through query string parameters. By tweaking the parameters, we can get an orange badge that displays the &lt;code&gt;object.sha&lt;/code&gt; value in the endpoint:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;API Endpoint:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  https://api.github.com/repos/joshuatz/git-sha-badges/git/refs/heads/main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Shield Badge Base Endpoint&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  https://img.shields.io/badge/dynamic/json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Shield Badge URL (raw)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  https://img.shields.io/badge/dynamic/json?color=orange&amp;#x26;label=Github SHA&amp;#x26;query=object.sha&amp;#x26;url=https://api.github.com/repos/joshuatz/git-sha-badges/git/refs/heads/main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Same URL, encoded:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  https://img.shields.io/badge/dynamic/json?color=orange&amp;#x26;label=Github%20SHA&amp;#x26;query=object.sha&amp;#x26;url=https%3A%2F%2Fapi.github.com%2Frepos%2Fjoshuatz%2Fgit-sha-badges%2Fgit%2Frefs%2Fheads%2Fmain&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Final badge code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Markdown:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ![Github SHA](https://img.shields.io/badge/dynamic/json?color=orange&amp;#x26;label=Github%20SHA&amp;#x26;query=object.sha&amp;#x26;url=https%3A%2F%2Fapi.github.com%2Frepos%2Fjoshuatz%2Fgit-sha-badges%2Fgit%2Frefs%2Fheads%2Fmain)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;HTML&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &amp;#x3C;img src=&quot;https://img.shields.io/badge/dynamic/json?color=orange&amp;#x26;label=Github%20SHA&amp;#x26;query=object.sha&amp;#x26;url=https%3A%2F%2Fapi.github.com%2Frepos%2Fjoshuatz%2Fgit-sha-badges%2Fgit%2Frefs%2Fheads%2Fmain&quot; alt=&quot;Github SHA&quot; /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, here it is (this is a live badge!):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.shields.io/badge/dynamic/json?color=orange&amp;#x26;label=Github%20SHA&amp;#x26;query=object.sha&amp;#x26;url=https%3A%2F%2Fapi.github.com%2Frepos%2Fjoshuatz%2Fgit-sha-badges%2Fgit%2Frefs%2Fheads%2Fmain&quot; alt=&quot;Github SHA&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;/h2&gt;
&lt;p&gt;I want to wrap up this post by pointing out that, although I gave some specific examples and code samples, the approaches throughout this post are fairly generic in nature and could be extended to solutions beyond just adding Git SHA badges.&lt;/p&gt;
&lt;p&gt;For example, if you maintain several build and deploy targets (e.g. &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;staging-alpha&lt;/code&gt;, &lt;code&gt;production&lt;/code&gt;, etc.), you could re-use most of the solutions through this post to add visual indicators to each environment that makes it clear what system you are looking at.&lt;/p&gt;
&lt;p&gt;Hope this post helps someone out there! (And kudos to anyone that understands and appreciates what my fancy SVG example is referencing).&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Rendering Non-Video Content in PiP - Experimental Project</title><link>https://joshuatz.com/projects/web-stuff/rendering-non-video-content-in-pip---experimental-project/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/rendering-non-video-content-in-pip---experimental-project/</guid><description>An experimental project that delves into the idea of rendering non-video content in Picture-in-Picture windows, use-cases, and a discussion of the future.</description><pubDate>Wed, 02 Dec 2020 20:30:45 GMT</pubDate><content:encoded>&lt;h2 id=&quot;what-is-it&quot;&gt;What Is It?&lt;/h2&gt;
&lt;p&gt;This was a project to explore creative ways to use the experimental Picture-in-Picture Web API to render non-video content in a floating always-on-top window. Eventually this ability might be offered natively by the browser (e.g. by PiP v2, or a different Web API), but I wanted to showcase some use-cases for why we should want it in the first place and the value it could bring.&lt;/p&gt;
&lt;p&gt;I believe that giving webpages the ability to draw content off-page, in windows they could control, opens up some amazing possibilities, and would be a step towards parity between native desktop apps and web apps. This project is an exploration of that idea.&lt;/p&gt;
&lt;h2 id=&quot;where-to-get-it&quot;&gt;Where to Get It&lt;/h2&gt;
&lt;p&gt;💾 Source Code: &lt;a href=&quot;https://github.com/joshuatz/pip-rendering-fun&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;🚀 Live Demos: &lt;a href=&quot;https://pip-rendering-fun.netlify.app/&quot;&gt;pip-rendering-fun.netlify.app&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;demo&quot;&gt;Demo&lt;/h2&gt;
&lt;p&gt;Here is a demo showing how it could be used to display real-time stats, in a persistent floating display:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://raw.githubusercontent.com/joshuatz/pip-rendering-fun/main/stats-demo.gif&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/joshuatz/pip-rendering-fun/main/stats-demo.gif&quot; alt=&quot;Screen capture showing a floating Picture-in-Picture window that updates with stats from a website analytics platform&quot; style=&quot;width:100%;max-width: 800px;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There are quite a few demos that I built out, including using PiP for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A time tracking application&lt;/li&gt;
&lt;li&gt;Displaying info about itself, through the web API&lt;/li&gt;
&lt;li&gt;Displaying the user’s own display / desktop&lt;/li&gt;
&lt;li&gt;Mirroring a drawable surface, in real-time&lt;/li&gt;
&lt;li&gt;The analytics / stats demo, as shown above&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To see all the demos above, and try them for yourself if you have a compatible browser, they are deployed at &lt;a href=&quot;https://pip-rendering-fun.netlify.app/&quot;&gt;pip-rendering-fun.netlify.app&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-i-used&quot;&gt;What I Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Web APIs
&lt;ul&gt;
&lt;li&gt;HTML Canvas&lt;/li&gt;
&lt;li&gt;MediaSource&lt;/li&gt;
&lt;li&gt;PictureInPictureWindow&lt;/li&gt;
&lt;li&gt;MediaDevices&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Netlify (deploying the demos)&lt;/li&gt;
&lt;li&gt;Documentation! (Both consuming and writing my own)&lt;/li&gt;
&lt;li&gt;Markdown&lt;/li&gt;
&lt;li&gt;Vanilla HTML and JavaScript&lt;/li&gt;
&lt;li&gt;Browser “quirks”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A lot of the web APIs I used are considering “experimental”; in fact, I had to use TypeScript’s ambient declaration support to augment the &lt;code&gt;Document&lt;/code&gt; interface, since the types for those APIs have not yet made it into the  &lt;code&gt;lib.dom.d.ts&lt;/code&gt; that ships with TypeScript!&lt;/p&gt;
&lt;p&gt;This was also a great opportunity to learn more about web standards, and how different browsers end up implementing the same specification (surprise, surprise - it is not always the same!). I also spent a fair amount of time writing my own documentation on the topic; covering some of the issues I ran into, discrepancies in how APIs were implemented, and browser quirks.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Investigating Stalled Video Segments in MediaSource Buffers</title><link>https://joshuatz.com/posts/2020/investigating-stalled-video-segments-in-mediasource-buffers/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/investigating-stalled-video-segments-in-mediasource-buffers/</guid><description>Various ways to investigate stalled content attached to a MediaSource instance, and different causes that can be evaluated and tested.</description><pubDate>Fri, 27 Nov 2020 18:00:18 GMT</pubDate><content:encoded>&lt;p&gt;I just finished finding the culprit to a very vexing problem, and I want to share my findings in the hope it will help someone.&lt;/p&gt;
&lt;p&gt;I’ve been working on constructing an HTML video dynamically, using MediaSource APIs / MSE and the methods outlined in &lt;a href=&quot;https://joshuatz.com/posts/2020/appending-videos-in-javascript-with-mediasource-buffers/&quot;&gt;my recent post on MediaSource approaches&lt;/a&gt;. I’m trying to build a seek-able video, out of distinct WEBM files, as opposed to byte chunks out of stream. Loading the files directly via &lt;code&gt;src=file.webm&lt;/code&gt; or one at time with MediaSource was working fine, but appending multiple segments together was triggering some bizarre behavior.&lt;/p&gt;
&lt;h2 id=&quot;issue&quot;&gt;Issue&lt;/h2&gt;
&lt;p&gt;After loading multiple segments via MediaSource’s &lt;code&gt;SourceBuffer.appendBuffer()&lt;/code&gt; and closing off the stream (&lt;code&gt;MediaSource.endOfStream()&lt;/code&gt;), there would be no fatal errors in the console, but the video element would exhibit the following behavior (not always all at the same time):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Player element would let me press play, but then would “stall” - time does not move forward, and UI shows video as “playing”, but it clearly is not
&lt;ul&gt;
&lt;li&gt;Sometimes it would play for a very brief amount (milliseconds) before stalling&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Player element would be seek-able, and I could scrub through timeline with correct preview. However, manually scrubbing position and then trying to resume playback would still result in stalled state&lt;/li&gt;
&lt;li&gt;Occasionally I would get the well known &lt;code&gt;play() request was interrupted by a call to pause()&lt;/code&gt; error, but this would be despite any calls pause()
&lt;ul&gt;
&lt;li&gt;I think this was actually caused by the stalled buffer &lt;em&gt;internally&lt;/em&gt; (not my code) requesting a pause to fire. This seems to be backed up &lt;a href=&quot;https://discourse.wicg.io/t/proposal-hint-attribute-on-htmlmediaelement-to-configure-rendering-latency/3567/9&quot;&gt;by this comment&lt;/a&gt;: “Say you have audio+video in a single &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; tag. If just audio underflows, chrome will immediately pause playback”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These issues occurred across both Chromium and Firefox, but again, not always together or consistently.&lt;/p&gt;
&lt;h2 id=&quot;investigating&quot;&gt;Investigating&lt;/h2&gt;
&lt;p&gt;I’ll admit it; my first step at investigation did not get me very far. I tried a bunch of trial-and-error, searching through docs on MediaSource, and switching between the &lt;code&gt;sequence&lt;/code&gt; mode of &lt;code&gt;SourceBuffer&lt;/code&gt; and &lt;code&gt;segments&lt;/code&gt; mode. Nothing helped.&lt;/p&gt;
&lt;h3 id=&quot;chrome-media-inspector&quot;&gt;Chrome Media Inspector&lt;/h3&gt;
&lt;p&gt;I next stumbled across an article that mentioned that Chrome &lt;a href=&quot;https://developers.google.com/web/tools/chrome-devtools/media-panel&quot;&gt;has a “Media” inspector tab in DevTools&lt;/a&gt;. Holy smokes is this thing awesome! Firing up the Media inspector and running my code again gave me much better insight into what to focus my investigation on: audio buffering.&lt;/p&gt;
&lt;p&gt;Looking at the timeline view, this is what I pretty consistently saw when the video stalled:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;/media/Chrome-Dev-Tools-Media-Inspector-Stalled-Audio-Buffering.gif&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;Chrome Dev Tools - Media Inspector - Stalled Audio Buffering&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;Uh… that’s not good 😰. Let’s look at some of the events that are firing when it starts stalling:&lt;/p&gt;
&lt;p&gt;kBufferingStateChanged&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;event &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	audio_buffering_state: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		reason: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;DEMUXER_UNDERFLOW&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		state: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;BUFFERING_HAVE_NOTHING&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;kBufferingStateChanged&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;event &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	pipeline_buffering_state: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		for_suspended_start: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		reason: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;DEMUXER_UNDERFLOW&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		state: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;BUFFERING_HAVE_NOTHING&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hmm. “DEMUXER_UNDERFLOW” doesn’t exactly have a nice sound to it… Also, a message like this would occasionally show up in the “Messages” panel:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Skipping audio splice trimming at PTS=11626999us. Found only 1us of overlap, need at least 1000us. Multiple occurrences may result in loss of A/V sync.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That definitely seems related, and might also point to an audio buffer issue. What’s the deal?&lt;/p&gt;
&lt;p&gt;To make this more confusing, there was a lot of things I could rule out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Buffer / Memory overflow / auto eviction: SourceBuffer is not an endless tube you can fill; it is more like a temporary reservoir. As such &lt;a href=&quot;https://developers.google.com/web/updates/2015/06/Media-Source-Extensions-for-Audio&quot;&gt;it has limits and is subject to garbage collection&lt;/a&gt;. However, I could rule this out because my tests were using very small files, and the overall buffer should have been &lt;em&gt;well&lt;/em&gt; below the 12 MB audio / 150 MB video limit.&lt;/li&gt;
&lt;li&gt;Mismatched codecs: Although I couldn’t rule out issues with my source files (more on this shortly), I could rule out that I was maybe using codecs that the browser did not support, as:
&lt;ul&gt;
&lt;li&gt;There were no errors to that effect&lt;/li&gt;
&lt;li&gt;Calling &lt;code&gt;MediaSource.isTypeSupported(mimeStr)&lt;/code&gt; showed support&lt;/li&gt;
&lt;li&gt;My codecs matched the MSE specification / WEBM specs&lt;/li&gt;
&lt;li&gt;Video playback worked just fine if videos were loaded directly, bypassing combined appendBuffer calls&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Network issue
&lt;ul&gt;
&lt;li&gt;Although there were no indicators of issues with &lt;code&gt;fetch()&lt;/code&gt; or any network requests, to be safe I even was trying loading videos directly via base64 strings in the JS. Same issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To rule out something wrong with my actual JavaScript code that was doing the loading (no errors in console does not rule this out), I swapped out my source files for some others that are commonly used throughout the web as WEBM samples. Issues immediately went away, signaling that this was indeed an issue with my source files.&lt;/p&gt;
&lt;p&gt;At this point though, that still didn’t clear up a huge amount. These files played just fine in standard media players, and to reiterate, also played just fine in &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; elements when loaded &lt;em&gt;one at a time&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&quot;checking-files-with-ffmpeg&quot;&gt;Checking Files with FFMPEG&lt;/h3&gt;
&lt;p&gt;I should have tried this from the very start, but at this point I realized I absolutely needed to inspect the video files themselves. Although I had never done this kind of thing before (other than viewing what VLC provides in their info popup, and that sort of thing), &lt;a href=&quot;https://ffmpeg.org/&quot;&gt;FFMPEG&lt;/a&gt; makes it super easy to do, with the &lt;code&gt;-i&lt;/code&gt; inspect flag, or with &lt;a href=&quot;https://ffmpeg.org/ffprobe.html&quot;&gt;the ffprobe tool&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To inspect a file, use:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ffmpeg&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -i&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; FILE_NAME&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Or, with ffprobe:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ffprobe&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; FILE_NAME&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;For more advanced file inspection, check out &lt;a href=&quot;https://trac.ffmpeg.org/wiki/FFprobeTips&quot;&gt;ffprobe tips&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When I inspected my source files, I saw something reallllly funky:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Duration: 00:00:01.65, start: -0.001000, bitrate: 22 kb/s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Stream #0:0: Video: vp9 (Profile 0), yuv420p(tv, bt709/unknown/bt709), 144x96, SAR 1:1 DAR 3:2, 10 fps, 10 tbr, 1k tbn, 1k tbc (default)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	Metadata:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;		DURATION        : 00:00:00.207000000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Stream #0:1: Audio: opus, 48000 Hz, mono, fltp (default)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	Metadata:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;		DURATION        : 00:00:01.648000000&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, not only is it a little strange that each stream has its own duration metadata (I don’t &lt;em&gt;think&lt;/em&gt; thats required), but it is &lt;em&gt;&lt;strong&gt;alarming&lt;/strong&gt;&lt;/em&gt; that the duration for the video segment completely does not match up with the audio! &lt;a href=&quot;https://superuser.com/a/1440888/1103513&quot;&gt;It sounds like these numbers don’t have to match exactly&lt;/a&gt;, but those numbers are so wildly off it seems suspect.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: A negative “start time” is actually an &lt;a href=&quot;https://www.metadata2go.com/file-info/start-time&quot;&gt;OK thing&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;fix&quot;&gt;Fix&lt;/h2&gt;
&lt;p&gt;I first tried the command suggested &lt;a href=&quot;https://superuser.com/a/1027311/1103513&quot;&gt;here&lt;/a&gt;, to patch the duration with FFMPEG:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ffmpeg -i broken.webm -c copy -fflags +genpts fixed.webm&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, that didn’t work.&lt;/p&gt;
&lt;p&gt;At this point, it seemed likely that the file was just seriously malformed. If there are chunks of missing data or corrupted bytes, that is not something FFMPEG copying is likely going to be able to fix.&lt;/p&gt;
&lt;p&gt;The file was created with a video editor (&lt;a href=&quot;https://shotcut.org/&quot;&gt;Shotcut&lt;/a&gt;), which has a lot of control over video output encoding, but rather than tweak every setting under the sun, my first thought was to update the program to the latest version. That… actually ended up fixing the issue. My guess is internally they upgraded their bundled version of the VP9 (or Opus) encoder, or how they were using it, and this in turn resulted in a “more valid” export file for my project.&lt;/p&gt;
&lt;p&gt;Here is a sample of the inspection of the “fixed” file:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Duration: 00:00:01.71, start: -0.001000, bitrate: 10 kb/s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Stream #0:0: Video: vp9 (Profile 0), yuv420p(tv, bt709), 144x96, SAR 1:1 DAR 3:2, 10 fps, 10 tbr, 1k tbn, 1k tbc (default)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	Metadata:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;		DURATION        : 00:00:01.707000000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Stream #0:1: Audio: opus, 48000 Hz, mono, fltp (default)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	Metadata:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;		DURATION        : 00:00:01.708000000&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;other-remarks&quot;&gt;Other Remarks&lt;/h3&gt;
&lt;p&gt;I also want to point out that in my previous MediaSource post and set of demos, I ran into a variant of this issue that only occurred in Firefox, and ffprobe results looked fine. The issue was specifically around multi-file appends, and I ended up just switching all my input files from &lt;code&gt;VP9&lt;/code&gt; to &lt;code&gt;VP8&lt;/code&gt;, which seemed to resolve the issue.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Appending Videos in JavaScript with MediaSource Buffers</title><link>https://joshuatz.com/posts/2020/appending-videos-in-javascript-with-mediasource-buffers/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/appending-videos-in-javascript-with-mediasource-buffers/</guid><description>Exploring different ways to append videos to a MediaSource instance with SourceBuffers and appendBuffer calls. Includes fully functional examples and tips.</description><pubDate>Wed, 25 Nov 2020 16:56:29 GMT</pubDate><content:encoded>&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#intro&quot;&gt;Intro&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#link-to-all-code-demos&quot;&gt;Link to All Code Demos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#getting-dummy-data-ready&quot;&gt;Getting Dummy Data Ready&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#loading-pre-generated-dummy-data&quot;&gt;Loading Pre-Generated Dummy Data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#general-approach&quot;&gt;General Approach&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#multi-file-support&quot;&gt;Multi-File Support&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#source-buffer-modes-segments-vs-sequence&quot;&gt;Source Buffer Modes: Segments vs Sequence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#better-option-streaming-formats&quot;&gt;Better Option: Streaming Formats&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#research-and-faq&quot;&gt;Research and FAQ&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#existing-tutorials-and-examples&quot;&gt;Existing Tutorials and Examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#issues-i-ran-into&quot;&gt;Issues I Ran Into&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;In &lt;a href=&quot;https://joshuatz.com/posts/2020/using-binary-data-with-front-end-javascript-and-the-web/&quot;&gt;my previous post about using raw data in JavaScript&lt;/a&gt;, I alluded to the concept of dynamically loading data into video elements using the newer &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API&quot;&gt;Media Source Extensions API&lt;/a&gt; (aka &lt;em&gt;MSE&lt;/em&gt;) and its collection of interfaces, buffers, and sources.&lt;/p&gt;
&lt;p&gt;I just finished brute-forcing my way through some of the basics (and I do mean &lt;em&gt;basics&lt;/em&gt;) of MSE, and wanted to share my findings on dynamically loading data into videos, specifically by using the &lt;code&gt;MediaSource&lt;/code&gt; interface, SourceBuffer, and the &lt;code&gt;appendBuffer&lt;/code&gt; method. MSE is a really complicated topic, and distinctly separate from my normal area of programming, so my examples are going to be a lot more basic level than what I normally post. Codecs and math-heavy coding are just not my bread-and-butter 🤷‍♂️.&lt;/p&gt;
&lt;p&gt;Anyways, I’m posting this because although my examples are simple, there is not nearly enough info out there on the web on how to even start with MSE / MediaSource. I’m hoping this helps someone.&lt;/p&gt;
&lt;h3 id=&quot;link-to-all-code-demos&quot;&gt;Link to All Code Demos&lt;/h3&gt;
&lt;p&gt;I have coded several fully-functional and well-commented examples that use the approaches from this post to dynamically load video. These will be discussed further on, but if you want to jump right to the source code, you can find it at:&lt;/p&gt;
&lt;div style=&quot;text-align:center; width:100%; font-size:1.4rem;&quot;&gt;
	&lt;span aria-hidden=&quot;true&quot;&gt;🔗&lt;/span&gt;&lt;a href=&quot;https://github.com/joshuatz/mediasource-append-examples&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;github.com/joshuatz/mediasource-append-examples&lt;/a&gt;
&lt;/div&gt;
&lt;h2 id=&quot;getting-dummy-data-ready&quot;&gt;Getting Dummy Data Ready&lt;/h2&gt;
&lt;p&gt;First thing; I need some video clips that are going to work with MediaSource (see &lt;a href=&quot;#research-and-faq&quot;&gt;“Research” section&lt;/a&gt; for more info on file formats). For my demo, I’m going to stick with Webm as the container, and &lt;a href=&quot;https://en.wikipedia.org/wiki/VP9&quot;&gt;VP9&lt;/a&gt; as the codec. I could use &lt;a href=&quot;https://ffmpeg.org/&quot;&gt;FFMPEG&lt;/a&gt; to convert videos I want to use to the right format directly, but in my case, I used &lt;a href=&quot;https://www.shotcut.org/&quot;&gt;Shotcut&lt;/a&gt; to handle both the trimming and conversion process.&lt;/p&gt;
&lt;p&gt;If you don’t have your own files to work with, I compiled &lt;a href=&quot;https://github.com/joshuatz/video-test-file-links&quot;&gt;a list of resources I found as I worked on this&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 I was a little lost on the state of video support across different browsers, until I found this awesome writeup by &lt;em&gt;@Vestride&lt;/em&gt;: &lt;a href=&quot;https://gist.github.com/Vestride/278e13915894821e1d6f&quot;&gt;Encoding Video for the Web&lt;/a&gt;. Mozilla also has a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs&quot;&gt;great guide&lt;/a&gt;. General idea is AVC-H.264/MP4+AAC offers widest compatibility, but is commercially licensed, whereas a better option with less support is VP8 or VP9 with Opus audio.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;loading-pre-generated-dummy-data&quot;&gt;Loading Pre-Generated Dummy Data&lt;/h3&gt;
&lt;p&gt;I know that I’m going to need the video files that I want to load &lt;em&gt;accessible&lt;/em&gt; from within my JavaScript code. This leaves me with two main options.&lt;/p&gt;
&lt;p&gt;The first is pretty standard. Upload the video clips somewhere (or find already hosted clips), and from JavaScript, use &lt;code&gt;fetch()&lt;/code&gt; to retrieve them via network request and get the binary blob.&lt;/p&gt;
&lt;p&gt;The other approach is something I want to do for fun, and to reuse some concepts from &lt;a href=&quot;https://joshuatz.com/posts/2020/using-binary-data-with-front-end-javascript-and-the-web/&quot;&gt;my last post on binary formats&lt;/a&gt;. I want to store my video clips, directly in JavaScript!&lt;/p&gt;
&lt;p&gt;To do this, first we need to get the binary data into a “stringified” format. You can’t just open a video file with Notepad and copy and paste the text into a JS file. However, Base64 encoding is a fast and easy way to store binary data:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;base64&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --wrap=0&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; sample_vid.webm&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; sample_vid-base64.txt&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;If you don’t have access to a local base64 converter, you can also use an &lt;a href=&quot;https://base64.guru/converter/encode/file&quot;&gt;online converter tool&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, I can store the base64 string directly in JS:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; vidClip&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; `GkXfo59...`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, when I’m ready to append the clip into a source buffer with MediaSource, I’m going to have to convert it back into a binary blob (more on this later).&lt;/p&gt;
&lt;h2 id=&quot;general-approach&quot;&gt;General Approach&lt;/h2&gt;
&lt;p&gt;For dynamically loading raw video data into a &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; element using the MediaSource API, the primary method we need to be focused on is &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer&quot;&gt;the &lt;code&gt;sourceBuffer.appendBuffer(buffer)&lt;/code&gt; method&lt;/a&gt;. This takes a chunk of raw data (as an ArrayBuffer) and appends it to an existing &lt;code&gt;SourceBuffer&lt;/code&gt; instance.&lt;/p&gt;
&lt;p&gt;However, to actually get to the point where we can use this to append to a video, there is a bunch of setup necessary, since our &lt;code&gt;SourceBuffer&lt;/code&gt; should belong to a &lt;code&gt;MediaSource&lt;/code&gt; instance, which in turn should be connected to a &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; through an Object URL.&lt;/p&gt;
&lt;p&gt;The general steps to put it all together look something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get a reference to a &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; element (in the DOM)&lt;/li&gt;
&lt;li&gt;Create a new instance of the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaSource&quot;&gt;MediaSource interface&lt;/a&gt; (&lt;code&gt;const mediaSource = new MediaSource()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Create an Object URL that points to that raw source. Point the src attribute of the video element to it
&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;videoElem.src = URL.createObjectURL(mediaSource)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Attach a listener to the mediaSource for the &lt;code&gt;sourceopen&lt;/code&gt; event (or define as the callback property &lt;code&gt;onsourceopen&lt;/code&gt;)
&lt;ul&gt;
&lt;li&gt;We need this to fire &lt;em&gt;before&lt;/em&gt; we can attach a buffer&lt;/li&gt;
&lt;li&gt;You can also check &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/readyState&quot;&gt;the &lt;code&gt;mediaSource.readyState&lt;/code&gt; property&lt;/a&gt;, to see if it &lt;code&gt;== &apos;open&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Once &lt;code&gt;sourceopen&lt;/code&gt; has fired, create an instance of the &lt;code&gt;SourceBuffer&lt;/code&gt; interface, which will hold data, and attach to to mediaSource with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/addSourceBuffer&quot;&gt;the &lt;code&gt;addSourceBuffer&lt;/code&gt; method&lt;/a&gt;:
&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;const sourceBuffer = mediaSource.addSourceBuffer(&apos;video/webm; codecs=&quot;vp9,opus&quot;&apos;);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Now that we have the buffer, we can start loading data with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer&quot;&gt;the &lt;code&gt;sourceBuffer.appendBuffer(arrayBuffer)&lt;/code&gt; method&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Every time that we append a buffer, we also need to listen for the &lt;code&gt;updateend&lt;/code&gt; event that will fire after the browser is done with the operation
&lt;ul&gt;
&lt;li&gt;If we have more chunks to append, we can start a new append operation, and repeat the listener cycle over again&lt;/li&gt;
&lt;li&gt;If we don’t have any more chunks, we need to close out / signal the end of the stream with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/endOfStream&quot;&gt;the &lt;code&gt;mediaSource.endOfStream()&lt;/code&gt; method&lt;/a&gt;. We also might want to call &lt;code&gt;videoElement.play()&lt;/code&gt; at this point, if the video is not set to autoplay (note: video must be muted or else this will fail due to autoplay rules).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;margin-left: 20px; font-size:1.2rem;&quot;&gt;
	&lt;span aria-hidden=&quot;true&quot;&gt;🔗&lt;/span&gt; - These exact steps are used in &lt;a href=&quot;https://github.com/joshuatz/mediasource-append-examples/blob/main/standard/index.js&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;my &quot;Standard&quot; approach example&lt;/a&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-jptremote=&quot;https://github.com/joshuatz/mediasource-append-examples/blob/main/standard/index.js&quot;&gt;Loading...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;multi-file-support&quot;&gt;Multi-File Support&lt;/h2&gt;
&lt;p&gt;In discussing multi-file support, it is important to make a clear distinction between multi-file video loading vs streamed “chunks”, as these often get confused. A single video file (&lt;code&gt;sample.webm&lt;/code&gt;) can be split into multiple chunks (e.g. ArrayBuffers), but you can also have multiple video files (&lt;code&gt;sample_a.webm&lt;/code&gt;, &lt;code&gt;sample_b.webm&lt;/code&gt;), which each get their own buffer (and/or are split into even smaller chunks for each file).&lt;/p&gt;
&lt;p&gt;You can use AppendBuffer for &lt;em&gt;both&lt;/em&gt; data chunks of a single video file, or entire disparate video files.&lt;/p&gt;
&lt;p&gt;This section is going to be discussing &lt;em&gt;multi-file&lt;/em&gt; appends, as opposed to single-file chunk appends, as there are some special caveats that apply when appending completely separate files.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For chunks of &lt;em&gt;the same file&lt;/em&gt;, the standard approach above for loading buffers should work just fine (as a basic example).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;source-buffer-modes-segments-vs-sequence&quot;&gt;Source Buffer Modes: Segments vs Sequence&lt;/h3&gt;
&lt;p&gt;One of the primary caveats that apply to multi-file appends is how they are handled depending on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/mode&quot;&gt;the sourceBuffer.mode property&lt;/a&gt; (&lt;a href=&quot;https://w3c.github.io/media-source/#webidl-x723564574&quot;&gt;spec&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;sequence&lt;/code&gt; mode, you are forcing new appends to be treated as adjacent to the previous, regardless of timestamps held within the files themselves. This works to our advantage with multiple file appends, but would be bad if we were appending chunks in a &lt;em&gt;random&lt;/em&gt; order and relying on their internal timestamps for placement.&lt;/p&gt;
&lt;div style=&quot;margin-left: 20px; font-size:1.2rem;&quot;&gt;
	&lt;span aria-hidden=&quot;true&quot;&gt;🔗&lt;/span&gt; - &lt;a href=&quot;https://github.com/joshuatz/mediasource-append-examples/blob/main/multi-file/sequence/index.js&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Sequence Mode Example&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;In &lt;code&gt;sequence&lt;/code&gt; mode, you will also see this warning in Chrome when using it with multiple files:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Warning: using MSE ‘sequence’ AppendMode for a SourceBuffer with multiple tracks may cause loss of track synchronization. In some cases, buffered range gaps and playback stalls can occur. It is recommended to instead use ‘segments’ mode for a multitrack SourceBuffer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In addition to the likelihood of sync bugs that the above warning is pointing out, there is also a chance that support for using &lt;code&gt;sequence&lt;/code&gt; mode with multiple files &lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=820489#c3&quot;&gt;might be deprecated in Chrome&lt;/a&gt;. Their recommendation would be to switch to &lt;code&gt;segments&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;segments&lt;/code&gt; mode, the &lt;em&gt;internal&lt;/em&gt; timestamps of the content of the buffer determine placement; this means that you can append chunks of a video in any order you want, and as long as they have &lt;a href=&quot;https://w3c.github.io/media-source/#coded-frame&quot;&gt;“coded frames”&lt;/a&gt; with timestamps. This is great for handling chunks passed over a network connection, as you cannot guarantee the order in which they will be returned from the server. &lt;em&gt;&lt;strong&gt;However&lt;/strong&gt;&lt;/em&gt;, this works to our &lt;em&gt;&lt;strong&gt;disadvantage&lt;/strong&gt;&lt;/em&gt; for multiple-file appends, as relying on internal timestamps for placement makes no sense if appending multiple files as chunks. The files do not know about each other when they are encoded (why would they?), so their timestamps only describe their own chunks, not their relation to other files on the timeline. In practice, this usually means that, without tweaking your code, if you append multiple files with mode set to &lt;code&gt;segments&lt;/code&gt;, they will overwrite each other in the buffer, and you end up with only one video getting played back.&lt;/p&gt;
&lt;p&gt;So, to use &lt;code&gt;segments&lt;/code&gt; mode with multiple files, you need to manage the timing offset between file appends &lt;em&gt;yourself&lt;/em&gt;. My demo, &lt;a href=&quot;https://github.com/joshuatz/mediasource-append-examples/blob/main/multi-file/segments/index.js&quot;&gt;here&lt;/a&gt;, shows this in action, using &lt;code&gt;sourceBuffer.timestampOffset&lt;/code&gt; to move the pointer to where the next append call will place data.&lt;/p&gt;
&lt;div style=&quot;margin-left: 20px; font-size:1.2rem;&quot;&gt;
	&lt;span aria-hidden=&quot;true&quot;&gt;🔗&lt;/span&gt; - &lt;a href=&quot;https://github.com/joshuatz/mediasource-append-examples/blob/main/multi-file/segments/index.js&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Segments Mode Example&lt;/a&gt;
&lt;/div&gt;
&lt;h3 id=&quot;better-option-streaming-formats&quot;&gt;Better Option: Streaming Formats&lt;/h3&gt;
&lt;p&gt;For live video feeds, or just a really robust video loading approach that can handle things like adaptive bitrate streaming, resolution switching, etc. - you probably want to switch to using a streaming video format paired with a well-supported player component.&lt;/p&gt;
&lt;p&gt;The two most popular streaming formats are &lt;a href=&quot;https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP&quot;&gt;DASH&lt;/a&gt; (aka &lt;em&gt;MPEG-DASH&lt;/em&gt;, &lt;em&gt;Dynamic Adaptive Streaming over HTTP&lt;/em&gt;) and &lt;a href=&quot;https://en.wikipedia.org/wiki/HTTP_Live_Streaming&quot;&gt;HLS&lt;/a&gt; (&lt;em&gt;HTTP Live Streaming&lt;/em&gt;). There are tons of differences between these two formats, better explained by those more qualified to do so than myself. However, the general gist is that DASH is newer, but gaining in native adoption and power, whereas HLS is older and not growing as fast, but has a lot of legacy support.&lt;/p&gt;
&lt;p&gt;These are some good starting places for learning more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MDN: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/Live_streaming_web_audio_and_video&quot;&gt;&lt;em&gt;Live Streaming Web Audio and Video&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cloudflare: &lt;a href=&quot;https://www.cloudflare.com/learning/video/what-is-mpeg-dash/&quot;&gt;&lt;em&gt;HLS vs DASH&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Eleven-Labs: &lt;a href=&quot;https://blog.eleven-labs.com/en/video-live-dash-hls/&quot;&gt;&lt;em&gt;DASH &amp;#x26; HLS&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In general, these streaming formats are complicated to implement by hand, so you would want to use them with a well-known and supported player or library, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multi-format players, with extra features:
&lt;ul&gt;
&lt;li&gt;Google’s &lt;a href=&quot;https://github.com/google/shaka-player&quot;&gt;Shaka Player&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Supports both DASH and HLS, plus tons of other features&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bitmovin.com/video-player&quot;&gt;Bitmovin Player&lt;/a&gt; (Commercial)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Format specific
&lt;ul&gt;
&lt;li&gt;Video-Dev: &lt;a href=&quot;https://github.com/video-dev/hls.js/&quot;&gt;hls.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Dash Industry Forum: &lt;a href=&quot;https://github.com/Dash-Industry-Forum/dash.js&quot;&gt;dash.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;There are also paid platforms out there that handle even &lt;em&gt;more&lt;/em&gt; of the process, such as automatic transcoding of a single high-resolution input file into multiple bitrates and resolutions, and player embed code generation&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;research-and-faq&quot;&gt;Research and FAQ&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;These are notes that I jotted down while learning about MSE and trying to build out my example demos.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Important questions&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MediaSource vs MediaStream
&lt;ul&gt;
&lt;li&gt;Good answer: &lt;a href=&quot;https://stackoverflow.com/questions/51843518/mediasource-vs-mediastream-in-javascript&quot;&gt;https://stackoverflow.com/questions/51843518/mediasource-vs-mediastream-in-javascript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Specs to know about
&lt;ul&gt;
&lt;li&gt;w3c/media-source aka MSE (Media Source Extensions API)
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/w3c/media-source&quot;&gt;github.com/w3c/media-source&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.chromestatus.com/feature/4563797888991232&quot;&gt;https://www.chromestatus.com/feature/4563797888991232&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/w3c/media-source&quot;&gt;w3c/media-source&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;What can make up a MediaSource stream? What MIME types?
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://w3c.github.io/media-source/byte-stream-format-registry.html&quot;&gt;https://w3c.github.io/media-source/byte-stream-format-registry.html&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;For video
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;video/webm&lt;/code&gt; (&lt;a href=&quot;https://w3c.github.io/media-source/webm-byte-stream-format.html&quot;&gt;https://w3c.github.io/media-source/webm-byte-stream-format.html&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vorbis&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opus&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vp8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vp9&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vp90...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;video/mp4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;video/mp2t&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;What does the MediaSource pipeline look like?
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://w3c.github.io/media-source/pipeline_model.svg&quot;&gt;https://w3c.github.io/media-source/pipeline_model.svg&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;How do you format the data that you feed into the buffer (e.g. through &lt;code&gt;appendBuffer()&lt;/code&gt;)?
&lt;ul&gt;
&lt;li&gt;See: &lt;a href=&quot;https://w3c.github.io/media-source/index.html#byte-stream-formats&quot;&gt;https://w3c.github.io/media-source/index.html#byte-stream-formats&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;How does WEBM fit into adaptive streaming?
&lt;ul&gt;
&lt;li&gt;See: &lt;a href=&quot;http://wiki.webmproject.org/adaptive-streaming&quot;&gt;http://wiki.webmproject.org/adaptive-streaming&lt;/a&gt;. It is often used with MPEG-DASH.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;existing-tutorials-and-examples&quot;&gt;Existing Tutorials and Examples&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://reference.codeproject.com/book/dom/mediasource/addsourcebuffer&quot;&gt;https://reference.codeproject.com/book/dom/mediasource/addsourcebuffer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/google-developers/use-video-loops-with-interactive-canvas-dc7503e95c6a&quot;&gt;https://medium.com/google-developers/use-video-loops-with-interactive-canvas-dc7503e95c6a&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/web/updates/2011/11/Stream-video-using-the-MediaSource-API&quot;&gt;https://developers.google.com/web/updates/2011/11/Stream-video-using-the-MediaSource-API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nickdesaulniers/netfix/blob/gh-pages/demo/bufferAll.html&quot;&gt;https://github.com/nickdesaulniers/netfix/blob/gh-pages/demo/bufferAll.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://webplatform.github.io/docs/apis/media_source_extensions/MediaSource/addSourceBuffer/&quot;&gt;https://webplatform.github.io/docs/apis/media_source_extensions/MediaSource/addSourceBuffer/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/web/updates/2016/03/mse-sourcebuffer&quot;&gt;https://developers.google.com/web/updates/2016/03/mse-sourcebuffer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ioncannon.net/utilities/1515/segmenting-webm-video-and-the-mediasource-api/&quot;&gt;https://ioncannon.net/utilities/1515/segmenting-webm-video-and-the-mediasource-api/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/samdutton/simpl/blob/gh-pages/mse/js/main.js&quot;&gt;https://github.com/samdutton/simpl/blob/gh-pages/mse/js/main.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/guest271314/MediaFragmentRecorder/blob/master/MediaFragmentRecorder.html&quot;&gt;https://github.com/guest271314/MediaFragmentRecorder/blob/master/MediaFragmentRecorder.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/w3c/media-source/issues/190&quot;&gt;https://github.com/w3c/media-source/issues/190&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jsfiddle.net/hcfvyx9k/1/&quot;&gt;https://jsfiddle.net/hcfvyx9k/1/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://plnkr.co/edit/9FYe4cJ6d4BC0B0LyOmN?p=preview&amp;#x26;preview&quot;&gt;http://plnkr.co/edit/9FYe4cJ6d4BC0B0LyOmN?p=preview&amp;#x26;preview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/video-dev/hls.js/blob/70fae539bb1da32403a4d505cb42d18e95bda106/src/controller/buffer-controller.ts&quot;&gt;https://github.com/video-dev/hls.js/blob/70fae539bb1da32403a4d505cb42d18e95bda106/src/controller/buffer-controller.ts&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Good example of the complexities involved!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Melatonin64/gapless-mse-audio/blob/master/main.js&quot;&gt;https://github.com/Melatonin64/gapless-mse-audio/blob/master/main.js&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;This is for appending audio, but uses some interesting approaches&lt;/li&gt;
&lt;li&gt;Also see discussion &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1222851&quot;&gt;on related bug report&lt;/a&gt; (closed)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;issues-i-ran-into&quot;&gt;Issues I Ran Into&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Uncaught DOMException: Failed to execute &apos;appendBuffer&apos; on &apos;SourceBuffer&apos;: This SourceBuffer has been removed from the parent media source&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;I ran into this error in Chromium, but not in Firefox. It can very often (and in my case too) be traced to mis-matched codecs (Firefox was more forgiving then Chromium in this instance)&lt;/li&gt;
&lt;li&gt;For example, when I saw this error, I ended up going to the &lt;code&gt;Media&lt;/code&gt; tab of dev tools in Chrome, and then the player I was interested in and then &lt;code&gt;Messages&lt;/code&gt;; I finally saw: &lt;code&gt;Audio stream codec opus doesn&apos;t match SourceBuffer codecs.&lt;/code&gt;! Aha!
&lt;ul&gt;
&lt;li&gt;In this case, the buffer I was trying to append did &lt;em&gt;&lt;strong&gt;correctly&lt;/strong&gt;&lt;/em&gt; use the opus codec, and opus is a supported audio type to bundled with VP9. &lt;em&gt;&lt;strong&gt;However&lt;/strong&gt;&lt;/em&gt;, I forgot to declare it as part of the &lt;code&gt;addSourceBuffer&lt;/code&gt; call&lt;/li&gt;
&lt;li&gt;I had to change &lt;code&gt;mediaSource.addSourceBuffer(&apos;video/webm; codecs=&quot;vp9&quot;&apos;)&lt;/code&gt; to &lt;code&gt;mediaSource.addSourceBuffer(&apos;video/webm; codecs=&quot;vp9,opus&quot;&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I think Firefox will automatically detect codecs from the data you append, whereas Chromium requires explicit declaration (just a guess)&lt;/li&gt;
&lt;li&gt;If you want to check if a mimetype is supported, you can query it with &lt;code&gt;MediaSource.isTypeSupported(mimeTypeStr)&lt;/code&gt;, like &lt;code&gt;MediaSource.isTypeSupported(&apos;video/webm; codecs=&quot;vp9,opus&quot;&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Gapless sequential playback (mode = &lt;code&gt;sequence&lt;/code&gt;) does not work in Chrome (stalls), but works in Firefox just fine (for &lt;em&gt;some&lt;/em&gt; files, see issue below this one)
&lt;ul&gt;
&lt;li&gt;If you are using multi-track appends (e.g. separate &lt;em&gt;files&lt;/em&gt; as opposed to chunks / stream), this is a complex and apparently buggy issue with Chromium (&lt;a href=&quot;https://github.com/w3c/media-source/issues/190&quot;&gt;media-source/#190&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;Chrome now even logs a message warning as such: &lt;code&gt;Warning: using MSE &apos;sequence&apos; AppendMode for a SourceBuffer with multiple tracks may cause loss of track synchronization. In some cases, buffered range gaps and playback stalls can occur. It is recommended to instead use &apos;segments&apos; mode for a multitrack SourceBuffer.&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;See my notes under &lt;a href=&quot;#source-buffer-modes-segments-vs-sequence&quot;&gt;“Segments vs Sequence”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In Firefox, multiple appends with separate files (or even chunks) seems &lt;em&gt;extremely&lt;/em&gt; buggy depending on the input files - with certain input files, I’m seeing extremely frequent random stalls with &lt;em&gt;&lt;strong&gt;zero&lt;/strong&gt;&lt;/em&gt; logged errors. I’m wondering if it is super picky about conformance to codecs. Or it could be evicting buffer segments way faster than it should be.
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1400587&quot;&gt;This issue&lt;/a&gt; seems highly related&lt;/li&gt;
&lt;li&gt;I don’t think it is just me; although &lt;a href=&quot;https://github.com/samdutton/simpl/issues/67&quot;&gt;this issue was closed&lt;/a&gt;, it doesn’t appear fixed in my version of Firefox (which is actually a lot newer than when that issue was posted and then marked as fixed).&lt;/li&gt;
&lt;li&gt;Same issue across stable, beta, and nightly build&lt;/li&gt;
&lt;li&gt;RESOLUTION: At least in my demos, switching over all the input files from &lt;code&gt;VP9/Opus&lt;/code&gt; to &lt;code&gt;VP8/Vorbis&lt;/code&gt; fixed the &lt;code&gt;segments&lt;/code&gt; mode demo. It could be that the files I originally picked just happened to be malformed VP9, but it also seems suspect that it kept happening with so many different VP9 files…&lt;/li&gt;
&lt;li&gt;Also, should be noted that although Firefox does not have the awesome &lt;code&gt;chrome://media-internals/&lt;/code&gt; that Chromium browsers have (or the media DevTools tab), there is the &lt;a href=&quot;https://addons.mozilla.org/en-US/firefox/addon/devtools-media-panel/&quot;&gt;Devtools Media Panel&lt;/a&gt; extension, which can be used with Firefox Nightly&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Migrating and Redirecting Subdomains with Netlify</title><link>https://joshuatz.com/posts/2020/migrating-and-redirecting-subdomains-with-netlify/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/migrating-and-redirecting-subdomains-with-netlify/</guid><description>The easy way to migrate from one subdomain to another, for the same site and primary domain, using Netlify Redirect settings and Custom Domain Aliases.</description><pubDate>Sun, 22 Nov 2020 18:00:46 GMT</pubDate><content:encoded>&lt;p&gt;There is lots of information out there on migrating entire domains with Netlify, but less for migrating from one subdomain to another on the same host (main) domain. I wanted to put this post out there to confirm that that this is possible, and actually &lt;em&gt;easy&lt;/em&gt; to do with &lt;em&gt;Netlify Redirects&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-old-way&quot;&gt;The Old Way&lt;/h2&gt;
&lt;p&gt;Normally, If I wanted to migrate from one subdomain to another, &lt;em&gt;&lt;strong&gt;with a site I’m hosting myself&lt;/strong&gt;&lt;/em&gt;, the process might look something like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create new subdomain entry through CPanel&lt;/li&gt;
&lt;li&gt;Add new DNS CNAME record for new subdomain, pointing to host
&lt;ul&gt;
&lt;li&gt;Depending on your host, adding the subdomain in previous step might automatically do this&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;After DNS has propagated, implement 301 redirect from old subdomain to new subdomain
&lt;ul&gt;
&lt;li&gt;This can be done in CPanel, with a wildcard redirect&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;with-netlify-redirects-and-hosting&quot;&gt;With Netlify Redirects and Hosting&lt;/h2&gt;
&lt;p&gt;With Netlify as the host, this works a little differently.&lt;/p&gt;
&lt;p&gt;I already had a CNAME entry pointing &lt;code&gt;old-subdomain.example.com&lt;/code&gt; to Netlify. I added another one to point &lt;code&gt;new-subdomain.example.com&lt;/code&gt; to the same Netlify host, but then had to sit and think for a second about the best way to move forward.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In order to use CPanel redirect settings for a subdomain, my host requires that the subdomain exist as a CPanel subdomain entity. This means it gets its own folder and CNAME entries&lt;/li&gt;
&lt;li&gt;For redirecting the old subdomain, this didn’t really make sense; I don’t want it as a CPanel subdomain, because I’m not doing any hosting - it is still Netlify. Furthermore, CPanel wouldn’t even let me create it as a subdomain, since I still had a CNAME entry for it pointing to Netlify, and the CPanel workflow won’t allow the creation of a duplicate.&lt;/li&gt;
&lt;li&gt;Theoretically, I could remove the CNAME for the old subdomain that points to Netlify, create a host folder, and then implement redirects to the new one, but that seems overly complicated…&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;solution&quot;&gt;Solution&lt;/h3&gt;
&lt;p&gt;A much cleaner solution emerged seconds later, and within minutes, I had everything live and working flawlessly. Here are the steps I ended up taking&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;IMPORTANT: Do not delete the old subdomain CNAME entry pointing to Netlify!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Create new CNAME entry, pointing &lt;code&gt;new-subdomain.example.com&lt;/code&gt; to Netlify (&lt;a href=&quot;https://docs.netlify.com/domains-https/custom-domains/configure-external-dns/#configure-a-subdomain&quot;&gt;Netlify Docs: Subdomains&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Go to &lt;code&gt;Netlify -&gt; Project -&gt; Site Settings -&gt; Domain Management&lt;/code&gt;, and &lt;a href=&quot;https://docs.netlify.com/domains-https/custom-domains/#assign-a-domain-to-a-site&quot;&gt;add new subdomain as alias&lt;/a&gt;. Once DNS change from step 1 is working, you can set it as the primary domain.&lt;/li&gt;
&lt;li&gt;✨ Use the Netlify special &lt;code&gt;_redirects&lt;/code&gt; file to 301 redirect &lt;em&gt;all&lt;/em&gt; pages from the old subdomain to the new one (&lt;a href=&quot;https://docs.netlify.com/routing/redirects/redirect-options/#domain-level-redirects&quot;&gt;Docs: Redirects&lt;/a&gt;) (see below for example). Push it live.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;_redirects&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Redirect old subdomain to new one&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;https://old-subdomain.example.com/* https://new-subdomain.example.com/:splat 301!&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And… that’s it 🤯! Seriously! No messing with CPanel subdomains or HTACCESS files! 😅&lt;/p&gt;
&lt;p&gt;All the redirection is handled directly by Netlify, and &lt;em&gt;&lt;strong&gt;as long as we don’t delete our old CNAME entry&lt;/strong&gt;&lt;/em&gt;, visitors can still follow links to our old subdomain and get 301 redirected to the same page on the new one. There might be a noticeable dip in organic traffic, but it should only be momentary, as Google and other search engines respect 301 redirects and should very quickly recognize your new subdomain as legitimate.&lt;/p&gt;
&lt;h2 id=&quot;reminder&quot;&gt;Reminder&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;As with any domain migration, don’t forget to also update:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Analytics settings (Google Analytics, etc.)&lt;/li&gt;
&lt;li&gt;Hard-coded internal links, and external links you have access to&lt;/li&gt;
&lt;li&gt;Config files (e.g. &lt;code&gt;siteUrl&lt;/code&gt; setting in &lt;code&gt;gatsby-config.js&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Any where else the hardcoded URL might be used (email signatures, LinkedIn, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Using Binary Data with Front-End JavaScript and the Web</title><link>https://joshuatz.com/posts/2020/using-binary-data-with-front-end-javascript-and-the-web/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/using-binary-data-with-front-end-javascript-and-the-web/</guid><description>A comprehensive write-up on different ways to convert and use binary data in front-end JavaScript code, web browser APIs, and even your local filesystem.</description><pubDate>Sat, 21 Nov 2020 00:59:26 GMT</pubDate><content:encoded>&lt;p&gt;I’m making this post because, despite binary files being a &lt;em&gt;huge&lt;/em&gt; part of what makes up both your local and online filesystem, I kept having difficulties finding explanations on how (and why) to deal with binary files &lt;em&gt;&lt;strong&gt;on the web and in JavaScript&lt;/strong&gt;&lt;/em&gt;. In addition, a lot has changed in the world of JavaScript over the past years, so a lot of information out there is rather dated.&lt;/p&gt;
&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#intro&quot;&gt;Intro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#converting-binary-data-to-strings&quot;&gt;Converting Binary Data to Strings&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#javascript-binary-data-to-base64-including-data-urls&quot;&gt;JavaScript: Binary Data to Base64 (including Data URLs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#javascript-base64-to-binary-data-blob&quot;&gt;JavaScript: Base64 to Binary Data Blob&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#converting-binary-files-to-base64-strings-on-your-local-computer--cli&quot;&gt;Converting Binary Files to Base64 Strings on your Local Computer / CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#converting-binary-files-to-base64---online-tools&quot;&gt;Converting Binary Files to Base64 - Online Tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#using-binary-data-as-source-for-elements&quot;&gt;Using Binary Data as Source For Elements&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#loading-data-via-object-urls&quot;&gt;Loading Data via Object URLs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#loading-data-via-base64-string-data-urls&quot;&gt;Loading Data via Base64 String Data URLs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#loading-data-via-buffers&quot;&gt;Loading Data via Buffers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#disclaimers&quot;&gt;Disclaimers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#more-resources&quot;&gt;More Resources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;First, some things that should be clarified before getting into the details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Binary file / data&lt;/strong&gt;: I’m using this to refer to pretty much any file or data that is not text-based. Examples: raw contents of image and video files, blobs, etc.&lt;/li&gt;
&lt;li&gt;This post is mostly about front-end manipulation of binary data, not server-side (although some parts also apply to NodeJS)&lt;/li&gt;
&lt;li&gt;Many examples used throughout the post require a somewhat modern web browser; e.g., it uses &lt;code&gt;async&lt;/code&gt; functions&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;converting-binary-data-to-strings&quot;&gt;Converting Binary Data to Strings&lt;/h2&gt;
&lt;h3 id=&quot;javascript-binary-data-to-base64-including-data-urls&quot;&gt;JavaScript: Binary Data to Base64 (including Data URLs)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: If you are trying to convert from binary to Base64 in order to do something like display an image, you might not need to do this in the first place; browsers now support using raw blobs for media loading via Object URLs. I have instructions for this &lt;a href=&quot;#loading-data-via-object-urls&quot;&gt;here&lt;/a&gt;.&lt;br&gt;&lt;br&gt;In general, loading raw binary data via Object URL is recommended &lt;em&gt;&lt;strong&gt;instead&lt;/strong&gt;&lt;/em&gt; of using Base64 URLs&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Convert a raw binary blob into a Base64 String&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {Blob}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; blob&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; Raw binary blob&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {boolean}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [asDataUrl]&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; Should full DataURI be returned&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@returns&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {Promise&amp;#x3C;string&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; blobToBase64&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;blob&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;asDataUrl&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; reader&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; FileReader&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		reader.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;onload&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;			/** &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {string}&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;			const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; dataUrl&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (reader.result);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;			if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (asDataUrl) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;				res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(dataUrl);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;				// Remove MIME / dataUrl prefix&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;				res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(dataUrl.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;data:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\w&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#85E89D;font-weight:bold&quot;&gt;\/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\w&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;;base64,&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		reader.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;readAsDataURL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(blob);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; runExample&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; binaryBlob&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia-logo-v2.png&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;blob&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; base64Url&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; blobToBase64&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(binaryBlob, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	document.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;img&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).src &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; base64Url;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;runExample&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;details&gt;
	&lt;summary&gt;Live Demo&lt;/summary&gt;
&lt;iframe height=&quot;474&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;zYBVowa&quot; src=&quot;https://codepen.io/joshuatz/embed/preview/zYBVowa?height=474&amp;#x26;theme-id=light&amp;#x26;default-tab=js,result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&gt;
  See the Pen &amp;#x3C;a href=&apos;https://codepen.io/joshuatz/pen/zYBVowa&apos;&gt;zYBVowa&amp;#x3C;/a&gt; by Joshua T
  (&amp;#x3C;a href=&apos;https://codepen.io/joshuatz&apos;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&apos;https://codepen.io&apos;&gt;CodePen&amp;#x3C;/a&gt;.
&lt;/iframe&gt;
&lt;/details&gt;
&lt;h3 id=&quot;javascript-base64-to-binary-data-blob&quot;&gt;JavaScript: Base64 to Binary Data Blob&lt;/h3&gt;
&lt;p&gt;First, I’ll point out that there are not a whole lot of good reasons to need to convert Base64 data to a binary blob in the browser to begin with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If your data is in Base64, you can already use it for images, videos, etc. - by using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs&quot;&gt;Data URLs&lt;/a&gt; (aka &lt;em&gt;Data URIs&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;AFAIK, converting to a binary blob if you &lt;em&gt;already&lt;/em&gt; have the data as a stored string is going to basically be double-allocating space for it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, if you really need to do this, there are some common approaches. The newest, and easiest to use, is to use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch&quot;&gt;the &lt;code&gt;fetch()&lt;/code&gt; web API&lt;/a&gt;, and its &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Body/blob&quot;&gt;.blob() return method&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Convert base64 string to blob&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {string}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; base64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {string}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [mimeType]&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; Mime type used by data held in base64 string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@returns&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {Promise&amp;#x3C;Blob&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; base64ToBlob&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;base64&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;mimeType&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;application/octet-stream&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`data:${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;mimeType&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;};base64,${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;base64&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;then&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;blob&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Example:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; runExample&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; imgStr&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; `Qk1aAAAAAAAAAEIAAAAoAAAABgAAAAYAAAABAAQAAAAAAAAAAADEDgAAxA4AAAMAAAADAAAAAAAA/wAA//8A2P//IAEgAAASAAABIAEAEgASACABIAAAEgAA`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; blob&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; base64ToBlob&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(imgStr, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;image/bmp&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	document.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;img&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).src &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; URL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;createObjectURL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(blob);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;runExample&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;details&gt;
	&lt;summary&gt;Live Demo&lt;/summary&gt;
&lt;iframe height=&quot;368&quot; style=&quot;width: 100%;&quot; scrolling=&quot;no&quot; title=&quot;Base64 to Blob&quot; src=&quot;https://codepen.io/joshuatz/embed/OJXebjM?height=368&amp;#x26;theme-id=light&amp;#x26;default-tab=js,result&quot; frameborder=&quot;no&quot; loading=&quot;lazy&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot;&gt;
  See the Pen &amp;#x3C;a href=&apos;https://codepen.io/joshuatz/pen/OJXebjM&apos;&gt;Base64 to Blob&amp;#x3C;/a&gt; by Joshua T
  (&amp;#x3C;a href=&apos;https://codepen.io/joshuatz&apos;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&apos;https://codepen.io&apos;&gt;CodePen&amp;#x3C;/a&gt;.
&lt;/iframe&gt;
&lt;/details&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also use &lt;code&gt;fetch(resource).then(res =&gt; res.blob())&lt;/code&gt; to convert &lt;em&gt;any&lt;/em&gt; fetch-able object to a blob.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you are targeting an older browser, you might not be able to use &lt;code&gt;fetch()&lt;/code&gt;, but there are still other solutions you can use. I would recommend looking at this StackOverflow question and reading through the responses to find what works best for you: &lt;a href=&quot;https://stackoverflow.com/q/16245767/11447682&quot;&gt;“Creating a BLOB from a Base64 string in JavaScript”&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;converting-binary-files-to-base64-strings-on-your-local-computer--cli&quot;&gt;Converting Binary Files to Base64 Strings on your Local Computer / CLI&lt;/h3&gt;
&lt;p&gt;If you have access to standard *nix utilities, the easiest tool to use is the aptly-named &lt;code&gt;base64&lt;/code&gt; utility.&lt;/p&gt;
&lt;p&gt;For example, if I had a JPG I wanted to get as a Base64 encoded string, I could use:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;base64&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --wrap=0&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; my-image.jpg&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; my-image-base64.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# You can also pipe to it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; my-image.jpg&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; base64&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --wrap=0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;You’ll note that I always use &lt;code&gt;--wrap=0&lt;/code&gt;; this is because the default behavior is to wrap (add line breaks) at 76 columns. This wastes space, and is completely unnecessary, especially if I’m just going to be pasting into a JS or HTML file.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On Windows, you can also get the string put right in your clipboard, with:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cat my-image.jpg | base64 --wrap=0 | clip&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;converting-binary-files-to-base64---online-tools&quot;&gt;Converting Binary Files to Base64 - Online Tools&lt;/h3&gt;
&lt;p&gt;If you don’t have access to a terminal, or just prefer using a web browser, there are dozens of online file-to-base64 converters, such as &lt;a href=&quot;https://www.browserling.com/tools/file-to-base64&quot;&gt;this one by browserling&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As discussed above, base64 conversion can be done entirely in front-end JavaScript, so I wouldn’t trust any online tool that actually “uploads” your file anywhere.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;using-binary-data-as-source-for-elements&quot;&gt;Using Binary Data as Source For Elements&lt;/h2&gt;
&lt;h3 id=&quot;loading-data-via-object-urls&quot;&gt;Loading Data via Object URLs&lt;/h3&gt;
&lt;p&gt;If you already have your data stored in a binary format, in memory, and need to load it into an element (for example an &lt;code&gt;img&lt;/code&gt; element), there is an alternative to Base64 DataURLs that is much preferable: &lt;em&gt;&lt;strong&gt;Object URLs&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;These are essentially virtual URLs that &lt;em&gt;point&lt;/em&gt; to raw data; this could be a blob of binary data stored in memory, or even a reference to a user’s local file from their OS. Because they are references to existing data location, they use less memory than Base64 strings (and you can even &lt;em&gt;release&lt;/em&gt; the pointer after loading)&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Example: Loading an image blob&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; blob&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia-logo-v2.png&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;blob&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; imageElem&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; document.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;img&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Good practice is to release once loaded&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;imageElem.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;onload&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	URL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;revokeObjectURL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(imageElem.src);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;imageElem.src &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; URL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;createObjectURL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(blob);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just like &lt;em&gt;Data URLs&lt;/em&gt;, Loading data through &lt;em&gt;Object URLs&lt;/em&gt; works for a variety of media element, including &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; elements!&lt;/p&gt;
&lt;h3 id=&quot;loading-data-via-base64-string-data-urls&quot;&gt;Loading Data via Base64 String Data URLs&lt;/h3&gt;
&lt;p&gt;Data URLs (formerly called &lt;em&gt;Data URIs&lt;/em&gt;) are essentially just strings that are comprised of the data itself, as opposed to a &lt;em&gt;pointer&lt;/em&gt; to where the data resides (how normal URLs or Object URLs work).&lt;/p&gt;
&lt;p&gt;They follow a standardized syntax:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;data:{mimeType};base64,{dataStr}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Example&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;data:image/bmp;base64,Qk1aAAAAAAAAAEIAAAAoAAAABgAAAAYAAAABAAQAAAAAAAAAAADEDgAAxA4AAAMAAAADAAAAAAAA/wAA//8A2P//IAEgAAASAAABIAEAEgASACABIAAAEgAA&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# If the data is *not* base64 encoded, you can leave off the `;base64` section&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Example:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;data:text/plain,hello&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They can be used in (AFAIK) pretty much any element with a &lt;code&gt;src&lt;/code&gt; attribute, even including &lt;code&gt;&amp;#x3C;iframe&gt;&lt;/code&gt; and &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; tags!&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;data:text/plain,alert(&apos;hi!&apos;)&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;&amp;#x3C;!-- Base64: alert(&apos;hi from base64&apos;) --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;data:text/plain;base64,YWxlcnQoJ2hpIGZyb20gYmFzZTY0JykgDQo=&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are not using base64 encoding with Data URIs, you should be extra careful about characters that are not URL safe; some browsers might reject these. You can always use something like &lt;code&gt;data:image/svg+xml;utf8,${encodeURIComponent(svgStr)}&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;loading-data-via-buffers&quot;&gt;Loading Data via Buffers&lt;/h3&gt;
&lt;p&gt;This is getting into the &lt;em&gt;advanced&lt;/em&gt; part of data loading in JavaScript, but I’d like to point out that certain elements (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement&quot;&gt;HTMLMediaElement&lt;/a&gt; based, i.e. video and audio) now support loading data more directly, via streams, buffers, and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaSource&quot;&gt;the MediaSource interface&lt;/a&gt;. Collectively, most of these technologies fall under the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API&quot;&gt;“Media Source Extensions API”&lt;/a&gt; (aka &lt;em&gt;MSE&lt;/em&gt;), which is an evolving specification for more advanced and dynamic media sources, such as append-able buffers.&lt;/p&gt;
&lt;p&gt;The advantage to this approach is that it allows you to load in data in chunks, as opposed to through a single resource URL or blob. This makes it ideal for streaming applications, where video or audio is progressively loaded (and/or rendered).&lt;/p&gt;
&lt;p&gt;Certain elements can even emit streams, which can be then captured and fed back into other media elements. A great example of this: you can stream data directly from &lt;code&gt;&amp;#x3C;canvas&gt;&lt;/code&gt; element to a &lt;code&gt;&amp;#x3C;video&gt;&lt;/code&gt; element on the same page (&lt;a href=&quot;https://webrtc.github.io/samples/src/content/capture/canvas-video/&quot;&gt;demo&lt;/a&gt;). Or &lt;a href=&quot;https://webrtc.github.io/samples/src/content/capture/video-video/&quot;&gt;stream from one video to another&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’m not going to get too in-depth into this, as it is an advanced topic (and I have an &lt;a href=&quot;https://joshuatz.com/posts/2020/appending-videos-in-javascript-with-mediasource-buffers/&quot;&gt;entire separate blog post on it&lt;/a&gt;), but the general minimal steps required to load data into a (video) element with a dynamic MediaSource are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaSource&quot;&gt;mediaSource instance&lt;/a&gt; (&lt;code&gt;new MediaSource()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Create an object URL that points to that source. Point the video to it
&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;videoElem.src = URL.createObjectURL(mediaSource)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;On the MediaSource instance, listen for the &lt;code&gt;sourceopen&lt;/code&gt; even. Once it fires, you can begin &lt;em&gt;preparing&lt;/em&gt; to load data into it&lt;/li&gt;
&lt;li&gt;Before you start loading data, you need to create a buffer to hold it. You do so by creating a &lt;code&gt;SourceBuffer&lt;/code&gt; attached to the source, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/addSourceBuffer&quot;&gt;with &lt;code&gt;mediaSource.addSourceBuffer(mimeType)&lt;/code&gt;&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;const sourceBuffer = mediaSource.addSourceBuffer(&apos;video/webm; codecs=&quot;vp9&quot;&apos;);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Once you have the buffer, you can finally start loading in raw chunks of binary data&lt;/li&gt;
&lt;li&gt;One way to load in a chunk is with ArrayBuffer(s), via &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer&quot;&gt;the &lt;code&gt;sourceBuffer.appendBuffer(ArrayBuffer)&lt;/code&gt; method&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;If you have a blob instead of an ArrayBuffer, you can use &lt;code&gt;myBlob.arrayBuffer()&lt;/code&gt; to get &lt;code&gt;Promise&amp;#x3C;ArrayBuffer&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Eventually, you also need to deal with signaling that the end of data has been reached, and controlling the playback&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can find a full example of these steps put together on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer#Examples&quot;&gt;MDN’s page for SourceBuffer&lt;/a&gt;, and an even more advanced example &lt;a href=&quot;https://w3c.github.io/media-source/#examples&quot;&gt;in the MediaSource / MSE spec&lt;/a&gt;. &lt;del&gt;As I mentioned, I’m also planning to write and release a blog post on the topic soon.&lt;/del&gt; I also just finished &lt;a href=&quot;https://joshuatz.com/posts/2020/appending-videos-in-javascript-with-mediasource-buffers/&quot;&gt;a blog post that goes into depth on this topic&lt;/a&gt;!&lt;/p&gt;
&lt;h2 id=&quot;disclaimers&quot;&gt;Disclaimers&lt;/h2&gt;
&lt;p&gt;I wrestled with the decision to include this section, as I don’t want to make any developers feel like I am trying to “shame” them, but I also feel that it is important to include it due to the level of abuse that Data URLs and Base64 often receive. This is an attempt to point out situations in which these alternative loading techniques are used, when maybe they shouldn’t be. Or ways in which they can used incorrectly. So, without further delay:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Base64 is &lt;em&gt;not&lt;/em&gt; a form of encryption. It cannot be used to hide passwords, secrets, or prevent assets from being downloaded&lt;/li&gt;
&lt;li&gt;Base64 is generally a less efficient way of storing binary data, and it is a non-zero amount of processing power required to convert Base64 strings into other formats
&lt;ul&gt;
&lt;li&gt;This is (part of) why taking a 30 second video and storing it directly as a Base64 encoded string in the page and loading via Data URL, instead of just using a normal URL as the &lt;code&gt;src&lt;/code&gt;, is very much a bad idea&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Data URLs are not interchangeable with regular URLs, or Object URLs. There &lt;em&gt;&lt;strong&gt;are&lt;/strong&gt;&lt;/em&gt; caveats to how they work
&lt;ul&gt;
&lt;li&gt;A &lt;em&gt;&lt;strong&gt;huge&lt;/strong&gt;&lt;/em&gt; drawback is this one: Data URLs are treated as &lt;a href=&quot;https://html.spec.whatwg.org/multipage/origin.html#concept-origin-opaque&quot;&gt;an &lt;code&gt;opaque origin&lt;/code&gt;&lt;/a&gt; as opposed to matching the origin of the page they are loaded into. (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs&quot;&gt;ref&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;This means they do things like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image&quot;&gt;taint HTML &lt;code&gt;&amp;#x3C;canvas&gt;&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://stackoverflow.com/q/21946383/11447682&quot;&gt;affect CORS usage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Another drawback is that there is actually limits to how long the Data URL string can be, and &lt;a href=&quot;https://stackoverflow.com/a/41755526/11447682&quot;&gt;it is not that large, nor is it the same across browsers&lt;/a&gt;. In comparison, Objects URLs and blobs have &lt;em&gt;enormous&lt;/em&gt; limits, that are governed more by the physical limitations of the computer being used.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Although &lt;code&gt;MediaSource&lt;/code&gt; / MSE has been worked on since &lt;a href=&quot;https://www.w3.org/TR/2013/WD-media-source-20130129/&quot;&gt;early 2013&lt;/a&gt;, it is a &lt;a href=&quot;https://w3c.github.io/media-source/&quot;&gt;constantly evolving spec&lt;/a&gt;, and it can be difficult to find info on it. Not all browsers support it equally either.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;more-resources&quot;&gt;More Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://javascript.info/binary&quot;&gt;javascript.info: Binary Data&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://javascript.info/blob&quot;&gt;Blob&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MDN
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications&quot;&gt;“Using Files from Web Applications”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs&quot;&gt;Data URLs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data&quot;&gt;“Sending and Receiving Binary Data”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yaz.in/p/blobs-files-and-data-uris/&quot;&gt;https://yaz.in/p/blobs-files-and-data-uris/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/eligrey/FileSaver.js/&quot;&gt;https://github.com/eligrey/FileSaver.js/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;more-about-me&quot;&gt;More About Me:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;🔗&lt;a href=&quot;https://joshuatz.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;joshuatz.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;👨‍💻&lt;a href=&quot;https://dev.to/joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;dev.to/joshuatz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💬&lt;a href=&quot;https://twitter.com/1joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;@1joshuatz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💾&lt;a href=&quot;https://github.com/joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;github.com/joshuatz&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Firefox Backup Bookmarks Options and Automated Scripts</title><link>https://joshuatz.com/posts/2020/firefox-backup-bookmarks-options-and-automated-scripts/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/firefox-backup-bookmarks-options-and-automated-scripts/</guid><description>An evaluation of different ways to backup and export Firefox Bookmarks, both manually and via automated scripts and tools.</description><pubDate>Tue, 17 Nov 2020 22:47:32 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been a long-time Firefox user - for over ten years at this point. I’ve amassed thousands of bookmarks, some of which I have spent time organizing and tagging. As such, lately I was thinking I really should set up some sort of backup system, but I had trouble locating any commands or scripts that could accomplish this.&lt;/p&gt;
&lt;p&gt;If you want to jump right to the solutions section, &lt;a href=&quot;#solutions&quot;&gt;click here&lt;/a&gt;, otherwise read on.&lt;/p&gt;
&lt;h2 id=&quot;find-your-profile-folder&quot;&gt;Find Your Profile Folder&lt;/h2&gt;
&lt;p&gt;Pretty much all of &lt;em&gt;your&lt;/em&gt; data in Firefox is stored in your &lt;em&gt;&lt;strong&gt;profile folder&lt;/strong&gt;&lt;/em&gt;. This is a special folder, whose location is unique to your computer due to its use of profile IDs and OS user directories.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data#w_how-do-i-find-my-profile&quot;&gt;This is the definitive documentation on how to find this folder location&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you have the basic *nix utilities installed, you can use this one-liner to get the path on Windows:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;tail&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;%APPDATA%\Mozilla\Firefox\profiles.ini&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; +2&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; head&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -n&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; sed&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -E&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;s/Default=//&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; xargs&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -I&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; %&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; echo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;%APPDATA%\Mozilla\Firefox\%&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; sed&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -E&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;s/\//\\\/g&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Example: &quot;C:\Users\Joshua\AppData\Roaming\Mozilla\Firefox\Profiles\29adfe2f.default-release&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;research&quot;&gt;Research&lt;/h2&gt;
&lt;p&gt;First, let’s investigate how Firefox stores user bookmarks.&lt;/p&gt;
&lt;p&gt;The best place to get this information is the Mozilla support page - &lt;a href=&quot;https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data&quot;&gt;“Where Firefox Stores User Data”&lt;/a&gt;. I’ll supplement this with some additional details below.&lt;/p&gt;
&lt;p&gt;Some key things to note, which affect our task:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bookmarks are contained in &lt;code&gt;place.sqlite&lt;/code&gt;, along with download metadata and browsing history
&lt;ul&gt;
&lt;li&gt;This makes extraction more difficult, as this &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Places/Database&quot;&gt;is a special database&lt;/a&gt;, can you can’t easily copy &amp;#x26; paste out of it, or view it, or easily import it into Firefox or other browsers&lt;/li&gt;
&lt;li&gt;This will be the &lt;strong&gt;most up-to-date&lt;/strong&gt; stored version of your bookmarks, as the automated backup options (see below) either run periodically, or when the browser shuts down. In comparison, &lt;code&gt;places.sqlite&lt;/code&gt; is updated in almost real-time as you edit bookmarks in Firefox.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;There are some automated backup options, which essentially output a subset of &lt;code&gt;places.sqlite&lt;/code&gt;, with just the bookmarks.
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/Profiles/{PROFILE_ID}/bookmarkbackups&lt;/code&gt; &lt;a href=&quot;https://support.mozilla.org/en-US/questions/1005048&quot;&gt;should contain &lt;strong&gt;daily&lt;/strong&gt; backups&lt;/a&gt; of bookmarks
&lt;ul&gt;
&lt;li&gt;However, these are stored in a somewhat obscure &lt;code&gt;.jsonlz4&lt;/code&gt; file type, which is JSON compressed with a &lt;em&gt;flavor&lt;/em&gt; of &lt;a href=&quot;https://en.wikipedia.org/wiki/LZ4_(compression_algorithm)&quot;&gt;the &lt;code&gt;LZ4&lt;/code&gt; algorithm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;By changing the config value (in &lt;code&gt;about:config&lt;/code&gt;) of &lt;code&gt;browser.bookmarks.autoExportHTML&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, you can have Firefox automatically dump a backup of the bookmarks in HTML format, &lt;strong&gt;on browser close&lt;/strong&gt;.
&lt;ul&gt;
&lt;li&gt;You can control the file location with config value &lt;code&gt;browser.bookmarks.file&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;See &lt;a href=&quot;https://support.mozilla.org/en-US/questions/1264643&quot;&gt;this post&lt;/a&gt; for details&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;converting-the-jsonlz4-bookmarks-files&quot;&gt;Converting the &lt;code&gt;.jsonlz4&lt;/code&gt; Bookmarks File(s)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: You don’t &lt;em&gt;&lt;strong&gt;need&lt;/strong&gt;&lt;/em&gt; to convert the files if all you want them for is a backup that you can re-import into Firefox; Firefox understands this file format and will let you use it as an import, no conversion required.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;code&gt;bookmarkbackups&lt;/code&gt; folder and its &lt;code&gt;.jsonlz4&lt;/code&gt; daily backup files are a nice existing backup to take advantage of, but how can we get the files in plain JSON, so we can do whatever we want with their contents (convert to CSV, cleanup, etc.).&lt;/p&gt;
&lt;p&gt;Existing options (found via a quick Google search):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Everything listed in &lt;a href=&quot;https://superuser.com/questions/1363747/how-to-decode-decipher-mozilla-firefox-proprietary-jsonlz4-format-sessionstor&quot;&gt;this thread&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Online tool: &lt;a href=&quot;https://www.jeffersonscher.com/ffu/bookbackreader.html&quot;&gt;Jefferson Scher: “BookBackReader”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Offline tools:
&lt;ul&gt;
&lt;li&gt;C / Binary release: &lt;a href=&quot;https://github.com/avih/dejsonlz4&quot;&gt;avih/dejsonlz4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NodeJS / CLI: &lt;a href=&quot;https://github.com/thrilleratplay/node-jsonlz4-decompress&quot;&gt;thrilleratplay/node-jsonlz4-decompress&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Python: &lt;a href=&quot;https://gist.github.com/Tblue/62ff47bef7f894e92ed5&quot;&gt;TBlue Github Gist&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;solutions&quot;&gt;Solutions&lt;/h2&gt;
&lt;h3 id=&quot;manual-methods&quot;&gt;Manual Methods&lt;/h3&gt;
&lt;p&gt;For manual bookmark exporting, there are some nice built-in options. These are covered &lt;a href=&quot;https://support.mozilla.org/en-US/kb/restore-bookmarks-from-backup-or-move-them#w_manual-backup&quot;&gt;here&lt;/a&gt;, but to summarize:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open the Bookmark Library (&lt;code&gt;CTRL + SHIFT + B&lt;/code&gt;, or use &lt;em&gt;Library Button&lt;/em&gt; in toolbar)&lt;/li&gt;
&lt;li&gt;Open the &lt;code&gt;Import and Backup&lt;/code&gt; menu&lt;/li&gt;
&lt;li&gt;Use either of:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Backup&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;this exports a &lt;code&gt;UTF-8&lt;/code&gt; JSON file, which is &lt;em&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;/em&gt; compressed with &lt;code&gt;lz4&lt;/code&gt; - you can open and read this file easily&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Export Bookmarks to HTML&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;This exports a HTML file, which you can easily open with any standard web browser&lt;/li&gt;
&lt;li&gt;The downside is that it is roughly 2x the size of the JSON export, and would be harder to parse if you wanted to automate the transformation of your bookmarks into another format&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, if you don’t care about backing them up to &lt;em&gt;a specific folder&lt;/em&gt;, than using the default automated backups (discussed above, &lt;a href=&quot;#research&quot;&gt;here&lt;/a&gt;), might be enough for you.&lt;/p&gt;
&lt;h3 id=&quot;automated-methods&quot;&gt;Automated Methods&lt;/h3&gt;
&lt;p&gt;For putting together a command / script to grab a Bookmarks backup, I have three main options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hook into the internals of Firefox
&lt;ul&gt;
&lt;li&gt;The best place would be &lt;a href=&quot;https://github.com/mozilla/gecko-dev/blob/1b0d5a3a70b694931024c96fa435f84093bf1a77/toolkit/components/places/BookmarkJSONUtils.jsm&quot;&gt;the &lt;code&gt;BookmarkJSONUtils.jsm&lt;/code&gt; utility&lt;/a&gt; - this has a native method for exporting to JSON - &lt;code&gt;exportToFile&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;You can actually get to this in the browser, with &lt;code&gt;resource://gre/modules/BookmarkJSONUtils.jsm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Although this would probably be the most “to-spec” option, it would also take me a while to figure out how to do, especially given some limitations around scripting Firefox
&lt;ul&gt;
&lt;li&gt;For example, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode&quot;&gt;the &lt;code&gt;headless mode&lt;/code&gt; of Firefox&lt;/a&gt; might let me &lt;code&gt;eval()&lt;/code&gt; JS to extract the bookmarks, but you cannot concurrently use that and have an open instance of Firefox with the profile you want to use&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Connect to the &lt;code&gt;places.sqlite&lt;/code&gt; database and extract just the bookmarks, converting on-the-fly to JSON or HTML
&lt;ul&gt;
&lt;li&gt;Complicated, and might have issues around concurrent db access&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Just grab the daily backups (&lt;code&gt;.jsonlz4&lt;/code&gt; files) and copy to backup folder of choice&lt;/strong&gt;&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;This is, by far, the easiest option.&lt;/li&gt;
&lt;li&gt;I’ve implemented this &lt;a href=&quot;#automated-methods---batch-script&quot;&gt;below&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;automated-methods---batch-script&quot;&gt;Automated Methods - Batch Script&lt;/h3&gt;
&lt;p&gt;I’m not thrilled with it, but here is a batch file I put together to copy the most recent JSON file to my Dropbox. It requires that some standard *nix utilities are installed (these come default with git-bash), and unfortunately, you have to hard-code the backup folder(s). But, it works!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ff-bookmarks-backup.bat&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;batch&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;REM&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; # Hard-coded paths - EDIT ME&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; FF_DAILY_BACK_FOLDER&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;C:\Users\Joshua\AppData\Roaming\Mozilla\Firefox\Profiles\29adfe2f.default-release\bookmarkbackups&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OUTPUT_FOLDER&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;C:\Users\Joshua\Dropbox\Program Settings&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;REM&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; # Remove quotes, as these are going to mess up paths later&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; FF_DAILY_BACK_FOLDER&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;%FF_DAILY_BACK_FOLDER:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OUTPUT_FOLDER&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;%OUTPUT_FOLDER:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;REM&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; # Go to bookmarks folder&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; %FF_DAILY_BACK_FOLDER%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;REM&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; # Workaround to capture variable that requires *nix utils&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;ls &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tail -n &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; backup_filename&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /f &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;%%i&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&apos;cat backup_filename&apos;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;do&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; LATEST_BACKUP_FILEPATH&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;%%i&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; LATEST_BACKUP_FILEPATH&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;%FF_DAILY_BACK_FOLDER%\%LATEST_BACKUP_FILEPATH%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; %LATEST_BACKUP_FILEPATH%&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;REM&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; # Copy file with rename&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;REM&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; # NOTE: This will overwrite, keeping a single file up to date with latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;cp &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;%LATEST_BACKUP_FILEPATH%&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;%OUTPUT_FOLDER%&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;\ff-bookmarks-latest.jsonlz4&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;REM&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; # Cleanup&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;rm backup_filename&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;automated-methods---a-better-solution&quot;&gt;Automated Methods - A Better Solution&lt;/h3&gt;
&lt;p&gt;If I had a lot more free time, and wanted this badly enough, I would code this as a NodeJS script, and compile it to a portable binary using something like &lt;a href=&quot;https://github.com/vercel/pkg&quot;&gt;vercel/pkg&lt;/a&gt;. If this is something you are interested in doing so yourself, my advice would be to have the script:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Parse OS environmental variables and paths to find the Firefox user profile folder&lt;/li&gt;
&lt;li&gt;Find the most recent file in the folder&lt;/li&gt;
&lt;li&gt;Decompress it using &lt;a href=&quot;https://github.com/thrilleratplay/node-jsonlz4-decompress&quot;&gt;thrilleratplay/node-jsonlz4-decompress&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Copy the raw and de-decompress versions to the output directly, specified by the user through a CLI argument
&lt;ol&gt;
&lt;li&gt;Having the tool copy both versions to output could be made optional&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Get and use local executables through NPM or Yarn</title><link>https://joshuatz.com/posts/2020/get-and-use-local-executables-through-npm-or-yarn/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/get-and-use-local-executables-through-npm-or-yarn/</guid><description>A short guide on how to get and use local binary paths for packages installed through NPM or Yarn, under node_modules, for multiple OSes.</description><pubDate>Tue, 10 Nov 2020 05:45:32 GMT</pubDate><content:encoded>&lt;p&gt;Here is a quick post on a common question; how do you get the path (and/or execute) a local executable (or cmd alias) that is installed locally via &lt;code&gt;yarn&lt;/code&gt; or &lt;code&gt;npm&lt;/code&gt;?&lt;/p&gt;
&lt;h2 id=&quot;getting-the-binary-folder&quot;&gt;Getting the Binary Folder&lt;/h2&gt;
&lt;p&gt;Both Yarn and NPM have a command to get the local binary directory:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# NPM&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; bin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Yarn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;yarn&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; bin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Sample output: `C:/temp/my-project/node_modules/.bin&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;However&lt;/strong&gt;&lt;/em&gt;, this command comes with a huge disclaimer; it returns the path where executables &lt;em&gt;will&lt;/em&gt; be installed (if called from current working directory), &lt;em&gt;not&lt;/em&gt; necessarily where they are &lt;em&gt;already&lt;/em&gt; installed in a nearest subdirectory.&lt;/p&gt;
&lt;p&gt;For example, with this folder structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;project/
&lt;ul&gt;
&lt;li&gt;README.md&lt;/li&gt;
&lt;li&gt;src/
&lt;ul&gt;
&lt;li&gt;node_modules/&lt;/li&gt;
&lt;li&gt;package.json&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;… running &lt;code&gt;npm bin&lt;/code&gt; in project root will echo &lt;code&gt;project/node_modules/.bin&lt;/code&gt;, instead of the &lt;strong&gt;existing&lt;/strong&gt; path of &lt;code&gt;project/src/node_modules/.bin&lt;/code&gt;. Thus, it is always a good idea to &lt;code&gt;cd&lt;/code&gt; to the directory with your &lt;code&gt;project.json&lt;/code&gt; file before trying to grab the path, or prefixing.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In general, the binary folder should always be &lt;code&gt;$PWD/node_modules/.bin&lt;/code&gt;, so you can also just hard-code the path in shell scripts and/or JS scripts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;running-a-local-binary&quot;&gt;Running a Local Binary&lt;/h2&gt;
&lt;h3 id=&quot;npx&quot;&gt;NPX&lt;/h3&gt;
&lt;p&gt;Many devs already know about this feature, so I’ll be brief on it; you can use &lt;a href=&quot;https://www.npmjs.com/package/npx&quot;&gt;&lt;code&gt;npx {command}&lt;/code&gt;&lt;/a&gt; to target a package installed locally through &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;By default, &lt;code&gt;npx&lt;/code&gt; will try to use a local install first, and if it can’t find it, it will download and execute a remote source. If you want to stop this, and have it just fail if the local copy does not exist, you can use the &lt;code&gt;--no-install&lt;/code&gt; flag.&lt;/p&gt;
&lt;h3 id=&quot;variable-substitution&quot;&gt;Variable Substitution&lt;/h3&gt;
&lt;p&gt;If for some reason you can’t use NPX, another option for shell scripting is to use variable substitution to capture the binary path and use it.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Executing binary directly&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; bin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)/local-binary&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Can use just like normal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npm&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; bin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)/local-binary --flag moreArgs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Passing in package name with `yarn bin`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;$(yarn&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; bin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; my-package&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;yarn&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; bin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; my-package&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) --flag moreArgs&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are on Windows, and you don’t have git-bash or anything like that installed, this should work in cmd:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bat&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /f &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;%%i&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&apos;npm bin&apos;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;do&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; NPM_BIN_PATH&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;%%i&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;%NPM_BIN_PATH%/my-package&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With both bash and CMD, you can always just hard-code the path of the binary folder and use it that way too.&lt;/p&gt;
&lt;h3 id=&quot;with-nodejs-child_process&quot;&gt;With NodeJS child_process&lt;/h3&gt;
&lt;p&gt;If you are using NodeJS’s &lt;code&gt;child_process&lt;/code&gt; to execute binaries from JavaScript code (e.g. with &lt;code&gt;.exec()&lt;/code&gt;), you might be wondering how to get it to &lt;em&gt;see&lt;/em&gt; locally installed binaries from node_modules.&lt;/p&gt;
&lt;p&gt;One option is to use a library, such as &lt;a href=&quot;https://www.npmjs.com/package/execa&quot;&gt;execa&lt;/a&gt;, which makes this easier.&lt;/p&gt;
&lt;p&gt;The vanilla approach is to combine the system &lt;code&gt;PATH&lt;/code&gt; with the local binary path, and pass that combined string as part of the &lt;code&gt;env&lt;/code&gt; object in &lt;code&gt;child_process&lt;/code&gt; method options:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Example is using `supports-color` package, which is installed locally&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * We are going to extract the help text and log it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Assumes script lives in same directory as package.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; LOCAL_BIN_PATH&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;path&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;normalize&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;__dirname&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/node_modules/.bin`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Execute a local binary / command&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; helpText&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;child_process&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;execSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`supports-color --help`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	env: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;process.env,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		path: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;process&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;env&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;};${&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;LOCAL_BIN_PATH&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(helpText);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap-Up&lt;/h2&gt;
&lt;p&gt;I hope this helps someone out there! Do you know any other tricks for using local binaries? Feel free to share below in the comments!&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Keep a Shell Open in NodeJS and Reuse for Multiple Commands</title><link>https://joshuatz.com/posts/2020/keep-a-shell-open-in-nodejs-and-reuse-for-multiple-commands/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/keep-a-shell-open-in-nodejs-and-reuse-for-multiple-commands/</guid><description>How to launch a shell with NodeJS, keep it open, and pipe commands in while capturing output. Also, a discussion on how this impacts performance.</description><pubDate>Sun, 08 Nov 2020 08:28:48 GMT</pubDate><content:encoded>&lt;p&gt;I’m making this post because this question has been bugging me, and I have had an unreasonably difficult time finding information on it. My question is simple:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Is there a way to spawn a shell in NodeJS, &lt;em&gt;&lt;strong&gt;keep it open&lt;/strong&gt;&lt;/em&gt;, and reuse it for multiple commands (perhaps even calling different binaries)?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Throughout this post, I’ll use this as the sample scenario:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Spawn a shell&lt;/li&gt;
&lt;li&gt;Execute &lt;code&gt;uname -a&lt;/code&gt; to get OS Info&lt;/li&gt;
&lt;li&gt;Execute &lt;code&gt;ls -a&lt;/code&gt; to list files in the current directory&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;without-reuse&quot;&gt;Without Reuse&lt;/h2&gt;
&lt;p&gt;The standard approach is to not care about re-using a process or shell and letting NodeJS just spawn &amp;#x26; kill them automatically. For example:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; childProcess&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;child_process&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; osInfo&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; childProcess.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;execSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;uname -a&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; files&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; childProcess.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;execSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ls -a&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	osInfo,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	files&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you wanted an async based approach, you could use something like:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; childProcess&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;child_process&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; osInfo&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;rej&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	childProcess.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;exec&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;uname -a&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;out&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(out));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; files&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;rej&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	childProcess.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;exec&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ls -a&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;out&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(out));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	osInfo,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	files&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;spawning-an-actual-shell-process&quot;&gt;Spawning an Actual Shell Process&lt;/h2&gt;
&lt;p&gt;With most of &lt;a href=&quot;https://nodejs.org/api/child_process.html&quot;&gt;the &lt;code&gt;child_process&lt;/code&gt; commands&lt;/a&gt; you can always specify that a command should execute inside of a shell:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;child_process&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;execSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;uname -a&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	shell: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In fact, you can even pass in whatever shell you want used, as a path:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// This will fail on Windows&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;child_process&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;execSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;uname -a&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	shell: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/bin/sh&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, the shell that is spawned by &lt;code&gt;execSync&lt;/code&gt;, &lt;code&gt;exec&lt;/code&gt;, etc., is sort of &lt;em&gt;managed&lt;/em&gt; by the method, and not exposed directly. If your command is &lt;code&gt;ls&lt;/code&gt;, even with &lt;code&gt;shell: true&lt;/code&gt;, you are still piping commands to &lt;code&gt;ls&lt;/code&gt;, not to the underlying shell. However… what if you spawn the shell &lt;em&gt;as&lt;/em&gt; your command? That works!&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; spawnedShell&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;child_process&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;spawn&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/bin/sh&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Capture stdout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;spawnedShell.stdout.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;data&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(d.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Write some input to it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;spawnedShell.stdin.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;write&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;uname -a&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// We can add more commands, but make sure to terminate&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;spawnedShell.stdin.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;write&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ls -a&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// End&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;spawnedShell.stdin.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;end&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is pretty neat! However, it is not super portable (&lt;code&gt;/bin/sh&lt;/code&gt; is certainly not going to work on Windows), and it is not really well-suited as a persistent shell that we can use in a nice async manner. Let’s refine it.&lt;/p&gt;
&lt;h2 id=&quot;reusing-an-open-shell-in-nodejs&quot;&gt;Reusing an Open Shell in NodeJS&lt;/h2&gt;
&lt;p&gt;The generic steps we can use to set up a long-lived shell to pipe data in and out of in NodeJS is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spawn the shell directly with &lt;code&gt;child_process.spawn()&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Optimal solution would automatically use the correct shell based on OS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add event listeners / hook into the subprocess &lt;em&gt;streams&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;At a minimum, we want to capture &lt;code&gt;stdout&lt;/code&gt;, catch &lt;code&gt;exit&lt;/code&gt;(s), and write to &lt;code&gt;stdin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Allow results to be awaited&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this in mind, I put together a tiny wrapper around &lt;code&gt;child_process.spawn&lt;/code&gt;, which launches &lt;a href=&quot;https://github.com/joshuatz/nodejs-child-process-testing/blob/640269824778a1897b16b5901d58fd25a8946282/persistent-shell.js#L1-L16&quot;&gt;the correct shell&lt;/a&gt; and exposes some utility methods. You can find it &lt;a href=&quot;https://github.com/joshuatz/nodejs-child-process-testing/blob/0932128608486c306f06fedea9e48a11cee68344/persistent-shell.js&quot;&gt;here&lt;/a&gt;, but keep in mind it is not optimized and comes with some caveats.&lt;/p&gt;
&lt;h2 id=&quot;performance&quot;&gt;Performance&lt;/h2&gt;
&lt;p&gt;The main reason why I was looking into this in the first place was that I was wondering if there would be any performance benefits to keeping a shell open and reusing it, rather than launching an entirely new process for every command. I’m working on a project that tries to execute hundreds of commands, so any increase in speed is something I’m interested in.&lt;/p&gt;
&lt;p&gt;Unfortunately, based on my &lt;a href=&quot;https://github.com/joshuatz/nodejs-child-process-testing/blob/0932128608486c306f06fedea9e48a11cee68344/performance-test.js&quot;&gt;simplistic test&lt;/a&gt;, reusing a shell actually is &lt;em&gt;less performant&lt;/em&gt; than just allowing NodeJS to spawn an entirely new process for each command. Sample results (200 commands executed):&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Total Time (ms) (lower is better)&lt;/th&gt;&lt;th&gt;Ops per Sec&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;spawn&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1892&lt;/td&gt;&lt;td&gt;105.7&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;spawnWithShell&lt;/code&gt;&lt;/td&gt;&lt;td&gt;2579&lt;/td&gt;&lt;td&gt;77.5&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;exec&lt;/code&gt;&lt;/td&gt;&lt;td&gt;2634&lt;/td&gt;&lt;td&gt;75.9&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;persistentShellWithCapture&lt;/code&gt; (**)&lt;/td&gt;&lt;td&gt;5008&lt;/td&gt;&lt;td&gt;39.9&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;spawnSync&lt;/code&gt;&lt;/td&gt;&lt;td&gt;5399&lt;/td&gt;&lt;td&gt;37.0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;persistentShell&lt;/code&gt; (**)&lt;/td&gt;&lt;td&gt;5770&lt;/td&gt;&lt;td&gt;34.7&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;execSync&lt;/code&gt;&lt;/td&gt;&lt;td&gt;8982&lt;/td&gt;&lt;td&gt;22.3&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;** = &lt;code&gt;persistentShell&lt;/code&gt; and &lt;code&gt;persistentShellWithCapture&lt;/code&gt; are the wrappers I created around &lt;code&gt;spawn&lt;/code&gt;, which spawn an instance of the default OS shell and pipe commands and data in and out.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;This test was performed on a laptop running Windows 10 and Node v12.x&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As you can see, &lt;code&gt;child_process.spawn&lt;/code&gt; easily performed the best out of all the approaches. This makes a lot of sense, as pretty much all the other approaches build &lt;em&gt;on-top of&lt;/em&gt; spawn, so by definition they are adding overhead. In fact, the idea that child_process methods are all extending from &lt;code&gt;.spawn&lt;/code&gt; is called out in the very first paragraph of &lt;a href=&quot;https://nodejs.org/api/child_process.html&quot;&gt;the official docs on &lt;code&gt;child_process&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;”[…] This capability is primarily provided by the child_process.spawn() function:”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At first, I was a little surprised at just how poorly my shell-reuse technique fared, but in hindsight, it makes sense. Even though the shell is reused, it still has to be launched (initially), and a communication layer maintained to pipe stdin/stdout. Furthermore, any command that is executed &lt;em&gt;still&lt;/em&gt; requires the running of the process to support it; whether you run &lt;code&gt;git log&lt;/code&gt; via a spawned shell or with Node’s &lt;code&gt;spawn&lt;/code&gt;, both cases require the git binary to actually run, and in many cases, spawning and keeping open a shell wrapper around those commands is pretty much pure overhead.&lt;/p&gt;
&lt;h2 id=&quot;other-approaches-libraries&quot;&gt;Other Approaches? Libraries?&lt;/h2&gt;
&lt;p&gt;So, although you definitely &lt;em&gt;can&lt;/em&gt; keep a shell open in NodeJS and pipe things in and out, in general it is not a great idea for both security and performance purposes.&lt;/p&gt;
&lt;p&gt;So, back to the original reason why I was researching this; is there a &lt;em&gt;different&lt;/em&gt; way to speed up slow command execution, especially in bulk? Well, I might have to get back to you, but I have a feeling that another avenue I need to explore more is native bindings.&lt;/p&gt;
&lt;p&gt;For example, &lt;code&gt;git&lt;/code&gt; exposes commands through its binary (which you can call through &lt;code&gt;child_process&lt;/code&gt; or your CLI), but there are also packages that expose &lt;em&gt;native bindings&lt;/em&gt; for Node, where Node talks directly to the low-level code of the program, rather than going through a CLI or spawning processes to execute the commands. In general, talking directly to lower-level (aka nearer to &lt;em&gt;bare metal&lt;/em&gt;) code is going to be faster than passing messages through higher level interfaces.&lt;/p&gt;
&lt;p&gt;A very interesting article I came across (of course, &lt;em&gt;after&lt;/em&gt; I had already written most of this post), is by Github’s engineering team: &lt;a href=&quot;https://github.blog/2017-05-16-integrating-git-in-atom/&quot;&gt;“Integrating Git in Atom”&lt;/a&gt;. They switched &lt;em&gt;from&lt;/em&gt; a native binding package (&lt;a href=&quot;https://www.nodegit.org/&quot;&gt;&lt;code&gt;nodegit&lt;/code&gt;&lt;/a&gt;), to &lt;a href=&quot;https://github.com/desktop/dugite&quot;&gt;executing git commands via spawn&lt;/a&gt; (via &lt;code&gt;child_process.spawn&lt;/code&gt; / &lt;code&gt;execFile&lt;/code&gt;). They noted that there was a significant performance &lt;em&gt;decrease&lt;/em&gt; caused by the overhead in process spawning per command, although they also noted some ways they helped mitigate the effect. This seems to confirm my point though, that native bindings are going to be more performant than spawning a new process for each command.&lt;/p&gt;
&lt;h2 id=&quot;links-of-interest&quot;&gt;Links of Interest&lt;/h2&gt;
&lt;p&gt;These are links I came across while researching this topic, and that were helpful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nodejs/node&quot;&gt;NodeJS source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nodejs/help/issues/1183&quot;&gt;github.com/nodejs/help/issues/1183&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Stack Overflow: &lt;a href=&quot;https://stackoverflow.com/a/16880465/11447682&quot;&gt;Exec - Keep Shell Alive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Stack Overflow: &lt;a href=&quot;https://stackoverflow.com/q/48698234/11447682&quot;&gt;Spawn vs Execute&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;FreeCodeCamp: &lt;a href=&quot;https://www.freecodecamp.org/news/node-js-child-processes-everything-you-need-to-know-e69498fe970a/&quot;&gt;NodeJS Child Processes - Everything You Need to Know&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Adding Extra Attributes to Style and Script Tags in Wordpress</title><link>https://joshuatz.com/posts/2020/adding-extra-attributes-to-style-and-script-tags-in-wordpress/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/adding-extra-attributes-to-style-and-script-tags-in-wordpress/</guid><description>A reusable method for adding arbitrary extra HTML attributes to enqueued styles and scripts in WordPress, using add_data and filters.</description><pubDate>Sat, 24 Oct 2020 19:09:09 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;In WordPress, there are many ways to output a stylesheet &lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt; or JS &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; tag, but it might be unclear how to add arbitrary HTML attributes.&lt;/p&gt;
&lt;p&gt;Through this guide, let’s pretend that our goal is to add the arbitrary attribute &lt;code&gt;data-theme&lt;/code&gt;, and a value of &lt;code&gt;light&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;diff&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FDAEB7&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;-&lt;/span&gt; &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;styles.css&quot; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;+&lt;/span&gt; &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;styles.css&quot; data-theme=&quot;light&quot; /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The easiest, but most unsafe way, is with the &lt;a href=&quot;https://developer.wordpress.org/reference/hooks/wp_head/&quot;&gt;wp_head&lt;/a&gt; or &lt;a href=&quot;https://developer.wordpress.org/reference/hooks/wp_footer/&quot;&gt;wp_footer&lt;/a&gt; hooks. For example, if you wanted to simply output the tag in the &lt;code&gt;&amp;#x3C;head&gt;&lt;/code&gt; of the document, you could use something like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; add_my_tags&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	?&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	&amp;#x3C;!--&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; We&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; can&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; output&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; whatever&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; we&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; want&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; here&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;including&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; arbitrary&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; attributes&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;link&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; rel&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;stylesheet&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; href&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;styles.css&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;theme&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;light&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	&amp;#x3C;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;add_action&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;wp_head&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;add_my_tags&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, this is not &lt;em&gt;ideal&lt;/em&gt;, and goes against best practices; since you are not &lt;em&gt;enqueuing&lt;/em&gt; the resources normally, there is no way for other plugins or theme files to know about it, and you might end up with duplicate scripts and styles getting loaded.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This can be especially problematic for JavaScript files, as they might be required to be loaded in a special order, or will error if loaded more than once.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;idea-reusable-and-shared-data&quot;&gt;Idea: Reusable and Shared Data&lt;/h2&gt;
&lt;p&gt;The safest way to add resource and scripts in WP is always through the global enqueue methods; &lt;a href=&quot;https://developer.wordpress.org/reference/functions/wp_enqueue_style/&quot;&gt;wp_enqueue_style()&lt;/a&gt; and &lt;a href=&quot;https://developer.wordpress.org/reference/functions/wp_enqueue_script/&quot;&gt;wp_enqueue_script()&lt;/a&gt;. However, these methods do not offer any way to add custom attributes to be echoed in the HTML.&lt;/p&gt;
&lt;p&gt;What about &lt;a href=&quot;https://developer.wordpress.org/reference/classes/wp_dependencies/add_data/&quot;&gt;the &lt;code&gt;add_data()&lt;/code&gt; method&lt;/a&gt;, hmm? That &lt;a href=&quot;https://stackoverflow.com/q/27199424/11447682&quot;&gt;looks pretty tempting&lt;/a&gt;…&lt;/p&gt;
&lt;p&gt;Ah, no, that is &lt;em&gt;very misleading&lt;/em&gt;. While &lt;code&gt;add_data()&lt;/code&gt; does allow you add arbitrary properties to a enqueued object &lt;strong&gt;internally in WordPress&lt;/strong&gt; (technically an instance of &lt;code&gt;_WP_Dependency&lt;/code&gt;), only certain attributes are actually processed by WP and affect the &lt;em&gt;echoed out HTML&lt;/em&gt;. For example, for style tags (&lt;code&gt;&amp;#x3C;link&gt;&lt;/code&gt;), WP will recognize:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rtl&lt;/code&gt; (&lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4aca25b296d5506597294b5c4437be08b365546a/wp-includes/class.wp-styles.php#L253&quot;&gt;link&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;suffix&lt;/code&gt; is combined with this (&lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4aca25b296d5506597294b5c4437be08b365546a/wp-includes/class.wp-styles.php#L255-L256&quot;&gt;link&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conditional&lt;/code&gt; (&lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4aca25b296d5506597294b5c4437be08b365546a/wp-includes/class.wp-styles.php#L171&quot;&gt;link&lt;/a&gt;) (used to inject legacy conditional comments)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alt&lt;/code&gt; (&lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4aca25b296d5506597294b5c4437be08b365546a/wp-includes/class.wp-styles.php#L226&quot;&gt;link&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt; (&lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4aca25b296d5506597294b5c4437be08b365546a/wp-includes/class.wp-styles.php#L227&quot;&gt;link&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is also called out / mentioned in the actual exposed functions documentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.wordpress.org/reference/functions/wp_script_add_data/&quot;&gt;wp_script_add_data()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.wordpress.org/reference/functions/wp_style_add_data/&quot;&gt;wp_style_add_data()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, this gives me an idea… what if we could use the power of add_data to store the attributes across the WP system, but then hook into the HTML generation filters to alter the output? We can; read-on for my solution.&lt;/p&gt;
&lt;h2 id=&quot;my-solution&quot;&gt;My Solution&lt;/h2&gt;
&lt;p&gt;Although arbitrary properties added through &lt;code&gt;add_data&lt;/code&gt; that WP does not know about do not affect output by default, that doesn’t mean we can’t use the underlying system to come up with our own solution, and &lt;em&gt;&lt;strong&gt;make&lt;/strong&gt;&lt;/em&gt; &lt;strong&gt;it work that way!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here is what I came up with, which is flexible and reusable. All the code on this page can also be found in &lt;a href=&quot;https://gist.github.com/joshuatz/3d37a01580645729e507837a4e0c3d4a&quot;&gt;this Github Gist&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@author&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; Joshua Tzucker&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@license&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; MIT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@see&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; https://joshuatz.com/posts/2020/adding-extra-attributes-to-style-and-script-tags-in-wordpress/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * These should stay global, so you can access them wherever&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * you need to hack on an extra attribute&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@example&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * ```php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * global $ARB_ATTRIB_PREFIX;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * wp_script_add_data($yourHandle,  $ARB_ATTRIB_PREFIX . &apos;your_key&apos;, &apos;your_val&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * ```&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * You can customize this prefix, but be careful about avoiding collisions, and escape any characters that will break building the regex pattern&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$ARB_ATTRIB_PREFIX &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;arb_att_&amp;#x26;#&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$ARB_ATTRIB_PATTERN &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; .&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $ARB_ATTRIB_PREFIX &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;(.+)/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Callback for WP to hit before echoing out an enqueued resource. This callback specifically checks for any key-value pairs that have been added through `add_data()` and are prefixed with a special value to indicate they should be injected into the final HTML&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; {string} $tag - Will be the full string of the tag (`&amp;#x3C;link&gt;` or `&amp;#x3C;script&gt;`)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; {string} $handle - The handle that was specified for the resource when enqueuing it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; {string} $src - the URI of the resource&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; {string|null} $media - if resources is style, should be the target media, else null&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; {boolean} $isStyle - If the resource is a stylesheet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; scriptAndStyleTagAttributeAdder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($tag, $handle, $src, $media, $isStyle){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    global&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $ARB_ATTRIB_PATTERN;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    $extraAttrs &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; array&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    $nodeName &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Get the WP_Dependency instance for this handle, and grab any extra fields&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ($isStyle) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $nodeName &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;link&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $extraAttrs &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; wp_styles&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;registered[$handle]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;extra;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $nodeName &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;script&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $extraAttrs &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; wp_scripts&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;registered[$handle]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;extra;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Check stored properties on WP resource instance against our pattern&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    $attribsToAdd &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; array&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    foreach&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ($extraAttrs &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $fullAttrKey &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $attrVal) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $matches &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; array&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        preg_match&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($ARB_ATTRIB_PATTERN, $fullAttrKey, $matches);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($matches) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            $attrKey &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $matches[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            $attribsToAdd[$attrKey] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $attrVal;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Actually do the work of adding attributes to $tag&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;count&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($attribsToAdd)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $dom &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; DOMDocument&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        @&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$dom&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;loadHTML&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($tag);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        /** &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@var&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; {DOMElement[]} */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $resourceTags &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $dom&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getElementsByTagName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($nodeName);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        foreach&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ($resourceTags &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $resourceTagNode) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            foreach&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ($attribsToAdd &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $attrKey &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $attrVal) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                $resourceTagNode&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setAttribute&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($attrKey, $attrVal);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $headStr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $dom&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;saveHTML&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($dom&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getElementsByTagName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;head&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // Capture content between &amp;#x3C;head&gt;&amp;#x3C;/head&gt;. Kind of hackish, but should be faster than preg_match&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        $content &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; substr&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($headStr, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;7&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;strlen&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($headStr) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 15&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $content;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $tag;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;add_filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;script_loader_tag&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($tag, $handle, $src){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; scriptAndStyleTagAttributeAdder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($tag, $handle, $src, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;add_filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;style_loader_tag&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($tag, $handle, $src, $media){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; scriptAndStyleTagAttributeAdder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($tag, $handle, $src, $media, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And now you can easily add any arbitrary attributes, like so!:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// This is all it takes now to add an attribute pair that will get picked up by our filter and injected into the HTML tag&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;global&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $ARB_ATTRIB_PREFIX;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Script&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp_script_add_data&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($handle, $ARB_ATTRIB_PREFIX &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;your_attrib_key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;your_attrib_val&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Stylesheet (identical syntax)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp_style_add_data&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;($handle, $ARB_ATTRIB_PREFIX &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;your_attrib_key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;your_attrib_val&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Going back to our original example:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;global&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $ARB_ATTRIB_PREFIX;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp_enqueue_style&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;my-theme&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get_stylesheet_uri&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wp_style_add_data&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;my-theme&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, $ARB_ATTRIB_PREFIX &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;data-theme&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;light&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is a more practical example; we can use this system to enforce &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity&quot;&gt;Subresource Integrity (SRI)&lt;/a&gt; on a &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; tag, to make sure the contents does not change from the time we add it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;php&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; addJqueryWithSRI&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	global&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; $ARB_ATTRIB_PREFIX;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	wp_enqueue_script&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;jquery-3&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	wp_script_add_data&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;jquery-3&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,  $ARB_ATTRIB_PREFIX &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;integrity&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	wp_script_add_data&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;jquery-3&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,  $ARB_ATTRIB_PREFIX &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;crossorigin&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;anonymous&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;add_action&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;wp_enqueue_scripts&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;addJqueryWithSRI&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;For those curious, this is not the first time I have written about hooking into the loader_tag filters - I had to use some similar code when figuring out how to add attributes like &lt;code&gt;defer&lt;/code&gt; and &lt;code&gt;async&lt;/code&gt; to tags, and wrote up my approach &lt;a href=&quot;https://joshuatz.com/posts/2019/wordpress-script-and-style-tags-adding-defer-async-and-lazy-load/&quot;&gt;here&lt;/a&gt;. The research and coding I did for that was very helpful for this situation, and you should checkout that post if you are looking for more info on how WP generates the final HTML for stylesheets and scripts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;why-a-special-prefix&quot;&gt;Why A Special Prefix?&lt;/h3&gt;
&lt;p&gt;You might be wondering why I’m using a unique string (&lt;code&gt;$ARB_ATTRIB_PREFIX&lt;/code&gt;) to prefix any attribute key names we are adding, when later, I’m removing it via Regex. The answer is two-fold:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Collision avoidance with WP Core
&lt;ul&gt;
&lt;li&gt;I want to be very sure that my function only adds attributes that were explicitly requested to be added; for example, the key &lt;code&gt;conditional&lt;/code&gt; is already in use by Wordpress to allow for conditional comment inclusion (this is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Conditional_comment&quot;&gt;legacy IE thing&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Although I could filter out attribute keys that I know already belong to WP, that is not wise, since I would have to manually update it as WP Core changes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Collision avoidance with other plugins / themes
&lt;ul&gt;
&lt;li&gt;I’m sure I’m not the only developer to ever think of hooking into &lt;code&gt;add_data&lt;/code&gt; to store arbitrary parameters and then use them elsewhere. By forcing a unique prefix, I reduce the likelihood of a given key colliding with one added by a plugin or theme.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The sharing of &lt;code&gt;$ARB_ATTRIB_PREFIX&lt;/code&gt; via &lt;code&gt;global&lt;/code&gt; is not required; you could manually just copy and paste the unique prefix whenever you use &lt;code&gt;add_data&lt;/code&gt;, but we all know how dangerous typos can be 😂&lt;/p&gt;
&lt;h3 id=&quot;room-for-improvement-and-alternatives&quot;&gt;Room for Improvement and Alternatives&lt;/h3&gt;
&lt;p&gt;I’m sure there are a lot of ways that the above solution could be improved or altered; it was whipped up in an afternoon to answer a question posed to me. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You could create a simple wrapper function around &lt;code&gt;wp_style_add_data&lt;/code&gt; and &lt;code&gt;wp_script_add_data&lt;/code&gt;, so you don’t have to always remember to grab and then use the global prefix
&lt;ul&gt;
&lt;li&gt;I’ve included sample code for this in &lt;a href=&quot;https://gist.github.com/joshuatz/3d37a01580645729e507837a4e0c3d4a#file-opt-add-data-wrapper-php&quot;&gt;my gist&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;You could bypass &lt;code&gt;add_data&lt;/code&gt; and come up with your system for associating handles with metadata&lt;/li&gt;
&lt;li&gt;Instead of using a unique prefix, you could use a global array (kind of like my &lt;code&gt;$specialLoadHnds&lt;/code&gt; solution in &lt;a href=&quot;https://joshuatz.com/posts/2019/wordpress-script-and-style-tags-adding-defer-async-and-lazy-load/&quot;&gt;this post&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;There might be cleaner (and/or safer) ways to transform the &lt;code&gt;$tag&lt;/code&gt; string and modify attributes; I went with &lt;a href=&quot;DOMDocument&quot;&gt;PHP’s DOM Library&lt;/a&gt; (e.i. the DOMDocument class) based on &lt;a href=&quot;https://stackoverflow.com/a/3577662/11447682&quot;&gt;this StackOverflow&lt;/a&gt;.
&lt;ul&gt;
&lt;li&gt;Although RegEx as a solution for DOM manipulation is almost never a good idea, sometimes it is OK for situations like this one, and might be faster&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-best-solution&quot;&gt;The Best Solution&lt;/h2&gt;
&lt;p&gt;The best solution for this would be to have adding arbitrary attributes to script and style tags &lt;strong&gt;supported natively in WordPress methods&lt;/strong&gt; &lt;em&gt;&lt;strong&gt;to begin with&lt;/strong&gt;&lt;/em&gt;!. This is not an unreasonable thing to ask of a CMS, nor is it a niche concern. What is frustrating is that is this has been proposed (multiple times), and there are unclosed tickets to add it (&lt;a href=&quot;https://core.trac.wordpress.org/ticket/22249&quot;&gt;#22249&lt;/a&gt;, &lt;a href=&quot;https://core.trac.wordpress.org/ticket/33948&quot;&gt;#33948&lt;/a&gt;) from &lt;em&gt;&lt;strong&gt;8 YEARS ago&lt;/strong&gt;&lt;/em&gt; (2012)!&lt;/p&gt;
&lt;p&gt;There have been entire new CMS frameworks, such as Gatsby, that have sprung to life and grown over just a fraction of that time period. Heck, the initial release of React was in 2013. I understand that the WordPress codebase is large and needs to cover a lot of legacy uses (old themes, plugins, etc.), but there is a point where this really should have seen more progress than it has, and that was several years ago IMHO.&lt;/p&gt;
&lt;p&gt;The best scenario is that, any day now, WordPress core is updated with this feature released, and this whole article becomes obsolete (except for legacy WP installs) 😄&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Make Ten Card Game with Svelte and TypeScript</title><link>https://joshuatz.com/projects/web-stuff/make-ten-card-game-with-svelte-and-typescript/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/make-ten-card-game-with-svelte-and-typescript/</guid><description>Online educational addition game, where you try to create sets of playing cards that add up to 10. Built with Svelte and TypeScript, published as open-source.</description><pubDate>Sun, 04 Oct 2020 09:02:32 GMT</pubDate><content:encoded>&lt;h2 id=&quot;how-to-get-it&quot;&gt;How to Get It!&lt;/h2&gt;
&lt;p&gt;🃏 Online Game: &lt;a href=&quot;https://make-ten-card-game.netlify.app/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;make-ten-card-game.netlify.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💾 Source Code: &lt;a href=&quot;https://github.com/joshuatz/make-ten-card-game&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-is-it&quot;&gt;What Is It?&lt;/h2&gt;
&lt;p&gt;I built this at the request of an elementary student tutor, who, due to the current pandemic, is running her sessions over Zoom. “Make Ten” is a simple card game, where you have to get rid of all the cards on the table by making sets that equal ten - for example, an 8 of spades, and a 2 of hearts. The cards are divided up into stacks, and once a stack is “cleared”, you can use its spot as a free place to move cards to in order to get them off the top of a stack.&lt;/p&gt;
&lt;video loop muted autoplay controls src=&quot;/media/make-ten-game-demo.mp4&quot; style=&quot;width: 90%; margin:auto; max-width:600px; display:block;&quot;&gt;
	Sorry, your browser doesn&apos;t support embedded videos.
&lt;/video&gt;
&lt;p&gt;This is meant for students that are learning the basics of addition, so that is about as complex as the game gets.&lt;/p&gt;
&lt;p&gt;It also features typical game functionality like a built-in game timer, pause / resume / restart buttons, and fun animations. I built it as a SPA (single-page application), with Svelte and TypeScript.&lt;/p&gt;
&lt;h2 id=&quot;tools-used&quot;&gt;Tools Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Svelte&lt;/a&gt; (I learned it as I built this; it is my first Svelte project!)&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;HTML&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;IndexedDB (with localForage)&lt;/li&gt;
&lt;li&gt;Netlify (automatic deployments from Github)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;development&quot;&gt;Development&lt;/h2&gt;
&lt;p&gt;This was my first &lt;a href=&quot;https://svelte.dev/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Svelte&lt;/a&gt; project, so I was learning the framework as I built the application. Honestly, I had a great time learning Svelte and will very likely use it in future applications; it was a joy to use, and made building components feel very natural and easy. In fact, the entire game was built in about two days, despite having to learn Svelte as I worked!&lt;/p&gt;
&lt;p&gt;The most complex parts of building the game were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Managing the state of cards / stack
&lt;ul&gt;
&lt;li&gt;I ended up using a multidimensional array, with identifiers for each card&lt;/li&gt;
&lt;li&gt;I had to be extra careful when moving the cards around; there are some particular nuances of how Svelte handles mutations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Animations
&lt;ul&gt;
&lt;li&gt;There are some fancy animations in the game (cards being selected, moved between stacks, discarded)&lt;/li&gt;
&lt;li&gt;Svelte fortunately has a lot of built-in helpers to assist with complex animations; for example, &lt;code&gt;crossfade&lt;/code&gt; and &lt;code&gt;send&lt;/code&gt; / &lt;code&gt;receive&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Offline-First Database Options for Web Applications in 2020</title><link>https://joshuatz.com/posts/2020/offline-first-database-options-for-web-applications-in-2020/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/offline-first-database-options-for-web-applications-in-2020/</guid><description>Researching and summarizing offline database options for modern web applications, including low-level APIs, helpful wrapper libraries, SDKs, and more.</description><pubDate>Mon, 28 Sep 2020 04:12:08 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;For most people building web applications, the general idea is that the user is connected to your service a fair amount of the time; there might be periods of time where they go offline, but the &lt;em&gt;goal&lt;/em&gt; is for a state of being “connected”. This assumption means that the most common methods for storing and retrieving user data for the UI are things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Standard AJAX requests
&lt;ul&gt;
&lt;li&gt;Database agnostic; the client-side code does not even necessarily know what type of DB is on the other end&lt;/li&gt;
&lt;li&gt;Could be REST, GraphQL, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Websocket session
&lt;ul&gt;
&lt;li&gt;Similar to AJAX; database agnostic, requires active connection&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Managed database services (such as Google Firestore and AWS DynamoDB)
&lt;ul&gt;
&lt;li&gt;These typically support an “offline” mode, but the main functionality is for sync / online operations&lt;/li&gt;
&lt;li&gt;You don’t need to have a server&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, there is a gap left by all of the above approaches. What if we are developing something that is &lt;em&gt;meant&lt;/em&gt; to be used offline, after the initial page load? Is there a way we can persist and query user data locally, without ever needing a server?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Apps that are designed to be fully functional without an internet connection are often categorized as “&lt;em&gt;&lt;strong&gt;offline-first&lt;/strong&gt;&lt;/em&gt;”. There is even a &lt;a href=&quot;https://github.com/offlinefirst&quot;&gt;community&lt;/a&gt; around the topic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;options&quot;&gt;Options&lt;/h2&gt;
&lt;p&gt;In 2020, the best overall option for local data persistence is going to be &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API&quot;&gt;the &lt;code&gt;IndexedDB&lt;/code&gt; browser API&lt;/a&gt;. The explanation for why this is the superior option (over &lt;code&gt;localStorage&lt;/code&gt;, cookies, etc.) is long and technical, so I’ll defer to the experts (&lt;a href=&quot;https://web.dev/storage-for-the-web/&quot;&gt;this is a good summary, from web.dev&lt;/a&gt;). However, using IndexedDB is a little tricky (it is a low-level API) and most developers (including myself) are going to prefer a &lt;em&gt;wrapper&lt;/em&gt; library around it, which will make writing code to use it easier.&lt;/p&gt;
&lt;p&gt;My research into the libraries supporting IndexedDB-backed local persistance and querying is the reason for this post. I was also interested in which libraries supported things like manual import / export shortcuts (so I don’t have to write a huge amount of code just so a user can download their &lt;em&gt;own&lt;/em&gt; data), sync, NodeJS support, and legacy fallbacks.&lt;/p&gt;
&lt;h3 id=&quot;comparison-table&quot;&gt;Comparison Table&lt;/h3&gt;
&lt;p&gt;I’ve been using Google Sheets to record my findings - &lt;strong&gt;you can access it &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1YkEKg8rJWEnssxelh7jK06FklWGK1_HS-axzR6PzMK8/edit?usp=sharing&quot;&gt;here&lt;/a&gt;&lt;/strong&gt; (no login required). I’ve also copied and pasted the main table below:&lt;/p&gt;





















































































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Library&lt;/th&gt;&lt;th&gt;IndexedDB&lt;/th&gt;&lt;th&gt;localstorage (fallback)&lt;/th&gt;&lt;th&gt;NodeJS&lt;/th&gt;&lt;th&gt;Sync?&lt;/th&gt;&lt;th&gt;Native Export / Import (aka dump imports)?&lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://dexie.org/&quot;&gt;Dexie.js&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No, &lt;a href=&quot;https://github.com/dfahlander/Dexie.js/issues/480&quot;&gt;not really&lt;/a&gt;.&lt;/td&gt;&lt;td&gt;/ = You can polyfill&lt;/td&gt;&lt;td&gt;/ = &lt;a href=&quot;https://dexie.org/docs/Syncable/Dexie.Syncable.js&quot;&gt;adapter required&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes (&lt;a href=&quot;https://dexie.org/docs/ExportImport/dexie-export-import&quot;&gt;adapter&lt;/a&gt;)&lt;/td&gt;&lt;td&gt;Although legacy support might not be as nice as localForage, and sync not as nice as PouchDB, overall has great performance and is well-liked. Also in active production use &lt;a href=&quot;https://dexie.org/docs/DerivedWork#products-that-use-dexie-what-we-know-of&quot;&gt;with some major players&lt;/a&gt;.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://pouchdb.com/&quot;&gt;PouchDB&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes (&lt;a href=&quot;https://pouchdb.com/adapters.html&quot;&gt;adapter&lt;/a&gt;)&lt;/td&gt;&lt;td&gt;Yes (&lt;a href=&quot;https://pouchdb.com/adapters.html&quot;&gt;adapter&lt;/a&gt;)&lt;/td&gt;&lt;td&gt;Yes!&lt;/td&gt;&lt;td&gt;/ = maybe with some &lt;a href=&quot;https://github.com/pouchdb-community/pouchdb-load&quot;&gt;other libs&lt;/a&gt;&lt;/td&gt;&lt;td&gt;CouchDB interoperability and sync features are the main selling points, but is also a very well thought out product, that is well-liked and maintained. If you are not used to NoSQL / CouchDB, the syntax can be a little daunting.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://rxdb.info/&quot;&gt;RxDB&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://rxdb.info/rx-database.html#dump&quot;&gt;Yes&lt;/a&gt;&lt;/td&gt;&lt;td&gt;RxDB is built on top of PouchDB, and you can think of it as providing a reactive layer between your data and your UI, whereas normally you might need to manually code timed synchronization events. Since it is built on PouchDB, syntax is still NoSQL / documents.&lt;br&gt;&lt;br&gt;It comes with a large feature-set, beyond just the reactivity; schema migrations, ORM capabilities, and field encryption are all features not present in PouchDB, but come out-of-the-box with RxDB.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/localForage/localForage&quot;&gt;localForage&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/localForage/localForage/issues/718&quot;&gt;No&lt;/a&gt;&lt;/td&gt;&lt;td&gt;localForage has been around for a long time, and is stable and well-liked, but it also remains focused on a simple localStorage like API. For some, this might not be enough and you might want to evaluate some of the alternatives with a larger feature set.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://nanosql.io/&quot;&gt;nanoSQL&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/ClickSimply/Nano-SQL/issues/18&quot;&gt;No&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.nanosql.io/query/import-export.html#smart-import-export&quot;&gt;Yes&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Seems like a very ambitious, but well thought out project, with good results. Extensibility and portability seem to be the key here, which is evident in the number of adapters, plugins, and customization options.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/agershun/alasql&quot;&gt;alaSQL&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/agershun/alasql/issues/110&quot;&gt;No&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Looks really neat for large in-memory queries and offers some unique compatibilities (supports Excel `.xls` and `.xlsx`), so might be a good pick for BI applications. For lots of constant persistent syncing (e.g. IndexedDB read / writes), might not be the best pick since the IndexedDB connection &lt;a href=&quot;https://github.com/agershun/alasql/wiki/IndexedDB&quot;&gt;is sort of manual&lt;/a&gt;.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/techfort/LokiJS&quot;&gt;LokiJS&lt;/a&gt;&lt;/td&gt;&lt;td&gt;/ = &lt;a href=&quot;https://github.com/techfort/LokiJS/wiki/LokiJS-persistence-and-adapters#example-using-more-scalable-lokiindexedadapter-&quot;&gt;adapter required&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes (default on Web)&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/techfort/LokiJS/blob/master/tutorials/Persistence%20Adapters.md&quot;&gt;It’s complicated / Depends&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Focus is on in-memory manipulation.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/typicode/lowdb&quot;&gt;lowdb&lt;/a&gt;&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Kind of (since it is just JSON, you should be able to dump and import)&lt;/td&gt;&lt;td&gt;Focus is on fast, easy-to-use, and small footprint. Supports simple `.json` files as persisted database stores. Good for fast prototyping, but might be under-powered for larger projects.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/Nozbe/WatermelonDB&quot;&gt;WatermelonDB&lt;/a&gt;&lt;/td&gt;&lt;td&gt;/ = &lt;a href=&quot;https://nozbe.github.io/WatermelonDB/Installation.html#set-up-database&quot;&gt;Using LokiJS Adapter&lt;/a&gt;&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://nozbe.github.io/WatermelonDB/Advanced/Sync.html&quot;&gt;Yes&lt;/a&gt;.&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/Nozbe/WatermelonDB/issues/221&quot;&gt;No&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Similar to RxDB, WatermelonDB is focused on data reactivity, tied to the React UI layer. Syntax uses a decent amount of ES6 decorators.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/typeorm/typeorm&quot;&gt;TypeORM&lt;/a&gt;&lt;/td&gt;&lt;td&gt;/ = &lt;a href=&quot;https://github.com/typeorm/typeorm/blob/5084e47be4fd42316ad47e6102645534fae45d9f/docs/connection-options.md#sqljs-connection-options&quot;&gt;Using sql.js + localForage adapters&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://typeorm.io/#/multiple-connections/replication&quot;&gt;Not really&lt;/a&gt;&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;ORM, not DB Layer&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://orbitjs.com/&quot;&gt;Orbit&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes (&lt;a href=&quot;https://orbitjs.com/v0.16/guide/data-sources.html&quot;&gt;std adapter&lt;/a&gt;)&lt;/td&gt;&lt;td&gt;Yes (&lt;a href=&quot;https://orbitjs.com/v0.16/guide/data-sources.html&quot;&gt;std adapter&lt;/a&gt;)&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://orbitjs.com/v0.15/guide/coordination.html#Sync-strategies&quot;&gt;Yes&lt;/a&gt;&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;This came from the Ember JS team and seems designed to solve a lot of complex problems. A lot of it is kind of over my head…&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/yathit/ydn-db&quot;&gt;ydn-db&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://yathit.github.io/ydn-db/doc/setup/setup.html#supported-database-storage-engines&quot;&gt;Yes&lt;/a&gt;&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://yathit.github.io/ydn-db/doc/sync/&quot;&gt;Yes&lt;/a&gt;&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;This does not seem in-use or maintained.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://jsstore.net/&quot;&gt;JsStore&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Main focus seems to be on providing SQL syntax, but with IndexedDB backing.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://turtle-db.github.io/&quot;&gt;turtleDB&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Abandoned?&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/Kinto/kinto.js/&quot;&gt;kinto.js&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/Kinto/kinto.js/blob/8bcc224fe4df20ffcf23899a5138ae5a0b02e1a5/docs/api.md#importing-a-data-dump-locally&quot;&gt;Yes&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Looks like this was built by Mozilla employees and used for several large-scale Firefox products / features (see &lt;a href=&quot;https://github.com/mozilla-services/servicedenuages.fr/blob/2a05eb46756ef31a310822119d11edb531d5892d/content/2017.12.kinto-at-mozilla.rst&quot;&gt;“Kinto at Mozilla”&lt;/a&gt;). kinto.js is also a part of a much larger eco-system - &lt;a href=&quot;https://www.kinto-storage.org/&quot;&gt;“Kinto-Storage”&lt;/a&gt;. For sync, it should be noted that this specifically requires a server running &lt;a href=&quot;https://github.com/Kinto/kinto&quot;&gt;the Kinto server&lt;/a&gt; (which runs on Python + PostgreSQL)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;side-note---managed-database-sdks&quot;&gt;Side Note - Managed Database SDKs&lt;/h2&gt;
&lt;p&gt;You might have noticed that some (very popular) managed / serverless databases were left out of the above table. Notably, Google Firestore and AWS DynamoDB. I’m always a little wary of recommending solutions like these, because they typically come with vendor lock-in, proprietary components, and cryptic pricing structures.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;However&lt;/em&gt;… let’s be honest, many developers prefer these frameworks because they are well-supported, thoroughly documented, and easy to use. Some examples of these types of services and how they might (or might not) fit our needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWS &lt;a href=&quot;https://docs.amplify.aws/lib/datastore/getting-started/q/platform/js&quot;&gt;Amplify DataStore&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;This might be the closest to what we are looking for, in a large-scale commercial offering, for several reasons:
&lt;ul&gt;
&lt;li&gt;Designed to align with the &lt;em&gt;offline-first&lt;/em&gt; paradigm; local on-device database, that takes advantage of modern standards (e.g. &lt;code&gt;IndexedDB&lt;/code&gt; on web), while also supporting syncing once online&lt;/li&gt;
&lt;li&gt;Amplify DataStore appears to support a completely offline mode - &lt;em&gt;no AWS account required&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Supports sync (&lt;code&gt;DataStore &amp;#x3C;-&gt; AWS AppSync &amp;#x3C;-&gt; Amazon DynamoDB&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Built-in conflict resolution&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud Firestore
&lt;ul&gt;
&lt;li&gt;My opinion: This does not look like a good fit for “offline-first” applications, for several reasons:
&lt;ul&gt;
&lt;li&gt;Offline data with Firestore is meant to be used for &lt;strong&gt;caching&lt;/strong&gt; or &lt;em&gt;&lt;strong&gt;temporary&lt;/strong&gt;&lt;/em&gt; network outages, but &lt;strong&gt;not&lt;/strong&gt; as an offline-first local database&lt;/li&gt;
&lt;li&gt;They &lt;a href=&quot;https://firebase.googleblog.com/2019/08/why-is-my-cloud-firestore-query-slow.html#:~:text=Your%20offline%20cache%20is%20too%20big&quot;&gt;openly admit&lt;/a&gt; that too much offline data slows down queries&lt;/li&gt;
&lt;li&gt;It &lt;a href=&quot;https://www.reddit.com/r/Firebase/comments/hzelma/lack_of_offlinefirst_firestore_is_driving_me_to/fzj7vj3/&quot;&gt;sounds like&lt;/a&gt; Google is not going to make efforts towards better offline support&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://realm.io/&quot;&gt;Realm&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Realm is an entire platform for data usage in mobile apps. A lot of it seems pretty proprietary. They also do not (yet) have a web version.&lt;/li&gt;
&lt;li&gt;If you are looking for real-time sync with advanced conflict resolution strategies, this might be what you are looking for, but seems a little niche and outside of what I’m looking for&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;side-note---nodejs-and-other-environments&quot;&gt;Side Note - NodeJS and Other Environments&lt;/h2&gt;
&lt;p&gt;Web is a very special environment, especially when it comes to databases. Part of the reason why things like IndexedDB are so important with web is because we don’t have access to the local filesystem with JavaScript; we can’t simply create a SQLite file and read/write to it. However, in NodeJS, as well as native environments like Android, iOS, and desktop applications, we are not subjected to the same restrictions.&lt;/p&gt;
&lt;p&gt;However, several of the approaches and solutions above &lt;em&gt;also&lt;/em&gt; work in non-web environments, such as NodeJS. For example, &lt;a href=&quot;https://rxdb.info/&quot;&gt;RxDB&lt;/a&gt; works with the web, as well as NodeJS, Electron, or even mobile (React-Native).&lt;/p&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;/h2&gt;
&lt;h3 id=&quot;what-should-i-use&quot;&gt;What Should I Use?&lt;/h3&gt;
&lt;p&gt;You might have noticed that I did not declare a &lt;em&gt;winner&lt;/em&gt; out of the options I came across in my research. That is because I don’t really think there is one &lt;em&gt;clear&lt;/em&gt; best pick for all types of web apps. The general conclusion reached is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No matter what, use something backed by the strongest standardized low-level API (that is &lt;code&gt;IndexedDB&lt;/code&gt; for the forseeable future)&lt;/li&gt;
&lt;li&gt;You should decide how much you care about the relationship between the UI and the data
&lt;ul&gt;
&lt;li&gt;Do you want bi-directional data binding? Optimistic UI updates?&lt;/li&gt;
&lt;li&gt;If you care a lot about reactivity, you might want RxDB or WatermelonDB&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Native export / import might not be worth that much as a feature, since you can always code a reusable method anyways, which can also handle versioning conflicts unique to your app&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;more-reading-material&quot;&gt;More Reading Material&lt;/h3&gt;
&lt;p&gt;Looking for other comparison tables of local database tools? Here are some links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/addyosmani/storage-on-the-web&quot;&gt;https://github.com/addyosmani/storage-on-the-web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/storage-for-the-web/&quot;&gt;https://web.dev/storage-for-the-web/&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/storage-for-the-web/#bonus:-why-use-a-wrapper-for-indexeddb&quot;&gt;https://web.dev/storage-for-the-web/#bonus:-why-use-a-wrapper-for-indexeddb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ClickSimply/Nano-SQL/issues/18#issuecomment-380341729&quot;&gt;https://github.com/ClickSimply/Nano-SQL/issues/18#issuecomment-380341729&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nanosql.io/welcome.html#comparison-with-other-projects&quot;&gt;https://nanosql.io/welcome.html#comparison-with-other-projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kinto-storage.org/en/stable/faq.html#how-does-kinto-compare-to-other-solutions&quot;&gt;https://docs.kinto-storage.org/en/stable/faq.html#how-does-kinto-compare-to-other-solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jaredforsyth.com/posts/in-search-of-a-local-first-database/&quot;&gt;https://jaredforsyth.com/posts/in-search-of-a-local-first-database/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://offlinefirst.org/sync/&quot;&gt;http://offlinefirst.org/sync/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/johannesjo/choosing-the-right-frontend-database-41lo&quot;&gt;https://dev.to/johannesjo/choosing-the-right-frontend-database-41lo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://reyesr.github.io/html5-storage-benchmark/&quot;&gt;http://reyesr.github.io/html5-storage-benchmark/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/reactnative/comments/awvn6x/offline_database_options_in_2019/&quot;&gt;https://www.reddit.com/r/reactnative/comments/awvn6x/offline_database_options_in_2019/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/vuejs/comments/a9hzgz/good_clientside_database_for_offline_first_apps/&quot;&gt;https://www.reddit.com/r/vuejs/comments/a9hzgz/good_clientside_database_for_offline_first_apps/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.simform.com/react-native-database-selection-guide/&quot;&gt;https://www.simform.com/react-native-database-selection-guide/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are looking for a much more technical analysis of offline-first database options than I can offer, you should check out this series of blog posts by Jared Forsyth: &lt;a href=&quot;https://jaredforsyth.com/posts/in-search-of-a-local-first-database/&quot;&gt;“In Search of a Local-First Database”&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;Cover image by &lt;a href=&quot;https://unsplash.com/@matti_johnson?utm_source=unsplash&amp;#x26;utm_medium=referral&amp;#x26;utm_content=creditCopyText&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Matti Johnson&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Adding an Emoji-Log Picker With Native VS Code Snippets</title><link>https://joshuatz.com/posts/2020/adding-an-emoji-log-picker-with-native-vs-code-snippets/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/adding-an-emoji-log-picker-with-native-vs-code-snippets/</guid><description>How VSCode Snippets can be used to add support for a customizable Emoji-Log picker, with just a few lines of code you can paste into settings.</description><pubDate>Tue, 22 Sep 2020 21:29:03 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a quick guide on how to add an Emoji-Log picker macro to your VSCode setup, with a custom snippet that is just a few lines of code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;/media/VSCode-Snippet-Emoji-Log.gif&quot; style=&quot;margin:auto;display:block;width:95%;height:auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/VSCode-Snippet-Emoji-Log.gif&quot; alt=&quot;Emoji Log Picker, implemented through a custom VSCode Snippet&quot; style=&quot;width:100%;max-width:768px;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-is-emoji-log&quot;&gt;What is Emoji-Log?&lt;/h2&gt;
&lt;p&gt;If you haven’t heard of the Emoji-Log project / standard (by &lt;a href=&quot;https://ahmadawais.com/&quot;&gt;Ahmad Awais&lt;/a&gt;), you should &lt;a href=&quot;https://github.com/ahmadawais/Emoji-Log&quot;&gt;go check it out&lt;/a&gt;. If you look at a lot of open-source repos, chances are you have actually come across its use in the wild at least once or twice. The TLDR is that it is a proposed standard / philosophy for writing the headline of commit messages, and using emoji prefixes to easily convey the type of change made. For example:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;🐛 FIX: Broken redirect pattern&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is already &lt;a href=&quot;https://github.com/ahmadawais/Emoji-Log#vscode-extension&quot;&gt;a VSCode extension for Emoji-Log&lt;/a&gt;, but I wanted to share how I added an Emoji-Log picker / macro to my VSCode configuration, with just a few lines of code and the support of VSCode Snippets. It serves as a good example of how simple, yet capable, VSCode snippets can be.&lt;/p&gt;
&lt;h2 id=&quot;vscode-snippets&quot;&gt;VSCode Snippets&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets&quot;&gt;VSCode snippets&lt;/a&gt; are an incredibly powerful, yet underutilized part of VSCode. With enough manipulation, they can essentially act as text-expanders, macros, and formatters (and &lt;a href=&quot;https://joshuatz.com/posts/2020/5-underrated-built-in-vscode-features/#user-defined-snippets&quot;&gt;this isn’t my first promoting them&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Picking a predefined text choice and inserting it is an excellent use-case for snippets, and the impetus for this post.&lt;/p&gt;
&lt;h2 id=&quot;solution-emoji-log-vscode-snippet&quot;&gt;Solution: Emoji-Log VSCode Snippet&lt;/h2&gt;
&lt;p&gt;Here is the snippet code:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;Emoji Log Picker&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;prefix&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Emoji Log Picker&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Pick an Emoji Log Emoji to use as a prefix&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;body&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;${1|📦 NEW,👌 IMPROVE,🐛 FIX,📖 DOC,🚀 RELEASE,🤖 TEST,‼️ BREAKING|}: ${2:IMPERATIVE_MESSAGE_GOES_HERE}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;details&gt;
	&lt;summary&gt;Click to see version that defaults to clipboard contents first&lt;/summary&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;Emoji Log Picker&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;prefix&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Emoji Log Picker&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Pick an Emoji Log Emoji to use as a prefix&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;body&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;${1|📦 NEW,👌 IMPROVE,🐛 FIX,📖 DOC,🚀 RELEASE,🤖 TEST,‼️ BREAKING|}: ${2:${CLIPBOARD:IMPERATIVE_MESSAGE_GOES_HERE}}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;h3 id=&quot;adding-the-snippet-to-vscode&quot;&gt;Adding the Snippet to VSCode&lt;/h3&gt;
&lt;p&gt;To add this to your VSCode environment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bring up the &lt;em&gt;command palette&lt;/em&gt; (&lt;code&gt;CTRL + SHIFT + P&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Start typing &lt;code&gt;snippets&lt;/code&gt;, and then select &lt;code&gt;Preferences: Configure User Snippets&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;If you already have a global snippets file, select it, otherwise create a new one with &lt;code&gt;New Global Snippets file&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;In the snippets file that opens, paste the above snippet&lt;/li&gt;
&lt;li&gt;You are done! Now, in almost anywhere in VSCode, you can press &lt;code&gt;CTRL + SPACE&lt;/code&gt; to bring up snippets, and then start typing &lt;code&gt;Emoji&lt;/code&gt; to bring up the log picker.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are looking for a more generic guide on VSCode snippets, &lt;a href=&quot;https://adamtheautomator.com/vs-code-snippets/&quot;&gt;this one&lt;/a&gt; looks good. And don’t forget &lt;a href=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets&quot;&gt;the official docs&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;breaking-down-how-it-works&quot;&gt;Breaking Down How It Works&lt;/h3&gt;
&lt;p&gt;Since this snippet is pretty simple, it serves as a good example for learning how VSCode Snippets work.&lt;/p&gt;
&lt;p&gt;First, let’s break down the main properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prefix&lt;/code&gt;: Serves as the main text string to be used for “triggering” the snippet; a better name for this might actually be &lt;em&gt;keywords&lt;/em&gt;. This can be an array of strings, but a single string seems to work just was well (since VS uses sub-string matching)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;: Optional. Gets displayed by IntelliSense, in that little popup window as it shows snippets to pick from&lt;/li&gt;
&lt;li&gt;&lt;code&gt;body&lt;/code&gt;: The main &lt;em&gt;thing&lt;/em&gt; that is inserted when the snippet is run
&lt;ul&gt;
&lt;li&gt;💡 TIP: The main way that snippets work is by &lt;em&gt;inserting&lt;/em&gt; content, but through some tricky syntax, you can use them for formatting, replacement, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Another helpful optional field is &lt;code&gt;scope&lt;/code&gt;. We could add that to our snippet with a value of &lt;code&gt;&quot;plaintext,markdown&quot;&lt;/code&gt;, if we only wanted our snippet to be suggested in those contexts. Without a &lt;code&gt;scope&lt;/code&gt; field, the snippet will appear everywhere (unless the snippet file itself is &lt;a href=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets#_language-snippet-scope&quot;&gt;scoped by language&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Next, let’s break down the &lt;code&gt;body&lt;/code&gt;, the crucial part of the snippet. For the purposes of explanation, we can simply from this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;${1|📦 NEW,👌 IMPROVE,🐛 FIX,📖 DOC,🚀 RELEASE,🤖 TEST,‼️ BREAKING|}: ${2:IMPERATIVE_MESSAGE_GOES_HERE}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;${ 1 | CHOICE_A,CHOICE_B | }: ${ 2: IMPERATIVE_MESSAGE_GOES_HERE }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt; |             |                 |                 |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt; |-&gt; Placeholder wrapper         |                 |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;               |                 |                 |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;               |-&gt; Choice        |-&gt; Tabstop       |-&gt; Plain text&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To break it down another way&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;${}&lt;/code&gt; are placeholder wrappers&lt;/li&gt;
&lt;li&gt;The number prefixes (&lt;code&gt;{$1}&lt;/code&gt;, &lt;code&gt;${2:}&lt;/code&gt;) inside the placeholders are &lt;a href=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets#_tabstops&quot;&gt;&lt;code&gt;tabstops&lt;/code&gt;&lt;/a&gt;.
&lt;ul&gt;
&lt;li&gt;Unfortunately, they &lt;a href=&quot;https://stackoverflow.com/a/47686131/11447682&quot;&gt;are mandatory with choice elements&lt;/a&gt;, which is why I have tab stop with the choice section, even though there should be nothing for the user to type in that spot&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;To offer choices, you need to use &lt;a href=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets#_choice&quot;&gt;a special syntax&lt;/a&gt; with pipe wrappers:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;| CHOICE_A,CHOICE_B |&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;: &lt;/code&gt; between placeholder wrappers is plain text that will be output&lt;/li&gt;
&lt;li&gt;The benefits to using a placeholder wrapper in the second part instead of just using &lt;code&gt;$2IMPERATIVE_MESSAGE_GOES_HERE&lt;/code&gt; are:
&lt;ul&gt;
&lt;li&gt;When we tab to the second part, anything we type will replace the entire placeholder text&lt;/li&gt;
&lt;li&gt;The placeholder syntax is easier to read&lt;/li&gt;
&lt;li&gt;It makes it easier to upgrade our snippet later and expand with variables, nested placeholders, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;adding-the-clipboard&quot;&gt;Adding the Clipboard&lt;/h4&gt;
&lt;p&gt;If we want the imperative message section to default to clipboard text, while still allowing the user to customize it, we can use the &lt;a href=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables&quot;&gt;placeholder + variable syntax&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We simply replace this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;${2:IMPERATIVE_MESSAGE_GOES_HERE}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;… with this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;${2:${CLIPBOARD:IMPERATIVE_MESSAGE_GOES_HERE}}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now have a nested placeholder element, where it will fill with the value of the &lt;code&gt;CLIPBOARD&lt;/code&gt; value, defaulting to &lt;code&gt;IMPERATIVE_MESSAGE_GOES_HERE&lt;/code&gt; if the value does not exist. And since it is at a tabstop, it is easy for the user (aka &lt;em&gt;you!&lt;/em&gt;) to customize the message.&lt;/p&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;/h2&gt;
&lt;p&gt;Even if you are not interested in using Emoji-Log, I hope you still learned something about VSCode snippets and maybe got some inspiration for your own workflow improvements!&lt;/p&gt;
&lt;!-- Uncomment on cross-posts --&gt;
&lt;!--
## More About Me:

 - 🔗&lt;a href=&quot;https://joshuatz.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;joshuatz.com&lt;/a&gt;
 - 👨‍💻&lt;a href=&quot;https://dev.to/joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;dev.to/joshuatz&lt;/a&gt;
 - 💬&lt;a href=&quot;https://twitter.com/1joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;@1joshuatz&lt;/a&gt;
 - 💾&lt;a href=&quot;https://github.com/joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;github.com/joshuatz&lt;/a&gt;
--&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Getting The Word Count of Multiple Markdown Files</title><link>https://joshuatz.com/posts/2020/getting-the-word-count-of-multiple-markdown-files/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/getting-the-word-count-of-multiple-markdown-files/</guid><description>How to use various tools to extract the word count from a markdown file, or even the summed count across multiple nested markdown files.</description><pubDate>Tue, 15 Sep 2020 16:57:45 GMT</pubDate><content:encoded>&lt;h2 id=&quot;issue&quot;&gt;Issue&lt;/h2&gt;
&lt;p&gt;I use Markdown on pretty much a daily basis. I enjoy its portability and wide acceptance, as well as its non-proprietary nature. However, certain &lt;em&gt;literary&lt;/em&gt; features tend to be lackluster in the Markdown ecosystem, primarily because Markdown is most commonly used by developers, not writers (not to say that there is not overlap!). One of those features that it took me a little bit of time to suss out is how to calculate word counts for markdown files.&lt;/p&gt;
&lt;h2 id=&quot;cli--shell-solutions-&quot;&gt;CLI / Shell Solutions 👩‍💻&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Most of the below code snippets assume you are using a Unix-based OS, or an OS that has common Unix commands available (such as Windows, with Git-Bash installed, or &lt;code&gt;coreutils&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We can build up a custom string of commands to get the results we want. Let’s break it down:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Getting word count (from plain text)
&lt;ul&gt;
&lt;li&gt;If we want to accomplish this from the command line, it turns out there is a handy command we can use, aptly named &lt;a href=&quot;https://ss64.com/bash/wc.html&quot;&gt;&lt;code&gt;wc&lt;/code&gt; (word count)&lt;/a&gt;. We can use &lt;code&gt;wc -w&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Getting word count (from markdown file)
&lt;ul&gt;
&lt;li&gt;We first need to convert the markdown file into plain text, before passing to &lt;code&gt;wc -w&lt;/code&gt;, to avoid inflating our count with MD syntax stuff&lt;/li&gt;
&lt;li&gt;We can use one of my favorite markdown processors, &lt;a href=&quot;https://pandoc.org/&quot;&gt;&lt;code&gt;pandoc&lt;/code&gt;&lt;/a&gt;, for converting MD to plain text.
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pandoc --strip-comments -t plain {markdown_file_path.md}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Putting the above together with wc for word count:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pandoc --strip-comments -t plain {markdown_file_path.md} | wc -w&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;(BONUS) - What about getting a total word count across multiple files?
&lt;ul&gt;
&lt;li&gt;For a bulk word count, we can use another command that will produce the list of filepaths, pass that into our previous command, and let &lt;code&gt;wc&lt;/code&gt; sum it all up&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;ls | xargs pandoc --strip-comments -t plain | wc -w&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;find . -iname &quot;*.md&quot; | xargs pandoc --strip-comments -t plain | wc -w&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;solution-summary&quot;&gt;Solution Summary&lt;/h3&gt;
&lt;p&gt;So, to summarize some quick options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Word count for &lt;strong&gt;single file&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pandoc --strip-comments -t plain {markdown_file_path.md}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Word count for &lt;strong&gt;multiple files&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;find . -iname &quot;*.md&quot; | xargs pandoc --strip-comments -t plain | wc -w&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;accuracy-&quot;&gt;Accuracy 🎯&lt;/h3&gt;
&lt;p&gt;What makes something a “word”? With the above commands, anything separated by spaces is a word, including code, URLs, and other snippets that many might argue should not be included in the word count calculation.&lt;/p&gt;
&lt;p&gt;The best way to address this, if you care to, is to use a Pandoc filter (either a &lt;a href=&quot;https://pandoc.org/MANUAL.html#option--filter&quot;&gt;program&lt;/a&gt;, or &lt;a href=&quot;https://pandoc.org/MANUAL.html#option--lua-filter&quot;&gt;Lua script&lt;/a&gt;). I won’t go into details, but here are two resources that cover how to use this feature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Torrecillas: &lt;a href=&quot;https://www.danieltorrecillas.com/blog/meaningful-word-count-for-markdown/&quot;&gt;“Meaningful Word Count for Markdown”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://superuser.com/a/1303683/1103513&quot;&gt;StackExchange / Superuser&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;editor--gui-solutions-&quot;&gt;Editor / GUI Solutions 🖱&lt;/h2&gt;
&lt;p&gt;Many Markdown editors support native word count reporting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nano (use &lt;code&gt;ALT + D&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://typora.io/&quot;&gt;Typora&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marktext.app/&quot;&gt;Mark Text&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://notepad-plus-plus.org/&quot;&gt;Notepad++&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Oddly enough, VSCode does not &lt;em&gt;natively&lt;/em&gt; offer a word-count feature, but there are extensions you can install (such as &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ms-vscode.wordcount&quot;&gt;this one&lt;/a&gt;) which will provide that feature.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠ Warning: Many of the above editors use the same word-count rules as &lt;code&gt;wc&lt;/code&gt;, which counts code and other non-prose content. See my notes under &lt;a href=&quot;#accuracy-&quot;&gt;“Accuracy”&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Selenium Webdriver - Disabling Chrome Logging Messages</title><link>https://joshuatz.com/posts/2020/selenium-webdriver---disabling-chrome-logging-messages/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/selenium-webdriver---disabling-chrome-logging-messages/</guid><description>How to disable Chrome&apos;s default stdout log messages that come through Selenium Webdriver on NodeJS, such as the &quot;DevTools listening on ws...&quot; message.</description><pubDate>Wed, 09 Sep 2020 06:50:16 GMT</pubDate><content:encoded>&lt;p&gt;This is a quick post, but took a little digging to come up with the solution featured.&lt;/p&gt;
&lt;h2 id=&quot;the-issue&quot;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;The issue, which you can find many users complaining about, is that when using Chrome as the browser with Selenium Webdriver and NodeJS, the default log preferences don’t seem to be respected and messages still appear in the console / stdout.&lt;/p&gt;
&lt;p&gt;To start with, it seems at first as though it is impossible to get rid of this log message: “&lt;code&gt;DevTools listening on ws://....&lt;/code&gt;” when first connecting.&lt;/p&gt;
&lt;p&gt;To be clear, even if you &lt;strong&gt;explicitly&lt;/strong&gt; tell Selenium that you want all logging off, you will still get this message. My first attempt tried to use this approach and failed:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; wdLogging&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;selenium-webdriver/lib/logging&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; loggingPrefs&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wdLogging.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Preferences&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; logType&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wdLogging.Type) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	loggingPrefs.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setLevel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(logType, wdLogging.Level.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;OFF&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// I still got messages after creating a webdriver instance with these preferences&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;/h2&gt;
&lt;p&gt;The solution involves the use of &lt;code&gt;excludeSwitches&lt;/code&gt; - these are switches / flags that you want to &lt;em&gt;exclude&lt;/em&gt; &lt;code&gt;ChromeDriver&lt;/code&gt; from &lt;em&gt;including&lt;/em&gt; by default. So, anything passed through &lt;code&gt;excludeSwitches&lt;/code&gt; basically negates / removes a flag normally set as true by &lt;code&gt;ChromeDriver&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Turns out, &lt;code&gt;ChromeDriver&lt;/code&gt; has a default switch that enables logging - &lt;code&gt;--enable-logging&lt;/code&gt;; you need to pass this to Selenium to turn it off. I found this via &lt;a href=&quot;https://stackoverflow.com/a/56408800/11447682&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;this S/O answer&lt;/a&gt; and the &lt;a href=&quot;https://bugs.chromium.org/p/chromedriver/issues/detail?id=2907&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;linked ChromeDriver Issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A concise example of adding the switch is this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;Options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;selenium-webdriver/chrome&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; chromeOptions&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;chromeOptions.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;excludeSwitches&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;enable-logging&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But, for a full copy-and-paste example, here is working code:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// @ts-check&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; path&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;path&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;ServiceBuilder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;Options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;ChromeOptions&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;selenium-webdriver/chrome&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; webdriver&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;selenium-webdriver&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// I&apos;m doing this because I hate adding things to my PATH 😅&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Normally, webdriver will automatically grab the EXE from PATH if you have followed a generic setup guide&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; driverBinPath&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; path.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;normalize&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`C:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\\&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Users&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\\&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Joshua&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\\&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Downloads&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\\&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;chromedriver_win32&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\\&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;chromedriver.exe`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; serviceBuilder&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ServiceBuilder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(driverBinPath);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	 * SOLUTION:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	 * By excluding the switch from defaults, this should disable the default logging&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	 * &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@see&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; https://bugs.chromium.org/p/chromedriver/issues/detail?id=2907#c3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	 */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; chromeOptions&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ChromeOptions&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	chromeOptions.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;excludeSwitches&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;enable-logging&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Put everything together&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; driver&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; webdriver.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;Builder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;forBrowser&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;chrome&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setChromeService&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(serviceBuilder)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setChromeOptions&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(chromeOptions)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;build&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Let&apos;s try something&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; driver.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`https://example.com`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; title&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; driver.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getTitle&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; title;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;then&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(console.log);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also find the source code pasted into &lt;a href=&quot;https://gist.github.com/joshuatz/93cd7798d20e26de0dc9e6795b71fdcb&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;this Github Gist&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;other-languages&quot;&gt;Other Languages&lt;/h3&gt;
&lt;p&gt;Since Selenium supports a lot of different programming languages, I’m not going to post the full code for each one, but the solution should still be the same, as long as Selenium is passing &lt;code&gt;excludeSwitches&lt;/code&gt; through for that implementation.&lt;/p&gt;
&lt;p&gt;For example, in Python, the main difference would be changing:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// NodeJS Code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;Options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;selenium-webdriver/chrome&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; chromeOptions&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;chromeOptions.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;excludeSwitches&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;enable-logging&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;… to:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Python Code&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;options &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; webdriver.ChromeOptions()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;options.add_experimental_option(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;excludeSwitches&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;enable-logging&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;some-more-background&quot;&gt;Some More Background&lt;/h3&gt;
&lt;p&gt;I didn’t want to spend too much time digging into this, but my guess at why this behavior was introduced and past logging options no longer works is that the new flag probably overrides any granular settings, such as &lt;a href=&quot;https://stackoverflow.com/a/20748376/11447682&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the old solution of &lt;code&gt;--log-level-3&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is also important to note that ChromeDriver is a completely separate project from Selenium Webdriver; part of why it took me a bit to find the solution is because I kept searching &lt;a href=&quot;https://github.com/SeleniumHQ/selenium&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Selenium’s source code&lt;/a&gt; for text that was too specific to ChromeDriver - ChromeDriver itself actually lives in &lt;a href=&quot;https://github.com/chromium/chromium&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the Google &lt;code&gt;chromium&lt;/code&gt; mono-repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There might be other solutions out there, but this one worked for me! Feel free to let me know if you find differently!&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Fixing JSX types between Preact and React Libraries</title><link>https://joshuatz.com/posts/2020/fixing-jsx-types-between-preact-and-react-libraries/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/fixing-jsx-types-between-preact-and-react-libraries/</guid><description>How to fix JSX TypeScript errors due to incompatible types between Preact and imported React component libraries, especially for JSX.Element.</description><pubDate>Sun, 06 Sep 2020 13:25:26 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-issue&quot;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;I have a small project that I’m starting, and I decided to try out Preact for the first time, using &lt;a href=&quot;https://github.com/preactjs-templates/typescript&quot;&gt;their TypeScript starter template&lt;/a&gt;. Everything installed (relatively) smoothly, and it seemed like everything was fine… until I tried to add a third-party component library. Then everything exploded.&lt;/p&gt;
&lt;p&gt;To be specific, I tried to add &lt;a href=&quot;https://chakra-ui.com/&quot;&gt;Chakra UI&lt;/a&gt;, and immediately got this error (“cannot be used as a JSX component”) when trying to add any component:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&apos;___&apos; cannot be used as a JSX component.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Its return type &apos;ReactElement&amp;#x3C;any, any&gt; | null&apos; is not a valid JSX element.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	Type &apos;ReactElement&amp;#x3C;any, any&gt;&apos; is not assignable to type &apos;Element&apos;.ts(2786)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trying to chase down this incompatible return type (“not a valid JSX element”) led me down a long trail, but the basics seemed to be that Preact components, such as &lt;code&gt;FunctionalComponent&lt;/code&gt; (used by the template’s default &lt;code&gt;App.tsx&lt;/code&gt; file) expect &lt;code&gt;preact.VNode&lt;/code&gt; elements to make up children / returned elements / etc. This makes sense; &lt;code&gt;preact.VNode&lt;/code&gt; is part of Preact’s VDOM implementation, which is distinctly different from React’s. It’s also worth noting where this comes from; in Preact’s JSX system, &lt;code&gt;Element&lt;/code&gt; (aka &lt;code&gt;JSX.Element&lt;/code&gt;) extends &lt;code&gt;preact.VNode&amp;#x3C;any&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What confused me about this error was two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why is this happening? According to &lt;a href=&quot;https://github.com/preactjs/preact-compat#preact-compat&quot;&gt;this page&lt;/a&gt;, &lt;code&gt;preact-compat&lt;/code&gt; is included by default in the newer versions of Preact. And I’ve followed the &lt;a href=&quot;https://preactjs.com/guide/v10/getting-started#typescript-preactcompat-configuration&quot;&gt;one step listed in the docs&lt;/a&gt; - setting &lt;code&gt;skipLibCheck&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;How do I fix this? Is there a way in TypeScript to override namespaces and module definitions, to the extent that it could fix third party imported code?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;the-solutions&quot;&gt;The Solutions&lt;/h2&gt;
&lt;p&gt;The first issue I found (&lt;a href=&quot;https://github.com/omgovich/react-colorful/issues/39&quot;&gt;react-colorful/#39&lt;/a&gt;) was very close to mine, but not yet solved. However, after some more digging, I finally ended up on &lt;a href=&quot;https://github.com/preactjs/preact/issues/2222&quot;&gt;preact #2222&lt;/a&gt;, &lt;a href=&quot;https://github.com/preactjs/preact/issues/2150&quot;&gt;#2150&lt;/a&gt;, and probably the most relevant, &lt;a href=&quot;https://github.com/preactjs/preact/pull/2329&quot;&gt;PR #2329&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🚧 Once PR #2329 is merged and released, I believe that &lt;strong&gt;this issue will no longer exist&lt;/strong&gt;, and you won’t have to do any manual patching to get third party React libraries to work.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To summarize, the main issue seems to be that, although Preact is “shipping” a lot of types needed for React compatibility, they aren’t necessarily getting automatically “pulled in” for TypeScript to understand that a third-party lib’s use of JSX Element should be aliased / pointed to Preact’s implementation.&lt;/p&gt;
&lt;h3 id=&quot;option-a---use-path-mapping&quot;&gt;Option A - Use Path Mapping&lt;/h3&gt;
&lt;p&gt;TypeScript’s config has an incredibly powerful option that allows import paths to be remapped to destinations of our choice; &lt;a href=&quot;https://www.typescriptlang.org/tsconfig#paths&quot;&gt;the &lt;code&gt;paths&lt;/code&gt; option&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For our case, we can use this to remap the resolution of &lt;code&gt;react&lt;/code&gt; and &lt;code&gt;react-dom&lt;/code&gt; to the &lt;code&gt;preact-compat&lt;/code&gt; layer, which exposes Preact’s &lt;code&gt;JSX&lt;/code&gt; implementation, among other Preact types.&lt;/p&gt;
&lt;p&gt;Here is the change to &lt;code&gt;tsconfig.json&lt;/code&gt; (thanks to &lt;a href=&quot;https://github.com/preactjs/preact/pull/2329#issuecomment-584589507&quot;&gt;this comment&lt;/a&gt;):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;compilerOptions&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Needed in most cases&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;skipLibCheck&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// You need a value set for baseUrl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;baseUrl&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;.&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// This is the key - module resolution path remapping&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;paths&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;			&quot;react&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;node_modules/preact/compat/&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;			&quot;react-dom&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;node_modules/preact/compat/&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠ Warning: If you are using a separate bundler / compiler (outside of TSC), you might also need to inform it of the path aliasing. For example, &lt;a href=&quot;https://github.com/preactjs/preact/issues/2150#issuecomment-558132626&quot;&gt;if you use Parcel&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the types exported from &lt;code&gt;preact/compat&lt;/code&gt; don’t cover it for you, and you need to provide more React type shims, another variant of the above solution is to create your own compatibility declaration file (&lt;code&gt;react/index.d.ts&lt;/code&gt;), and use the &lt;code&gt;paths&lt;/code&gt; option to map to it. This approach is used in &lt;a href=&quot;https://github.com/preactjs/preact/issues/2150#issuecomment-558325544&quot;&gt;the issue #2150 thread&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;option-b---manually-override-types&quot;&gt;Option B - Manually Override Types&lt;/h3&gt;
&lt;p&gt;I couldn’t get this working 100% (at least not without turning on &lt;code&gt;skipLibCheck&lt;/code&gt;), but I believe it &lt;em&gt;might&lt;/em&gt; be possible to “monkey-patch” the definitions for the React module in-place, by using TypeScript’s &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation&quot;&gt;module augmentation and declaration merging features&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This was my (bad) attempt, which was close to working, but violated a lot of TS rules:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; React &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;react&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;declare&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; module&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;react&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	namespace&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; React&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; FC&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;P&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {}&gt; &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; preact&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;FunctionComponent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;P&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; React;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; namespace&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; React&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, I have a feeling this is the wrong approach; &lt;em&gt;&lt;strong&gt;namespace&lt;/strong&gt;&lt;/em&gt; merging is a complicated topic with TypeScript, and it looks like the best solution for global module augmentation is to follow the pattern of:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creating a folder with the module name&lt;/li&gt;
&lt;li&gt;Creating an index file in that folder (&lt;code&gt;/{moduleName}/index.d.ts&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Telling TSC about the file (if necessary)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Again, these steps are outlined &lt;a href=&quot;https://github.com/preactjs/preact/issues/2150#issuecomment-558325544&quot;&gt;in this comment&lt;/a&gt; on #2150, and partially &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-modifying-module-d-ts.html&quot;&gt;in the TS Docs&lt;/a&gt;. If you can find better documentation / best practices, &lt;em&gt;please&lt;/em&gt; let me know; this is a part of TS that I always feel I could use a better grasp of.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Getting Files In a Project Based on Gitignore Exclusions</title><link>https://joshuatz.com/posts/2020/getting-files-in-a-project-based-on-gitignore-exclusions/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/getting-files-in-a-project-based-on-gitignore-exclusions/</guid><description>How to get a list of all files in a project or directory, with inclusion and exclusion rules applied based on a .gitignore file.</description><pubDate>Wed, 02 Sep 2020 16:32:03 GMT</pubDate><content:encoded>&lt;p&gt;Given that so many projects use git and its &lt;code&gt;.gitignore&lt;/code&gt; file for specifying which files should be tracked as part of the repo’s source code, it is very tempting to reuse the &lt;code&gt;.gitignore&lt;/code&gt; file as input into various devops tools and automated scripts. How can we do this? 🤔&lt;/p&gt;
&lt;h2 id=&quot;with-git-itself&quot;&gt;With Git Itself&lt;/h2&gt;
&lt;p&gt;If we have access to &lt;code&gt;git&lt;/code&gt; and its command line interface, there are several ways that we can natively get a list of files based on the gitignore patterns.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One-by-one: &lt;code&gt;check-ignore&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;You can check whether a specific file is excluded, by using &lt;a href=&quot;https://git-scm.com/docs/git-check-ignore&quot;&gt;the &lt;code&gt;git-check-ignore&lt;/code&gt; command&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;git check-ignore {myFilePath}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bulk: &lt;code&gt;ls-files&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;You can use &lt;a href=&quot;https://git-scm.com/docs/git-ls-files&quot;&gt;the &lt;code&gt;git-ls-files&lt;/code&gt; command&lt;/a&gt; to query git for lots of info related to its file tracking&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;git ls-files --full-name&lt;/code&gt; to get full paths of files that are &lt;em&gt;not&lt;/em&gt; ignored&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limitation&lt;/strong&gt;: Slightly complicated to capture everything you need:
&lt;ul&gt;
&lt;li&gt;Tracked files &lt;em&gt;only&lt;/em&gt;: standard &lt;code&gt;git ls-files&lt;/code&gt; works&lt;/li&gt;
&lt;li&gt;Untracked &lt;em&gt;only&lt;/em&gt;, but respect &lt;code&gt;.gitignore&lt;/code&gt;: Need to use &lt;code&gt;git ls-files --others --exclude-standard&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;BOTH&lt;/strong&gt;&lt;/em&gt; tracked and untracked, and respect ignores: &lt;code&gt;git ls-files --others --exclude-standard --cached&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bulk: &lt;code&gt;ls-tree&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;You can use &lt;a href=&quot;https://git-scm.com/docs/git-ls-tree&quot;&gt;the &lt;code&gt;git-ls-tree&lt;/code&gt; command&lt;/a&gt; for &lt;em&gt;tree object&lt;/em&gt; based file ls operations&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;git ls-tree --full-tree -r --name-only HEAD&lt;/code&gt; - get all file paths, from root dir (&lt;a href=&quot;https://stackoverflow.com/a/8533413/11447682&quot;&gt;credit&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;cd subdir &amp;#x26;&amp;#x26; git ls-tree -r --name-only --full-name HEAD&lt;/code&gt; to get full filepaths from a subdirectory
&lt;ul&gt;
&lt;li&gt;Note that we drop &lt;code&gt;--full-tree&lt;/code&gt; and add &lt;code&gt;--full-name&lt;/code&gt; to make sure we get files only for the subdirectory, but with absolute paths&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limitation&lt;/strong&gt;: Requires a reference to a git &lt;em&gt;object&lt;/em&gt; (e.g. &lt;code&gt;HEAD&lt;/code&gt;, SHA, etc.). Therefore, &lt;em&gt;&lt;strong&gt;fatally fails&lt;/strong&gt;&lt;/em&gt; on a brand new repo (no commit history).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;best-git-command&quot;&gt;Best Git Command?&lt;/h3&gt;
&lt;p&gt;Based on the above research, the best command to get an exhaustive file list of &lt;em&gt;&lt;strong&gt;included&lt;/strong&gt;&lt;/em&gt; files, when you care about respecting &lt;code&gt;.gitignore&lt;/code&gt; but don’t care about tracking status, is:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ls-files&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --full-name&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --others&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --cached&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --exclude-standard&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;#                |--&gt; Get full file name (abs path)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;#                            |--&gt; Include *untracked*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;#                                     |--&gt; Include *tracked*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;#                                                   |---&gt; Apply `.gitignore` rules&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want the same as above, but want the filenames of those files specifically &lt;em&gt;&lt;strong&gt;excluded&lt;/strong&gt;&lt;/em&gt; (the inverse of above), add the &lt;code&gt;--ignored&lt;/code&gt; flag.&lt;/p&gt;
&lt;h2 id=&quot;with-libraries&quot;&gt;With Libraries&lt;/h2&gt;
&lt;p&gt;Using the Git commands above have the benefit of being portable and requiring only access to &lt;code&gt;git&lt;/code&gt; and a terminal. However, they also come with caveats, complexity, and the limitations that come with shell scripting (😬).&lt;/p&gt;
&lt;p&gt;If you have access to NodeJS, there are a lot of libraries out there that will make retrieving file lists a lot less of a headache 😅&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modules / API only
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/ignore&quot;&gt;NPM: &lt;code&gt;ignore&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Glob expanders that respect &lt;code&gt;.gitignore&lt;/code&gt; or allow ignore patterns to be used
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/globby&quot;&gt;NPM: &lt;code&gt;globby&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CLI
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/globby-cli&quot;&gt;NPM: &lt;code&gt;globby-cli&lt;/code&gt;&lt;/a&gt; (this is a wrapper around &lt;code&gt;globby&lt;/code&gt;, above)
&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;npx globby-cli &quot;images/**&quot; --gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Make sure you include the &lt;code&gt;--gitignore&lt;/code&gt; flag, since the default for respecting it is false.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/globstar&quot;&gt;NPM: &lt;code&gt;globstar&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>NPM Package: static-build-cache CLI and Module</title><link>https://joshuatz.com/projects/web-stuff/npm-package-static-build-cache-cli-and-module/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/npm-package-static-build-cache-cli-and-module/</guid><description>CLI and module tool - static-build-cache package - built to help avoid redundant builds for static website projects.</description><pubDate>Fri, 28 Aug 2020 05:34:29 GMT</pubDate><content:encoded>&lt;h2 id=&quot;how-to-get-it&quot;&gt;How to Get It!&lt;/h2&gt;
&lt;p&gt;📦 NPM Package: &lt;a href=&quot;https://www.npmjs.com/package/static-build-cache&quot;&gt;&lt;code&gt;static-build-cache&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💾 Source Code: &lt;a href=&quot;https://github.com/joshuatz/static-build-cache&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-is-it&quot;&gt;What Is It?&lt;/h2&gt;
&lt;p&gt;A tool for projects that have a build step that produces static website content; it uses Git to check for committed changes and avoids redundant rebuilds if nothing has actually changed. Speeds up &lt;code&gt;build &amp;#x26; serve&lt;/code&gt; type deployments.&lt;/p&gt;
&lt;p&gt;For more details, make sure to checkout the &lt;a href=&quot;https://github.com/joshuatz/static-build-cache&quot;&gt;repository and README&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;how-does-it-work&quot;&gt;How Does It Work?&lt;/h2&gt;
&lt;p&gt;The easiest way to use it is to add it as a dependency and then call it from the command line, or &lt;code&gt;package.scripts&lt;/code&gt;, with the exposed command: &lt;code&gt;static-build-cache&lt;/code&gt;. You can also import it and use it as a JS module.&lt;/p&gt;
&lt;p&gt;It is designed to be able to run with a zero-config input, but has a bunch of optional parameters (such as the serve command to execute) which can be specified.&lt;/p&gt;
&lt;h2 id=&quot;tools-used&quot;&gt;Tools Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Commander (CLI parsing)&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;Bash / Cmd scripting&lt;/li&gt;
&lt;li&gt;Ava (tests)&lt;/li&gt;
&lt;li&gt;NYC (coverage)&lt;/li&gt;
&lt;li&gt;Github Actions (automated test runs)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>How to Fix Android Messages Having No Notification Sound</title><link>https://joshuatz.com/posts/2020/how-to-fix-android-messages-having-no-notification-sound/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/how-to-fix-android-messages-having-no-notification-sound/</guid><description>How to fix Android&apos;s default Messages app having no notification sound for incoming text messages, even with ringer volume set to maximum.</description><pubDate>Wed, 26 Aug 2020 04:22:15 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This guide is specifically for &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.google.android.apps.messaging&quot;&gt;Google’s “Messages” app (&lt;code&gt;com.google.android.apps.messaging&lt;/code&gt;)&lt;/a&gt; - this is the default / stock messages app that comes with many new Android phones, including the newest Pixel 4A model. &lt;em&gt;However&lt;/em&gt;, some of the advice about generic notifications settings is applicable to all apps and Android in general. Especially for apps that have their own notification controls.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-issue&quot;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;The problem is pretty straightforward.&lt;/p&gt;
&lt;p&gt;After getting a brand new phone (Pixel 4a, with Android 10), incoming text messages / SMS no longer triggered an audible notification sound. Even after tweaking global notifications settings and setting ringer volume to maximum, no text messages could trigger an audio alert. There would be a &lt;strong&gt;visual&lt;/strong&gt; notification shown, but no sound.&lt;/p&gt;
&lt;p&gt;Searching across the internet, it is clear this is not an isolated incident. At first, I was going to blame myself, but when the same exact thing happened to some one else I knew, I realized there is a clear issue at hand.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;After tweaking every setting I could find related to notifications, I finally found the solution in a menu buried several layers deep. Turns out that Messages has its &lt;em&gt;own&lt;/em&gt; notifications settings that override &lt;em&gt;global&lt;/em&gt; settings, and for some reason, &lt;em&gt;&lt;strong&gt;the notification sound was set to none!&lt;/strong&gt;&lt;/em&gt; Setting it to any other sound option fixed it.&lt;/p&gt;
&lt;p&gt;There are a few ways to get to this buried setting.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the “Messages” App&lt;/li&gt;
&lt;li&gt;Tap the three dots in the upper right to open the menu, then select &lt;code&gt;Settings&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tap the &lt;code&gt;Notifications&lt;/code&gt; menu option&lt;/li&gt;
&lt;li&gt;Tap the &lt;code&gt;Incoming messages&lt;/code&gt; menu option&lt;/li&gt;
&lt;li&gt;Make sure the setting on this page is set to “Alerting” and not “Silent”. Now look towards the bottom of the screen and tap the &lt;code&gt;Advanced&lt;/code&gt; label to expand the advanced sub menu&lt;/li&gt;
&lt;li&gt;In advanced sub menu, look for the &lt;code&gt;Sound&lt;/code&gt; option. If it is set to &lt;code&gt;None&lt;/code&gt;, then this is definitely your issue!&lt;/li&gt;
&lt;li&gt;Tap the &lt;code&gt;Sound&lt;/code&gt; option and select one of your ringtones / notification; this will be the sound that plays when you get a new message. &lt;em&gt;&lt;strong&gt;Make sure to hit save&lt;/strong&gt;&lt;/em&gt;!&lt;/li&gt;
&lt;li&gt;You are done! Try to test it by having someone send you a text message. Or use a site like &lt;a href=&quot;https://www.anonymoustext.com/&quot;&gt;this one&lt;/a&gt; to test it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Alternatively, you can get to the setting by starting a slightly different way:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Long press the “Messages” app icon in your app drawer or home screen, and tap the &lt;code&gt;App Info&lt;/code&gt; option or icon&lt;/li&gt;
&lt;li&gt;Tap the &lt;code&gt;Notifications&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;You can now start on step 4 of the above section.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you want to see these steps in action, below is a screen recording I made:&lt;/p&gt;
&lt;iframe style=&quot;display:block; margin: auto;&quot; width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube-nocookie.com/embed/oqwHJoqGCb4&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also find the video above &lt;a href=&quot;https://www.youtube.com/watch?v=oqwHJoqGCb4&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;If this fix did not solve your issue, feel free to explore &lt;a href=&quot;#further-troubleshooting&quot;&gt;the “Further troubleshooting” section below&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-culprit--theories&quot;&gt;The Culprit / Theories&lt;/h2&gt;
&lt;p&gt;Since I observed this issue happening to two different users who were both switching to new phones (with Android 10), my guess is that it has to do with Google’s protocol for device migration and setting sync (related: &lt;a href=&quot;https://developer.android.com/guide/topics/data/backup&quot;&gt;backup overview&lt;/a&gt;). My theory is something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User is on an older version of Android. They have their messaging app notifications set to either &lt;code&gt;default notification sound&lt;/code&gt;, a system specific sound, or a custom sound&lt;/li&gt;
&lt;li&gt;User gets a brand new Android phone and / or upgrades to Android 10+&lt;/li&gt;
&lt;li&gt;The Android setup process tries to transfer the old setting, but the previous setting is not an option on the new phone. This could be because the ringtone file did not get transferred over, the previous user used a system level notification sound which was deprecated in Android 10, or some other reason.&lt;/li&gt;
&lt;li&gt;Rather than default to a specific notification sound, the system defaults to &lt;code&gt;null&lt;/code&gt; / &lt;code&gt;None&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If I could talk to a Google engineer, I would point out that this feels like a UX process that could be improved. I would argue that the majority of users probably want &lt;em&gt;some&lt;/em&gt; sound set as the default, even if they can’t keep their old setting.&lt;/p&gt;
&lt;h2 id=&quot;further-troubleshooting&quot;&gt;Further troubleshooting&lt;/h2&gt;
&lt;p&gt;If the above fix did not solve your issue, here are some further things you can try (in combination with making sure the above has been tried):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make sure there is a default notification sound set:
&lt;ul&gt;
&lt;li&gt;Search: “default notification sound”&lt;/li&gt;
&lt;li&gt;Path: &lt;code&gt;Settings -&gt; Apps &amp;#x26; Notifications -&gt; Notifications -&gt; Advanced -&gt; Default notification sound&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Make sure “Do Not Disturb” mode (aka &lt;em&gt;DND&lt;/em&gt;) is &lt;em&gt;&lt;strong&gt;OFF&lt;/strong&gt;&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;Search “do not disturb” OR “dnd”&lt;/li&gt;
&lt;li&gt;Path: &lt;code&gt;Settings -&gt; Sound -&gt; Do Not Disturb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;You can also toggle DND through the Android pull down shade, and depending on your settings, you might also have DND configured to turn on automatically based on schedule and/or other triggers&lt;/li&gt;
&lt;li&gt;If it is ON, you should see a persistent icon in the status bar that looks something like this:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/media/android-do-not-disturb-dnd-icon-in-status-bar.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/android-do-not-disturb-dnd-icon-in-status-bar.png&quot; alt=&quot;Android&amp;#x27;s &amp;#x27;Do Not Disturb&amp;#x27; (aka DND) Mode Icon in Status Bar&quot;&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Make sure your &lt;em&gt;&lt;strong&gt;Ring volume&lt;/strong&gt;&lt;/em&gt; is &lt;em&gt;not&lt;/em&gt; set to zero / vibrate only
&lt;ul&gt;
&lt;li&gt;It used to be that “ringtone” volume was separate from “notifications” volume, but starting with Android 10 (?), these have been combined into one setting: &lt;em&gt;“Ring volume”&lt;/em&gt;. If it is set to zero or vibrate only, then &lt;em&gt;no&lt;/em&gt; notifications will emit sound (unless there is an app-specific override?)
&lt;ul&gt;
&lt;li&gt;If it is set to vibrate only, you should see this icon in your status bar:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/media/android-vibrate-only-icon-in-status-bar.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/android-vibrate-only-icon-in-status-bar.png&quot; alt=&quot;Android vibrate only ringtone mode icon in status bar&quot;&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If it is set to zero &lt;em&gt;and&lt;/em&gt; vibrate is off, you should see this icon in your status bar:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/media/android-ringtone-set-to-silent-icon-in-status-bar.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/android-ringtone-set-to-silent-icon-in-status-bar.png&quot; alt=&quot;Android&amp;#x27;s icon for completely silent ringer, with vibration off, in status bar&quot;&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Warning: Using the physical rocker buttons on your phone to adjust volume no longer adjusts the ring volume (starting with Android Pie?) - it adjusts the &lt;em&gt;Media&lt;/em&gt; volume. I agree with &lt;a href=&quot;https://support.google.com/pixelphone/thread/13644854?hl=en&quot;&gt;those dissenting&lt;/a&gt; that this is a silly change for Android to make.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Make sure that you don’t have individual per-contact / conversation notifications set to silent
&lt;ul&gt;
&lt;li&gt;See below section&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;per-contact-messages-notification-settings&quot;&gt;Per Contact Messages Notification Settings&lt;/h3&gt;
&lt;p&gt;Android actually lets you set the messaging notifications setting as granular as per-contact / conversation. So, for example, contact “Joshua” that texts you too many jokes that don’t need immediate attention can be set to “silent”, whereas “Boss” is set to “priority”, since you never want to miss a text from them.&lt;/p&gt;
&lt;p&gt;Unfortunately, this is also a way that your notifications settings can get screwed up. There are a few ways to check for this, and remedy it.&lt;/p&gt;
&lt;p&gt;If you want to check to see if any contacts have a customized messaging notification setting, navigate to &lt;code&gt;Settings -&gt; Apps &amp;#x26; Notifications -&gt; Conversations&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This screen shows you any text conversations / contacts that have a special non-default notification setting applied (or if you even opened up that panel in the past)&lt;/li&gt;
&lt;li&gt;There seems to be a glitch with this settings page, at least on my device:
&lt;ul&gt;
&lt;li&gt;All entries show as &lt;code&gt;Default settings&lt;/code&gt; within the list, even if they are actually set to &lt;code&gt;Silent&lt;/code&gt; or something else, which does show up if you click through to change the setting&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To fix this, and restore contacts to the default setting, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use the above route ( &lt;code&gt;Settings -&gt; Apps &amp;#x26; Notifications -&gt; Conversations&lt;/code&gt;) and change each entry in the list to &lt;code&gt;Default&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Within individual messaging conversations within the &lt;em&gt;Messages&lt;/em&gt; app, you can access the notification setting for that conversation by clicking the three dots in the upper right, then &lt;code&gt;Details&lt;/code&gt;, and finally &lt;code&gt;Notifications&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bulk: In my research, the only bulk approach that I came across was by clearing the data for the messaging app. According to multiple comments, &lt;a href=&quot;https://www.reddit.com/r/GooglePixel/comments/ivnxp0/help_reset_conversations_settings_android_11/gc004bi&quot;&gt;this will reset all the per-conversation notification settings, but not delete any messages&lt;/a&gt;.
&lt;ul&gt;
&lt;li&gt;I am slightly reluctant to recommend this, as it is something I have never tried. If you were to try this, I would strongly recommend backing up all your messages first (like &lt;a href=&quot;https://www.androidcentral.com/how-back-up-restore-text-messages-android&quot;&gt;in this guide&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;video-overview&quot;&gt;Video Overview&lt;/h3&gt;
&lt;p&gt;This video does an excellent job of covering an exhaustive list of reasons why incoming messages might not be triggering audio alerts:&lt;/p&gt;
&lt;iframe style=&quot;display:block; margin: auto;&quot; width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube-nocookie.com/embed/D7-9Jhkqb0o&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;p&gt;It is also uploaded &lt;a href=&quot;https://www.youtube.com/watch?v=D7-9Jhkqb0o&quot;&gt;here&lt;/a&gt;, and &lt;a href=&quot;https://www.youtube.com/watch?v=jYHGQFl3uAk&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Video PiP (Picture-in-Picture) Enabler Bookmarklet</title><link>https://joshuatz.com/mini-tools/2020/video-pip-picture-in-picture-enabler-bookmarklet/</link><guid isPermaLink="true">https://joshuatz.com/mini-tools/2020/video-pip-picture-in-picture-enabler-bookmarklet/</guid><description>Bookmarklet to enable Picture-in-Picture (PiP) support on sites that try to disable it, such as Hulu, and especially in browsers that honor it, such as Chrome.</description><pubDate>Sat, 22 Aug 2020 04:24:30 GMT</pubDate><content:encoded>&lt;h2 id=&quot;vid-popper-bookmarklet&quot;&gt;Vid Popper Bookmarklet&lt;/h2&gt;
&lt;p&gt;This tool is pretty simple. It is a bookmarklet (bookmark that executes a tiny bit of JavaScript when clicked) that removes any HTML attributes or JavaScript properties that might try and prevent the use of PiP, and then automatically pops out the first playing video on the page, or if on Hulu, the main video player.&lt;/p&gt;
&lt;p&gt;This enables PiP mode on Hulu, on &lt;strong&gt;both&lt;/strong&gt; Firefox and Chrome.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This tool will override the disabled elements in both Firefox and Chrome, but will only auto-pop the videos in Chrome, since Firefox does not yet support &lt;code&gt;requestPictureInPicture()&lt;/code&gt;. See &lt;a href=&quot;https://support.mozilla.org/en-US/kb/about-picture-picture-firefox&quot;&gt;this page&lt;/a&gt; for how to manually pop videos in Firefox.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are still unable to use PiP, make sure you have the newest version of your browser, that your browser supports PiP, and that it is not disabled behind a &lt;em&gt;feature-flag&lt;/em&gt;. See &lt;a href=&quot;https://support.mozilla.org/en-US/kb/about-picture-picture-firefox&quot;&gt;details for Firefox&lt;/a&gt;, and &lt;a href=&quot;https://developers.google.com/web/updates/2017/09/picture-in-picture&quot;&gt;details for Chrome&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;usage&quot;&gt;Usage:&lt;/h3&gt;
&lt;p&gt;You can find the source code &lt;a href=&quot;https://gist.github.com/joshuatz/61f72bd9d5432f758743ccfe5e8f4ca9&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Or, more simply, drag this bookmark to your toolbar and click it when on a site you want to use PiP on: &lt;a href=&quot;javascript:void%20function(){let%20a=!1;document.querySelectorAll(%22video%22).forEach(b=%3E{b.removeAttribute(%22disablepictureinpicture%22),b.disablePictureInPicture=!1,b.paused%26%26%22content-video-player%22!==b.id||a||%22function%22!=typeof%20b.requestPictureInPicture||b.requestPictureInPicture().then(()=%3E{a=!0}).catch(a=%3E{console.error(&amp;#x60;failed%20to%20pop%20video&amp;#x60;,b,a)})})}();&quot;&gt;Vid Popper&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;
&lt;p&gt;I am a huge fan of browsers offering PiP (picture-in-picture) for video playback. I often watch videos while I tinker on the computer, picking shows that are light in plot and I can kind of tune in &amp;#x26; out of while I do things, and PiP is perfect for this. It lets me shrink a video down as small as possible, so I can put the majority of my focus on the task at hand, but also keep the video on top, as I move windows around.&lt;/p&gt;
&lt;p&gt;I recently noticed that for Hulu, despite PiP being available and working on Firefox, PiP options were curiously absent on Chrome. I figured Firefox probably just had more of the spec implemented and didn’t think much of it, until it started to bother me and I decided to poke around the webpage.&lt;/p&gt;
&lt;p&gt;I quickly noticed that Hulu had actually added some overrides to their video elements to purposely &lt;em&gt;disable&lt;/em&gt; PiP support - namely an HTML attribute (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#:~:text=disablePictureInPicture&quot;&gt;disablePictureInPicture&lt;/a&gt;) as well as JavaScript properties. The reason why it was working in Firefox but not Chrome, is that the overrides are part of the spec that is still “up in the air” so speak (more accurately, marked as &lt;em&gt;“experimental”&lt;/em&gt;), and Chrome is currently honoring them, while Firefox is not.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Testing Git Outside Source - Thank You Temp Directories!</title><link>https://joshuatz.com/posts/2020/testing-git-outside-source---thank-you-temp-directories/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/testing-git-outside-source---thank-you-temp-directories/</guid><description>How and why to use OS temporary directories to create files outside your source code directory for scripted tests.</description><pubDate>Thu, 20 Aug 2020 06:07:58 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I use Git on a daily basis, as my default version control system and integration with Github. I also am acquainted with writing tests, and how to integrate those with Git - e.g., which test files should be &lt;code&gt;gitignore&lt;/code&gt;’d vs tracked. However, I just encountered a new wrinkle, which is how to deal with tests that rely on Git itself, or rather, the absence of Git. This pretty much only comes up when building tools for the developer toolchain / devops, but that is exactly what I’m doing at the moment.&lt;/p&gt;
&lt;p&gt;I’ve actually &lt;a href=&quot;https://github.com/joshuatz/git-date-extractor&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;previously created a Git based tool&lt;/a&gt; and test suites for it; however, those tests used the easy trick of nested git repositories. If you have &lt;code&gt;project&lt;/code&gt; which is a git project, and you create &lt;code&gt;project/git-test&lt;/code&gt; and then run &lt;code&gt;git init&lt;/code&gt; inside &lt;code&gt;git-test&lt;/code&gt;, you end up with a separate git repository inside your actual source code’s one, complete with separate tracking (there are issues however, if you forget to gitignore the child repository).&lt;/p&gt;
&lt;p&gt;The complication is when you need to test your tool against a directory that is &lt;em&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;/em&gt; part of a git-initialized repository. For example, if your test makes sure that your code throws the appropriate error when ran against a non-git project, where &lt;code&gt;git log&lt;/code&gt; will fail with &lt;code&gt;fatal: not a git repository&lt;/code&gt;, you need such a directory to exist to test against. &lt;em&gt;&lt;strong&gt;Where do you create that test directory?&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you create it inside your source code, like &lt;code&gt;src/__tests__/tmp&lt;/code&gt;, it will inherit the git tracking of &lt;code&gt;src&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you try to go up a level from your source code (e.g. &lt;code&gt;src/../tmp&lt;/code&gt;) and create it there, you can’t be sure that won’t lead to a directory collision, or that your code hasn’t ended up nested in another git repository.&lt;/li&gt;
&lt;li&gt;Creating it at the highest level (e.g. &lt;code&gt;/&lt;/code&gt;) seems like a bad idea, and one might argue it doesn’t respect the User’s filesystem&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-solution-os-designated-temporary-paths&quot;&gt;The Solution: OS Designated Temporary Paths&lt;/h2&gt;
&lt;p&gt;I found the solution to this issue in two places, at roughly the same time. The first place was in checking out how an existing tool, &lt;a href=&quot;https://github.com/vaab/gitchangelog&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;gitchangelog&lt;/code&gt;&lt;/a&gt;, creates temporary directories with Python, and the second place was in the NodeJS &lt;a href=&quot;https://nodejs.org/api/fs.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;fs&lt;/code&gt; docs&lt;/a&gt;, when I was trying to look up some methods.&lt;/p&gt;
&lt;p&gt;If you are more seasoned with {{devops, IT sysadmin, etc.}} than I am, you might have already identified the solution long ago; &lt;a href=&quot;https://en.wikipedia.org/wiki/Temporary_folder&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;OS temporary directories&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Essentially, over time a standard practice has emerged for operating systems to expose a &lt;em&gt;temporary&lt;/em&gt; directory, which is periodically cleaned, and guaranteed to exist on boot.&lt;/p&gt;
&lt;h3 id=&quot;standard-library-wrappers-and-implementations&quot;&gt;Standard Library Wrappers and Implementations&lt;/h3&gt;
&lt;p&gt;Using temporary directories is such a common practice that many “standard libraries” have methods built-in to get the OS temp dir path, as well as create temporary folders directly.&lt;/p&gt;
&lt;p&gt;Here are some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NodeJS
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;os.tmpdir()&lt;/code&gt; to get system temp dir (&lt;a href=&quot;https://nodejs.org/api/os.html#os_os_tmpdir&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;docs&lt;/a&gt;, &lt;a href=&quot;https://github.com/nodejs/node/blob/03293aa3a1e810c5ae6938cae41bf62ae418bb5f/lib/os.js#L130-L145&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;implementation&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;Also see &lt;a href=&quot;https://github.com/sindresorhus/temp-dir&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;sindresorhus/temp-dir&lt;/a&gt;, for version that always returns real path, even if system returns symlink&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fs.mkdtemp()&lt;/code&gt; to make temporary directory (&lt;a href=&quot;https://nodejs.org/api/fs.html#fs_fs_mkdtemp_prefix_options_callback&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;docs&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Python - &lt;code&gt;tempfile&lt;/code&gt; (&lt;a href=&quot;https://docs.python.org/3/library/tempfile.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;docs&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tempfile.gettempdir()&lt;/code&gt; to get system temp dir (&lt;a href=&quot;https://docs.python.org/3/library/tempfile.html#tempfile.gettempdir&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;docs&lt;/a&gt;, &lt;a href=&quot;https://github.com/python/cpython/blob/3.8/Lib/tempfile.py#L279-L289&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;implementation&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;Wrapper around &lt;code&gt;_get_default_tempdir()&lt;/code&gt; to get system temp dir (private, &lt;a href=&quot;https://github.com/python/cpython/blob/3.8/Lib/tempfile.py#L177-L220&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;implementation&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;OS default strings are from &lt;code&gt;_candidate_tempdir_list()&lt;/code&gt; (&lt;a href=&quot;https://github.com/python/cpython/blob/3.8/Lib/tempfile.py#L150-L175&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;implementation&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tempfile.mkdtemp()&lt;/code&gt; to make temporary directory (&lt;a href=&quot;https://docs.python.org/3/library/tempfile.html#tempfile.mkdtemp&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;docs&lt;/a&gt;, &lt;a href=&quot;https://github.com/python/cpython/blob/3.8/Lib/tempfile.py#L334-L372&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;implementation&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;QT5
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;QDir::tempPath()&lt;/code&gt; to get system temp dir (&lt;a href=&quot;https://doc.qt.io/qt-5/qdir.html#tempPath&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;QTemporaryDir&lt;/code&gt; to make temporary directory (&lt;a href=&quot;https://doc.qt.io/qt-5/qtemporarydir.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;docs&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that using temporary directories goes beyond just finding the right directory; the sub-directories that you create also need to have unique names that won’t collide - many of the methods above automatically do this for you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;usage-considerations&quot;&gt;Usage Considerations&lt;/h2&gt;
&lt;p&gt;Using temporary directories comes with some important caveats, the most important being that your data is unlikely to be persisted for very long. Some OSes will periodically clear temporary storage, or do so on events like reboots, or users might even choose to manually clear their own temporary folder when their HDD is nearing capacity.&lt;/p&gt;
&lt;p&gt;For my use-case, creating a non-git-initialized directory with a few files for testing, this is perfect. In fact, best practice would be to clear / remove this test directory after each run, so the fact that it might not be persisted past a reboot is a moot concern anyways.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>How to Fix Create-React-App Projects Stalling on Glitch</title><link>https://joshuatz.com/posts/2020/how-to-fix-create-react-app-projects-stalling-on-glitch/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/how-to-fix-create-react-app-projects-stalling-on-glitch/</guid><description>Details on how default create-react-app projects will stall and fail to start on Glitch, and how to fix this issue with just a simple edit.</description><pubDate>Sat, 15 Aug 2020 19:09:40 GMT</pubDate><content:encoded>&lt;p&gt;I recently ran into a perplexing issue with some of my React based projects on Glitch. They would stall in the startup stage (&lt;code&gt;package.json -&gt; scripts -&gt; start&lt;/code&gt;), and eventually the live view would simply display an error in the form of:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;failed to start application on PROJECT_NAME.glitch.me
&lt;br&gt;&lt;br&gt;This is most likely because your project has a code error.
&lt;br&gt;Check your project logs, fix the error and try again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;/media/glitch-failed-to-start-error.jpg&quot; style=&quot;margin: auto; display:block; max-width: 1200px; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/glitch-failed-to-start-error.jpg&quot; alt=&quot;Glitch - Project failing to start after preparing from inactive state&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Normally, I would agree with the error message that in all probability, the issue is probably with my code. However, this was happening with old repos, that had worked before, and the final straw was that it happened 100% of the time with a brand new blank React project scaffolded with Create-React-App.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Click &lt;a href=&quot;#the-fix---quick-packagejson-edit&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt; to jump right to the solution. Read on for full details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-issue&quot;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;A quick search to see if anyone else was experiencing this issue brought me to two posts on the Glitch forums:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://support.glitch.com/t/cant-play-with-with-create-react-app-on-glitch/9514/3&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;“Can’t play with create react app on glitch”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.glitch.com/t/create-react-app-project-never-leaves-starting-state/23793/7&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;“&lt;code&gt;create-react-app&lt;/code&gt; project never leaves “Starting” state”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both of those threads point to the post &lt;a href=&quot;https://dev.to/glitch/create-react-app-and-express-together-on-glitch-28gi&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;“create-react-app and express together on Glitch”&lt;/a&gt; as being the best answer to this issue.&lt;/p&gt;
&lt;p&gt;Although &lt;a href=&quot;https://dev.to/glitch/create-react-app-and-express-together-on-glitch-28gi&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the post mentioned above&lt;/a&gt; does indeed contain a solution, complete with &lt;a href=&quot;https://glitch.com/edit/#!/starter-cra-and-express&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;working Glitch project that you can clone / remix&lt;/a&gt;, I personally find the solution reached to be rather complicated and overall something that founds like a substantial workaround. Not to mention, it starts to become pretty far removed from the default CRA starter…&lt;/p&gt;
&lt;p&gt;However, after some digging, I actually found a far simpler solution. It looks like the crux of the issue might have less to do with conflicting ports, and more to do with CRA’s (and React-Scripts) reliance on having an &lt;em&gt;interactive&lt;/em&gt; shell available (&lt;code&gt;tty&lt;/code&gt; + open &lt;code&gt;stdin&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;There are tons of posts that indicate this &lt;em&gt;could&lt;/em&gt; be the issue, including the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CRA GH: &lt;a href=&quot;https://github.com/facebook/create-react-app/issues/8688&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Issue #8688&lt;/a&gt; (“Fails to start in Docker”)&lt;/li&gt;
&lt;li&gt;S/O: &lt;a href=&quot;https://stackoverflow.com/q/60895246/11447682&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;React App exiting in docker container with exit code 0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I can’t find Glitch’s Docker config to see if they have TTY turned off, but it certainly would line up with the results I’m seeing. And the fact that this appears to be a newer change to CRA, which would explain why repos using older versions of react-scripts still work.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;Thankfully, someone on the thread for issue #8688 identified an easier solution than rolling back CRA scripts or changing the Docker config (which would be impossible on Glitch). In &lt;a href=&quot;https://github.com/facebook/create-react-app/issues/8688#issuecomment-602084087&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;this comment&lt;/a&gt;, they mention using &lt;code&gt;CI=true&lt;/code&gt; to set the environmental variable &lt;code&gt;CI&lt;/code&gt; to be true.&lt;/p&gt;
&lt;p&gt;To clarify, &lt;code&gt;CI&lt;/code&gt; stands for “continuous integration”, and in the context of React, this is important for automated testing. Setting &lt;code&gt;CI=true&lt;/code&gt; is meant to be used &lt;a href=&quot;https://create-react-app.dev/docs/running-tests/#on-your-own-environment&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;when running tests&lt;/a&gt;, for a proper setup. However, it also makes sense that it would work for the &lt;code&gt;tty&lt;/code&gt; issue, since setting &lt;code&gt;CI=true&lt;/code&gt; likely also forces React into a non-interactive mode that does not require an interactive shell, since many CI pipelines also lack an interactive shell for executing automated tests (reasonable).&lt;/p&gt;
&lt;h3 id=&quot;the-fix---quick-packagejson-edit&quot;&gt;The Fix - Quick Package.json Edit&lt;/h3&gt;
&lt;p&gt;If you need to set &lt;code&gt;CI=true&lt;/code&gt; for Glitch, that could be as easy as changing your start entry from &lt;code&gt;react-scripts start&lt;/code&gt; to &lt;code&gt;CI=true react-scripts start&lt;/code&gt;. However, that is going to disable the interactive mode of CRA &lt;em&gt;everwhere&lt;/em&gt;, not just on Glitch!&lt;/p&gt;
&lt;p&gt;If you want to only apply the fix &lt;em&gt;&lt;strong&gt;only&lt;/strong&gt;&lt;/em&gt; on Glitch, there is another trick up my sleeve for you!&lt;/p&gt;
&lt;p&gt;You can take advantage of the fact that Glitch sets certain environmental variables within it’s own system. For example, this will work right now to only apply the &lt;code&gt;CI&lt;/code&gt; fix on Glitch:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;scripts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;start&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;echo $PROJECT_REMIX_CHAIN | grep -E &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#x26;&amp;#x26; CI=true react-scripts start || react-scripts start&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An even &lt;em&gt;easier&lt;/em&gt; fix is to use a tool I built that takes care of checking the environment for you - &lt;a href=&quot;https://www.npmjs.com/package/detect-is-on-glitch&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;“detect-is-on-glitch”&lt;/a&gt;. With that tool, the entire command above could be shortened to:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;scripts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;start&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;isglitch &amp;#x26;&amp;#x26; CI=true react-scripts start || react-scripts start&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;isglitch&lt;/code&gt; is another way to call &lt;code&gt;detect-is-on-glitch&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Plus, my tool scans multiple environmental values, works across multiple OSes, and I’ll update it if Glitch makes changes that stop certain variables from being exposed.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;WARNING: the &lt;code&gt;CI=true&lt;/code&gt; syntax will fail on Windows - see section below for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;cross-env-considerations&quot;&gt;Cross-Env Considerations&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;WARNING: The &lt;code&gt;CI=true&lt;/code&gt; is the Unix / bash syntax for setting environmental variables.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you want a way to set environmental values across all operating systems, you will need to add &lt;a href=&quot;https://www.npmjs.com/package/cross-env&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;cross-env&lt;/code&gt;&lt;/a&gt; as a dependency, and then change &lt;code&gt;CI=true&lt;/code&gt; to &lt;code&gt;cross-env CI=true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In addition, for &lt;strong&gt;reading&lt;/strong&gt; environmental variables in a cross-env nature, you can use a quick hack of &lt;code&gt;node -p &quot;process.env.VARIABLE_TO_CHECK&quot;&lt;/code&gt;. If you want to ensure you receive either the value itself or an empty string (but &lt;em&gt;not&lt;/em&gt; undefined), use &lt;code&gt;node -p &quot;process.env.VARIABLE_TO_CHECK || &apos;&apos;&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;using-glitchjson&quot;&gt;Using glitch.json&lt;/h3&gt;
&lt;p&gt;If you are reluctant to edit your package.json to accommodate Glitch, an alternative solution can be to use &lt;code&gt;glitch.json&lt;/code&gt; to tell Glitch to use the special start command. However, this takes some extra configuring; you would have to find the global path of NPM (or &lt;code&gt;pnpm&lt;/code&gt; for Glitch, likely), setup a shell script to call react-scripts with CI=true, and then point &lt;code&gt;glitch.json&lt;/code&gt; at the shell script.&lt;/p&gt;
&lt;p&gt;If you really want to go down that route, make sure you review &lt;a href=&quot;https://cheatsheets.joshuatz.com/cheatsheets/cloud-services/glitch&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;my Glitch guide&lt;/a&gt;, and in particular, the &lt;a href=&quot;https://cheatsheets.joshuatz.com/cheatsheets/cloud-services/glitch/#troubleshooting&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Troubleshooting section&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;demo&quot;&gt;Demo&lt;/h2&gt;
&lt;p&gt;I put together a quick demo / starter repo, generated from Create-React-App, and configured with my fix for Glitch. Source code is &lt;a href=&quot;https://github.com/joshuatz/create-react-app-glitch&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;on GitHub&lt;/a&gt;, and the live Glitch is &lt;a href=&quot;https://glitch.com/edit/#!/joshuatz-create-react-app-glitch&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;conflicting-ports&quot;&gt;Conflicting Ports&lt;/h2&gt;
&lt;p&gt;Even if the above does not work for you, and you are convinced it is an issue with conflicting ports, you don’t have to roll your own proxy code like they did in that linked post. There is actually a built-in feature in &lt;code&gt;react-scripts&lt;/code&gt; that will take care of it for you; all you have to do is declare the proxy destination in &lt;code&gt;package.json -&gt; proxy&lt;/code&gt; field. This will proxy all unknown (e.g. non-static files and routes) to the defined proxy.&lt;/p&gt;
&lt;p&gt;For example, to proxy all non-static requests to port &lt;code&gt;3001&lt;/code&gt;, use:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;proxy&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;http://localhost:3001&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can find the docs on this feature &lt;a href=&quot;https://create-react-app.dev/docs/proxying-api-requests-in-development/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;, and Dave Ceddia has &lt;a href=&quot;https://daveceddia.com/create-react-app-express-backend/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;an excellent writeup on using it with CRA + Express&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: You can also use environmental variables to change the port dev server listens on (with &lt;code&gt;PORT=#&lt;/code&gt;) and the host it is bound to (with &lt;code&gt;HOST=___&lt;/code&gt;). Refer to &lt;a href=&quot;https://create-react-app.dev/docs/advanced-configuration/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the advanced configuration docs&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;!--
## The Fix with Added Performance Boost
There is a tool that I built to improve the startup performance of projects that produce a static build output; the same tool (&lt;a href=&quot;...&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;glitch-static-cache&lt;/a&gt;) also fixes this issue, because it runs CRA in production build mode to get the output, not development mode (which requires the TTY).

&lt;a href=&quot;...&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Check it out&lt;/a&gt; if you are interested in fixing this, plus better startup time on Glitch!
--&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>NPM Package: detect-is-on-glitch CLI, module, and script</title><link>https://joshuatz.com/projects/web-stuff/npm-package-detect-is-on-glitch-cli-module-and-script/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/npm-package-detect-is-on-glitch-cli-module-and-script/</guid><description>Simple NPM script for detecting whether your code is executing inside of Glitch or not. Detects based on environmental variables and/or hosting info.</description><pubDate>Fri, 14 Aug 2020 05:59:45 GMT</pubDate><content:encoded>&lt;h2 id=&quot;how-to-get-it&quot;&gt;How to Get It!&lt;/h2&gt;
&lt;p&gt;📦 NPM Package: &lt;a href=&quot;https://www.npmjs.com/package/detect-is-on-glitch&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;detect-is-on-glitch&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💾 Source Code: &lt;a href=&quot;https://github.com/joshuatz/detect-is-on-glitch&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The package is lightweight; works fast even via &lt;code&gt;npx detect-is-on-glitch&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;h3 id=&quot;what-is-glitch&quot;&gt;What is Glitch?&lt;/h3&gt;
&lt;p&gt;For those not aware, &lt;a href=&quot;https://glitch.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Glitch&lt;/a&gt; is an online code editor, with a focus on collaboration, sharing, and live editing. You can edit and run static websites, as well as full-stack projects that depend on build tools, NodeJS scripts, and other bits and pieces.&lt;/p&gt;
&lt;h3 id=&quot;what-is-this-tool&quot;&gt;What is this tool?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;detect-is-on-glitch&lt;/code&gt; is exactly what it sounds like; a tool to detect whether or not your code is running on Glitch, as opposed to your local PC, build server, or anywhere else.&lt;/p&gt;
&lt;p&gt;The idea for this is that you might want your app to behave differently on Glitch versus anywhere else. Maybe you want to serve a slimmed down version of your app for Glitch’s live hosting, or display special console instructions to those remixing your project versus regular users. This tool can let you do that.&lt;/p&gt;
&lt;h2 id=&quot;how-does-it-work&quot;&gt;How Does It Work&lt;/h2&gt;
&lt;p&gt;This will probably the least complex NodeJS package I ever create. You can check out &lt;a href=&quot;https://github.com/joshuatz/detect-is-on-glitch&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the source code&lt;/a&gt; yourself, but to sum up; it checks &lt;code&gt;process.env&lt;/code&gt; and the hosted domain for any traces that Glitch normally leaves.&lt;/p&gt;
&lt;p&gt;Right now, this works for all the samples I have tried, but might need to be updated in the future if Glitch introduces new changes.&lt;/p&gt;
&lt;p&gt;You can call it via CLI (alias is &lt;code&gt;isglitch&lt;/code&gt;), NodeJS, or even front-end code.&lt;/p&gt;
&lt;h2 id=&quot;tools-used&quot;&gt;Tools Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;NodeJS&lt;/li&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Glitch&lt;/li&gt;
&lt;li&gt;🧠—&gt; Research, to find what Glitch exposed as unique properties&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;sample&quot;&gt;Sample&lt;/h2&gt;
&lt;p&gt;If you are still trying to think of how this could be used, here is a quick sample: running a different “start” command on Glitch versus anywhere else&lt;/p&gt;
&lt;p&gt;When Glitch detects a code change, or your project has “gone to sleep” due to inactivity, it will run your &lt;code&gt;start&lt;/code&gt; script. You can read more about Glitch’s deterministic task setup &lt;a href=&quot;https://cheatsheets.joshuatz.com/cheatsheets/cloud-services/glitch/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;in my Glitch cheatsheet&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Using this tool, you can have a different command execute (&lt;code&gt;node server-light.js&lt;/code&gt;) for Glitch&lt;/p&gt;
&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;scripts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;start&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;npx detect-is-on-glitch &amp;#x26;&amp;#x26; node server-light.js || node sever.js&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>How to Delete Files Based on a Pattern with SHX and ShellJS</title><link>https://joshuatz.com/posts/2020/how-to-delete-files-based-on-a-pattern-with-shx-and-shelljs/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/how-to-delete-files-based-on-a-pattern-with-shx-and-shelljs/</guid><description>Instructions on how to delete multiple files based on a pattern, even when nested, with SHX and ShellJS. Extra tips on using with NPM packages!</description><pubDate>Thu, 23 Jul 2020 23:13:37 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro-to-shx&quot;&gt;Intro to SHX&lt;/h2&gt;
&lt;p&gt;One of my personal goals with NPM package development is to get better at cross-OS support; I don’t want to distribute code that only works in one specific environment. As part of my effort in working towards this, I discovered the &lt;a href=&quot;https://github.com/shelljs/shx&quot;&gt;&lt;code&gt;shx&lt;/code&gt; CLI package&lt;/a&gt;, which is really mostly a wrapper around &lt;a href=&quot;https://github.com/shelljs/shelljs&quot;&gt;shelljs&lt;/a&gt;. Both of these tools let you write commands that use standard &lt;code&gt;unix&lt;/code&gt; built-ins, like &lt;code&gt;cat&lt;/code&gt;, but will work across any OS (Win / Mac / Linux).&lt;/p&gt;
&lt;p&gt;For example, &lt;code&gt;shx cat index.js&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&quot;using-shx-to-delete-files-based-on-a-pattern&quot;&gt;Using SHX to Delete Files Based on a Pattern&lt;/h2&gt;
&lt;h3 id=&quot;problem&quot;&gt;Problem:&lt;/h3&gt;
&lt;p&gt;If you are used to writing your commands without concern for portability, you might write a command to delete files based on a pattern like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Delete all .md files in /dist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;find&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; dist&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -name&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;*.md&apos;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -delete&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, this command will fail with &lt;code&gt;shx&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;shx&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; find&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; dist&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -name&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;*.md&apos;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -delete&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;# ERROR: find: no such file or directory: -name&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is because the arguments to &lt;code&gt;find&lt;/code&gt; are not currently supported by &lt;code&gt;shx&lt;/code&gt; - you can find this tracked as &lt;a href=&quot;https://github.com/shelljs/shx/issues/177&quot;&gt;issue #177&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;solution&quot;&gt;Solution:&lt;/h3&gt;
&lt;p&gt;Since &lt;code&gt;shx&lt;/code&gt; supports automatic glob expansion to standard commands, there is no reason why you can’t write the delete command as this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;shx&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; rm&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;dist/**/*.md&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;best-solution&quot;&gt;Best Solution&lt;/h3&gt;
&lt;p&gt;There is a secondary problem here, which exists with many commands, which is that the command might exit with a non-zero code, preventing chained commands (ala &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt;) from running. This is especially problematic for NPM packages and the &lt;code&gt;scripts&lt;/code&gt; section.&lt;/p&gt;
&lt;p&gt;For example, if our &lt;code&gt;package.json&lt;/code&gt; has this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;scripts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;clean&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;shx rm &apos;dist/**/*.md&apos; &amp;#x26;&amp;#x26; yarn build&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;… and there are no &lt;code&gt;.md&lt;/code&gt; files inside &lt;code&gt;dist&lt;/code&gt;, then the first part of the &lt;code&gt;clean&lt;/code&gt; command will exit with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;error Command failed with exit code 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;… and &lt;code&gt;yarn build&lt;/code&gt; will not execute.&lt;/p&gt;
&lt;p&gt;There is any easy workaround though! Just use the &lt;code&gt;&amp;#x26;&lt;/code&gt; instead of the &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt; operator, which lets the second command execute regardless of the success of the first.&lt;/p&gt;
&lt;p&gt;You can also force the command to return &lt;code&gt;exit 0&lt;/code&gt;, by piping and then returning true:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;scripts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;clean&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;shx rm &apos;dist/**/*.md&apos; || shx true &amp;#x26;&amp;#x26; yarn build&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Tip: You can also use &lt;code&gt;shx --silent&lt;/code&gt; to suppress the displaying of the error message.&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Using Google Forms for Quick Website Feedback and Bug Reports</title><link>https://joshuatz.com/posts/2020/using-google-forms-for-quick-website-feedback-and-bug-reports/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/using-google-forms-for-quick-website-feedback-and-bug-reports/</guid><description>How to use Google Forms for quick user feedback collection, bug reporting, and the pros and cons of this approach.</description><pubDate>Tue, 30 Jun 2020 08:19:50 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;When building a new website or application (SPA, PWA, etc.), one of the things you think about before launching is a way for users to leave feedback and/or report issues. For large-scale releases and long-term setups, you really should use a professional feedback and bug reporting system; something like &lt;a href=&quot;https://usersnap.com/&quot;&gt;Usersnap&lt;/a&gt;, or a custom solution built in-house.&lt;/p&gt;
&lt;p&gt;However, if you are building a demo, MVP, or personal project, a great temporary, or even long-term solution, can be using Google Forms with Google Sheets.&lt;/p&gt;
&lt;h2 id=&quot;google-forms-for-user-feedback-and-bug-reports&quot;&gt;Google Forms for User Feedback and Bug Reports&lt;/h2&gt;
&lt;p&gt;The simple overview of how Google Forms works is that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You use the &lt;a href=&quot;https://docs.google.com/forms/u/0/&quot;&gt;Google Forms&lt;/a&gt; builder to visually build out a form to your liking. It is super easy to use; drag-and-drop fields, easily add validation logic, etc.&lt;/li&gt;
&lt;li&gt;Once you have the form to your liking, you can pick how you want to add it to your site:
&lt;ul&gt;
&lt;li&gt;Have it open in a new tab: Click &lt;code&gt;Send&lt;/code&gt;, then the link icon, then copy the URL. Now you can paste that URL into the &lt;code&gt;href&lt;/code&gt; of a standard link element, router system, etc.&lt;/li&gt;
&lt;li&gt;Embed it directly in your site: Click &lt;code&gt;Send&lt;/code&gt;, then the embed icon, then copy the &lt;code&gt;iframe&lt;/code&gt; embed code. Now you can paste that code into a static HTML file, React component, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Once someone submits the form, their response will be saved with the form, and visible to the form creator inside Google Drive.
&lt;ul&gt;
&lt;li&gt;Optionally, you can also connect the form to a Google Sheet file, so each response automatically creates a new row in a spreadsheet&lt;/li&gt;
&lt;li&gt;You can also set up email alerts, and advanced automations (see below sections)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Forms-Sample-Feedback-Response-Form.png&quot; style=&quot;margin: auto; display:block; max-width: 1600px; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/Google-Forms-Sample-Feedback-Response-Form.png&quot; alt=&quot;Sample Google Form for Collecting User Responses&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;simple-integration&quot;&gt;Simple Integration&lt;/h3&gt;
&lt;p&gt;As covered above, the most simple approach to integration is either dropping a link to the form in the code of your site/app, or embedding directly via the iframe option. Both work with almost any website or app, including most WYSIWYG builders like Wix or Squarespace.&lt;/p&gt;
&lt;h3 id=&quot;advanced-integration&quot;&gt;Advanced Integration&lt;/h3&gt;
&lt;p&gt;One of the lesser known features of Google Forms, and what makes it an actual suitable solution for small use-cases, is that it supports pre-filling / auto-filling form fields with URL query string parameters. This means that you can do things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Autofill a &lt;code&gt;username&lt;/code&gt; field based on logged-in info&lt;/li&gt;
&lt;li&gt;Autofill a &lt;code&gt;where did this happen?&lt;/code&gt; field with the current webpage URL&lt;/li&gt;
&lt;li&gt;Map existing form elements on your page to the Google Form and pre-fill the values&lt;/li&gt;
&lt;li&gt;Etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I recommend following the tips from these three pages to get started on using URL pre-filling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://www.cagrimmett.com/til/2016/07/07/autofill-google-forms.html&quot;&gt;cagrimmett.com/til/2016/07/07/autofill-google-forms.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.google.com/a/users/answer/9308781&quot;&gt;support.google.com/a/users/answer/9308781&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.google.com/docs/thread/3341555&quot;&gt;support.google.com/docs/thread/3341555&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;If you want to see a live example, I use the prefill trick to auto-fill the URL field in my feedback form on &lt;code&gt;cheatsheets.joshuatz.com&lt;/code&gt;. For example, go to &lt;a href=&quot;https://cheatsheets.joshuatz.com/cheatsheets/typescript/&quot;&gt;this page&lt;/a&gt;, hit the question mark icon in the upper right, then the &lt;code&gt;Feedback&lt;/code&gt; button, and then notice how the form, which opens in a new tab, has the URL field pre-filled with the URL of the page that you were just on! The feedback button and its corresponding URL is actually generated and rendered in React.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;limitations&quot;&gt;Limitations&lt;/h3&gt;
&lt;p&gt;There are a lot of limitations with Google Forms, which make it unsuitable for many &lt;em&gt;professional&lt;/em&gt; and/or large-scale setups. Google Forms was not built for bug reporting or large-scale user feedback collection, so most of these limitations are understandable given the intended user-base. Here is a sampling of some of the major limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lack of native hidden fields
&lt;ul&gt;
&lt;li&gt;This is a big limitation, because this is often how user feedback forms collect automated information (such as user-agent string) without cluttering the form for the user or allowing for accidental field editing&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lack of &lt;em&gt;official&lt;/em&gt; integrations
&lt;ul&gt;
&lt;li&gt;There are lots of ways that you can jury-rig connections between Google Forms and other systems, often by using something like Zapier, but these require manual setup and maintenance, and are liable to break if Google changes things&lt;/li&gt;
&lt;li&gt;In comparison, many other user feedback and bug reporting SaaS platforms have native integrations with other systems like Jira or PagerDuty&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lack of plug-n-play visual integration
&lt;ul&gt;
&lt;li&gt;The two integration options, a standard web link or iframe embed, do not always visually blend well with an existing site. It might require a bunch of custom coding to incorporate nicely.&lt;/li&gt;
&lt;li&gt;In comparison, many feedback SaaS products offer copy-and-paste embed codes that seamlessly blend with your site or app, including advanced UI widgets like floating invite buttons and scroll animations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;google-forms-automation&quot;&gt;Google Forms Automation&lt;/h2&gt;
&lt;p&gt;One perk of using Google Forms is that it is integrated nicely into the whole Google Apps Script (GAS) ecosystem. In addition to having &lt;a href=&quot;https://developers.google.com/apps-script/reference/forms&quot;&gt;its own service layer&lt;/a&gt;, if you are piping responses from Forms into Sheets, you could also use &lt;a href=&quot;https://developers.google.com/apps-script/guides/sheets&quot;&gt;the Google Sheet scripting service&lt;/a&gt; to build your own integrations or automations.&lt;/p&gt;
&lt;p&gt;Google Apps Script is a highly extensible and powerful scripting platform that is tightly integrated with many of Google’s services. Connecting Forms to GAS unlocks an almost endless number of customized possibilities.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Twitter Iframe Embed Generator</title><link>https://joshuatz.com/projects/web-stuff/twitter-iframe-embed-generator/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/twitter-iframe-embed-generator/</guid><description>Project and tool for generating IFrame embeds for any tweet from just the URL, no coding required or third-party scripts.</description><pubDate>Tue, 16 Jun 2020 02:04:25 GMT</pubDate><content:encoded>&lt;h2 id=&quot;links&quot;&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;⚡LIVE TOOL: &lt;a href=&quot;https://joshuatz-twitter-iframe-generator.glitch.me/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Glitch Hosted&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;👩‍💻: &lt;a href=&quot;https://github.com/joshuatz/twitter-iframe-generator&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Github Repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;Occasionally I have wanted to embed a specific Tweet from Twitter into a post on my website, and have been irked by the fact that the default embed method requires me to add a third-party Javascript embed to my site, which could potentially be used in malicious ways. I almost always prefer to use IFrames to embed third party content, as they provide a superior level of &lt;em&gt;“sandboxing”&lt;/em&gt; and content isolation.&lt;/p&gt;
&lt;p&gt;I realized that building an Iframe embed generator for Tweets should be fairly easy, and this tool is the result of that goal. It allows you to plug in the URL of a tweet, and get an IFrame embed code, with configurable options.&lt;/p&gt;
&lt;h2 id=&quot;the-tool&quot;&gt;The Tool&lt;/h2&gt;
&lt;p&gt;The tool is fairly simple: Plug in the URL of a tweet, hit the “generate” button, and get back IFrame embed code that you can paste into your own website, or forums that accept IFrame embeds. Unlike other approaches (e.g. &lt;a href=&quot;https://twitframe.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Twitframe&lt;/a&gt;), my tool simply wraps the Twitter embed code in an IFrame wrapper, using some IFrame generation tricks.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Twitter-IFrame-Embed-Generator-Demo.png&quot; style=&quot;margin: auto; display:block; max-width: 1400px; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/Twitter-IFrame-Embed-Generator-Demo.png&quot; alt=&quot;Twitter IFrame Embed Generator - Demo Screenshot&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I built it using vanilla JS - no frameworks. I’ve used React, Vue, and other frameworks on other projects, but wanted to get some practice coding things from scratch on this, especially since I wanted a fast build.&lt;/p&gt;
&lt;h2 id=&quot;learnings--tools-used&quot;&gt;Learnings / Tools Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JSONP still is useful!
&lt;ul&gt;
&lt;li&gt;It has been a while since I have had to use the JSONP workaround method, but it proved invaluable on this project, since Twitter does not provide the right headers for CORS, but &lt;em&gt;does&lt;/em&gt; support JSONP responses.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-oembed&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Twitter’s OEmbed API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;My &lt;a href=&quot;https://joshuatz.com/posts/2019/embedding-iframes-static-third-party-and-dynamic-options/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;previous research on different iframe generation methods&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://materializecss.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Materialize CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Fairly vanilla JavaScript&lt;/li&gt;
&lt;li&gt;SRI&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-not-use-sri&quot;&gt;Why Not Use SRI?&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;S&lt;/strong&gt;**ub&lt;/em&gt;&lt;em&gt;&lt;strong&gt;r&lt;/strong&gt;**esource&lt;/em&gt; &lt;em&gt;&lt;strong&gt;I&lt;/strong&gt;**ntegrity&lt;/em&gt; (&lt;em&gt;&lt;strong&gt;SRI&lt;/strong&gt;&lt;/em&gt;) (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;MDN Docs&lt;/a&gt;) is a security feature baked into several modern browsers, that provides a way to verify (and enforce) that the external resource received from a remote source &lt;strong&gt;exactly&lt;/strong&gt; matches what was requested.&lt;/p&gt;
&lt;p&gt;You can use it by including a “hash digest” when you add &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; and other resource tags to your website, as an &lt;code&gt;integrity&lt;/code&gt; attribute. In practical terms, this means if you add a script tag that loads &lt;code&gt;fake-external-website.com/script.js&lt;/code&gt;, but you hash and add the hash as an integrity key when adding it, you can be guaranteed that you will always get the exact same &lt;code&gt;script.js&lt;/code&gt; each time. This is an important security feature, because you can audit scripts as you add them, and if four months after adding them, the vendor suddenly adds a keylogger, the script will get blocked because the hash will no longer match the original hash.&lt;/p&gt;
&lt;p&gt;This all sounds great, so why not use it with Twitter? Well, you can! But there are some downsides, which I’ll cover first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every time Twitter changes their code (in &lt;code&gt;https://platform.twitter.com/widgets.js&lt;/code&gt;), it will break &lt;em&gt;&lt;strong&gt;all&lt;/strong&gt;&lt;/em&gt; embeds that rely on it
&lt;ul&gt;
&lt;li&gt;Since the hashes have to exactly match, even them adding a single semicolon will cause the script to start getting blocked&lt;/li&gt;
&lt;li&gt;Each time they make an update, you would have to audit the change, then compute a new hash digest and update your embed code&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SRI is &lt;strong&gt;not&lt;/strong&gt; the same as sandboxing
&lt;ul&gt;
&lt;li&gt;With SRI, the only guarantee is that the external code has not changed; not that it is &lt;strong&gt;safe to use&lt;/strong&gt;. Auditing the code is up to you, as a developer, and if you miss something, there could be catastrophic results (embedded keyloggers, PII stealers, etc.)&lt;/li&gt;
&lt;li&gt;In comparison, IFrames provide a high level of sandboxing by default&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SRI does not provide isolated styles
&lt;ul&gt;
&lt;li&gt;Another benefit of Iframes are that they prevent conflicting CSS rules, by isolating the content inside the iframe from the content outside&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SRI is &lt;a href=&quot;https://caniuse.com/#feat=subresource-integrity&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;not supported by all browsers&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Adoption is growing though, and already has about ~95% global support&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you don’t mind all of those downsides, here is how you would go about enforcing SRI on a Twitter embed.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Compute the hash digest of the Twitter embed JS file, with a given hash function. Once you have it, prefix the hash with the function type and a hyphen (e.g. &lt;code&gt;sha256-&lt;/code&gt;)
&lt;ul&gt;
&lt;li&gt;At the time of writing this, for Twitter’s &lt;code&gt;widgets.js&lt;/code&gt;, the string would be: &lt;code&gt;sha384-SR82MxVe9kFn41L4YvfLllMtR3HJ/jl5ubYdBBHjbTUBG31k2Zo4ZTuZXFxeP3Gd&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;You can use online tools to get the hash, like &lt;a href=&quot;https://www.srihash.org/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;srihash.org&lt;/a&gt;, or the OpenSSL CLI, with &lt;code&gt;cat FILENAME.js | openssl dgst -sha384 -binary | openssl base64 -A&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In the Twitter embed snippet, find the &lt;code&gt;widget.js&lt;/code&gt; &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt;, and add the integrity attribute with the string you generated above
&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;&amp;#x3C;script async src=&quot;https://platform.twitter.com/widgets.js&quot; integrity=&quot;sha384-SR82MxVe9kFn41L4YvfLllMtR3HJ/jl5ubYdBBHjbTUBG31k2Zo4ZTuZXFxeP3Gd&quot; charset=&quot;utf-8&quot;&gt;&amp;#x3C;/script&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Because Twitter does not return the right CORS headers, and SRI checking requires a value for the &lt;code&gt;crossorigin&lt;/code&gt; attribute, you also need to add &lt;code&gt;crossorigin=&quot;anonymous&quot;&lt;/code&gt; as an attribute key-pair.
&lt;ul&gt;
&lt;li&gt;Update example: &lt;code&gt;&amp;#x3C;script async src=&quot;https://platform.twitter.com/widgets.js&quot; integrity=&quot;sha384-SR82MxVe9kFn41L4YvfLllMtR3HJ/jl5ubYdBBHjbTUBG31k2Zo4ZTuZXFxeP3Gd&quot; crossorigin=&quot;anonymous&quot; charset=&quot;utf-8&quot;&gt;&amp;#x3C;/script&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;why-not-twitframe&quot;&gt;Why Not Twitframe?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://twitframe.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Twitframe&lt;/a&gt; is a cool project that has the same end goal as mine; replace Twitter’s third party JS embeds with IFrame wrappers. However, Twitframe uses their own servers to proxy the Twitter request; this means that, at the end of the day, using their service still requires a level of trust of a third party. Since the content is now being served / proxied through Twitframe, you now have to trust that they will simply mirror the content from Twitter, and not alter it in any way.&lt;/p&gt;
&lt;p&gt;Since the IFrame protects you from JS exploits, the worst case scenario using Twitframe is unwanted visual content on your site. For example, they could get hacked and someone could force it to response to every embed request with obscene content or clickbait malvertising.&lt;/p&gt;
&lt;p&gt;My approach does not alter Twitter’s response or embed code in any way; it is truly just a “dumb” wrapper / sandbox around it. The actual network request for tweet content goes directly from your visitor’s computer to Twitter, and is not proxied through any third-party servers.&lt;/p&gt;
&lt;h2 id=&quot;downsides--caveats&quot;&gt;Downsides / Caveats&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Limited IE support
&lt;ul&gt;
&lt;li&gt;IE only supports Data URIs (the default IFrame method my tool uses) for images and resources, not HTML / frames (see &lt;a href=&quot;https://caniuse.com/#feat=datauri&quot;&gt;caniuse&lt;/a&gt; for details).&lt;/li&gt;
&lt;li&gt;I &lt;em&gt;could&lt;/em&gt; code a workaround for this, but it would likely lose cross-origin protections (&lt;a href=&quot;https://stackoverflow.com/a/39079810/11447682&quot;&gt;example&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;No Responsive Design:
&lt;ul&gt;
&lt;li&gt;The biggest drawback from this method is that the IFrames are not responsive (by default). Meaning, you must specify the height beforehand, and if the tweet ends up being larger (longer) than the height you set it at, there will either be a scrollbar in the embed to see the rest of the content, or, if you turned off overflow, it will simply be hidden.&lt;/li&gt;
&lt;li&gt;I could design a way to make these embeds responsive (likely using &lt;code&gt;postMessage()&lt;/code&gt;), but it would take a bunch of extra JavaScript and come with further caveats&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Blocking by user or extension
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Potentially&lt;/em&gt;, a user could have their browser set to block IFrames by default. However, this is extremely rare, and even ad-blockers don’t usually interfere with IFrames&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Elliptical Street View Controller - Arduino USB Adapter</title><link>https://joshuatz.com/projects/electronics/elliptical-street-view-controller---arduino-usb-adapter/</link><guid isPermaLink="true">https://joshuatz.com/projects/electronics/elliptical-street-view-controller---arduino-usb-adapter/</guid><description>A custom built adapter box that turns an elliptical exercise machine into a Google Street View controller, via USB HID. Powered by Arduino.</description><pubDate>Wed, 10 Jun 2020 00:48:19 GMT</pubDate><content:encoded>&lt;p&gt;This is actually a rather old project of mine that I meant to post about a long time ago, but didn’t get around to it until now. I believe I originally built this around October 2013, but my original notes on this build are pretty sparse.&lt;/p&gt;
&lt;h2 id=&quot;idea&quot;&gt;Idea&lt;/h2&gt;
&lt;p&gt;The idea for this was pretty simple; I wanted my elliptical machine to control Google StreetView running on a computer, to where walking forward on the machine would translate into moving forward in the street view webpage. I also wanted to be able to easily adjust the POV of the Street View with a hand-held controller, so I could change direction without needing to step off the machine and go to the computer.&lt;/p&gt;
&lt;p&gt;Even with the little bit of knowledge I had of electronics and programming at the time (I hadn’t really started learning to code much at this point in my life yet), I knew this should be very easy to accomplish with a microcontroller, a sensing circuit, and a little bit of code.&lt;/p&gt;
&lt;h2 id=&quot;execution&quot;&gt;Execution&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/media/Elliptical-to-USB-Keyboard-Input-Adapter-Arduino-Powered.jpg&quot; style=&quot;margin: auto; display:block; max-width: 400px; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/Elliptical-to-USB-Keyboard-Input-Adapter-Arduino-Powered.jpg&quot; alt=&quot;Elliptical to USB Keyboard Input Adapter (Arduino Powered)&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;demo-video&quot;&gt;Demo Video&lt;/h3&gt;
&lt;iframe style=&quot;display:block; margin:auto;&quot; width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube-nocookie.com/embed/Xm21VqQqBhM&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also find the video &lt;a href=&quot;https://youtu.be/Xm21VqQqBhM&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;hardware&quot;&gt;Hardware&lt;/h3&gt;
&lt;p&gt;This project is comprised of just a handful of components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://amzn.to/2Ym97iG&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Arduino Leonardo&lt;/a&gt; (Microprocessor) (brain and keyboard emulator)&lt;/li&gt;
&lt;li&gt;Automobile Power Mirror Control Switch (got in parts auction)&lt;/li&gt;
&lt;li&gt;Resistors, capacitors, etc.&lt;/li&gt;
&lt;li&gt;Shift Register (drives LED counter display)&lt;/li&gt;
&lt;li&gt;7 Segment LED Display (counter)&lt;/li&gt;
&lt;li&gt;Red and green LEDs&lt;/li&gt;
&lt;li&gt;3.5mm mono jacks (to connect to the elliptical)&lt;/li&gt;
&lt;li&gt;Radioshack enclosure&lt;/li&gt;
&lt;li&gt;Push buttons (for adjusting step-to-keyboard press ratio)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My favorite part of the build was integrating the recycled automotive power mirror switch. Even though I built this 7 years ago or so, I can still remember sitting down and reverse engineering the pin mappings on it, since I couldn’t find a wiring diagram and really wanted to integrate it. There is something really fun about reusing scrap components in a way that is far outside their original intended use, especially as inputs (I feel like &lt;a href=&quot;https://twitter.com/Foone&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Foone&lt;/a&gt; would be proud of me).&lt;/p&gt;
&lt;p&gt;Here is my assembled adapter box, connected to the original elliptical display unit via 3.5mm cables, and to the computer via USB:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Elliptical-to-USB-Keyboard-Input-Passthrough-With-Guthy-Renker-Fitness-Display-scaled.jpg&quot; style=&quot;margin: auto; display:block; max-width: 800px; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/Elliptical-to-USB-Keyboard-Input-Passthrough-With-Guthy-Renker-Fitness-Display-scaled.jpg&quot; alt=&quot;Elliptical to USB Keyboard Input - Passthrough With Guthy Renker Fitness Display&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;software&quot;&gt;Software&lt;/h3&gt;
&lt;p&gt;The software side of things is fairly simple. A tiny routine, written in C++, runs on the Arduino. It listens for button presses, as well as polls an analog sensor tied to the elliptical output in order to sense steps.&lt;/p&gt;
&lt;p&gt;The way that my specific elliptical (an old “Guthy-Renker Powertrain”) talks to its LCD readout / console display is with a single wire, and a variable resistance. During a few tiny parts of each step, the resistance of the wire falls to almost zero (closed circuit) and allows current to flow, and during the rest of the step, the resistance approaches infinity (open circuit) allowing no current to flow. I use this by reading the change in voltage (as a proxy for resistance, V=IR) with the Arduino’s analog input pin. A certain number of voltage changes equals a certain number of steps, which I translate into a keyboard press to move along the Street View image.&lt;/p&gt;
&lt;p&gt;Since the Arduino Leonardo has built-in support for HID emulation, sending keypresses to the computer over USB is easy as including &lt;code&gt;Keyboard.h&lt;/code&gt; and using something like &lt;code&gt;Keyboard.press(KEY_UP_ARROW);&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There is extra logic for adjusting how many steps equal one keypress, as well as handling the shift register for driving the LED counter.&lt;/p&gt;
&lt;h3 id=&quot;gamification-as-motivation&quot;&gt;Gamification as Motivation&lt;/h3&gt;
&lt;p&gt;For me personally, a big part of why I’ve struggled with staying motivated to exercise is the lack of an immediate goal. To clarify, my long-term goal has always been to lose weight and stay in shape, but each time I exercise, I never see any part of that goal reflected in the &lt;em&gt;immediate&lt;/em&gt; result of what I just did.&lt;/p&gt;
&lt;p&gt;For a certain percentage of the population, the release of endorphins, or other physical stimuli and feedback loops, is the immediate goal that provides motivation to exercise. For me, this has never been the case; I have &lt;strong&gt;never&lt;/strong&gt; gotten that positive experience that so many others describe, and exercise has always been something that taxes my body in an uncomfortable way.&lt;/p&gt;
&lt;p&gt;To solve my motivation problem while building this project, I decided to implement a game for myself. The rules were simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I had to pick a real route, one that I would be interested in taking in real life
&lt;ul&gt;
&lt;li&gt;Completing this route in Street View would be the long term goal&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Each time I exercised, the following rules applied:
&lt;ul&gt;
&lt;li&gt;I had to resume street view from the same place where I stopped my previous exercise session&lt;/li&gt;
&lt;li&gt;As much as possible, I tried to keep freeways off limits, as that felt like cheating, since you would not be able to jog / bike on those in real life&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Fun Rule&lt;/strong&gt;&lt;/em&gt;: In order to end an exercise session, I had to make it (or at least &lt;em&gt;try to make it&lt;/em&gt;) to a place that would be a resting place &lt;em&gt;in real life&lt;/em&gt;. For example, hotels, motels, camp grounds, and rest stops would all be acceptable spots to end a session at
&lt;ul&gt;
&lt;li&gt;This was something that I felt added a lot to the gamification aspect and addressed my motivation issue; it gave me a short-term, immediate, and achievable goal to aim for each session. I also felt that it pushed me a lot harder than a simple time limit, since a distance goal requires a specific amount of movement, whereas a time limit does not dictate that.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Progress along the route was tracked with all of the following:
&lt;ul&gt;
&lt;li&gt;Spreadsheet (start address, end address, and all exercise stats)&lt;/li&gt;
&lt;li&gt;Custom google map to visually show progress along route&lt;/li&gt;
&lt;li&gt;Calories also tracked in dieting app&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;plans-for-the-future&quot;&gt;Plans for the Future&lt;/h2&gt;
&lt;p&gt;Ideally, I would love to reboot this project sometime in the future. If I was going to go for my dream project, it would be to focus on the software side; I think you could easily turn a variant of this idea into a profitable SaaS product.&lt;/p&gt;
&lt;p&gt;For example, instead of requiring specific exercise equipment or sensors for tracking input, you could use the accelerometer of a phone to track movement. The input could be fed into a street view program, VR games, etc. This would allow for extreme flexibility; you could theoretically design a piece of software that would work with any stationary exercise equipment (treadmill, elliptical, bike, rower), and have multiple mini-games with progress tracking. Throw in the requirement of a monthly subscription, and you have a nice monthly revenue generator.&lt;/p&gt;
&lt;p&gt;You could also greatly expand on the gamification aspect; track your progress on multiple routes, gain XP, unlock achievements and “powerups”, etc.&lt;/p&gt;
&lt;h2 id=&quot;wrap-up-and-related-projects&quot;&gt;Wrap Up and Related Projects&lt;/h2&gt;
&lt;p&gt;This topic (VR exercise) fascinates me a great deal, and is something I think needs to be talked about more. Unless we make some big changes to public transit and focus on increasing pedestrian areas while reducing roads (which we &lt;strong&gt;should&lt;/strong&gt; be doing), it will continue to get harder and harder to find open areas to jog / bike / stroll. Especially areas that are not crowded with people. As such, VR exercise might eventually become a more frequent alternative to the real thing.&lt;/p&gt;
&lt;p&gt;Since this interests me so much, I have collected (and continue to collect) related projects and resources, &lt;a href=&quot;https://docs.google.com/document/d/1y-sZcYQsvFS1CZvaJwpomTvGx9GUY0-YphbR9-rEcY8/edit?usp=sharing&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;as a shared Google doc&lt;/a&gt;. Feel free to check it out, and if you think something is worth adding, drop a comment below, or email me (see top menu bar for contact info).&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Finding a Cheap Webcam in 2020, and Alternative Solutions</title><link>https://joshuatz.com/posts/2020/finding-a-cheap-webcam-in-2020-and-alternative-solutions/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/finding-a-cheap-webcam-in-2020-and-alternative-solutions/</guid><description>How to find a webcam solution in 2020, during the shortage, that is affordable and usable for Zoom, Teams, and other video conferencing.</description><pubDate>Sun, 07 Jun 2020 17:00:49 GMT</pubDate><content:encoded>&lt;p&gt;Due to COVID-19 and the related shift to remote work, there has been a shortage of webcams for the past couple months, especially those that are both name brand, and in the lower price range (under $100). For whatever reason, this shortage doesn’t appear to have an end in sight, but in the meantime, I thought I would share some workarounds and alternatives that I found, while trying to come up with a solution for myself to use for Zoom or other software.&lt;/p&gt;
&lt;h2 id=&quot;off-brand-webcams-on-ebay&quot;&gt;Off Brand Webcams on eBay&lt;/h2&gt;
&lt;p&gt;For some reason, even the knock-off webcams on Amazon are still &lt;em&gt;&lt;strong&gt;way&lt;/strong&gt;&lt;/em&gt; overpriced - cameras that would normally go for $10 are selling at $50+. However, on eBay, the prices are closer to what they should be, and you can get an unbranded 720P or 1080P webcam for as little as $15!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.ebay.com/sch/i.html?_from=R40&amp;#x26;_nkw=webcam&amp;#x26;_sacat=0&amp;#x26;_udlo=1&amp;#x26;_udhi=40&amp;#x26;LH_BIN=1&amp;#x26;LH_ItemCondition=1000&amp;#x26;_sop=15&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Here are some filtered search results&lt;/a&gt; that show what I mean.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Warning: I would recommend only buying from sellers that have a decent amount of positive feedback, to avoid being scammed. That being said, eBay / PayPal generally sides on the buyers side, so you are protected no matter what.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Most of these are &lt;em&gt;completely&lt;/em&gt; unbranded, or an unrecognized brand that is just slapped on. This usually also means that you will probably receive your webcam with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A rather plain cardboard box, with little to no branding&lt;/li&gt;
&lt;li&gt;No model number, or something generic, like A870&lt;/li&gt;
&lt;li&gt;With either no manual, or something barely readable and not very helpful&lt;/li&gt;
&lt;li&gt;With no customer support contacts, other than the eBay seller that you purchased it from&lt;/li&gt;
&lt;li&gt;No dedicated control software&lt;/li&gt;
&lt;li&gt;No warranty&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/media/Off-Brand-Webcam-Weicha-A870-Black-with-Orange-Accents.jpg&quot; style=&quot;margin: auto; display:block; max-width: 300px; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/Off-Brand-Webcam-Weicha-A870-Black-with-Orange-Accents.jpg&quot; alt=&quot;Off Brand A870 Webcam&quot; title=&quot;Off Brand A870 Webcam&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;However, most of these negatives don’t actually &lt;em&gt;matter&lt;/em&gt; much. As for actually &lt;em&gt;using&lt;/em&gt; the webcam, the majority of these unbranded models are completely plug-n-play, so there is not even any software to install anyways.&lt;/p&gt;
&lt;p&gt;Now, I know what you are thinking (“you get what you pay for!”), but, at least from the videos I’ve seen, the image quality actually isn’t as bad as you might think for the price, and definitely passable for video conferencing.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Btw, I have a pretty low opinion of &lt;em&gt;name-brand&lt;/em&gt; webcams; I think Logitech and similar brands have not really done much to improve quality over the years, especially in comparison to smartphone cameras, so I feel like it doesn’t take much to match their quality.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;smartphones-as-webcams&quot;&gt;Smartphones as Webcams&lt;/h2&gt;
&lt;p&gt;The easiest solution, if you have a smartphone, is to simply use its built in camera and join your meetings on your phone instead of on your computer, using the mobile app version of your meeting software (Zoom, Teams, Skype, etc). If you need to be able to share your computer screen, check if your meeting software allows multiple concurrent sign-ins, and if not, see if you could join on one device as a logged-in user, and on the other, as a guest.&lt;/p&gt;
&lt;p&gt;If you want to use your smartphone as a “true webcam”, where it is providing a camera feed into your laptop or desktop computer, that is a possibility, although it takes some work. No matter what, you are probably going to need to buy a piece of software either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Android: &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.dev47apps.droidcam&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;DroidCam&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;iOS: &lt;a href=&quot;https://apps.apple.com/us/app/ndi-hx-camera/id1477266080&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;NDI HX Camera&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a more in-depth page, check out &lt;a href=&quot;https://gizmodo.com/how-to-turn-your-smartphone-into-a-webcam-1842783844&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;this guide from Gizmodo&lt;/a&gt;. However, I think you are much better off just using the meeting app directly on your phone, rather than trying to route video from your phone &lt;em&gt;through&lt;/em&gt; your computer (and &lt;a href=&quot;https://lifehacker.com/use-your-phone-a-webcam-no-special-apps-required-1843243270&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Lifehacker came to the same conclusion as a I did&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id=&quot;action-cameras-as-webcams&quot;&gt;Action Cameras as Webcams&lt;/h2&gt;
&lt;p&gt;This surprised me, but I came across a recommendation on Twitter to checkout action cameras (e.g. GoPro and clones) as an alternative to dedicated webcams, since many models support a “webcam mode”, where you can plug it in to your computer via USB and pull the camera feed. I’m glad I checked it out, because this ended up being the best option for me. With webcam prices being inflated as they are, you can buy a great action camera for the same price, or less, as a dedicated webcam.&lt;/p&gt;
&lt;p&gt;I put together &lt;a href=&quot;https://docs.google.com/spreadsheets/d/18rx5_xEzccgucoDShjMf5sB4-yrwwPR1Fq1TbRLTVjw/edit?usp=sharing&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;a comparison table&lt;/a&gt; between several of the lower-priced options. &lt;del&gt;I ended up going with the &lt;a href=&quot;https://amzn.to/3gWP1Ep&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Victure AC700&lt;/a&gt;, which although pricier, has much better image quality than a lot of the competition.&lt;/del&gt;***&lt;/p&gt;
&lt;p&gt;This option is also great for those planning on returning to work soon, or for whatever other reason only need a webcam temporarily. After you are done using it as a webcam, you now have a fully functional action camera that you can use!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;*** = I actually have been having a lot of issues with the Victure AC700 and using it as a webcam. Certain applications work fine with (for example, Windows Camera), but with Zoom and some other apps, I have to fight against the image constantly going dark, or dropping out entirely. I can no longer recommend it as a webcam replacement 😢&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;security-cameras-as-webcams&quot;&gt;Security Cameras as Webcams&lt;/h2&gt;
&lt;p&gt;Some security cameras can pull double duty as a webcam.&lt;/p&gt;
&lt;p&gt;A great example is &lt;a href=&quot;https://amzn.to/3f1OrTJ&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the Wyze Cam&lt;/a&gt;. It costs around $25, supports 1080P, and due to the Corona Virus, Wyze released &lt;a href=&quot;https://support.wyzecam.com/hc/en-us/articles/360041605111&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;a free firmware upgrade that lets you use it as a webcam&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;old-devices-as-dedicated-video-conferencing-devices&quot;&gt;Old Devices as Dedicated Video Conferencing Devices&lt;/h2&gt;
&lt;p&gt;This is also a great time to think about putting some new life into old devices you might have floating around the house. Outdated tablet? Old backup smartphones? Ancient laptop? It doesn’t take much to run most meeting software, and even if that is all the device can run, you could always keep it as a dedicated conference device.&lt;/p&gt;
&lt;h2 id=&quot;dslrs--high-end-cameras-as-webcams&quot;&gt;DSLRs / High-End Cameras as Webcams&lt;/h2&gt;
&lt;p&gt;If you have a nice “professional” camera, like a DSLR, there are ways you can use it as a webcam and really upgrade your meeting image quality.&lt;/p&gt;
&lt;p&gt;For certain cameras, there might be an existing free way that you can use it as webcam. For example, if you own a Canon DLSR, you should check out &lt;a href=&quot;https://lifehacker.com/turn-your-canon-dslr-into-a-webcam-with-this-free-app-1843162662&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;this article from Lifehacker on the release of Canon’s EOS Webcam Utility&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For pretty much any camera that offers HDMI out, you can always use a &lt;em&gt;“capture card”&lt;/em&gt; to capture the video output on your computer. In fact, this is what many professional video streamers use in part of their setup. Unfortunately, these are usually pretty pricey ($50-200 range).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://amzn.to/375UqnU&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Elgato Cam Link 4K&lt;/a&gt; (normally ~$130, sold out right now)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://amzn.to/2AIDW9a&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;BlueAVS 1080P Capture Card&lt;/a&gt; (~$50)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;A common issue with using a DSLR as a webcam that you should be aware of is overheating; many cameras will automatically shut off after a set amount of time (such as 30 minutes) to avoid overheating.&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Export LinkedIn Profiles and Connections to VCard or Sync</title><link>https://joshuatz.com/posts/2020/export-linkedin-profiles-and-connections-to-vcard-or-sync/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/export-linkedin-profiles-and-connections-to-vcard-or-sync/</guid><description>Different options for exporting profiles and connections from LinkedIn, and importing on other platforms, including VCard exports and integrations.</description><pubDate>Fri, 05 Jun 2020 17:46:28 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://en.wikipedia.org/wiki/VCard&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;vCard&lt;/code&gt; file format&lt;/a&gt; (often used with the extension &lt;code&gt;.vcf&lt;/code&gt;, for &lt;em&gt;Virtual Contact File&lt;/em&gt;) is a standardized way of storing contact information, such as name and telephone number, in a text based file. Since it is standardized, &lt;code&gt;vCard&lt;/code&gt; files generated by one application, such as Google Contacts, can easily be read by another, such as Microsoft Outlook.&lt;/p&gt;
&lt;p&gt;This article discusses the various ways that you can export profiles from LinkedIn as a vCard file, so you can add them to Outlook, Google Contacts, iOS, etc. It also discusses alternative ways to sync LI contacts / connections between different platforms.&lt;/p&gt;
&lt;h2 id=&quot;alternative-methods&quot;&gt;Alternative Methods&lt;/h2&gt;
&lt;p&gt;Before I get into ways to generate vCard exports, I first want to cover some alternatives that are less technical and complex for most users, but will work if your goal is simply to sync contacts from LinkedIn to an external contact book.&lt;/p&gt;
&lt;h3 id=&quot;alternative-method---native-sync&quot;&gt;Alternative Method - Native Sync&lt;/h3&gt;
&lt;p&gt;One less complex option that is available to LinkedIn users is to use the native sync feature. LinkedIn offers a built-in &lt;em&gt;sync&lt;/em&gt; feature that synchronizes your LinkedIn connections with different external services; right now only the following are supported (accessible via &lt;a href=&quot;https://www.linkedin.com/mynetwork/settings/manage-syncing/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;your “Manage Sync Options” page&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google (aka Google Contacts)&lt;/li&gt;
&lt;li&gt;Microsoft Outlook (both Work and Personal)&lt;/li&gt;
&lt;li&gt;Mobile Devices (through official LinkedIn App)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To my knowledge, &lt;a href=&quot;https://docs.microsoft.com/en-us/exchange/recipients-in-exchange-online/manage-linkedin-contact-sync&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the Outlook integration&lt;/a&gt; is the only option that allows for &lt;em&gt;bi-directional&lt;/em&gt; sync, where LinkedIn connections will get exported &lt;em&gt;from&lt;/em&gt; LI &lt;em&gt;to&lt;/em&gt; your contacts, whereas the others only support importing &lt;em&gt;from&lt;/em&gt; your contacts &lt;em&gt;to&lt;/em&gt; LI.&lt;/p&gt;
&lt;p&gt;The other major downside here is that you are giving LI a lot of your private information when you setup a sync, and many people might not be comfortable with that level of trust.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Bidirectional sync on mobile used to be supported, but &lt;a href=&quot;https://www.linkedin.com/help/linkedin/answer/50169/adding-your-linkedin-connections-to-your-mobile-device-no-longer-available&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;has been deprecated&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;alternative-method---third-party-services&quot;&gt;Alternative Method - Third Party Services&lt;/h3&gt;
&lt;p&gt;Although there is a limited number of &lt;em&gt;native&lt;/em&gt; contact sync integrations built-in to LinkedIn, there is a growing number of third-party services (usually paid) that have their own integrations built with LinkedIn’s API, or the new &lt;em&gt;Sales Navigator&lt;/em&gt; system.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Warning: I have not tried, nor do I endorse, any of the following. Use at your own risk.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Zoominfo’s &lt;a href=&quot;https://help.zoominfo.com/48498-overview-of-basic-functions/336599-reachout-chrome-extension#reachout-on-linkedin&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;ReachOut Chrome Extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/free-linkedin-email-finde/mlhacebjlefifkldmkbilohcaiednbik&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;SalesQL Chrome Extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://business.linkedin.com/sales-solutions/partners/find-a-partner#crm&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;List of Official SNAP (Sales Navigator) Partners with CRM Support&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Example: &lt;a href=&quot;https://www.zoho.com/crm/linkedin.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Zoho CRM - LinkedIn Integration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;alternative-method---scrapers&quot;&gt;Alternative Method - Scrapers&lt;/h3&gt;
&lt;p&gt;I really would rather not discuss this much, as I can think of very few good reasons to engage in this type of behavior, and hundreds of &lt;em&gt;bad reasons&lt;/em&gt; to do so. Plus, scraping PII data off LI is, in general, against their TOS, and is a great way to get your account banned or limited.&lt;/p&gt;
&lt;p&gt;There are enough bad things in life; please don’t scrape and spam from LinkedIn results.&lt;/p&gt;
&lt;h2 id=&quot;generating-exports-and-converting-to-vcard&quot;&gt;Generating Exports and Converting To vCard&lt;/h2&gt;
&lt;p&gt;If you don’t want to use a synchronization service, and want to manually pull contacts from LinkedIn into a different program, your options are somewhat limited, especially since LinkedIn is restrictive in allowing PII data to flow &lt;em&gt;out&lt;/em&gt; of their platform.&lt;/p&gt;
&lt;h3 id=&quot;csv-export-from-linkedin&quot;&gt;CSV Export from LinkedIn&lt;/h3&gt;
&lt;p&gt;If you want to bulk export all your LinkedIn connections contact data, you can do so, via &lt;a href=&quot;https://www.linkedin.com/help/linkedin/answer/50191&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the &lt;code&gt;Request Archive&lt;/code&gt; feature&lt;/a&gt; of LinkedIn’s Privacy controls.&lt;/p&gt;
&lt;p&gt;You can follow &lt;a href=&quot;https://www.systoolsgroup.com/updates/export-linkedin-contacts-to-import-them-in-mac-outlook-office365/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;this step-by-step guide&lt;/a&gt; on how to request the CSV file, and then, once you have it, you can do any of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Import into another program that accepts CSV inputs (Google Contacts, Outlook, etc.)&lt;/li&gt;
&lt;li&gt;Use option above, and then export back out of the program in a different format (such as &lt;code&gt;.vcf&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Use a converter to bulk convert the CSV rows into a different format such as &lt;code&gt;.vcf&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Since this is a niche request, &lt;a href=&quot;https://www.bitrecover.com/blog/export-linkedin-connections-to-vcard/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;these tools often come at a price&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Warning: &lt;em&gt;&lt;strong&gt;Most&lt;/strong&gt;&lt;/em&gt; of your contacts will likely show up in your export &lt;strong&gt;without an email address&lt;/strong&gt;, even if you can see their email when logged into LinkedIn; this is due to a privacy settings specifically for downloading contact data. See the footnote in &lt;a href=&quot;https://www.linkedin.com/help/linkedin/answer/50191&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;this LI answer page&lt;/a&gt; for details, and &lt;a href=&quot;https://techcrunch.com/2018/11/21/linkedin-email-privacy/&quot;&gt;this article&lt;/a&gt; that explains the policy change and why so many people have email export disabled.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;my-tool---exporter-chrome-extension&quot;&gt;My Tool - Exporter Chrome Extension&lt;/h3&gt;
&lt;p&gt;I previously built a Chrome Browser Extension (&lt;a href=&quot;https://github.com/joshuatz/linkedin-to-jsonresume&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;linkedin-to-jsonresume&lt;/a&gt;) to export any given LinkedIn profile to the JSON Resume format; this is a schema for representing Resume information (work history, education, etc.) with JSON. Recently, I was asked if I could add &lt;code&gt;vCard&lt;/code&gt; exporting to it, which is what actually prompted me to write this post, as I first wanted to research existing solutions before I spent any time writing my own.&lt;/p&gt;
&lt;p&gt;I ended up adding basic vCard / VCF exporting support to the extension, which landed in &lt;a href=&quot;https://github.com/joshuatz/linkedin-to-jsonresume/releases/tag/v1.1.0&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;my &lt;code&gt;v1.1.0&lt;/code&gt; release&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Please note that my extension is primarily focused on exporting an individual profile, and does &lt;em&gt;not&lt;/em&gt; support bulk exports.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>VR PSA - Check Your Cable Setup When Using Extensions</title><link>https://joshuatz.com/posts/2020/vr-psa---check-your-cable-setup-when-using-extensions/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/vr-psa---check-your-cable-setup-when-using-extensions/</guid><description>How to extend VR cables without experiencing tracking issues and glitches, and what solutions are available, such as USB extenders.</description><pubDate>Wed, 03 Jun 2020 09:39:07 GMT</pubDate><content:encoded>&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#discovering-the-issue&quot;&gt;Discovering the Issue:&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-issue&quot;&gt;The Issue:&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-fix---ways-to-properly-extend-vr-usb-cables&quot;&gt;The Fix - Ways to Properly Extend VR USB Cables&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#vive-link-power-adapters&quot;&gt;Vive Link Power Adapters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#issue-with-self-powered-active-extenders&quot;&gt;Issue With Self-Powered “Active Extenders”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sensors-vs-headsets&quot;&gt;Sensors VS Headsets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#samsung-odyssey-and-odyssey-peculiarities&quot;&gt;Samsung Odyssey and Odyssey+ Peculiarities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#why-is-hdmi-less-of-an-issue&quot;&gt;Why Is HDMI Less of An Issue?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#tldr&quot;&gt;TLDR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#extra-troubleshooting&quot;&gt;Extra Troubleshooting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#extra-reading&quot;&gt;Extra Reading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Disclaimer: This post contains several Amazon affiliate links.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’m writing this post because I really wish someone had warned me about this when I first bought my VR headset (a WMR Samsung Odyssey+), and if they had, it would have saved me a lot grief. For a while now, I’ve been struggling to overcome certain issues with the headset, such as unreliable tracking and random glitches.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Windows-Mixed-Reality-WMR-Lost-Boundary-Error-Message.jpg&quot; style=&quot;margin: auto; display:block; max-width: 550px; width: 90%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/Windows-Mixed-Reality-WMR-Lost-Boundary-Error-Message.jpg&quot; alt=&quot;Windows Mixed Reality (WMR) - Lost Boundary Error Message&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I think I finally just found the culprit, &lt;em&gt;&lt;strong&gt;an underpowered cable setup&lt;/strong&gt;&lt;/em&gt;, and this post was written as a guide to explain the issue, as well as solutions for properly extending VR cables without introducing issues.&lt;/p&gt;
&lt;h2 id=&quot;discovering-the-issue&quot;&gt;Discovering the Issue:&lt;/h2&gt;
&lt;p&gt;These random glitches and tracking issues had been getting worse and worse recently, and at a certain point the headset felt unusable.&lt;/p&gt;
&lt;p&gt;At that point, while troubleshooting everything I could possibly think of, I tried plugging the headset &lt;em&gt;directly&lt;/em&gt; into the computer (no extension cables), and &lt;strong&gt;BAM&lt;/strong&gt; - it started working a million times better. No tracking issues, no lost boundaries, and no glitches.&lt;/p&gt;
&lt;h2 id=&quot;the-issue&quot;&gt;The Issue:&lt;/h2&gt;
&lt;p&gt;Like many other VR headsets, the Samsung Odyssey’s cable is really too short (4 meters / 13.1 feet) for &lt;em&gt;comfortable&lt;/em&gt; room-scale play, so shortly after I bought it, I also bought and started using a &lt;a href=&quot;https://www.amazon.com/gp/product/B00C7SA21U/ref=as_li_ss_tl?ie=UTF8&amp;#x26;psc=1&amp;#x26;linkCode=ll1&amp;#x26;tag=jtz-20&amp;#x26;linkId=24d1c6b8dafe4041ff21f5a22bedad81&amp;#x26;language=en_US&quot;&gt;10 foot USB 3.0 extension cable&lt;/a&gt;. At the time, I didn’t think much about adding it to my setup - I knew at least that it was important to use 3.0 (or, more likely, a 3.1 variant) instead of 2.0, since VR needs the higher data speed and throughput, and this cable satisfied that need, so I thought I was good to go. Heck, the cable even advertises itself as being “for … VR Headset(s)”.&lt;/p&gt;
&lt;p&gt;However, &lt;strong&gt;it’s not that simple&lt;/strong&gt;. See, like many people, I was unaware that the recommended maximum length for a run of USB 3.0/3.1 cable is only about &lt;a href=&quot;http://janaxelson.com/usb3faq.htm#ca_maximum&quot;&gt;3 meters (9.8 feet)&lt;/a&gt;(!), depending on cable thickness. The cable from the VR headset &lt;em&gt;already&lt;/em&gt; exceeds that length, so adding an extension cable puts it way over the recommended limit, and explains why removing the extension cable suddenly fixed all my issues.&lt;/p&gt;
&lt;h2 id=&quot;the-fix---ways-to-properly-extend-vr-usb-cables&quot;&gt;The Fix - Ways to Properly Extend VR USB Cables&lt;/h2&gt;
&lt;p&gt;Playing with the headset directly plugged into the computer without extension cables obviously works the best, but it is not practical for room-scale games, or even most standing games for that matter. So how can we extend the cable without exceeding the limits? It depends.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Best overall solution: HTV Vive Link Box (&lt;a href=&quot;https://www.amazon.com/HTC-Vive-Link-Box-pc/dp/B01LXR6DKV/ref=as_li_ss_tl?dchild=1&amp;#x26;keywords=vive+link&amp;#x26;qid=1591096817&amp;#x26;sr=8-1&amp;#x26;linkCode=ll1&amp;#x26;tag=jtz-20&amp;#x26;linkId=0b038e58bc59c89ba3e4774240a45701&amp;#x26;language=en_US&quot;&gt;Amazon product page&lt;/a&gt;, &lt;a href=&quot;https://www.vive.com/us/accessory/link-box/&quot;&gt;Official Product Page&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;Pros:
&lt;ul&gt;
&lt;li&gt;Multi-purpose: repeats both USB &lt;em&gt;&lt;strong&gt;and&lt;/strong&gt;&lt;/em&gt; HDMI signals, as well as converts DisplayPort to HDMI&lt;/li&gt;
&lt;li&gt;High quality: You can check out the internals in &lt;a href=&quot;https://imgur.com/a/PuY2X&quot;&gt;this teardown&lt;/a&gt;. For example, the USB hub chip is a &lt;a href=&quot;https://www.microchip.com/wwwproducts/en/USB5744&quot;&gt;Microchip USB5744&lt;/a&gt;, which supports full USB 3.1 Gen 1 speeds.&lt;/li&gt;
&lt;li&gt;Great for mounting on ceiling, as single repeater for both HDMI and USB cable&lt;/li&gt;
&lt;li&gt;Built-in Bluetooth, in case your computer doesn’t already have it&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cons:
&lt;ul&gt;
&lt;li&gt;Pricey&lt;/li&gt;
&lt;li&gt;Requires a separate power adapter (see &lt;a href=&quot;#vive-link-power-adapters&quot;&gt;below&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Requires a separate male-to-male USB cable&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Powered USB 3.0 Hub (&lt;a href=&quot;https://amzn.to/301BBks&quot;&gt;Amazon search results&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;Pros:
&lt;ul&gt;
&lt;li&gt;Relatively inexpensive, ~$10-20 for most&lt;/li&gt;
&lt;li&gt;You might already have one laying around&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cons:
&lt;ul&gt;
&lt;li&gt;Won’t repeat / extend HDMI connection&lt;/li&gt;
&lt;li&gt;Requires an electrical outlet&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;“Active Extension Cable” (&lt;a href=&quot;https://amzn.to/3gM6W0a&quot;&gt;Amazon search results&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;Pros:
&lt;ul&gt;
&lt;li&gt;Some are self-powered (over the same USB connection it is extending)&lt;/li&gt;
&lt;li&gt;Less clutter (single cable, instead of a connecting box)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cons:
&lt;ul&gt;
&lt;li&gt;Tends to have way more issues than alternatives that are externally powered, &lt;em&gt;&lt;strong&gt;especially with VR headsets&lt;/strong&gt;&lt;/em&gt; - see &lt;a href=&quot;#issue-with-self-powered-active-extenders&quot;&gt;my explanation as to why, below&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Externally-powered ones, like &lt;a href=&quot;https://amzn.to/3gMXlGv&quot;&gt;this one&lt;/a&gt;, tend to work better, but require an extra power cable, so you might as well just get a powered hub&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Self-powered models only boosts signal, &lt;strong&gt;not&lt;/strong&gt; voltage&lt;/li&gt;
&lt;li&gt;Can have limited or no support for backwards compatibility (USB 2.0) or variation in spec&lt;/li&gt;
&lt;li&gt;Often limited in disclosing the full specifications of their product&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;vive-link-power-adapters&quot;&gt;Vive Link Power Adapters&lt;/h3&gt;
&lt;p&gt;As great as the Vive Link Box is, a major drawback is that it does not come with a power adapter, and finding a suitable one to use with it actually took me a while.&lt;/p&gt;
&lt;p&gt;To save you the same hassle, I’ll warn you that you will likely not be able to find an original AC adapter for sale (at least not “new”), and the requirements are as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;12V (DC)&lt;/li&gt;
&lt;li&gt;1.5A (minimum)&lt;/li&gt;
&lt;li&gt;Center Positive Polarity&lt;/li&gt;
&lt;li&gt;Barrel Jack Size: 3.5mm x 1.35mm&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With these requirements in mind, I ended up getting &lt;a href=&quot;https://amzn.to/3eIrAMT&quot;&gt;this&lt;/a&gt; adapter, and so far it has been working great. The most unique thing about the adapter is the barrel jack size; it is much easier to find 5.5 x 2.1mm 12V adapters, and then purchase a size adapter, like &lt;a href=&quot;https://www.amazon.com/gp/product/B07L5HVGM8/ref=as_li_ss_tl?smid=A33YL1HODFD2CL&amp;#x26;psc=1&amp;#x26;linkCode=ll1&amp;#x26;tag=jtz-20&amp;#x26;linkId=dfc63adebd222c6347a4f026136759a3&amp;#x26;language=en_US&quot;&gt;this 2 pack&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Thank you to these Reddit threads for providing details: &lt;a href=&quot;https://www.reddit.com/r/Vive/comments/a968fn/minimum_voltage_for_link_box_htc_vive_help/&quot;&gt;this one&lt;/a&gt;, and &lt;em&gt;especially&lt;/em&gt; &lt;a href=&quot;https://www.reddit.com/r/Vive/comments/51h1rn/power_adapter_answers/&quot;&gt;this one (&lt;em&gt;power adapter answers&lt;/em&gt;)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;issue-with-self-powered-active-extenders&quot;&gt;Issue With Self-Powered “Active Extenders”&lt;/h3&gt;
&lt;p&gt;There are a bunch of cables on Amazon that don’t require external power adapter, unlike USB hubs or the Vive Link Box, but still call themselves things like “Signal Booster”, “Active Extender Cable”, and “Active Repeater Cable”. Although these aren’t “snake oil” products, the way they work can have some negative consequences for VR applications.&lt;/p&gt;
&lt;p&gt;Most of these cables include some sort of repeater IC (Integrated Circuit) chip, built into the cable itself. Usually this is noticeable, because one end of the cable will be very bulky to accommodate the IC circuit. To power these circuits, the cable basically siphons current from it’s own wiring - the connection it is extending.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is often referred to as being &lt;em&gt;“bus powered”&lt;/em&gt;, since the power is drawn from the computer’s USB bus, rather than an external power source.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For many devices, these cables work fine. However, VR devices tend to have &lt;em&gt;very&lt;/em&gt; tight requirements and tolerances, since their main concern is latency; delivering an image with an added 10ms of delay (that’s just &lt;code&gt;0.01&lt;/code&gt; of a second) can be the difference between a great VR experience, and one that makes you feel like you are going to throw up from motion sickness.&lt;/p&gt;
&lt;p&gt;To get back to the issue, there are two major problems with these types of cables:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Those repeater chips (often called &lt;em&gt;“redrivers”&lt;/em&gt;) often only need a small amount of power to be siphoned off. For example, the &lt;a href=&quot;https://www.nxp.com/docs/en/data-sheet/PTN36241B.pdf&quot;&gt;&lt;code&gt;NXP PTN36241B&lt;/code&gt;&lt;/a&gt;, which is used in the &lt;a href=&quot;https://amzn.to/3cqS6sD&quot;&gt;CableCreation 16.4 Foot Active Extender&lt;/a&gt;, only requires about 100ma of current while active, although the circuit integrating the chip probably draws a little over that. 100ma is not much, but the VR headset itself &lt;em&gt;also&lt;/em&gt; draws a significant amount of power, to run the displays and assorted circuits, and it can be the case that there simply is not 100ma to spare, especially if you have an older computer with a low maximum supply current for USB.
&lt;ul&gt;
&lt;li&gt;This is an issue for the Samsung Oyssey+ in particular, because of how much current the device draws. The Odyssey and Odyssey+ draw about 900ma, which is right at the border of what the USB spec says a port is required to provide.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The “bus powered” repeater chips boost the &lt;em&gt;data&lt;/em&gt; signal strength, but do not usually boost the voltage, since realistically this would require an external power supply (to handle the current required by the boost circuit itself). This can be a big problem, because voltage will naturally drop as a cable extends in length, and many VR headsets are &lt;em&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;/em&gt; tolerant of lower voltage supplies.
&lt;ul&gt;
&lt;li&gt;For example, in a 16 foot run of USB cable, without a voltage boost, the voltage drop could easily exceed 0.5v, which could be outside the USB specifications, and likely to to cause issues with a VR headset&lt;/li&gt;
&lt;li&gt;Normally, your motherboard and USB port can adjust to compensate for a voltage drop, but combined with the issue above, it might not be able to with the load of both the repeater and headset running&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These two limitations of self-powered repeaters, especially when combined, are often deal-breakers for VR headsets. To this point, note the small FAQ section the product page linked above for the CableCreation active extender:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q: Does this extension work Samsung Odyssey VR?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;A: Yes, but because of the extended length […] you need to connect a powered USB hub or adapter for external power supply.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, &lt;em&gt;&lt;strong&gt;if you buy a self-powered repeater cable, you might also have to buy a powered USB hub to plug it into, which kind of defeats the purpose…&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;sensors-vs-headsets&quot;&gt;Sensors VS Headsets&lt;/h3&gt;
&lt;p&gt;You might be wondering why on a lot of Amazon reviews for various extenders / repeaters / cables, you see a common pattern where there are positive reviews saying “this worked great for ____ VR &lt;strong&gt;sensor&lt;/strong&gt;!”, but also negative reviews of “this didn’t work at all with ___ VR &lt;strong&gt;headset&lt;/strong&gt;”.&lt;/p&gt;
&lt;p&gt;The reasons for this are pretty simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sensors usually require a lot less &lt;em&gt;total&lt;/em&gt; throughput, compared with a headset (which also often has built-in sensors itself)&lt;/li&gt;
&lt;li&gt;Sensors require much less &lt;strong&gt;power&lt;/strong&gt; than headsets, since they aren’t driving bright displays combined with audio, processors, and more.&lt;/li&gt;
&lt;li&gt;Sensors often have greater tolerances for different types of hosts (for example, the Rift Sensor can work over USB 2.0 in addition to 3.0, whereas most headsets &lt;strong&gt;require&lt;/strong&gt; 3.0)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In general, a standalone sensor just requires a lot less “stuff” to function, in comparison to a headset, which is built with a bunch of sensors, plus dozens (if not hundreds) of other components.&lt;/p&gt;
&lt;h3 id=&quot;samsung-odyssey-and-odyssey-peculiarities&quot;&gt;Samsung Odyssey and Odyssey+ Peculiarities&lt;/h3&gt;
&lt;p&gt;I’ve touched on this above, in various sections, but I think part of why the Samsung Odyssey+ in particular seems to have so many users complaining about issues with extending USB length is because the device was built to basically be right at the edge of all “maximums”, out of the box. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Various manuals and webpages make it sound like the device absolutely needs a full 900ma to function. That is &lt;em&gt;literally&lt;/em&gt; the maximum required by &lt;a href=&quot;https://en.wikipedia.org/wiki/USB_3.0&quot;&gt;the USB 3.0 spec&lt;/a&gt;.
&lt;ul&gt;
&lt;li&gt;USB ports are free to supply &lt;em&gt;more&lt;/em&gt; than that amount, but are not required to, and it is up to the PC manufacturer.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Various sources also make it sound like the Odyssey is not tolerant of voltage drops; e.g. it needs the USB supply voltage to be as close to 5V as possible
&lt;ul&gt;
&lt;li&gt;This is especially problematic since the Odyssey+ comes with a 4 meter (13.1 feet) USB cable, which is &lt;em&gt;already&lt;/em&gt; long enough to be in the danger zone (or right at the edge) for causing voltage drops. It helps that the USB cable is thicker than most.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Disclaimer: I’m not an Electrical Engineer, so most of this is based on quick research, not prior knowledge.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;These Reddit threads (&lt;a href=&quot;https://www.reddit.com/r/WindowsMR/comments/agv3ls/samsung_odyssey_and_my_successful_quest_for/&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://www.reddit.com/r/WindowsMR/comments/b7y77m/thoughts_on_samsung_odyssey_plus_cable_extension/&quot;&gt;2&lt;/a&gt;, &lt;a href=&quot;https://www.reddit.com/r/WindowsMR/comments/91e68n/update_on_extension_cables_samsung_odyssey/&quot;&gt;3&lt;/a&gt;, &lt;a href=&quot;https://www.reddit.com/r/WindowsMR/comments/8vv8o8/extension_cables_samsung_odyssey/&quot;&gt;4&lt;/a&gt;) are good examples of what I mean by this being a common issue for Odyssey users (who could probably benefit from my advice on this page).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;why-is-hdmi-less-of-an-issue&quot;&gt;Why Is HDMI Less of An Issue?&lt;/h3&gt;
&lt;p&gt;You might be wondering why, so far, I have written pretty much exclusively around how extending USB is an issue, and doing it incorrectly leads to issues with VR headsets. Why not HDMI as well?&lt;/p&gt;
&lt;p&gt;I’m not on expert on these things, but I think I can simplify without being incorrect, in saying that extending the HDMI cable with VR headsets is generally not as much of an issue as USB because of the following two factors:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;HDMI has a longer maximum length than USB to start with
&lt;ul&gt;
&lt;li&gt;There is not a specific maximum length put forward in a standardized HDMI spec, but generally, &lt;a href=&quot;https://www.popularmechanics.com/home/how-to/a6751/how-to-extend-your-hdmi-cables/&quot;&gt;25-50 feet is considered the maximum&lt;/a&gt;, without a repeater.&lt;/li&gt;
&lt;li&gt;In comparison, a standard thickness (24-28 awg) USB 3.0 cable should not exceed somewhere around 10 feet.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HDMI is being used for less &lt;em&gt;things&lt;/em&gt; than USB, on a VR HMD
&lt;ul&gt;
&lt;li&gt;With a standard VR headset, HDMI carries the video feed, and maybe the audio feed. This data (mostly) only has to flow in one direction, and there is some tolerance built-in&lt;/li&gt;
&lt;li&gt;USB, on the other hand, is carrying the power supply, sync data, microphone feeds, and sensor data. Sensor data could be multiple bi-directional feeds, encompassing live cameras, accelerometers, gyroscopes, compasses, and more. Many of these pieces of data have to be delivered with minimum latency, as any delay will increase the risk of motion sickness.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Having said all that, extending the HDMI cable could still be an issue for you, either over longer lengths or with thinner cables, which is why I still recommend &lt;a href=&quot;https://amzn.to/3cp6ktZ&quot;&gt;the Vive Link Box&lt;/a&gt; as the best extender solution, since it extends both the USB and HDMI connection in one device.&lt;/p&gt;
&lt;h2 id=&quot;tldr&quot;&gt;TLDR&lt;/h2&gt;
&lt;p&gt;Here is the TLDR of this post; if your VR headset frequently has tracking issues, loses boundaries, or other random glitches, take the time to check your cable setup and &lt;em&gt;&lt;strong&gt;make sure you are providing enough power for the length of cable used&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;List of symptoms that could appear with cable length issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frequent “lost boundary” error message and/or room starts spinning&lt;/li&gt;
&lt;li&gt;Controllers lose tracking and start floating randomly
&lt;ul&gt;
&lt;li&gt;Tends to happen with fast games, like Beat Saber&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;“phantom” controller input; random key presses and trigger pulls
&lt;ul&gt;
&lt;li&gt;This one also seems to be caused by sunlight and/or reflective surfaces around the play area&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Entire headset randomly restarts
&lt;ul&gt;
&lt;li&gt;Especially when cable is moved&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Headset takes a long time to recognize environment and/or sync with controllers&lt;/li&gt;
&lt;li&gt;Random audio “hiss” or “static” noise&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Best ways to extend cable length without issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a powered repeater box (powered USB 3.0 hub, or “Vive Link Box”)&lt;/li&gt;
&lt;li&gt;Use cables marked as “Active Extension” and/or “Active Repeater”, especially those that require external power sources&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;extra-troubleshooting&quot;&gt;Extra Troubleshooting&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Make sure your USB cable is plugged into a 3.0/3.1 port. Usually, these are marked with a “SS” logo, for &lt;em&gt;SuperSpeed&lt;/em&gt;.
&lt;ul&gt;
&lt;li&gt;If your PC has any USB ports labeled “high power” or something like that, switch your VR headset over to that port&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Make sure you are using an HDMI connection to match the requirements of your HMD, usually 2.0&lt;/li&gt;
&lt;li&gt;Check that controllers are charged up&lt;/li&gt;
&lt;li&gt;Check for firmware updates&lt;/li&gt;
&lt;li&gt;Try re-pairing controllers&lt;/li&gt;
&lt;li&gt;Make sure your play area is well-lit, but not with sunlight, and is free of reflective surfaces&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;extra-reading&quot;&gt;Extra Reading&lt;/h2&gt;
&lt;p&gt;If you are nerdy like I am (🤓), you might share my interest in these resources that I came across while writing this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intel / HP / TI / Microsoft / Etc. : &lt;a href=&quot;https://www.usb3.com/whitepapers/USB%203%200%20(11132008)-final.pdf&quot;&gt;&lt;em&gt;Universal Serial Bus 3.0 Specification&lt;/em&gt;&lt;/a&gt; (This is the full USB 3.0 specification document)&lt;/li&gt;
&lt;li&gt;USB org / Intel: &lt;a href=&quot;https://www.usb.org/sites/default/files/power_delivery_motherboards.pdf&quot;&gt;&lt;em&gt;Power Delivery Design Issues for Hi-Speed USB on Motherboards&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NTI (Network Technologies Incorporated): &lt;a href=&quot;https://www.usb.org/sites/default/files/power_delivery_motherboards.pdf&quot;&gt;&lt;em&gt;An Introduction to USB Extenders&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;USB org: &lt;a href=&quot;https://www.usb.org/sites/default/files/usb-if_logo_usage_guidelines_final_as_of_august_3_2018_locked.pdf&quot;&gt;Logo Guidelines&lt;/a&gt; (found when searching for port identifiers)&lt;/li&gt;
&lt;li&gt;Techquickie: &lt;a href=&quot;https://youtu.be/p9nqQuu4lmQ&quot;&gt;&lt;em&gt;When Does Cable Length Matter&lt;/em&gt;&lt;/a&gt; (&lt;a href=&quot;https://youtu.be/p9nqQuu4lmQ?t=184&quot;&gt;queued up to USB part&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Microsoft - Windows Mixed Reality: &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/mixed-reality/enthusiast-guide/troubleshooting-windows-mixed-reality&quot;&gt;&lt;em&gt;Enthusiast Guide&lt;/em&gt; - &lt;em&gt;Troubleshooting Windows Mixed Reality&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>VSCode Intellisense Autocomplete for Webpack Config Files</title><link>https://joshuatz.com/posts/2020/vscode-intellisense-autocomplete-for-webpack-config-files/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/vscode-intellisense-autocomplete-for-webpack-config-files/</guid><description>How to easily enable VSCode Intellisense and autocomplete within Webpack config JS or JSON files.</description><pubDate>Fri, 15 May 2020 02:27:36 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Click &lt;a href=&quot;#solution-a-use-typescript-definition&quot;&gt;here&lt;/a&gt; to jump right to the recommended solution.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Warning: I will use the terms “intellisense”, “autocomplete”, and “hints”, somewhat interchangeably throughout this post. They are not technically equivalent, but you should be able to get the gist of what I mean.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;One of my favorite parts of how the developer experience has improved is how much better IDEs have gotten at providing “hints”, or warnings about “silly” mistakes. Intellisense for config files is something I deeply appreciate, as I don’t care to memorize hundreds of different schemas in my head, especially when the IDE can do it for me, as well as validate my input.&lt;/p&gt;
&lt;p&gt;There are many config files that have intellisense support built-in when edited in VSCode, such as &lt;code&gt;.babelrc&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, and &lt;code&gt;jsconfig.json&lt;/code&gt;, plus hundreds more are contributed by extensions and modules.&lt;/p&gt;
&lt;h2 id=&quot;problem&quot;&gt;Problem:&lt;/h2&gt;
&lt;p&gt;You might have noticed that, by default when you are building a &lt;code&gt;webpack.config.js&lt;/code&gt; file, VSCode does not provide intellisense / hints as you type. Given the somewhat notorious complexity of webpack, this is something I was somewhat surprised to find, and doubly surprised to find very little information out there on how to enable. Thus, the reason for making this post.&lt;/p&gt;
&lt;h2 id=&quot;solutions&quot;&gt;Solution(s):&lt;/h2&gt;
&lt;h3 id=&quot;solution-a-use-typescript-definition&quot;&gt;Solution A: Use TypeScript Definition&lt;/h3&gt;
&lt;p&gt;Using TypeScript definitions for webpack config intellisense was actually previously suggested on multiple related Webpack issues (&lt;a href=&quot;https://github.com/microsoft/vscode/issues/24270#issuecomment-294058829&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;VSCode #24270&lt;/a&gt;, &lt;a href=&quot;https://github.com/microsoft/vscode/issues/24657#issuecomment-294058005&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;VSCode #24657&lt;/a&gt;, and &lt;a href=&quot;https://github.com/Microsoft/TypeScript/issues/13333&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;TS #13333&lt;/a&gt;), but for a while, was not working. Now, thanks to improvements in VSCode’s handling of TS-Powered JSDoc comments (very impressive), it works just fine.&lt;/p&gt;
&lt;p&gt;To use, simply prefix where you are declaring the config object with a JSDoc type annotation, like so:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/** &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; {import(&apos;webpack&apos;).Configuration}&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    mode: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;development&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here it is in action:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/SzJL3X1.gif&quot; alt=&quot;VSCode Intellisense in webpack.config.js - powered by TypeScript and JSDoc comment&quot;&gt;&lt;/p&gt;
&lt;p&gt;The interface declaration comes from &lt;a href=&quot;https://www.npmjs.com/package/@types/webpack&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;@types/webpack&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you run into issues, make sure you have already added Webpack as a dependency, and as a last resort, you can add &lt;code&gt;@types/webpack&lt;/code&gt; as a devDependency.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;solution-b-use-json-config-and-published-schema-file&quot;&gt;Solution B: Use JSON Config and Published Schema File&lt;/h3&gt;
&lt;p&gt;According to &lt;a href=&quot;https://github.com/webpack/webpack/issues/6138&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;this issue&lt;/a&gt;, Webpack already supports passing in a &lt;code&gt;.json&lt;/code&gt; config file instead of JS. However, creating a &lt;code&gt;webpack.config.json&lt;/code&gt; &lt;em&gt;&lt;strong&gt;still&lt;/strong&gt;&lt;/em&gt; won’t get Intellisense to kick in automatically, so you have to go one more step - tell VSCode what schema to use.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I don’t want to go into too much detail on this, but JSON Schema is a very powerful mechanism, and is well-supported in VSCode. I have a cheatsheet section devoted to it, &lt;a href=&quot;https://cheatsheets.joshuatz.com/cheatsheets/js/js-devops/#json-schema&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After adding webpack as a dependency, in &lt;code&gt;webpack.config.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;$schema&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;./node_modules/webpack/schemas/WebpackOptions.json&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;mode&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;development&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This method is &lt;em&gt;&lt;strong&gt;not recommended&lt;/strong&gt;&lt;/em&gt; in comparison to method A, because you lose the ability to do anything outside of the JSON spec - e.g., you can’t use comments, you can’t use variables and globals (no &lt;code&gt;__dirname&lt;/code&gt;!), and many other restrictions. Plus, now that method A works, there is no real benefit to this approach, and I’m mainly including it as a demonstration of how VSCode supports and resolves JSON Schema.&lt;/p&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap-Up&lt;/h2&gt;
&lt;p&gt;Hope this helps someone! Feel free to let me know in the comments if I missed anything!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;More About Me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🔗&lt;a href=&quot;https://joshuatz.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;joshuatz.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;👨‍💻&lt;a href=&quot;https://dev.to/joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;dev.to/joshuatz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💬&lt;a href=&quot;https://twitter.com/1joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;@1joshuatz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💾&lt;a href=&quot;https://github.com/joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;github.com/joshuatz&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Building a Perfectly-Sized Dynamic Cover Image with Cloudinary</title><link>https://joshuatz.com/posts/2020/building-a-perfectly-sized-dynamic-cover-image-with-cloudinary/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/building-a-perfectly-sized-dynamic-cover-image-with-cloudinary/</guid><description>How to generate the perfectly sized cover images, with any input image, using Cloudinary&apos;s URL based transformations, overlays, and effects.</description><pubDate>Wed, 13 May 2020 08:30:54 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;For those unfamiliar with Cloudinary, it is a cloud service that is focused on image serving, with unique features like transformations, responsive generation, and a speedy CDN for delivery. I have no affiliation with Cloudinary, other than being a fan of their service.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;One of the big changes in web development over the years has been a shift from “fixed” designs to “responsive”. For example, where we used to define layouts or the display of assets with fixed units, we now try to use percentages or other ratio based approaches to adapt to any display size. This is something to be celebrated, and Cloudinary has &lt;a href=&quot;https://cloudinary.com/features/responsive_images&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;an entire feature set built around this&lt;/a&gt;, but &lt;strong&gt;today I want to talk about when the &lt;em&gt;opposite&lt;/em&gt; approach is needed.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We often have “cover images”, “hero images”, and/or images that we need for social media sharing sites, and these frequently have specific desired pixel sizes or aspect ratios. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dev.to cover image: 1000 x 420 (see &lt;a href=&quot;https://dev.to/p/editor_guide&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;em&gt;Editor Guide&lt;/em&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Twitter: (depends) 16:9 (1.77:1), 2:1, 1200x628 (1.91:1)&lt;/li&gt;
&lt;li&gt;Facebook feed: 1.91:1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finding images that perfectly fit these aspect ratios is often difficult and time-consuming, and manually cropping and resizing existing images to fit, equally so.&lt;/p&gt;
&lt;p&gt;What if there was a better way to generate the perfectly sized hero image, with &lt;em&gt;any&lt;/em&gt; image as the input?…&lt;/p&gt;
&lt;h2 id=&quot;our-goal&quot;&gt;Our Goal&lt;/h2&gt;
&lt;p&gt;So, here is our example goal. We want to produce a 1000 x 420 pixel (2.38:1) cover image for Dev.to, but do so automatically, with a method that accepts any image as the input. If the input aspect ratio does not match the desired output, then the input will be centered, and a blurred background based on the same input shown behind it.&lt;/p&gt;
&lt;p&gt;Sample Input:&lt;/p&gt;
&lt;img src=&quot;https://res.cloudinary.com/demo/image/upload/sample&quot; alt=&quot;Sample Input&quot; style=&quot;margin: auto; display:block; max-width: 800px; width: 70%; height: auto;&quot;&gt;
&lt;p&gt;Sample &lt;em&gt;&lt;strong&gt;goal&lt;/strong&gt;&lt;/em&gt; output:&lt;/p&gt;
&lt;img src=&quot;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420,e_outline:outer:2:100,fl_no_overflow/sample&quot; alt=&quot;Sample Output&quot; style=&quot;margin: auto; display:block; max-width: 800px; width: 80%; height: auto;&quot;&gt;
&lt;blockquote&gt;
&lt;p&gt;The cover image for this very post was created with the method outlined below! But, you’ll see that later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;building-it-with-cloudinary&quot;&gt;Building It With Cloudinary&lt;/h2&gt;
&lt;p&gt;I’m going to walk through how to build this with &lt;a href=&quot;https://cloudinary.com/documentation/image_transformations&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudinary image transformations&lt;/a&gt;, using URL parameters alone. This makes it very easy to use anywhere, with any backend or frontend, since all we need is a URL.&lt;/p&gt;
&lt;p&gt;Base Image: &lt;a href=&quot;https://res.cloudinary.com/demo/image/upload/sample&quot;&gt;https://res.cloudinary.com/demo/image/upload/sample&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When you see &lt;em&gt;&lt;strong&gt;Bold and Italic&lt;/strong&gt;&lt;/em&gt; text in the URL, that indicates what was added in the step.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;steps&quot;&gt;Steps:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Start with the “Base Image”: &lt;code&gt;https://res.cloudinary.com/demo/image/upload/&lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;sample&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;Added: &lt;em&gt;&lt;strong&gt;&lt;code&gt;sample&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;
&lt;img src=&quot;https://res.cloudinary.com/demo/image/upload/sample&quot; style=&quot;margin: auto; display:block; max-width: 600px; width: 50%; height: auto;&quot;&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use image to &lt;em&gt;fill&lt;/em&gt; exact desired size
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://res.cloudinary.com/demo/image/upload/&lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;c_fill,w_1000,h_420/&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;code&gt;sample&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Added: &lt;em&gt;&lt;strong&gt;&lt;code&gt;/c_fill,w_1000,h_420/&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;
&lt;img src=&quot;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420/sample&quot; style=&quot;margin: auto; display:block; max-width: 600px; width: 50%; height: auto;&quot;&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Since this is actually going to be the background, apply a blur effect and reduce opacity
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420&lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;,e_blur:1500,o_75&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;code&gt;/sample&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Added: &lt;em&gt;&lt;strong&gt;&lt;code&gt;e_blur:1500,o_75&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Blur &lt;a href=&quot;https://cloudinary.com/documentation/image_transformation_reference#effect_parameter&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;is on a scale from 1 to 2000&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Opacity is on a scale from 0 to 100 (100 being completely opaque)
&lt;img src=&quot;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/sample&quot; style=&quot;margin: auto; display:block; max-width: 600px; width: 50%; height: auto;&quot;&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Overlay the same image, but &lt;em&gt;&lt;strong&gt;limit&lt;/strong&gt;&lt;/em&gt; its size to the “canvas”
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75&lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;/l_sample,c_limit,w_1000,h_420/&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;code&gt;sample&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Added: &lt;em&gt;&lt;strong&gt;&lt;code&gt;/l_sample,c_limit,w_1000,h_420/&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Notice how &lt;code&gt;l_{uploadedId}&lt;/code&gt; is used to generate an &lt;a href=&quot;https://cloudinary.com/documentation/image_transformations#adding_image_overlays&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;em&gt;image overlay&lt;/em&gt; layer&lt;/a&gt;.
&lt;img src=&quot;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420/sample&quot; style=&quot;margin: auto; display:block; max-width: 600px; width: 50%; height: auto;&quot;&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add a nice blurred border to our overlay
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420&lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;,e_outline:outer:2:100&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;code&gt;/sample&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Added: &lt;em&gt;&lt;strong&gt;&lt;code&gt;e_outline:outer:2:100&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;You might notice that the border now pushes the overlay outside the &lt;em&gt;canvas&lt;/em&gt;…
&lt;img src=&quot;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420,e_outline:outer:2:100/sample&quot; style=&quot;margin: auto; display:block; max-width: 600px; width: 50%; height: auto;&quot;&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Prevent “overflow” caused by new border
&lt;ul&gt;
&lt;li&gt;Adding a border to the overlay actually made it larger than the boundaries of the background (base image). We have two options to remedy this:&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Option A:&lt;/strong&gt; Re-crop: Since adding a border to the overlay made it larger than the boundaries of the background, we can simply re-crop the output
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420,e_outline:outer:2:100&lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;/c_crop,w_1000,h_420/&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;code&gt;sample&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Added: &lt;em&gt;&lt;strong&gt;&lt;code&gt;/c_crop,w_1000,h_420/&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;If we wanted to re-crop just the overlay, we could use &lt;code&gt;fl_layer_apply&lt;/code&gt; to prevent crop action from applying to the entire chain&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Option B:&lt;/strong&gt; Use the &lt;code&gt;no_overflow_flag&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;This is a super helpful flag to remember! From &lt;a href=&quot;https://cloudinary.com/documentation/transformation_flags&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the docs&lt;/a&gt;:
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;Prevents Cloudinary from extending the image canvas beyond the original dimensions when overlaying text and other images.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420,e_outline:outer:2:100&lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;,fl_no_overflow&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;code&gt;/sample&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Added: &lt;em&gt;&lt;strong&gt;&lt;code&gt;fl_no_overflow&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Final output, using option B:&lt;/p&gt;
&lt;img src=&quot;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420,e_outline:outer:2:100,fl_no_overflow/sample&quot; style=&quot;margin: auto; display:block; max-width: 800px; width: 90%; height: auto;&quot;&gt;
&lt;h3 id=&quot;extras&quot;&gt;Extras:&lt;/h3&gt;
&lt;p&gt;There is still more we can do with this!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add compression
&lt;ul&gt;
&lt;li&gt;To save some bytes, we can tell Cloudinary to serve as a compressed JPG with a quality of 80%&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420,e_outline:outer:2:100/c_crop,w_1000,h_420&lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;,q_80&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;code&gt;/sample&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For our sample image, this gives us a saving of approximately 50%!!!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add overlay text
&lt;ul&gt;
&lt;li&gt;We can add &lt;a href=&quot;https://cloudinary.com/documentation/image_transformations#adding_text_captions&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;overlay text&lt;/a&gt;, such as the title of the post that our cover image is for&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420,e_outline:outer:2:100,fl_no_overflow&lt;/code&gt;&lt;em&gt;&lt;strong&gt;&lt;code&gt;/l_text:Roboto_100_normal_stroke_:Hero%20Image,co_rgb:FFFFFF,bo_8px_solid_black,g_south,y_30/&lt;/code&gt;&lt;/strong&gt;&lt;/em&gt;&lt;code&gt;sample&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;stroke&lt;/em&gt;, and &lt;em&gt;border&lt;/em&gt; (&lt;code&gt;bo_&lt;/code&gt;) are used to provide an outline around the text
 &lt;img src=&quot;https://res.cloudinary.com/demo/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_sample,c_limit,w_1000,h_420,e_outline:outer:2:100,fl_no_overflow/l_text:Roboto_100_normal_stroke_:My%20Image,co_rgb:FFFFFF,bo_8px_solid_black,g_south,y_30/sample&quot; style=&quot;margin: auto; display:block; max-width: 800px; width: 80%; height: auto;&quot;&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap-Up&lt;/h2&gt;
&lt;p&gt;This might have seemed like a bunch of work, but don’t forget; now that we have the transformation chain built, we can reuse it across an unlimited number of images, simply replacing &lt;code&gt;{uploadedId}&lt;/code&gt; with our desired input:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; myCloudId&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;acb123&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// pretend `myImages` is an array of hundreds of image IDs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;myImages.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;forEach&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;uploadedId&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; heroImage&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; `https://res.cloudinary.com/${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;myCloudId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/image/upload/c_fill,w_1000,h_420,e_blur:1500,o_75/l_${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;uploadedId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;},c_limit,w_1000,h_420,e_outline:outer:2:100,fl_no_overflow/${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;uploadedId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    saveImage&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(heroImage);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In fact, here it is in action, working its magic across any input I give it (large GIF, give a second to load…):&lt;/p&gt;
&lt;img src=&quot;https://i.imgur.com/UIi99Do.gif&quot; alt=&quot;Cloudinary Dynamic Cover Image - Demo With Multiple Inputs&quot; style=&quot;margin: auto; display:block; max-width: 1038px; width: 100%; height: auto;&quot;&gt;
&lt;p&gt;Nice, right!?&lt;/p&gt;
&lt;p&gt;We could also…&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Save this as a &lt;a href=&quot;https://cloudinary.com/documentation/chained_and_named_transformations#named_transformations&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;em&gt;Named Transformation&lt;/em&gt;&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Once we do this, reusing it becomes as easy as &lt;code&gt;/image/upload/{transformationName}/{uploadedId}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Implement this via Cloudinary API&lt;/li&gt;
&lt;li&gt;Integrate into any backend (WordPress, Gatsby, etc.) for dynamic &lt;code&gt;meta&lt;/code&gt; image tags to serve to Twitter, FB / OpenGraph, etc.&lt;/li&gt;
&lt;li&gt;… and many more things!&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;If this was interesting to you, I have a few other Cloudinary related projects that might be worth checking out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cheatsheets.joshuatz.com/cheatsheets/apis/cloudinary/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudinary Cheatsheet&lt;/a&gt; - WIP quick cheatsheet&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://joshuatz.com/projects/applications/desktop-cloud-transform-dct/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Desktop Cloud Transform&lt;/a&gt; - Transform local images with Cloudinary, with easy drag-and-drop GUI&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://joshuatz.com/projects/web-stuff/cloudinary-wysiwyg-visual-editor-for-transformations/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudinary WYSIWYG&lt;/a&gt; - Visually build Cloudinary transformations with interactive canvas&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;More About Me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🔗&lt;a href=&quot;https://joshuatz.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;joshuatz.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;👨‍💻&lt;a href=&quot;https://dev.to/joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;dev.to/joshuatz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💬&lt;a href=&quot;https://twitter.com/1joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;@1joshuatz&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;💾&lt;a href=&quot;https://github.com/joshuatz&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;github.com/joshuatz&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Scripting My Morning Wake-Up Alarm and Lights with Android and Kasa</title><link>https://joshuatz.com/posts/2020/scripting-my-morning-wake-up-alarm-and-lights-with-android-and-kasa/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/scripting-my-morning-wake-up-alarm-and-lights-with-android-and-kasa/</guid><description>Using TP-Link Kasa, Google Apps Script, Android, and Automagic to automate my morning wake up alarm and turn on the lights.</description><pubDate>Wed, 06 May 2020 03:56:20 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;TLDR: This post covers how (and why) I setup an Android automation flow that automatically turns on lights in my room whenever my morning alarm goes off, &lt;em&gt;&lt;strong&gt;regardless&lt;/strong&gt;&lt;/em&gt; of which alarm app I use!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;I have something to admit. I’m one of those people that has struggled with my sleep schedule my entire life. It &lt;em&gt;&lt;strong&gt;constantly&lt;/strong&gt;&lt;/em&gt; shifts, and when I force myself to wake up at a certain time with an alarm clock, both my mind and body fight against the process. Don’t get me wrong (especially potential employers 😀) - I can always force myself to get up when I need to, but it has not been easy.&lt;/p&gt;
&lt;p&gt;As such, it should be no surprise that I also have tried many different types of alarms, and have settled on “challenge” alarm apps. These are alarm apps that force you to complete certain tasks before the alarm will shut off, such as scanning a barcode, solving math problems, or shaking your phone.&lt;/p&gt;
&lt;h2 id=&quot;can-i-get-some-light-in-here&quot;&gt;Can I Get Some Light in Here?&lt;/h2&gt;
&lt;p&gt;One of the challenges that my alarm clock app (&lt;a href=&quot;https://play.google.com/store/apps/details?id=com.urbandroid.sleep&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Sleep as Android&lt;/a&gt;) has is called “Let there be light”; in order to complete the task, you must expose your phone to a high level of light for a minimum amount of time (adjustable).&lt;/p&gt;
&lt;p&gt;I sleep with blackout curtains because I’m light-sensitive, and this is great for falling asleep, but makes it too easy to sleep-in late in the day. Thus, this challenge has been great for forcing me to bring some light into my pitch-black environment in the mornings.&lt;/p&gt;
&lt;p&gt;…but, I was wondering; can I take this a step further? Can I have my lights automatically turn on whenever my alarm goes off?&lt;/p&gt;
&lt;h2 id=&quot;automating-power-outlets-with-android&quot;&gt;Automating Power Outlets with Android&lt;/h2&gt;
&lt;p&gt;I already had a &lt;a href=&quot;https://amzn.to/2VWrV8m&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;TP-Link Kasa Wi-Fi Plug&lt;/a&gt; that I wasn’t doing much with, so I set out upon figuring out how to automate it based on my Android alarm settings.&lt;/p&gt;
&lt;p&gt;The Kasa software does support built-in scheduling, but you have to manually enter times for it to turn off and on; I wanted an automatic solution that would hook into the system-level Android alarm and would work regardless of what alarm app I was using, and no matter how many times I changed my alarm time.&lt;/p&gt;
&lt;h3 id=&quot;google-apps-script-wrapper&quot;&gt;Google Apps Script Wrapper&lt;/h3&gt;
&lt;p&gt;My first step was to create a “wrapper” script / API around the unofficial Kasa API. I wanted to do this, because using the Kasa API requires a multi-step token exchange, and I wanted to be able to control my switch with a single network request (which I knew would make the automation piece easier down the line).&lt;/p&gt;
&lt;p&gt;I used &lt;a href=&quot;https://developers.google.com/apps-script&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Google Apps Script&lt;/a&gt; for this, since it is a free and very easy to use JavaScript serverless environment; similar to AWS Lambda, but without the complicated pricing or web console. Its ease of use makes it great for one-off hobby projects like this one.&lt;/p&gt;
&lt;p&gt;I won’t go too much into the details of the script here, but the basics are that it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Caches my credentials and token to avoid extra API requests&lt;/li&gt;
&lt;li&gt;Refreshes the auth token when it expires&lt;/li&gt;
&lt;li&gt;Simplifies using the Kasa API by wrapping and formatting requests for it&lt;/li&gt;
&lt;li&gt;Published as a “web app”, so I have an endpoint I can POST to&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ve posted my script &lt;a href=&quot;https://gist.github.com/joshuatz/5266d8cc85ef3e0e67561de3573a1ff5&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;, as a Github gist.&lt;/p&gt;
&lt;h3 id=&quot;integrating-with-android-via-automagic&quot;&gt;Integrating with Android via Automagic&lt;/h3&gt;
&lt;p&gt;Since my Google Apps Script gives me a single endpoint I can turn my lights on with a single request, all I need to automate my flow in Android is an app that can A) listen for the alarm trigger, and B) make a POST request with a payload. Luckily, I already have a go-to automation app that meets both those requirements. Meet &lt;a href=&quot;https://play.google.com/store/apps/details?id=ch.gridvision.ppam.androidautomagic&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Automagic&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Automagic flow is super simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Trigger = &lt;a href=&quot;https://automagic4android.com/trigger_next_alarm_en.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Next Alarm&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Fires when alarm goes off&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Make sure I’m at home (connected to home WiFi SSID)
&lt;ul&gt;
&lt;li&gt;If false, stop flow&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Fire POST request to my Google Apps Script endpoint
&lt;ul&gt;
&lt;li&gt;JSON payload says to turn my bedroom light on&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Capture and log response from script, just in case something failed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And here is what that flow look like in the Automagic UI:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/automagic-alarm-clock-flow.png&quot; style=&quot;margin: auto; display:block; max-width: 200px; width: 50%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/automagic-alarm-clock-flow.png&quot; alt=&quot;Automagic flow for turning on light based on alarm clock going off trigger&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The actual POST request that triggers the light turning on is very simple as well (thanks to the Google Apps Script wrapper). Here is an example:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;     &quot;pass&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;REDACTED&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;     &quot;deviceId&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;REDACTED&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;     &quot;action&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;on&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;android-internals---intent-broadcasts&quot;&gt;Android Internals - Intent Broadcasts&lt;/h3&gt;
&lt;p&gt;Because I was curious about how Automagic might have built that trigger behind-the-scenes, and how you might approach this with other Android apps, I did some digging into Android Intents. The TLDR of an Android Intent, is that it is a request and/or message that is broadcasted from one app to another (or the general system). The Android OS itself broadcasts many of these messages, which makes it easy to automate things like this.&lt;/p&gt;
&lt;p&gt;For example, if I wanted to have my light come on 10 minutes &lt;em&gt;&lt;strong&gt;before&lt;/strong&gt;&lt;/em&gt; my alarm goes off, I could use something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Listen for &lt;a href=&quot;https://developer.android.com/reference/android/app/AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;android.app.action.NEXT_ALARM_CLOCK_CHANGED&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;a href=&quot;https://developer.android.com/reference/android/app/AlarmManager#getNextAlarmClock(&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;getNextAlarmClock()&lt;/code&gt;&lt;/a&gt;) to determine the time of the next alarm, then subtract &lt;code&gt;10 minutes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use the calculated time to set a trigger for the POST action&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;You can read more about Intents here: &lt;a href=&quot;https://developer.android.com/guide/components/intents-filters&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Docs - Intents and Intent Filters&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;no-code-options&quot;&gt;No-Code Options&lt;/h2&gt;
&lt;p&gt;I thought it important to note that this kind of automation is very close to being a prime candidate for a “no-code” approach, where WYSIWYG tools are used to build the automation as opposed to coding.&lt;/p&gt;
&lt;p&gt;Most of what I put together could &lt;em&gt;probably&lt;/em&gt; be replicated with a combination of some of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IFTTT (one of the few official integrations for Kasa)&lt;/li&gt;
&lt;li&gt;Zapier&lt;/li&gt;
&lt;li&gt;… etc.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Get ALT Keyboard Shortcuts Working Quickly With JavaScript</title><link>https://joshuatz.com/posts/2020/get-alt-keyboard-shortcuts-working-quickly-with-javascript/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/get-alt-keyboard-shortcuts-working-quickly-with-javascript/</guid><description>Overriding the default behavior of keyboard key presses in browsers with JavaScript, to get ALT and arrow key combinations working with a SPA.</description><pubDate>Tue, 28 Apr 2020 23:18:09 GMT</pubDate><content:encoded>&lt;p&gt;I was trying to use a web app today, and ran into an interesting issue. It uses &lt;code&gt;ALT&lt;/code&gt; + &lt;code&gt;Left Arrow Key&lt;/code&gt; as a shortcut for a very handy operation, one that I would like to use frequently. However, at least on Windows, that keyboard shortcut gets stolen by the browser, and navigates me back a page. How do I work around this?&lt;/p&gt;
&lt;p&gt;Well, there are multiple solutions. This post is meant as a quick notes session as I explore them, so for more depth solutions, please do your own research.&lt;/p&gt;
&lt;h2 id=&quot;for-the-designer-of-the-web-app&quot;&gt;For the Designer of the Web App&lt;/h2&gt;
&lt;p&gt;If I was the creator of the web app, there would be a very simple solution. You can simply use the &lt;code&gt;event.preventDefault()&lt;/code&gt; call inside an eventListener callback, to prevent the default browser action. For example, if I wanted to prevent the backward page navigation and do something else, I could use this code:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;document.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;addEventListener&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;keydown&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (evt.keyCode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 37&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; evt.altKey) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Stop browser action&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		evt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;preventDefault&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// Do something here&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ALT + Left Arrow Pressed&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;		// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can actually this type of code to override any browser keyboard shortcut - for example, if you wanted to override &lt;code&gt;F5&lt;/code&gt; to have it do something else instead of refreshing the current page.&lt;/p&gt;
&lt;p&gt;The only thing you really cannot override is Operating-System level keyboard hooks, such as &lt;code&gt;CTRL&lt;/code&gt;+&lt;code&gt;ALT&lt;/code&gt;+&lt;code&gt;DEL&lt;/code&gt; on Windows.&lt;/p&gt;
&lt;p&gt;For more details on &lt;code&gt;event.preventDefault()&lt;/code&gt;, see &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the MDN doc page&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;for-myself-as-a-user-of-the-app&quot;&gt;For myself, as a user of the app&lt;/h2&gt;
&lt;p&gt;Since I’m not the creator the app, if I wanted to fix this myself I would either need to make a pull-request to the codebase (assuming it is open source), or implement a local workaround. I’m going to explore the second option now, since it will also be faster for a temporary workaround.&lt;/p&gt;
&lt;h3 id=&quot;injecting-code&quot;&gt;Injecting Code&lt;/h3&gt;
&lt;p&gt;As a user of the web app, although I can’t touch the source code, I can inject whatever JavaScript I want into the page through the developer’s console, browser extension, bookmarklet, or other method, and there is not much the app can do to prevent that.&lt;/p&gt;
&lt;p&gt;Assuming that the app I’m using is a SPA (&lt;em&gt;Single Page Application&lt;/em&gt;), then a simple bookmarklet should do the job, since the JS environment is not reset between actions (as opposed to a regular web site, where actions usually cause a full page refresh). I’ll write a little JavaScript that calls &lt;code&gt;preventDefault()&lt;/code&gt; on all &lt;code&gt;alt&lt;/code&gt; key combinations, and wrap it up in a bookmarklet. In fact, here is that code and bookmarklet:&lt;/p&gt;
&lt;p&gt;Code:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;document.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;addEventListener&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;keydown&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (evt.altKey) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		evt.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;preventDefault&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bookmarklet - Drag the below link to your bookmarks:
&lt;a href=&quot;javascript:(function()%7Bdocument.addEventListener(&amp;#x27;keydown&amp;#x27;%2C%20(evt)%20%3D%3E%20%7Bif%20(evt.altKey)%20%7Bevt.preventDefault()%3B%7D%7D)%7D)()&quot;&gt;ALT preventDefault&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;exceptions&quot;&gt;Exceptions:&lt;/h2&gt;
&lt;p&gt;As already mentioned, there are exceptions to the ability to prevent keyboard presses from triggering browser or OS level events (for example, &lt;code&gt;CTRL + ALT + WIN&lt;/code&gt; on Windows).&lt;/p&gt;
&lt;p&gt;Another issue is that the implementation of keyboard events and their propagation is some what left up to the browser. For example, my above code will successfully block &lt;code&gt;ALT&lt;/code&gt; keyboard &lt;em&gt;combos&lt;/em&gt; on both Chrome and Firefox, but for a single &lt;code&gt;ALT&lt;/code&gt; keypress, it will only block the menu from opening on Chrome, and not on Firefox. However, this might be a bug rather than a specific choice - see &lt;a href=&quot;https://bugzilla.mozilla.org/show_bug.cgi?id=1574057&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;this bug tracker&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>React Github Contribution Graph Mock Tool</title><link>https://joshuatz.com/projects/web-stuff/react-github-contribution-graph-mock-tool/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/react-github-contribution-graph-mock-tool/</guid><description>An experimental ReactJS project to turn text input into a point based graphical output, shown as a Github Contributions Graph mockup.</description><pubDate>Tue, 14 Apr 2020 23:03:34 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;Project Links:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/joshuatz/github-contributions-mock&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Code (Github)&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://joshuatz-github-contributions-mock.glitch.me/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Live App (Glitch Hosted)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What is this?&lt;/h2&gt;
&lt;p&gt;This is a tool, purely for fun, to put text and different graphics into a mocked-up Github Contributions Graph (see &lt;a href=&quot;#what-is-a-github-contribution-graph&quot;&gt;this&lt;/a&gt; if you don&apos;t know what that means). This was a random idea that I had, and wanted to see if it was possible to build from scratch, with minimal component libraries. It ended up working better than I had expected; just take a look below!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/github-contributions-mock-text-demo.gif&quot;&gt;&lt;img class=&quot;size-full wp-image-892 aligncenter&quot; src=&quot;/media/github-contributions-mock-text-demo.gif&quot; alt=&quot;Github Contribution Graph Animated Demo&quot; width=&quot;836&quot; height=&quot;417&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Tech Stack and Learning:&lt;/h2&gt;
&lt;p&gt;Most of what I used to build this was pretty vanilla React stuff:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;JavaScript&lt;/li&gt;
	&lt;li&gt;ReactJS&lt;/li&gt;
	&lt;li&gt;Materialize CSS (for styling)&lt;/li&gt;
	&lt;li&gt;Canvas API&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most challenging part, and also the most interesting, was turning the text input into a point-based visual representation. Mostly, this meant learning how to use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the Canvas API&lt;/a&gt; (JavaScript &amp;#x26; HTML) and processing the raw data it returns.&lt;/p&gt;
&lt;p&gt;I also tried to use React Hooks more on this project than I previously have (I&apos;m used to TypeScript Class or JS ES6 Class based components), which required some additional research and self-educating.&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;what-is-a-github-contribution-graph&quot;&gt;&lt;/a&gt;What is a Github Contribution Graph?&lt;/h2&gt;
&lt;p&gt;For those unfamiliar with the site &lt;a href=&quot;https://github.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Github&lt;/a&gt;, it is hard to describe the full depth of what they offer, but at its core, Github is a service that developers can use to store their code, as well as track changes and collaborate with other developers.&lt;/p&gt;
&lt;p&gt;As a Github user, you get your own profile page (for example, &lt;a href=&quot;https://github.com/joshuatz&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;github.com/joshuatz&lt;/a&gt;), which you can customize with some links and text. One of the fun features of Github&apos;s profile page is an automated widget, commonly referred to as the &lt;em&gt;Github Contribution Graph&lt;/em&gt;. It shows how frequently you have been pushing code through Github, with different intensities of green to signify more activity on a given day. Here is an example:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/github_contributions_graph_example.png&quot;&gt;&lt;img class=&quot;size-full wp-image-891 aligncenter&quot; src=&quot;/media/github_contributions_graph_example.png&quot; alt=&quot;Github Contributions Graph Example&quot; width=&quot;548&quot; height=&quot;216&quot;&gt;&lt;/a&gt;This type of data representation is also often referred to as a &lt;em&gt;heatmap&lt;/em&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Remote Tutoring Software - Screen Sharing and Video Call Showdown</title><link>https://joshuatz.com/posts/2020/remote-tutoring-software---screen-sharing-and-video-call-showdown/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/remote-tutoring-software---screen-sharing-and-video-call-showdown/</guid><description>Documenting my search for the right software to use for remote elementary school tutoring sessions.</description><pubDate>Sat, 04 Apr 2020 19:35:12 GMT</pubDate><content:encoded>&lt;!-- omit in toc --&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#intro&quot;&gt;Intro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#different-types-of-software&quot;&gt;Different Types of Software&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#note-on-applicability&quot;&gt;Note on Applicability&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#deciding&quot;&gt;Deciding&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#requirements&quot;&gt;Requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#screen-sharing-software-comparison---2020&quot;&gt;Screen Sharing Software Comparison - 2020&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#side-note-web-rtc&quot;&gt;Side Note: Web RTC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#close-fits--honorable-mentions&quot;&gt;Close Fits &amp;#x26; Honorable Mentions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#our-picks&quot;&gt;Our Picks&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#runner-up&quot;&gt;Runner Up:&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#finalist&quot;&gt;🎉 Finalist 🎉:&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#research-scratchpad&quot;&gt;Research Scratchpad&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#search-terms-i-used&quot;&gt;Search Terms I Used:&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#lists-i-found-helpful&quot;&gt;Lists I found helpful:&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#recurring-issues&quot;&gt;Recurring Issues&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#click-and-drag-on-mobile&quot;&gt;Click-and-drag on Mobile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#mobile-pip-support&quot;&gt;Mobile PiP Support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#final-note&quot;&gt;Final Note:&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;Due to recent events (Covid-19), my mom, a bit of a technophobe, has suddenly taken an interest in using technology as part of her tutoring business. She tutors children (K-12, with a focus on elementary), so her requirements are different from college professors that might only need basic video conferencing tech to lecture for an hour at a time.&lt;/p&gt;
&lt;p&gt;As she asked me questions about what she could use, I started to realize that a lot of her software needs are actually going unmet by the industry, and there actually exists a strange gap in the market for &lt;em&gt;quality&lt;/em&gt; educational collaborative software. Since others might find it interesting or helpful, especially if they have similar needs, I thought I would document my research here, and outline what is available and what is not, as well as what I thought the best option was.&lt;/p&gt;
&lt;img src=&quot;/media/walk-this-way-directions.jpg&quot; alt=&quot;Arrow showing which way to continue&quot; style=&quot;margin: auto; display:block; max-width: 500px; width: 50%; height: auto;&quot;&gt;
&lt;h2 id=&quot;different-types-of-software&quot;&gt;Different Types of Software&lt;/h2&gt;
&lt;p&gt;First, a discussion of what types of software are out there that could be used in her tutoring business. These categories are also applicable to dozens of other industries:&lt;/p&gt;























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;th&gt;Examples&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Video Conferencing&lt;/td&gt;&lt;td&gt;Software where the focus is on video conferencing, video chat, etc. Might also include features such as screen-sharing, remote control, or other built-ins, but these are often are not full-featured.&lt;/td&gt;&lt;td&gt;Zoom, Skype&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Collaboration Platform&lt;/td&gt;&lt;td&gt;These are usually marketed directly towards businesses, and meant to serve as a one-stop shop for collaborating with peers. Common features are text chat channels / instant messaging, video chat, project  planning, calendars, and enterprise software integrations.&lt;br&gt;&lt;br&gt;These are usually overkill for smaller businesses, and especially for tutoring.&lt;/td&gt;&lt;td&gt;Microsoft Teams, Zoho Cliq&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Screen Sharing&lt;/td&gt;&lt;td&gt;Software that lets you share what you see on your screen with another user, or vice-versa.&lt;br&gt;&lt;br&gt;This is rarely ever its own category of software, and usually just an included feature in either &lt;em&gt;Video Conferencing&lt;/em&gt; (see above) or &lt;em&gt;Remote Control&lt;/em&gt; (see below) software.&lt;/td&gt;&lt;td&gt;Screenleap, Slack Screenshare&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Remote Control / Remote Desktop / VNC&lt;/td&gt;&lt;td&gt;Software where the focus is on letting one user control another user’s computer. Does not usually allow for more than two concurrent users at a time (not meant for conferencing)&lt;/td&gt;&lt;td&gt;TeamViewer,  GoToMyPC, Windows RDP, Chrome Remote Desktop&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cobrowsing&lt;/td&gt;&lt;td&gt;A way for multiple users (2+, but often limited to just 2) to synchronize browsing of the same web page and/or web browser. There are variations in how this is done, and some cobrowsing software is geared towards technical support or live customer assistance chat (e.g. web commerce), not educational or personal use.&lt;/td&gt;&lt;td&gt;Samesurf, Surfly, Acquire, Upscope, Vectera&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Shared Whiteboarding&lt;/td&gt;&lt;td&gt;A shared digital whiteboard or notebook where multiple users can edit content and have changes reflected live in all participants’ views. Ideally the content that is created during a session can be persisted (saved), and edit history preserved.&lt;br&gt;&lt;br&gt;This feature is baked into many video-conferencing solutions, such as Zoom and Teams.&lt;/td&gt;&lt;td&gt;Twiddla, Miro, LiveBoard, Microsoft Whiteboard, OneNote,  Google Drive (to an extent…)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LMS (Learning Management System)&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Learning_management_system&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;LMS software&lt;/a&gt; is an extremely broad category of product offering, so features vary greatly from one software platform to another. For example, some LMS programs offer built-in video conferencing and meeting tools, while others focus only on content and webpages (which makes them basically just an expensive educational focused CMS…)&lt;br&gt;&lt;br&gt;Most LMS are focused on building a “virtual classroom”, with  core features centered around content, moderation, and support for a large number of participants. LMS are often purchased not by the educator, but by the institution (college, high school, etc.). These factors combined often make it a bad choice for tutoring software.&lt;/td&gt;&lt;td&gt;Open edX, Google Classroom, Blackboard, Canvas, Electa Live&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Virtual Assistance&lt;/td&gt;&lt;td&gt;Usually this software is sold to tech support teams / call centers, and not geared towards education use. This type of software allows an “operator” or “agent” to provide virtual assistance to a “client” or “customer”, in order to walk through a specific problem.&lt;br&gt;&lt;br&gt;Common features are co-browsing (“let me control the webpage you are on to show you how to use X feature”), live chat, remote desktop control, and multi-agent / ticketing system support.&lt;br&gt;&lt;br&gt;These are often missing collaborative features, like webcam sharing, whiteboards, or allowing more than two participants at a time.&lt;/td&gt;&lt;td&gt;Zoho Assist, ConnectWise, AnyDesk, BeyondTrust, LogMeIn Rescue, TeamViewer&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Sales, Sales Funnel Assistance&lt;/td&gt;&lt;td&gt;There is a specific niche of collaboration software that is for salespeople pitching to potential clients / customers. Core features might include slide decks, video conferencing, co-browsing, email integration, etc.&lt;br&gt;&lt;br&gt;Features that are often left out are annotation, whiteboard, and other “creative” collaboration tools, since the goal is to sell something existing, not brainstorm a solution.&lt;/td&gt;&lt;td&gt;DemoDesk, CrankWheel&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;note-on-applicability&quot;&gt;Note on Applicability&lt;/h3&gt;
&lt;p&gt;If you are looking at the above table and feeling lost, I can offer a few pointers. In general, each of the software categories above is marketed towards a certain industry and/or type of occupation, but that doesn’t mean it is only &lt;em&gt;useful&lt;/em&gt; for that industry. For example, Remote Control software is always heavily marketed to tech support and IT professionals, but it has a million uses beyond that field. So, focus on what the software &lt;em&gt;does&lt;/em&gt;, not how it is marketed.&lt;/p&gt;
&lt;img src=&quot;/media/looking-at-advertising-billboards.jpg&quot; alt=&quot;Looking at Advertising Billboards&quot; style=&quot;margin: auto; display:block; max-width: 500px; width: 50%; height: auto;&quot;&gt;
&lt;p&gt;Another example: DemoDesk, which markets itself for sales and customer support, was remarkably close to what I was looking for, and I have a feeling a lot of educators might actually find it useful.&lt;/p&gt;
&lt;p&gt;And hey, marketing departments? Take note; you don’t always know your target demographics as well as you think you do.&lt;/p&gt;
&lt;h2 id=&quot;deciding&quot;&gt;Deciding&lt;/h2&gt;
&lt;h3 id=&quot;requirements&quot;&gt;Requirements&lt;/h3&gt;
&lt;p&gt;Here are the main features that we consider &lt;strong&gt;required&lt;/strong&gt; in our search:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Video chat that can stay up throughout session, even while switching to remote desktop control&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tutor can see student, student can see tutor, &lt;strong&gt;entire time&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Support for remote desktop control, where host computer can be controlled by participant&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Student can control tutor’s screen, in order to share browser / educational desktop apps&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mobile / Tablet / Touchscreen Support&lt;/strong&gt;: There must be touchscreen support, and that includes the requirement that remote desktop control support “click-and-drag”, just like how you can with a mouse&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Necessary because lots of elementary school activities require drawing or writing, instead of typing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This is where most offerings fell short, especially on &lt;em&gt;click-and-drag&lt;/em&gt; support - see &lt;a href=&quot;#recurring-issues&quot;&gt;recurring issues section&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These would be nice bonus features, but not required:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native browser support (no client to install)&lt;/li&gt;
&lt;li&gt;Built-in whiteboard and annotation support&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The basic experience I was looking to find was one where the tutor would feel like the student was sitting next to them, sharing the mouse and keyboard of their own computer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Coincidentally, this set of requirements is almost identical to what most programmers are looking for in remote “pair programming” solutions!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;img src=&quot;/media/tutoring-student-side-by-side-with-laptop.jpg&quot; alt=&quot;ALT_TEXT&quot; style=&quot;margin: auto; display:block; max-width: 500px; width: 50%; height: auto;&quot;&gt;
&lt;h3 id=&quot;screen-sharing-software-comparison---2020&quot;&gt;Screen Sharing Software Comparison - 2020&lt;/h3&gt;
&lt;p&gt;Based on the above requirements, the software category that best fit was screen sharing, in places where it overlaps with &lt;em&gt;Remote Control&lt;/em&gt; and &lt;em&gt;Video Conferencing&lt;/em&gt; software. This combination also overlaps with &lt;em&gt;Remote Pair Programming&lt;/em&gt; software, which has similar requirements. Knowing this, I researched (and actually tried out!) a multitude of options, and compiled the results into a comparison table.&lt;/p&gt;
&lt;p&gt;Click &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1G_82j1ldliEmN4n2cCSAe-Gh1HGJ_Y-x1nUbKpqVn_0/edit?usp=sharing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;here&lt;/a&gt; to view the raw Google Sheet document in a new tab.&lt;/p&gt;
&lt;p&gt;Embedded:&lt;/p&gt;
&lt;iframe src=&quot;https://docs.google.com/spreadsheets/d/e/2PACX-1vQ5uWdDcWQBYSkhL5iwKj5f5DE3dwT-aYzlzIbzEH0JN_ECcULm5_5ml9Uly2umJqvs1PaGYlxP-TsD/pubhtml?gid=0&amp;#x26;single=true&amp;#x26;widget=true&amp;#x26;headers=false&quot; style=&quot;width:100%; height: 480px;&quot;&gt;&lt;/iframe&gt;
&lt;h3 id=&quot;side-note-web-rtc&quot;&gt;Side Note: Web RTC&lt;/h3&gt;
&lt;p&gt;Technical: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Web RTC&lt;/a&gt; is a set of web APIs and standards that enables and streamlines the process of capturing and streaming multimedia (including video, such as from a webcam) data streams, in an efficient and robust manner. It allows for zero-install setups, since these are native browser APIs (albeit still changing).&lt;/p&gt;
&lt;p&gt;Web RTC, in my view, &lt;strong&gt;is an incredibly strong &lt;em&gt;indicator&lt;/em&gt; of how on top of modern technology a given video chat / screen sharing program is&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In fact, to my knowledge, the three pieces of software that I’m going to recommend for my use-case all rely on Web RTC to deliver their streams. Jitsi Meet, one of three, and an amazing piece of open-source video conferencing technology, even &lt;a href=&quot;https://jitsi.org/news/a-simple-congestion-test-for-zoom/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;published a post on how they can out-perform Zoom with Web RTC&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;: Web RTC is awesome, and usually means there is no software for your user (student, client, etc.) to install or setup.&lt;/p&gt;
&lt;h3 id=&quot;close-fits--honorable-mentions&quot;&gt;Close Fits &amp;#x26; Honorable Mentions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.braincert.com/online-virtual-classroom&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BrainCert&lt;/a&gt; - LMS
&lt;ul&gt;
&lt;li&gt;Looks like a really cool LMS, but no mouse sharing / remote control is a deal-breaker for our use-case&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bigbluebutton.org/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;BigBlueButton&lt;/a&gt; - Education Focused Web Conferencing Solution
&lt;ul&gt;
&lt;li&gt;BigBlueButton (aka &lt;em&gt;BBB&lt;/em&gt;) is an extremely impressive piece of software, even more so since it is both open-source and free. It has been put to the test as well; it is the default video conferencing solution baked into the Canvas LMS.&lt;/li&gt;
&lt;li&gt;BBB combines video conferencing, interactive shared whiteboards, and screen-sharing, with a focus on educational features (such as polling and special presenter controls)&lt;/li&gt;
&lt;li&gt;One major drawback to BBB is that it is &lt;em&gt;&lt;strong&gt;only&lt;/strong&gt;&lt;/em&gt; self-hosted, meaning that you need to have access to a (powerful) dedicated server you can run it on. As a nerd and open-source supporter, I’m a big fan of having a self-hosted option, but… I would not expect the average teacher or tutor to know how to provision a Linux server, interact with bash scripts, and setup port forwarding…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.join.me/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Join.Me&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Superior to Zoom in that it supports “no-install” use; guests can jump into a meeting and get access to all features from their web browser, without installing any software. This includes the use of remote control and share screen.&lt;/li&gt;
&lt;li&gt;UI is very intuitive and pleasant to use&lt;/li&gt;
&lt;li&gt;Mobile client (app) does not support remote cursor control at all
&lt;ul&gt;
&lt;li&gt;You can get around this by opening the meeting link in Chrome for Android, in an incognito window, with “Desktop site” emulation setting checked.
&lt;ul&gt;
&lt;li&gt;However, this has the same “click-and-drag” issue as Zoom :(&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://demodesk.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;DemoDesk&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;This was pretty close to being exactly what we needed in so many ways:
&lt;ul&gt;
&lt;li&gt;No bulky software to install&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Co-browsing support&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mobile touch screens support click-and-drag&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Built-in video conferencing&lt;/li&gt;
&lt;li&gt;The “playbooks” feature is a neat way that a tutor could preload content that is going to be worked on during the session&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Pretty much the only reason why this was not  a suitable fit is because remote control / mouse sharing &lt;em&gt;&lt;strong&gt;only&lt;/strong&gt;&lt;/em&gt; works for co-browsing webpages or uploaded documents - it does &lt;strong&gt;not&lt;/strong&gt; work for sharing your &lt;em&gt;desktop&lt;/em&gt; or OS applications&lt;/li&gt;
&lt;li&gt;I still think this is a very unique and powerful web-app that might be the perfect for other use-cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;our-picks&quot;&gt;Our Picks&lt;/h3&gt;
&lt;h4 id=&quot;runner-up&quot;&gt;Runner Up:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://screen.so/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Screen.so&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;WOW! &lt;strong&gt;So close&lt;/strong&gt; to being exactly what I was looking for!&lt;/li&gt;
&lt;li&gt;Similar to &lt;em&gt;USE Together&lt;/em&gt;, but adds video chat and annotations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blockers&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Buggy. This is understandable, as I was testing it just days after it was released to the public&lt;/li&gt;
&lt;li&gt;Odd touchscreen to mouse mapping: &lt;em&gt;trackpad&lt;/em&gt; mode
&lt;ul&gt;
&lt;li&gt;Clicking at point &lt;code&gt;0,0&lt;/code&gt; on your screen does not place the host mouse at &lt;code&gt;0,0&lt;/code&gt; - but dragging your finger from &lt;code&gt;0,0&lt;/code&gt; to &lt;code&gt;100,0&lt;/code&gt;, will move the host mouse 100 units to the right (scaled) - just like a trackpad&lt;/li&gt;
&lt;li&gt;To click-and-drag, you first long-press, then start moving your finger&lt;/li&gt;
&lt;li&gt;Maybe in the future they will support multiple mapping modes and let you switch between? That would be nice!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If the &lt;em&gt;blockers&lt;/em&gt; are ever addressed, I will probably recommend to my mom that she instantly switch to using &lt;em&gt;Screen&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;finalist&quot;&gt;🎉 Finalist 🎉&lt;/h4&gt;
&lt;p&gt;The final pick is actually a combination of &lt;em&gt;two&lt;/em&gt; pieces of software together:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Piece A: &lt;em&gt;Video Conferencing&lt;/em&gt;&lt;/strong&gt; - One of…&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.skype.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Skype&lt;/a&gt; (yes, it’s still around)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Most important feature: &lt;strong&gt;picture-in-picture support&lt;/strong&gt; on both mobile and desktop!!!
&lt;ul&gt;
&lt;li&gt;As already mentioned, this is crucial so that tutor and student can keep seeing each other even as other applications are taking focus&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Supported on many different devices, been around forever&lt;/li&gt;
&lt;li&gt;Negative: Cannot set password on ad-hoc “meet now” meetings that don’t require participants to login&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;&amp;#x3C; ==== Or… ====&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://jitsi.org/jitsi-meet/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Jitsi (aka Jitsi Meet)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Open-Source!!!&lt;/strong&gt; - &lt;a href=&quot;https://github.com/jitsi&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;github.com/jitsi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Powers 8x8 video conferencing&lt;/li&gt;
&lt;li&gt;Jitsi has a lot of features that other video conferencing apps are missing:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;picture-in-picture support&lt;/strong&gt; on both mobile and desktop!!!&lt;/li&gt;
&lt;li&gt;Optional self-hosting / server control&lt;/li&gt;
&lt;li&gt;No clients to install (native browser support)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Piece B: &lt;em&gt;Remote Desktop Sharing&lt;/em&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remote desktop sharing: &lt;a href=&quot;https://www.use-together.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;USE Together&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;I found it to have the smoothest cursor sharing experience - very little lag&lt;/li&gt;
&lt;li&gt;Unfortunately, basically the only feature offered is cursor sharing; there is no video chat built in, or annotation support
&lt;ul&gt;
&lt;li&gt;This is why I have to combine it with a video chat app, so that the tutor can see the student (and vice-versa) while sharing control of the computer&lt;/li&gt;
&lt;li&gt;There &lt;em&gt;is&lt;/em&gt; built-in audio chat, so if you don’t need video, you could get by with just this application&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using these two pieces of software together feels very close to what I was hoping for; the experience of simply sharing the keyboard and mouse with the other person, while seeing them the whole time.&lt;/p&gt;
&lt;h2 id=&quot;research-scratchpad&quot;&gt;Research Scratchpad&lt;/h2&gt;
&lt;h3 id=&quot;search-terms-i-used&quot;&gt;Search Terms I Used:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Collaborative web browser&lt;/li&gt;
&lt;li&gt;synchronized web browsing&lt;/li&gt;
&lt;li&gt;live webpage sharing&lt;/li&gt;
&lt;li&gt;lms with rdp&lt;/li&gt;
&lt;li&gt;video conferencing with shared cursor&lt;/li&gt;
&lt;li&gt;tablet share mouse control video conference&lt;/li&gt;
&lt;li&gt;android share mouse control video conference&lt;/li&gt;
&lt;li&gt;web rtc rdp&lt;/li&gt;
&lt;li&gt;android video chat “SYSTEM_ALERT_WINDOW” OR “pip” OR “picture in picture”&lt;/li&gt;
&lt;li&gt;remote pair programming with video chat&lt;/li&gt;
&lt;li&gt;remote desktop with video chat&lt;/li&gt;
&lt;li&gt;pair programming “remote” “video chat” OR “video call” OR “webcam”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;lists-i-found-helpful&quot;&gt;Lists I found helpful:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Process.st - &lt;a href=&quot;https://www.process.st/screen-sharing/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;The 11 Best Screen Sharing Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;FreeConferenceCall - &lt;a href=&quot;https://www.freeconferencecall.com/blog/best-remote-desktop-screen-sharing-software/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Best Remote Desktop Screen Sharing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Wikipedia:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Comparison_of_remote_desktop_software&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Comparison of Remote Desktop Software&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Comparison_of_web_conferencing_software&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Comparison of Web Conference Software&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.capterra.com/screen-sharing-software/&quot;&gt;https://www.capterra.com/screen-sharing-software/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.g2.com/categories/screen-sharing&quot;&gt;https://www.g2.com/categories/screen-sharing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Using “alternative to ScreenHero” as a base
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=13620626&quot;&gt;ask HN&lt;/a&gt; (A huge amount of recommendations have completely shut down 😭)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.producthunt.com/alternatives/screenhero&quot;&gt;Product Hunt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/codingzeal/a-guide-to-remote-pair-programming-tools-9ee20e06aa0c&quot;&gt;A guide to remote pair programming tools&lt;/a&gt; (they actually have a very similar set of criteria to us!)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alternativeto.net/software/screenhero/&quot;&gt;alternativeto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Computer World: &lt;a href=&quot;https://www.computerworld.com/article/3241891/22-free-screen-sharing-apps-for-work-at-home-collaboration.html&quot;&gt;22 Free Screen Sharing Apps for Work at Home Collaboration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;recurring-issues&quot;&gt;Recurring Issues&lt;/h2&gt;
&lt;h3 id=&quot;click-and-drag-on-mobile&quot;&gt;Click-and-drag on Mobile&lt;/h3&gt;
&lt;p&gt;A constant recurring issue I ran into when looking for suitable software was that many pieces of software that allow remote control don’t allow click-and-drag from a mobile touchscreen (particularly Android). This is a &lt;strong&gt;deal-breaker&lt;/strong&gt; for educational uses, since many interactive games and applications (such as SmartBoard software) &lt;em&gt;&lt;strong&gt;require&lt;/strong&gt;&lt;/em&gt; that the student click and drag things across the screen.&lt;/p&gt;
&lt;p&gt;I wrote up a summary of my research and conclusion as to why this feature is so often omitted - you can find it &lt;a href=&quot;https://joshuatz.com/posts/2020/click-and-drag-mobile-touchscreen-mouse-mapping/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;. The short answer is “because it adds complexity” 🤷‍♂️.&lt;/p&gt;
&lt;h3 id=&quot;mobile-pip-support&quot;&gt;Mobile PiP Support&lt;/h3&gt;
&lt;p&gt;For those not familiar, &lt;strong&gt;P&lt;/strong&gt;icture &lt;strong&gt;i&lt;/strong&gt;n &lt;strong&gt;P&lt;/strong&gt;icture, aka &lt;strong&gt;PiP&lt;/strong&gt;, is a feature supported in both the iOS and Android operating systems that allows an application to continue displaying a video in small box overlaid on the screen, while you, the user, navigates to and uses a completely different application. Here is the demo from &lt;a href=&quot;https://developer.android.com/guide/topics/ui/picture-in-picture&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Google’s dev page on the topic&lt;/a&gt;, which just happens to be for a video calling app:&lt;/p&gt;
&lt;p&gt;&lt;video controls muted autoplay loop style=&quot;width:150px; height:250px; display: block; margin: auto;&quot;&gt;&lt;source src=&quot;https://developer.android.com/images/pip.mp4&quot; type=&quot;video/mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Here is the perplexing thing; despite PiP support on mobile having been a thing for a while already (Android: August 2017, iOS: ~2016 ), &lt;strong&gt;barely any video conferencing apps support it as of 2020&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The most damning current example I can give is Zoom; the company has a market cap of &lt;strong&gt;+$40 &lt;em&gt;billion&lt;/em&gt;&lt;/strong&gt;, and over &lt;strong&gt;1,900 employees&lt;/strong&gt;, and somehow hasn’t gotten around to supporting PiP. For example, on Android if you have the Zoom app open, and then switch to a different app (the web browser, for example), you can keep talking to and hearing the person on the other end, but you lose video.&lt;/p&gt;
&lt;p&gt;The lack of PiP support in video conferencing apps is particularly annoying for our particular use-case, because we needed to combine a video chat app with &lt;em&gt;another&lt;/em&gt; app for remote control, so PiP (or something like it, such as split-screen or floating windows) was a necessity.&lt;/p&gt;
&lt;p&gt;And it’s not like supporting PiP in a video call app is &lt;em&gt;impossible&lt;/em&gt;; it is the sample demo video provided by Google, and already implemented in these Android apps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Whatsapp&lt;/li&gt;
&lt;li&gt;FB messenger&lt;/li&gt;
&lt;li&gt;Skype&lt;/li&gt;
&lt;li&gt;Google Duo&lt;/li&gt;
&lt;li&gt;Netflix&lt;/li&gt;
&lt;li&gt;YouTube&lt;/li&gt;
&lt;li&gt;Mixer&lt;/li&gt;
&lt;li&gt;Etc…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As another, very positive example, the open-source video conferencing app &lt;a href=&quot;https://jitsi.org/jitsi-meet/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Jitsi Meet&lt;/a&gt; supports Android PiP flawlessly.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Technical side note for Android: Some apps might use the &lt;code&gt;SYSTEM_ALERT_WINDOW&lt;/code&gt; (aka “draw over other apps permission”) and generate floating windows instead of using the actual PiP API. Or they might not support PiP, but support split-screen. Any of these are acceptable to me, although PiP is definitely the preference.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-note&quot;&gt;Final Note:&lt;/h2&gt;
&lt;p&gt;I hope writing all this up helps others in their search for software! If you think there is a piece of software that I overlooked or missed, please leave a comment and let me know!&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Prettier Function Parenthesis Spacing - Opinionated is Key</title><link>https://joshuatz.com/posts/2020/prettier-function-parenthesis-spacing---opinionated-is-key/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/prettier-function-parenthesis-spacing---opinionated-is-key/</guid><description>A reminder of the emphasis on opinions in Prettier&apos;s guiding principles, and how this can conflict with ESLint.</description><pubDate>Fri, 03 Apr 2020 19:20:10 GMT</pubDate><content:encoded>&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;As I progress as a developer, I’ve tried to embrace code formatting tools more and more frequently. That hasn’t stopped my frustrations with them however, and I wanted to provide a short example from a very recent frustrating day that reminded me of their limitations.&lt;/p&gt;
&lt;h2 id=&quot;the-issue&quot;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;I had an existing codebase that was poorly formatted (🙃), so I was planning on using ESLint + Prettier (common combo) to clean it up, and enforce some standards going forward. Upon running Prettier, I noticed that it kept adding spaces between the function keyword and the argument parenthesis. For example (&lt;a href=&quot;https://prettier.io/playground/#N4Igxg9gdgLgprEAuc0DOMAEBbAngMQFcoxMBeTAM2LBgEtoAKASk2AB0pNvMB6XgHRDOAXxAAaEBAAO9dMlABDAE7KIAdwAKKhGmQhFAG3WLceyQCNlisAGs4MAMrSbdKAHNkMZYTiSAFjDYhgDq-nTwaC5gcI66EXQAbhG4+mBo5iBuaHDKMJrW7tiKyJRGOZIAVmgAHgBC1nYOjorYcAAybnCl5X4g1TWObu6GcACKhBDwPYYVIC7KOcr6FooWcIYS88puMCF0ACYw-sgAHAAMktJqOSHW0vrXcEuJ3ZIAjpPwBTJ6KIpoAC0UDgcAOYK2yjgnzoUIKiiKJSQZVmfRy2DoXh8aOGowmU26yN6khga32RxOSAATCTrHRDMMAMIQbDFfTPACsW0IOQAKms-ii5olfABJKDg2COMA7WQAQQljhguFGMxyIhEQA&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;demo&lt;/a&gt;):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Before ESLint + Prettier&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; myFunc&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    //...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// After formatting&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; myFunc&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    //...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For some reason it was bothering me, and, at least for this particular repo, I was hoping to be able to preserve my style of having &lt;em&gt;no space&lt;/em&gt; before the parenthesis.&lt;/p&gt;
&lt;p&gt;However, when I tried to go and actually change the formatting default, I ran into issues.&lt;/p&gt;
&lt;h3 id=&quot;eslint-rules&quot;&gt;ESLint Rules&lt;/h3&gt;
&lt;p&gt;A quick search of my issue made it seem like this was because of a very specific ESLint rule: &lt;a href=&quot;https://eslint.org/docs/rules/space-before-function-paren&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;space-before-function-paren&lt;/code&gt;&lt;/a&gt;. Knowing this, I tried every variation I could think of to turn the rule off.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// .eslintrc.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Standard approach&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;rules&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;space-before-function-paren&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;never&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Applying via plugin rule&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;rules&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;prettier&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;space-before-function-paren&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;never&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also tried variations with “off”, etc.&lt;/p&gt;
&lt;p&gt;Nothing seemed to work. This is because I rarely think about Prettier, and basically forgot the main premise of its functionality.&lt;/p&gt;
&lt;h3 id=&quot;prettier---an-opinionated-formatter&quot;&gt;Prettier - An &lt;em&gt;&lt;strong&gt;Opinionated&lt;/strong&gt;&lt;/em&gt; Formatter&lt;/h3&gt;
&lt;p&gt;Here is a refresher on Prettier (or introduction, depending on your experience):&lt;/p&gt;
&lt;p&gt;I’m going to paste two &lt;em&gt;&lt;strong&gt;really important&lt;/strong&gt;&lt;/em&gt; bullet points from &lt;a href=&quot;https://prettier.io/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the home page of Prettier&lt;/a&gt;, under the “What is Prettier?” section (emphasis mine):&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;em&gt;&lt;strong&gt;opinionated&lt;/strong&gt;&lt;/em&gt; code formatter&lt;/li&gt;
&lt;li&gt;Has &lt;strong&gt;&lt;em&gt;few&lt;/em&gt; options&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Basically, the main point of Prettier is to stop developers from wasting precious time fretting or arguing about formatting styles, hand-editing code to meet style requirements, and messing with complicated config steps. Using Prettier &lt;em&gt;should&lt;/em&gt; be a quick set-and-forget step, and this is both its strength and its weakness.&lt;/p&gt;
&lt;p&gt;The weakness is that there are very few ways to modify Prettiers’ options, &lt;strong&gt;and this rule is just one of many that you cannot override&lt;/strong&gt; 🔒.&lt;/p&gt;
&lt;h3 id=&quot;space-after-function---prettier-20&quot;&gt;Space After Function - Prettier 2.0&lt;/h3&gt;
&lt;p&gt;I think part of why this issue might have stuck out to me is because, even though I’ve only used Prettier a handful of times in the past, I recognized that this was new behavior that I was not used to. Sure enough, after a bunch of digging, I found the exact discussions and PRs that brought this in as the new default style in Prettier.&lt;/p&gt;
&lt;p&gt;This change was merged and released in the major 2.0 release, very recently in March 2020. You can find confirmation of this in the official release notes, &lt;a href=&quot;https://prettier.io/blog/2020/03/21/2.0.0.html#always-add-a-space-after-the-function-keyword-3903httpsgithubcomprettierprettierpull3903-by-j-f1httpsgithubcomj-f1-josephfrazierhttpsgithubcomjosephfrazier-sosukesuzukihttpsgithubcomsosukesuzuki-thorn0httpsgithubcomthorn0-7516httpsgithubcomprettierprettierpull7516-by-bakkothttpsgithubcombakkot&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;, or in the main merge PR, &lt;a href=&quot;https://github.com/prettier/prettier/pull/3903&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;#3903&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🔎: For those interested, you can also find lively discussion on the decision on &lt;a href=&quot;https://github.com/prettier/prettier/issues/1139&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;#1139&lt;/a&gt;, &lt;a href=&quot;https://github.com/prettier/prettier/issues/3847&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;#3847&lt;/a&gt;, &lt;a href=&quot;https://github.com/prettier/prettier/issues/3845&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;#3845&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;/h2&gt;
&lt;h3 id=&quot;solution-a-dont-use-prettier&quot;&gt;Solution A: Don’t use Prettier:&lt;/h3&gt;
&lt;p&gt;This is harsh advice, but if you are the kind of person that cares &lt;strong&gt;strongly&lt;/strong&gt; about this type of thing, enough to research how to override it, Prettier &lt;em&gt;might&lt;/em&gt; not be for you. Again, the whole point of Prettier is to make those kinds of decisions for you, and t&lt;a href=&quot;https://prettier.io/docs/en/option-philosophy.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;hey are strongly against exposing overrides as options&lt;/a&gt;, as that allows for more time wasted on manual configuration and bickering over how to set it up.&lt;/p&gt;
&lt;p&gt;Plus, &lt;strong&gt;there are other auto-formatting options&lt;/strong&gt;. For example, ESLint + AirBnB is a great common combination, where you get a bunch of common sense formatting defaults, but you can also configure the setup exactly how you like it using ESLint configs. In fact, just like how Prettier can auto-fix issues, &lt;a href=&quot;https://eslint.org/docs/user-guide/command-line-interface#fixing-problems&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;so can ESLint&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;solution-b-force-an-old-version-of-prettier&quot;&gt;Solution B: Force an Old Version of Prettier&lt;/h3&gt;
&lt;p&gt;Since this change was introduced with version 2.0 of Prettier, if you force your IDE to use an older version of Prettier, prior to the v2.0 release, you shouldn’t see this behavior. There are multiple ways to do this, but the easiest and most recommended way is to “pin” the version in your local &lt;code&gt;package.json&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;For more details, assuming VSCode as your environment, refer to this documentation for &lt;a href=&quot;https://github.com/prettier/prettier-vscode#prettier-resolution&quot;&gt;“Prettier Resolution” in prettier-vscode&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Warning: Solution B is not recommended as a long term solution, since you will be missing out future (and post-2.0) performance improvements, added features, and security patches.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;solution-c-let-eslint-override-prettier&quot;&gt;Solution C: Let ESLint override Prettier&lt;/h3&gt;
&lt;p&gt;Part of the reason why my attempts at overriding Prettier’s default with ESLint’s rule (&lt;a href=&quot;https://eslint.org/docs/rules/space-before-function-paren&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;space-before-function-paren&lt;/a&gt;) did not work, is because I was following &lt;a href=&quot;https://prettier.io/docs/en/integrating-with-linters.html#recommended-configuration&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Prettier’s recommended setup for ESLint&lt;/a&gt;, which includes telling ESLint to disable rules that conflict with Prettier. I’m guessing one of those “conflicting” rules is the space in function declarations (particularly anonymous function declarations).&lt;/p&gt;
&lt;p&gt;This brings me to my second solution; let ESLint override Prettier.&lt;/p&gt;
&lt;p&gt;This is pretty convoluted, and I’m not even 100% sure it would work, but I figured I would outline the steps anyways&lt;/p&gt;
&lt;p&gt;First, you would need to stop telling ESLint to drop rules that conflict / override Prettier. For example, going from this (in &lt;code&gt;.eslintrc.json&lt;/code&gt;):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;extends&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;airbnb-base&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;plugin:prettier/recommended&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;rules&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        //...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;… to this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;extends&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;airbnb-base&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;rules&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        //...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, you would need to setup your formatting workflow to call Prettier first (e.g., &lt;code&gt;prettier --write .&lt;/code&gt;), to apply it’s opinionated styles, and &lt;strong&gt;then&lt;/strong&gt; call ESLint’s formatter (e.g., &lt;code&gt;eslint --fix&lt;/code&gt;). You could do this with chained command line commands, or, if you are using VSCode, via extensions and some custom tasks or something like that.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Solution B is not recommended, and is mostly included as an additional way to illustrate how ESLint and Prettier interact&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;This&lt;/a&gt; is the recommended ESLint extension for VSCode, which includes a built-in formatter&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;/h2&gt;
&lt;p&gt;Hope this helps someone! Feel free to leave a comment if you found an additional workaround or have any feedback to share!&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Click and Drag Mobile Touchscreen Mouse Mapping</title><link>https://joshuatz.com/posts/2020/click-and-drag-mobile-touchscreen-mouse-mapping/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/click-and-drag-mobile-touchscreen-mouse-mapping/</guid><description>Why is mapping remote mouse controls, particularly click-and-drag, so complex and often left out of major mobile applications? I cover the options and answers.</description><pubDate>Mon, 30 Mar 2020 10:26:16 GMT</pubDate><content:encoded>&lt;!-- omit in toc --&gt;
&lt;h1 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#intro&quot;&gt;Intro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#research&quot;&gt;Research&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#initial-thoughts&quot;&gt;Initial Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#real-world-examples&quot;&gt;Real-World Examples&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#apps-missing-click-and-drag-support&quot;&gt;Apps Missing Support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#different-mapping-options&quot;&gt;Different Mapping Options&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#web-complexity&quot;&gt;Web Complexity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#hackish-workaround-zoom&quot;&gt;Hackish Workaround (Zoom)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;intro&quot;&gt;Intro&lt;/h1&gt;
&lt;p&gt;While trying to help someone find the perfect remote desktop / remote control / VNC / etc. app, I realized that many Android and iOS apps are missing a key feature when it comes to controlling a desktop mouse with your phone… click-and-drag!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To be clear, a click-and-drag operation would be when you left click your mouse, and then, while still holding down the left mouse button, you move your cursor to a new position, and once there, release it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At first, this might not seem like a huge deal; there are many things you can do on a computer without needing to click-and-drag. However, when it comes to collaborative and creative tools, this becomes a nightmare as pretty much everything requires click-and-drag support! Annotating, drawing, moving virtual objects, rearranging slides, etc…&lt;/p&gt;
&lt;img src=&quot;https://media.giphy.com/media/sIE0hveuiwCNG/giphy.gif&quot; style=&quot;margin: auto; display:block; max-width: 500px; width: 50%; height: auto;&quot;&gt;
&lt;p&gt;The more I thought about this, and came across more and more “big league” apps ( &gt; million downloads) without click-and-drag, I started to doubt my own assumptions about mobile development. Is there a good reason why this feature is so often omitted from remote control apps? Is there an OS-level API blocker? Permissions issues?&lt;/p&gt;
&lt;img src=&quot;https://media.giphy.com/media/c4Nc0v0g15g9G/giphy.gif&quot; style=&quot;margin: auto; display:block; max-width: 500px; width: 50%; height: auto;&quot;&gt;
&lt;p&gt;I decided to do some digging…&lt;/p&gt;
&lt;h1 id=&quot;research&quot;&gt;Research&lt;/h1&gt;
&lt;h2 id=&quot;initial-thoughts&quot;&gt;Initial Thoughts&lt;/h2&gt;
&lt;p&gt;My guess as to why this is so often omitted, is because mapping a touchscreen to a mouse input is slightly complicated by the fact that there are multiple ways to do it, and there is no perfect mapping system. There is no perfect mapping, because a mouse doesn’t act like a touchscreen. With a mouse, you can move from point A to point B without holding down any buttons. But to move from point A to point B on a touchscreen, &lt;em&gt;without jumping&lt;/em&gt; in between, you have to hold down your finger to the screen while moving. How should this be interpreted?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If we map “user holds finger to screen and moves from A to B” to the mouse simply moving from A to B, then we cannot, at the same time, interpret it as a click and drag operation.
&lt;ul&gt;
&lt;li&gt;This might be called “touch mouse” mode&lt;/li&gt;
&lt;li&gt;This is why many apps require a secondary indicator action from the user (long press, multi-finger tap, etc) as the &lt;strong&gt;start&lt;/strong&gt; of a click-and-drag operation. But an equal, or greater amount of apps, seem to not bother spending the time to implement this at all, and just omit the click-and-drag ability altogether (looking at you, Zoom)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If we map “user holds finger to screen and moves from A to B” to the mouse clicking and dragging, without requiring that the user long press first or something like that, than we lose the ability for the user to move the mouse in a continuous path, &lt;strong&gt;without&lt;/strong&gt; dragging at the same time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;real-world-examples&quot;&gt;Real-World Examples:&lt;/h2&gt;
&lt;h3 id=&quot;apps-missing-click-and-drag-support&quot;&gt;Apps Missing Click-and-Drag Support&lt;/h3&gt;
&lt;p&gt;Here are some examples of apps not supporting click-and-drag on mobile for remote mouse control:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://crankwheel.com/introducing-crankwheel-remote-control/#limitations&quot;&gt;https://crankwheel.com/introducing-crankwheel-remote-control/#limitations&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;A viewer on a mobile device has slightly more limited control than a  viewer on a desktop or laptop computer. They can press to simulate a  left mouse-button click anywhere on the screen share, and they can  perform keyboard entry by first bringing up the keyboard using a button  provided in their viewing interface. Dragging and dropping, or dragging  scrollbars, is not supported, nor are keyboard shortcuts such as Ctrl+B  since there is no way of generating them on most mobile devices.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Zoom: &lt;a href=&quot;https://support.zoom.us/hc/en-us/articles/201362673-Request-or-Give-Remote-Control&quot;&gt;there is no control to start click-and-drag&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Many don’t allow for remote mouse control from a mobile touchscreen, &lt;strong&gt;period&lt;/strong&gt;!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://support.goto.com/meeting/help/give-keyboard-and-mouse-control-g2m040010&quot;&gt;GoToMeeting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Microsoft Teams&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.bluejeans.com/s/article/Remote-Desktop-Control&quot;&gt;Blue Jeans&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;…and many more (😢)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;different-mapping-options&quot;&gt;Different Mapping Options&lt;/h3&gt;
&lt;p&gt;Here are some examples of developers using some different mapping options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Github thread on the topic: &lt;a href=&quot;https://github.com/novnc/noVNC/issues/1267&quot;&gt;https://github.com/novnc/noVNC/issues/1267&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RealVNC’s &lt;a href=&quot;https://help.realvnc.com/hc/en-us/articles/360018541231-How-do-I-use-VNC-Viewer-for-Android-and-iOS-#mouse-interaction-mode-android-and-ios--0-2&quot;&gt;unique mapping model&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Double-tap to start click-and-drag&lt;/li&gt;
&lt;li&gt;Even middle-click is supported (three finger tap)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Android-VNC-Viewer &lt;a href=&quot;https://code.google.com/archive/p/android-vnc-viewer/wikis/Documentation.wiki#Input_Modes&quot;&gt;allows for the user to switch between mapping modes&lt;/a&gt;!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is very unique; I wish every remote control app offered this!&lt;/li&gt;
&lt;li&gt;This is also a good way to show how complex mapping is&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Google Chrome Remote Desktop: &lt;a href=&quot;https://www.androidpolice.com/2014/04/16/hands-on-with-googles-new-chrome-remote-desktop-app/&quot;&gt;Android Police writeup&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;Click and drag is also a bit confusing at first. Most apps use  double-tap and drag, but not Chrome Remote Desktop. With this app you  long-press until a ripple effect appears around the cursor. At that  point you can begin dragging where you want.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TeamViewer: On Android, uses double-tap to start click and drag&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Splashtop: &lt;a href=&quot;https://support-splashtoppersonal.splashtop.com/hc/en-us/articles/230005607-Introduction-to-Android-tablet-gestures&quot;&gt;Tablet and Mobile Phone Mappings&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;web-complexity&quot;&gt;Web Complexity&lt;/h2&gt;
&lt;p&gt;An added “wrinkle” in developing a touchscreen-to-mouse mapping is that if your app is a webpage / wrapper around a web app, then you are going to be receiving &lt;strong&gt;both&lt;/strong&gt; mouse and touch events, and you need to add logic to differentiate between and map appropriately.&lt;/p&gt;
&lt;p&gt;For example, you might get all of the below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pointerover&lt;/code&gt; -&gt; &lt;code&gt;pointerenter&lt;/code&gt;-&gt;&lt;code&gt;pointerdown&lt;/code&gt;-&gt;&lt;code&gt;touchstart&lt;/code&gt;-&gt;&lt;code&gt;pointerup&lt;/code&gt;-&gt;&lt;code&gt;pointerout&lt;/code&gt;-&gt;&lt;code&gt;pointerleave&lt;/code&gt;-&gt;&lt;code&gt;touchend&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mouseover&lt;/code&gt;-&gt;&lt;code&gt;mousemove&lt;/code&gt;-&gt;&lt;code&gt;mousedown&lt;/code&gt;-&gt;&lt;code&gt;mouseup&lt;/code&gt;-&gt;&lt;code&gt;click&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Related article (although dated): &lt;a href=&quot;https://www.html5rocks.com/en/mobile/touchandmouse/&quot;&gt;html5rocks.com - Touch And Mouse&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It looks like, although this is a common issue, there are plenty of exceptions that make it clear that this is not an OS limitation of Android or iOS, but has more to do with how each individual app developer decided to implement the handling of touch events.&lt;/p&gt;
&lt;p&gt;For example, Android-VNC-Viewer, one of the earlier VNC clients for Android, &lt;a href=&quot;https://code.google.com/archive/p/android-vnc-viewer/wikis/Documentation.wiki#:~:text=There%20are%20several%20modes%20for%20adapting%20the%20input%20controls&quot;&gt;has a great breakdown in their wiki&lt;/a&gt; on different mapping modes that they make available to the user, which includes click-and-drag support.&lt;/p&gt;
&lt;p&gt;So, the TLDR? Most likely this feature is omitted &lt;strong&gt;simply because of the added complexity&lt;/strong&gt;. Eye-roll please.&lt;/p&gt;
&lt;img src=&quot;https://media.giphy.com/media/3h5pe45FM9qUM/giphy.gif&quot; style=&quot;margin: auto; display:block; max-width: 500px; width: 50%; height: auto;&quot;&gt;
&lt;h1 id=&quot;hackish-workaround-zoom&quot;&gt;Hackish Workaround (Zoom)&lt;/h1&gt;
&lt;p&gt;For Zoom, I built a quick workaround kludge, that intercepts when the Zoom Android client sends a right click (which happens on a “long press”) and turns it into the start of a click-and-drag operation. You can find it here: &lt;a href=&quot;https://github.com/joshuatz/right-click-and-drag&quot;&gt;github.com/joshuatz/right-click-and-drag&lt;/a&gt;&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Right Click and Drag - AutoHotkey Program</title><link>https://github.com/joshuatz/right-click-and-drag</link><guid isPermaLink="true">https://github.com/joshuatz/right-click-and-drag</guid><description>Utility program, built with AutoHotkey, to use the right mouse button to start a click and drag operation. Useful for certain remote control / VNC clients.</description><pubDate>Thu, 26 Mar 2020 06:02:00 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_full_page_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Using Windows Task Scheduler to Automate NodeJS Scripts</title><link>https://joshuatz.com/posts/2020/using-windows-task-scheduler-to-automate-nodejs-scripts/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/using-windows-task-scheduler-to-automate-nodejs-scripts/</guid><description>How to setup NodeJS based tasks in Windows Task Scheduler, with details for both Yarn and NPM entries. Also explores comparison with CRON and alternatives.</description><pubDate>Thu, 20 Feb 2020 17:33:44 GMT</pubDate><content:encoded>&lt;p&gt;This is a post about using Windows Task Scheduler to automate the execution of NodeJS scripts, and other NPM / Yarn based tasks. &lt;strong&gt;If you don’t use Windows, this post is probably not for you&lt;/strong&gt; (but feel free to read anyways 🤷‍♀️)&lt;/p&gt;
&lt;h1 id=&quot;why-&quot;&gt;Why? 🤔&lt;/h1&gt;
&lt;p&gt;Tons of reasons! Maybe you are trying to emulate a production environment that has NodeJS scripts as scheduled CRON tasks. Or, for your own productivity or fun you want to script things to happen based on Window events.&lt;/p&gt;
&lt;p&gt;For example, you could write a NodeJS script that talks to your project tracker of choice via API and stops any running timers when you lock your computer to take a break.&lt;/p&gt;
&lt;h2 id=&quot;why-not-just-use-crontab-under-wsl&quot;&gt;Why not just use &lt;code&gt;crontab&lt;/code&gt; under WSL?&lt;/h2&gt;
&lt;p&gt;Good question! If you have WSL (&lt;em&gt;Windows Subsystem for Linux&lt;/em&gt;) installed, and you &lt;strong&gt;only&lt;/strong&gt; &lt;em&gt;want to trigger actions based on time&lt;/em&gt;, then you should totally give crontab under WSL a shot!&lt;/p&gt;
&lt;p&gt;Although there used to be issues with it (in past versions, WSL used to kill background tasks when you closed the console), I just gave it a shot, and had success. If there is interest, I might make a separate post about how to setup crontab under WSL.&lt;/p&gt;
&lt;p&gt;However, Task Scheduler still has value as a separate tool, since &lt;strong&gt;more than just time can be used as a trigger&lt;/strong&gt;; you can execute tasks based on computer unlocks, power events, and more. You can’t do that with crontab.&lt;/p&gt;
&lt;h1 id=&quot;how-&quot;&gt;How? 🤓&lt;/h1&gt;
&lt;p&gt;Steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find where the binary / application you need to run is stored
&lt;ul&gt;
&lt;li&gt;You can use &lt;code&gt;where npm&lt;/code&gt; or &lt;code&gt;where yarn&lt;/code&gt; from the command line to find the path
&lt;ul&gt;
&lt;li&gt;Example: My yarn path is &lt;code&gt;C:\Program Files (x86)\Yarn\bin\yarn.cmd&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;Task Scheduler&lt;/code&gt; (search in programs, or &lt;code&gt;WIN+R, taskschd.msc&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Start the task creation process by clicking on “Create Basic Task” or “Create Task” in the sidebar
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/i/hcs5idqr2lfzu9l7ssso.png&quot; alt=&quot;Windows Task Scheduler - Create Task Buttons&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick a trigger&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;“On a Schedule” (like CRON)&lt;/li&gt;
&lt;li&gt;“At log on”&lt;/li&gt;
&lt;li&gt;Etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add your Action&lt;/strong&gt;: Action -&gt; &lt;code&gt;Start a Program&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;“Program/script”:
&lt;ul&gt;
&lt;li&gt;Here is where you plug in the path to the application you found in step 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;“Add arguments” - You should put whatever you would put after &lt;code&gt;npm&lt;/code&gt; or &lt;code&gt;yarn&lt;/code&gt; normally.
&lt;ul&gt;
&lt;li&gt;If normally execute &lt;code&gt;npm run myScheduledTask&lt;/code&gt;, you would want arguments to be &lt;code&gt;run myScheduledTask&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If you are calling a &lt;code&gt;scripts&lt;/code&gt; entry in a &lt;code&gt;package.json&lt;/code&gt; file, you need to tell scheduler to run this where your &lt;code&gt;package.json&lt;/code&gt; file is located.
&lt;ul&gt;
&lt;li&gt;If using Yarn, you can pass the working directory through args, &lt;a href=&quot;https://stackoverflow.com/q/46891622/11447682&quot;&gt;with &lt;code&gt;cwd&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Otherwise, use the &lt;code&gt;start in (optional)&lt;/code&gt; field to specify the directory&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/i/pvyegxwgl69a9dbmu29b.png&quot; alt=&quot;Setting&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are concerned about keeping track of the results of what ran, you can also capture the result of anything spit out to the console by using &lt;code&gt;&gt;&gt; task_log.txt&lt;/code&gt; or something like that.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;---you-can-combine-actions-and-triggers&quot;&gt;👩‍🍳 - You can combine actions and triggers&lt;/h2&gt;
&lt;p&gt;One nice feature of task scheduler that I did not notice immediately is that it does not have to be a 1:1 mapping of task-trigger-action.&lt;/p&gt;
&lt;p&gt;For example, you can group ten different actions under a single task with a shared trigger.&lt;/p&gt;
&lt;h2 id=&quot;---you-can-use-git-bash-for-more-advanced-scripting&quot;&gt;✨ - You can use Git Bash for more advanced scripting&lt;/h2&gt;
&lt;p&gt;Instead of targeting NPM, Yarn, or Windows CMD, if you have Git Bash (comes packaged with &lt;em&gt;Git for Windows&lt;/em&gt;), you can it as the target “Program/Script”, and then execute a more advanced command that uses some bash tools. For example, a sample task that performs some backups for a project might look like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Program/Script”: &lt;code&gt;C:\Program Files\Git\git-bash.exe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;“Add Arguments”: &lt;code&gt;cd C:/projects/my-proj &amp;#x26;&amp;#x26; node prep-dirs.js &amp;#x26;&amp;#x26; npm run backup &gt;&gt; backup_log.txt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;---1-is-not-a-valid-win32-application&quot;&gt;💥 - &lt;code&gt;%1 is not a valid Win32 application&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;If you see this error, you have probably selected the wrong application as the &lt;code&gt;Program/Script&lt;/code&gt; to execute. For example, using &lt;code&gt;/yarn&lt;/code&gt; instead of &lt;code&gt;yarn.cmd&lt;/code&gt; will result in this error.&lt;/p&gt;
&lt;h2 id=&quot;---stop-the-cmd-window-from-popping-up&quot;&gt;⚙ - Stop the Cmd window from popping up&lt;/h2&gt;
&lt;p&gt;If the black windows Command prompt window keeps popping up whenever your task runs, you need to change one of the basic settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Change security options to: &lt;code&gt;Run whether user is logged on or not&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;🔐You will probably also want to check &lt;code&gt;Do not store password&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is no harm in having it show up; but it might get annoying if your task is scheduled to run frequently.&lt;/p&gt;
&lt;h2 id=&quot;---how-to-schedule-more-frequently-than-every-5-minutes&quot;&gt;⏰ - How to schedule more frequently than every 5 minutes&lt;/h2&gt;
&lt;p&gt;You might have already noticed that the smallest interval that shows up in the &lt;code&gt;repeat task every&lt;/code&gt; duration picker, under trigger settings, is &lt;code&gt;5 minutes&lt;/code&gt;. Uh-oh!&lt;/p&gt;
&lt;p&gt;Actually, this is an easy fix - you can actually type a custom interval in that box! So if you wanted an entry that was equivalent to a CRON of &lt;code&gt;* * * * *&lt;/code&gt; (every minute), just type in the box &lt;code&gt;1 minute&lt;/code&gt; and set &lt;code&gt;for a duration of&lt;/code&gt; to &lt;code&gt;Indefinitely&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is what that looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/i/eb47j4ggcvil4hncxm36.png&quot; alt=&quot;Windows Task Scheduler - Creating a time based entry that repeats every minute, like CRON of * * * * *&quot;&gt;&lt;/p&gt;
&lt;h1 id=&quot;comparison-with-cron&quot;&gt;Comparison with CRON&lt;/h1&gt;
&lt;p&gt;Since this is likely to come up in the comments (I can already hear the annoying response;  &lt;em&gt;”why don’t you use a real operating system? lol!”&lt;/em&gt;) - yes, Task Scheduler is not a perfect replacement for CRON on windows. But it is not really meant to be, and this post is not advocating as such either.&lt;/p&gt;
&lt;p&gt;Plus, you &lt;em&gt;can&lt;/em&gt; use crontab under WSL now (see my note under “why?”).&lt;/p&gt;
&lt;h1 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;/h1&gt;
&lt;p&gt;I hope this was helpful! This is a bit different from what I normally write about, but felt compelled to publish it as I had trouble finding existing resources on the topic.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/i/1quk2nax86o8zd6tblli.png&quot; alt=&quot;Success!&quot;&gt;&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Gatsby - Wrapping Markdown in HTML Divs with Custom Classes</title><link>https://joshuatz.com/posts/2020/gatsby---wrapping-markdown-in-html-divs-with-custom-classes/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/gatsby---wrapping-markdown-in-html-divs-with-custom-classes/</guid><description>An adventure in trying to write a Gatsby remark plugin to wrap specific markdown blocks (aka MD AST nodes) in custom classed div HTML elements.</description><pubDate>Tue, 18 Feb 2020 18:49:35 GMT</pubDate><content:encoded>&lt;h1 id=&quot;intro&quot;&gt;Intro&lt;/h1&gt;
&lt;p&gt;For reasons I won’t go into, I wanted to build a Gatsby plugin which wraps elements / blocks of my choice in a div with a custom class name applied. I’m using Markdown as the source material for my Gatsby project, so here is the basics of how I wanted it to work - this…&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;markdown&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;&amp;#x3C;!-- CUSTOM_CLASS_WRAPPER=&quot;special-heading-style&quot; --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF;font-weight:bold&quot;&gt;# Hello World&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…would get transformed to this…&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;special-heading-style&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;h1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Hello World&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;h1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also wanted it to work with Markdown components that generate multiple HTML elements. For example, this…&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;markdown&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;&amp;#x3C;!-- CUSTOM_CLASS_WRAPPER=&quot;special-list-style&quot; --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; My list&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;	 -&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Sub Item&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…would become&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;special-list-style&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;ul&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;li&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;My List&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;ul&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;li&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;Sub Item&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;li&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;ul&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;li&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;ul&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I thought this would be simple. After all, this is actually a tiny modification, and Gatsby is designed around extensibility and plugins. But things weren’t as simple as they seemed…&lt;/p&gt;
&lt;h1 id=&quot;background-on-markdown-conversion&quot;&gt;Background on Markdown Conversion&lt;/h1&gt;
&lt;p&gt;For the uninitiated, the majority of Gatsby projects that take Markdown as the source content, such as the &lt;a href=&quot;https://www.gatsbyjs.org/starters/gatsbyjs/gatsby-starter-blog/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;go-to starter blog&lt;/a&gt;, under the hood use a plugin called &lt;code&gt;gatsby-transformer-remark&lt;/code&gt; to convert the markdown to HTML. This, in turn, can accept hundreds of other plugins that extend it and modify how it handles the markdown conversion.&lt;/p&gt;
&lt;p&gt;When you are writing a plugin, you basically are writing a callback function that &lt;code&gt;gatsby-transformer-remark&lt;/code&gt; will call with, among other arguments, a Markdown AST (more on this later), which is a representation of what was extracted out of the markdown source. Your plugin can modify that content, by adding, deleting, or changing nodes in the AST.&lt;/p&gt;
&lt;h1 id=&quot;the-problem&quot;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;Going back to what we want to accomplish, it seems like writing our plugin should be straightforward. Receive the AST, look for our special HTML comment, and if found, wrap the first adjacent sibling in a new HTML node. Otherwise, do nothing. Here is a rough attempt:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; visit&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;unist-util-visit&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; markdownAST&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; args.markdownAST;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; commentClassPatt&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;[ ]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;&amp;#x3C;!--&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\s&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;ADD_CLASS=&quot;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\r\n&gt;&quot;]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;)&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\s&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;--&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;[ ]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*$&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;	visit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(markdownAST, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`html`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;node&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;index&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;parent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		commentClassMatch &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; node.value.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;match&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(commentClassPatt);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;		if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (commentClassMatch) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;			const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; commentClassMatch[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;			const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; siblings&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; parent.children &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;			const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nextSibling&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; siblings[index &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;			if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nextSibling) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;				visit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(nextSibling, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;node&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;index&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;parent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;					const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; wrapperNode&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;						type: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;html&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;						value: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`&amp;#x3C;div class=&quot;${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;className&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}&quot;&gt;&amp;#x3C;/div&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;					};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;					wrapperNode.children &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;						{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;							...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;node&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;						}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;					];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;					node &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; wrapperNode;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;				});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; markdownAST;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; plugin&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, once again, our steps look like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Take the Markdown AST spit out by the main transformer, and look for our special magic HTML comment&lt;/li&gt;
&lt;li&gt;If found, check if it has an immediate adjacent sibling&lt;/li&gt;
&lt;li&gt;If sibling is found, wrap with a &lt;code&gt;&amp;#x3C;div&gt;&lt;/code&gt;, with the desired class name&lt;/li&gt;
&lt;li&gt;Replace the sibling with the wrapper, with itself as child&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If I add breakpoints and logging output, I can see that my code is finding the right triggers and modifying nodes in place. However, this &lt;em&gt;does not work&lt;/em&gt;! Why? Well, it comes down to a misunderstanding of how Gatsby, the Markdown AST, and HTML all fit together.&lt;/p&gt;
&lt;h1 id=&quot;finding-the-solution&quot;&gt;Finding the Solution&lt;/h1&gt;
&lt;p&gt;The missing piece of understanding here is twofold.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In a Markdown AST, you can have nested markdown (AST) nodes, but you can’t really have an HTML node that contains nested markdown
&lt;ul&gt;
&lt;li&gt;You can see this yourself in the handy AST Explorer tool - &lt;a href=&quot;https://astexplorer.net/#/gist/54079bf1de3cb7b1c1df56d173b528b3/latest&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;sample&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Using a Markdown node within an HTML node requires conversion first&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, re-evaluating things… if the AST node we want to wrap in a custom div…&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;…is markdown
&lt;ul&gt;
&lt;li&gt;We first need to convert it to HTML, then wrap&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;…is HTML
&lt;ul&gt;
&lt;li&gt;We could wrap it in place&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BUT, either way, we must replace the node we want to wrap with a &lt;strong&gt;single&lt;/strong&gt; HTML node, with the value as a string.&lt;/p&gt;
&lt;h2 id=&quot;markdown-to-html&quot;&gt;Markdown to HTML?&lt;/h2&gt;
&lt;p&gt;So far, so good. But how do we actually convert a snippet of markdown to HTML in Gatsby? This is where there is a bit of a gap in documentation, as most plugins are generating HTML from internal logic, rather than from markdown.&lt;/p&gt;
&lt;h3 id=&quot;option-a-use-remark--unified-library-from-scratch&quot;&gt;Option A) Use &lt;code&gt;remark&lt;/code&gt; / &lt;code&gt;unified&lt;/code&gt; library from scratch&lt;/h3&gt;
&lt;p&gt;If you &lt;a href=&quot;https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-transformer-remark&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;peek inside&lt;/a&gt; the &lt;code&gt;gatsby-transformer-remark&lt;/code&gt; plugin, you will see that Gatsby actually uses &lt;a href=&quot;https://remark.js.org/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;remark&lt;/code&gt;&lt;/a&gt; for markdown to HTML transformation, and that in turn is heavily powered by the &lt;a href=&quot;https://github.com/unifiedjs/unified&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;unified&lt;/code&gt;&lt;/a&gt; suite of tools - there is a reason Gatsby is a gold sponsor of unified :)&lt;/p&gt;
&lt;p&gt;There is nothing stopping you, as a plugin developer, from using these libraries directly. In fact, I would probably recommend it for situations like these; that way if some of the Gatsby internal APIs change, you are not forced to update.&lt;/p&gt;
&lt;h3 id=&quot;option-b-use-methods-from-gatsby-transformer-remark&quot;&gt;Option B) Use methods from &lt;code&gt;gatsby-transformer-remark&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Rather than rolling our own code for converting a bit of markdown to HTML, shouldn’t we be able to just use transformer-remark itself? After all, that is the main point of the plugin; converting markdown to HTML.&lt;/p&gt;
&lt;p&gt;At first glance, it seems like we are in luck; there is a method exposed in the plugin arguments - &lt;code&gt;options.compiler.generateHTML&lt;/code&gt;, and if we look at &lt;a href=&quot;https://github.com/gatsbyjs/gatsby/blob/e0656b735331e9a8e7c93fcf59939cd864e11452/packages/gatsby-transformer-remark/src/extend-node-type.js#L186&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;the source for it&lt;/a&gt;, it says it takes a markdown node and returns &lt;code&gt;html&lt;/code&gt;. PERFECT! Let’s try passing in a markdown node from &lt;code&gt;visit()&lt;/code&gt; that we want to convert to HTML and then wrap…&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;UNHANDLED REJECTION Cannot read property ‘contentDigest’ of undefined&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;… now is the time when angry words are muttered.&lt;/p&gt;
&lt;p&gt;OK, what now? Well, it turns out that we have mixed up some terminology. There are really two types of markdown nodes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, there is a markdown AST node - which is a node of the markdown AST generated from the markdown file
&lt;ul&gt;
&lt;li&gt;This is what we just passed to &lt;code&gt;generateHTML()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;But, then there is a &lt;em&gt;&lt;strong&gt;Gatsby&lt;/strong&gt;&lt;/em&gt; markdown node, which is a special node specific to the Gatsby ecosystem, and has more to do with GraphQL and Gatsby internal than ASTs
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;That&lt;/strong&gt;&lt;/em&gt; is what the function is expecting&lt;/li&gt;
&lt;li&gt;You can see more about that data structure &lt;a href=&quot;https://www.gatsbyjs.org/docs/node-interface/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;further-digging-can-we-create-a-node-to-pass-to-generatehtml&quot;&gt;Further digging: Can we create a node to pass to &lt;code&gt;generateHTML()&lt;/code&gt;?&lt;/h4&gt;
&lt;p&gt;Aha! Yes we can!&lt;/p&gt;
&lt;p&gt;You can manually construct Gatsby nodes by hand - either by constructing the raw object by hand, or using &lt;a href=&quot;https://www.gatsbyjs.org/docs/actions/#createNode&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;actions.createNode&lt;/code&gt;&lt;/a&gt; - &lt;em&gt;&lt;strong&gt;however&lt;/strong&gt;&lt;/em&gt;, you need to have the string value of the node (raw markdown string) stored as &lt;code&gt;node.internal.content&lt;/code&gt;, because that is what the method &lt;a href=&quot;https://github.com/gatsbyjs/gatsby/blob/e0656b735331e9a8e7c93fcf59939cd864e11452/packages/gatsby-transformer-remark/src/extend-node-type.js#L196&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;uses as input to remark.parse()&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So, how do we get the raw markdown string &lt;em&gt;back&lt;/em&gt; out of the MDAST node? We can use &lt;code&gt;unified&lt;/code&gt; and their module &lt;code&gt;remark-stringify&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;​	 - &lt;code&gt;unified().user(remarkStringify).stringify(node)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Let’s say we have a MDAST node that we want to convert to a Gatsby MD node - here is how we could do so:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; unified&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;unified&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; remarkStringify&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;remark-stringify&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;createContentDigest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;createNodeId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; args&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;	// Assume we have MDAST `node` from unist-util-visit, or something similar&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; rawMarkdown&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; unified&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(remarkStringify).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(node);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; gatsbyMdNode&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		id: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;createNodeId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(rawMarkdown),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		children: [],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		parent: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		fields: {},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		internal: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			content: rawMarkdown,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			contentDigest: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;createContentDigest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(rawMarkdown),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			owner: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;gatsby-transformer-remark&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;			type: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;MarkdownRemark&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;For the above, you could also use the &lt;a href=&quot;https://www.gatsbyjs.org/docs/actions/#createNode&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;actions.createNode&lt;/code&gt;&lt;/a&gt; utility method (and probably should…)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, we can pass that Gatsby MD node to the generateHTML function without the contentDigest error.&lt;/p&gt;
&lt;h2 id=&quot;new-problem&quot;&gt;&lt;em&gt;New&lt;/em&gt; Problem&lt;/h2&gt;
&lt;p&gt;The HTML generator introduces a new problem to be aware of… it is an async function! How are we supposed to this inside a Gatsby plugin for transform-remark?&lt;/p&gt;
&lt;p&gt;Luckily, we have we have &lt;a href=&quot;https://www.huy.dev/2018-05-remark-gatsby-plugin-part-3/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;a great blog post by Huy Nguyen&lt;/a&gt; that discusses this exact issue.&lt;/p&gt;
&lt;p&gt;Based on that post, we know that all we really have to do is have our plugin return a &lt;code&gt;promise&lt;/code&gt; to Gatsby. There are a a bunch of ways you can do this; wrap the entire body in a promise and return it, write it as an async function, return &lt;code&gt;Promise.all()&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;I like the async / await syntax, so I just made my entire plugin function an async function.&lt;/p&gt;
&lt;p&gt;If you are looking for some resources are writing an async  plugin,  here are some helpful links I’ve found:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.huy.dev/2018-05-remark-gatsby-plugin-part-3/&quot;&gt;https://www.huy.dev/2018-05-remark-gatsby-plugin-part-3/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spectrum.chat/unified/remark/async-plugins~e3916709-9c3e-494c-9d8e-af3efbf08e80&quot;&gt;https://spectrum.chat/unified/remark/async-plugins~e3916709-9c3e-494c-9d8e-af3efbf08e80&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spectrum.chat/unified/syntax-tree/is-there-any-way-to-execute-async-work-when-visiting-a-node-in-unist-util-visit~28177826-628e-44e3-ac3e-0ffb53c195c6&quot;&gt;https://spectrum.chat/unified/syntax-tree/is-there-any-way-to-execute-async-work-when-visiting-a-node-in-unist-util-visit~28177826-628e-44e3-ac3e-0ffb53c195c6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://swizec.com/blog/buildremark-plugin-supercharge-static-site/swizec/8860&quot;&gt;https://swizec.com/blog/buildremark-plugin-supercharge-static-site/swizec/8860&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;other-issues&quot;&gt;Other Issues:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Replacing an AST node in-place
&lt;ul&gt;
&lt;li&gt;According to everything I could find, assigning my new node to the reference to the old one should replace it; however, I could not seem to get this to work
&lt;ul&gt;
&lt;li&gt;Replacing it in the&lt;code&gt;parent.children&lt;/code&gt; array also seemed troublesome&lt;/li&gt;
&lt;li&gt;The best working method I found was to leave the reference in place, but override the props with &lt;code&gt;Object.assign(existingNode, replacementNode)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Irregular Gatsby types
&lt;ul&gt;
&lt;li&gt;Be warned: it can be hard to find up-to-date type definitions for all the internals of Gatsby&lt;/li&gt;
&lt;li&gt;It helps to look through Gatsby’s source code, and existing TypeScript plugins&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;solution---final-working-version&quot;&gt;Solution - Final Working Version&lt;/h1&gt;
&lt;p&gt;After all that fuss and troubleshooting, the final code is actually pretty simple and short:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; visit&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;unist-util-visit&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; unified&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;unified&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; mdStringify&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;remark-stringify&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;createContentDigest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;markdownAST&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;createNodeId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; args;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; markToHtml&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; args.compiler.generateHTML;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; transformPromises&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; wrapMdAstNode&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;node&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;index&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;parent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;className&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; generatedNode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; wrap&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt; htmlContent&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        type: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;html&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        value: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`&amp;#x3C;div class=&quot;${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;className&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;htmlContent&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}&amp;#x3C;/div&gt;`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        children: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;undefined&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Process&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (node.type &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;html&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      generatedNode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; wrap&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(node.value);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; rawMarkdown&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; unified&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(mdStringify)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(node);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; dummyNode&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        id: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;createNodeId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(rawMarkdown),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        children: [],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        parent: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        fields: {},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        internal: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          content: rawMarkdown,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          contentDigest: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;createContentDigest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(rawMarkdown),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          owner: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;gatsby-transformer-remark&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          type: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;MarkdownRemark&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; html&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; markToHtml&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(dummyNode);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      generatedNode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; wrap&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(html);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Object.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;assign&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(node, generatedNode);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; node;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; commentClassPatt&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;[ ]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;&amp;#x3C;!--&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\s&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;ADD_CLASS=&quot;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\r\n&gt;&quot;]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;)&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\s&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;--&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;[ ]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*$&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  visit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(markdownAST, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`html`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;node&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;index&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;parent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; commentClassMatch&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      typeof&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; node.value &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; node.value.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;match&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(commentClassPatt);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (commentClassMatch) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; className&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; commentClassMatch[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; siblings&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; parent.children &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nextSibling&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; siblings[index &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nextSibling) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        transformPromises.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;          wrapMdAstNode&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(nextSibling, index, parent, className)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;all&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(transformPromises);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; markdownAST;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;module&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;exports&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; plugin;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>5 Underrated Built-In VSCode Features</title><link>https://joshuatz.com/posts/2020/5-underrated-built-in-vscode-features/</link><guid isPermaLink="true">https://joshuatz.com/posts/2020/5-underrated-built-in-vscode-features/</guid><description>5 powerful features that are built-in to Visual Studio Code and do not require any extensions or special toolchains to start using. Improve your dev workflow!</description><pubDate>Thu, 02 Jan 2020 01:59:00 GMT</pubDate><content:encoded>&lt;!-- Dev URL: https://dev.to/joshuatz/5-underrated-built-in-vscode-features-3koc --&gt;
&lt;h1 id=&quot;intro&quot;&gt;Intro&lt;/h1&gt;
&lt;p&gt;First, to get this out the way, this post is &lt;strong&gt;not&lt;/strong&gt; a list of “top extensions you need to install”. Although those lists are helpful (albeit “clickbaity”), and I love that VSCode has an extremely “healthy” ecosystem of extensions and plugins, I noticed very few posts about the amazing stuff that is &lt;strong&gt;built-in to VSCode from the get-go&lt;/strong&gt;, and that is what I want to talk about.&lt;/p&gt;
&lt;p&gt;These are features of VSCode that are baked into the editor (no extensions or tooling to install), but I personally don’t think are getting enough attention and could use a PSA.&lt;/p&gt;
&lt;h2 id=&quot;1-javascript-type-safety&quot;&gt;#1: JavaScript Type Safety&lt;/h2&gt;
&lt;p&gt;Although the rest of the list is not really ordered, this &lt;em&gt;&lt;strong&gt;definitely&lt;/strong&gt;&lt;/em&gt; deserves a #1 spot, by a large margin. I’m not going to shove TypeScript or type-safe JS (what?!) down your throat, but I’d like to just point out how VSCode has a feature that allows you to get some of the benefits of type-safety, without needing to switch to TS files, transpiling / compiling, or requiring that other devs use TypeScript.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is a wonderful half-way solution for those looking to introduce some type-safety into their JS code immediately, with minimal changes and setup.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I don’t want to go too in-depth on this, as there are multiple guides on this out there, but I’ll cover some “quickstart” basics:&lt;/p&gt;
&lt;h3 id=&quot;enabling-js-type-checking&quot;&gt;Enabling JS Type Checking&lt;/h3&gt;
&lt;p&gt;There are multiple ways to enable type checking in JS files, but the easiest if you are looking to just try it out, is to add &lt;code&gt;// @ts-check&lt;/code&gt; to the top of your file.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://code.visualstudio.com/docs/nodejs/working-with-javascript#_type-checking-javascript&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;“Type Checking JavaScript” section&lt;/a&gt; of VSCode’s “Working with JavaScript” docs for more details.&lt;/p&gt;
&lt;h3 id=&quot;what-does-this-get-you&quot;&gt;What does this get you?&lt;/h3&gt;
&lt;p&gt;If you don’t want to do any extra work (such as annotating types or setting up configs), simply adding &lt;code&gt;// @ts-check&lt;/code&gt; is a way to instantly get TypeScript powered type checking in VSCode.&lt;/p&gt;
&lt;p&gt;For example, checkout this screenshot comparing a JS file without and with TS-checking on:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/v1577919189/joshuatz-posts/Visual_Studio_Code_-_Regular_JS_vs_TS-Check_On_for_Type_Checking.png&quot; style=&quot;margin: auto; display:block; max-width: 1600px; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/v1577919189/joshuatz-posts/Visual_Studio_Code_-_Regular_JS_vs_TS-Check_On_for_Type_Checking.png&quot; alt=&quot;Visual Studio Code - Regular JS vs TS-Check On for Type Checking&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In the above example, the developer has forgotten that &lt;code&gt;querySelectorAll&lt;/code&gt; does not return an &lt;code&gt;array&lt;/code&gt;, it returns something special - a &lt;code&gt;NodeList&lt;/code&gt; - which does not have the built in &lt;code&gt;map()&lt;/code&gt; method that arrays have. With type checking on, the fatal error triggered by the use of &lt;code&gt;map&lt;/code&gt; is caught in VSCode and flagged.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For those wondering, an easy way to convert from a NodeList to an array is to use &lt;code&gt;Array.from&lt;/code&gt;: &lt;code&gt;Array.from(document.querySelectorAll(&apos;a[href]&apos;))&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;annotating-types-with-jsdoc-and-advanced-usage&quot;&gt;Annotating Types with JSDoc and Advanced Usage&lt;/h3&gt;
&lt;p&gt;The “power-user” part of this feature is the ability to use JSDoc comments to tell VSCode’s TypeScript powered intellisense system about advanced types that are within your JS code. This doc - &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;“Type Checking JavaScript Files”&lt;/a&gt; goes into depth on it, but for brevity, I’ll just paste a cool example:&lt;/p&gt;
&lt;img src=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/joshuatz-posts/VSCode_-_Advanced_TS_Check_Types_with_JSDoc_Annotations.png&quot; alt=&quot;VSCode - Advanced TS-Check JS Types with JSDoc Annotations&quot; style=&quot;margin: auto; display:block; max-width: 1200px; width: 95%; height: auto;&quot;&gt;
&lt;p&gt;For even more advanced usage, there are some great resources out there, starting with &lt;a href=&quot;https://cheatsheets.joshuatz.com/settings/vscode/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;my VSCode cheatsheet&lt;/a&gt; which has a whole section devoted to JSDoc powered type checking and intellisense 🙂. It also has a list of links to other helpful related resources.&lt;/p&gt;
&lt;h2 id=&quot;2-making-things-easier-for-contributors&quot;&gt;#2: Making things easier for contributors&lt;/h2&gt;
&lt;p&gt;Spending time to streamline the process for other people to contribute code to your codebase might feel like a chore, but in the long-run it makes things better for everyone. The easier and more “mistake-proof” the process is for devs to contribute, the less likely it is that you will have to field duplicate questions, reject PRs for violating guidelines, and deal with unnecessary back and forth.&lt;/p&gt;
&lt;p&gt;What you might be surprised to learn is that VSCode has some built-in features that help with this goal.&lt;/p&gt;
&lt;h3 id=&quot;recommended-extensions&quot;&gt;Recommended Extensions&lt;/h3&gt;
&lt;p&gt;One of them I actually learned about when I poked through Dev.to’s own source code, as it was the first time I had seen it used - &lt;em&gt;“Recommended Extensions”&lt;/em&gt;. The way it works is that you add a special file - &lt;code&gt;.vscode/extensions.json&lt;/code&gt; - to your project root, with a list of extensions that you want all contributors to be using (for example, Prettier), and VSCode will pick up on that and popup a recommendation to Devs to install them when they clone and open your repo.&lt;/p&gt;
&lt;p&gt;You can find more details on how to use this in VSCode’s &lt;a href=&quot;https://code.visualstudio.com/docs/editor/extension-gallery#_workspace-recommended-extensions&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;workspace recommended extensions&lt;/a&gt; documentation.&lt;/p&gt;
&lt;h3 id=&quot;workspace-tools-and-settings&quot;&gt;Workspace tools and settings&lt;/h3&gt;
&lt;p&gt;Built-in to VSCode are multiple places where you can override user settings and craft per project configurations, both of which can make it easier for contributors to work in your codebase with VSCode.&lt;/p&gt;
&lt;p&gt;For example, if you manage a mono-repo with distinct parts, you might want to take a look at &lt;a href=&quot;https://code.visualstudio.com/docs/editor/multi-root-workspaces&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;“multi-root workspaces”&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For overriding settings, such as default indent and spacing options, you can commit &lt;code&gt;.vscode/settings.json&lt;/code&gt; to the root of your repo with the overrides, but be respectful of your contributors and don’t override anything that you want them to be able to change.&lt;/p&gt;
&lt;h2 id=&quot;3-user-defined-snippets&quot;&gt;#3: User-Defined Snippets&lt;/h2&gt;
&lt;p&gt;Although VSCode does not yet support advanced macros (without the use of an extension that is), did you know that it &lt;em&gt;does&lt;/em&gt; already support advanced snippets? At first glance, these might seem like they can only insert basics blocks of text, but you can actually do some advanced things with them.&lt;/p&gt;
&lt;p&gt;For example, here is a snippet that I quickly whipped up to convert a markdown style bullet list into Github styled checkbox list:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;MD List to GH Checkbox&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;scope&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;markdown&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;prefix&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;list-to-checkbox&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;body&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;			&quot;${TM_SELECTED_TEXT/^([&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\t&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ]*-) ?(.*)$/$1 [ ] $2/gm}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;		],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Transform MD list to MD checkbox list&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here it is working in action:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/v1577919782/joshuatz-posts/VSCode_Snippet-Markdown_List_To_Github_Checkboxes.gif&quot; style=&quot;margin: auto; display:block; max-width: 800px; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/ds9kio5gn/image/upload/v1577919782/joshuatz-posts/VSCode_Snippet-Markdown_List_To_Github_Checkboxes.gif&quot; alt=&quot;Animated GIF showing how a VSCode snippet can turn a markdown bullet list into Github styled markdown checkboxes&quot; style=&quot;width:100%;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The main docs page for creating custom snippets is &lt;a href=&quot;https://code.visualstudio.com/docs/editor/userdefinedsnippets#_create-your-own-snippets&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;4-reordering-imports-automatically&quot;&gt;#4: Reordering Imports Automatically&lt;/h2&gt;
&lt;p&gt;First, I would like to thank Ryan Chenkie, for this tweet that brought this to my attention:&lt;/p&gt;
&lt;!-- For Dev.To --&gt;
&lt;!-- {% twitter 1201883268527927301 %} --&gt;
&lt;!-- For Website --&gt;
&lt;iframe style=&quot;border:none;&quot; width=&quot;550&quot; height=&quot;620&quot; data-tweet-url=&quot;https://twitter.com/ryanchenkie/status/1201883268527927301&quot; src=&quot;data:text/html;charset=utf-8,%3Cblockquote%20class%3D%22twitter-tweet%22%3E%3Cp%20lang%3D%22en%22%20dir%3D%22ltr%22%3EJust%20learned%20about%20the%20quick%20key%20in%20VS%20Code%20to%20remove%20unused%20imports%20and%20reorder%20used%20imports%3A%20Shift%20+%20Option%20+%20O.%3Cbr%3E%3Cbr%3ESave%20yourself%20some%20time%20when%20you%20want%20to%20clean%20up%20imports%20%uD83D%uDE4C%20%3Ca%20href%3D%22https%3A//t.co/DxVQdWehSs%22%3Epic.twitter.com/DxVQdWehSs%3C/a%3E%3C/p%3E%26mdash%3B%20Ryan%20Chenkie%20%28@ryanchenkie%29%20%3Ca%20href%3D%22https%3A//twitter.com/ryanchenkie/status/1201883268527927301%3Fref_src%3Dtwsrc%255Etfw%22%3EDecember%203%2C%202019%3C/a%3E%3C/blockquote%3E%0A%3Cscript%20async%20src%3D%22https%3A//platform.twitter.com/widgets.js%22%20charset%3D%22utf-8%22%3E%3C/script%3E%0A%3Cstyle%3Ehtml%7Boverflow%3Ahidden%20%21important%3B%7D%3C/style%3E&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;For Windows users, that key combo is SHIFT + ALT + O, by default.&lt;/p&gt;
&lt;p&gt;As a reply to that tweet pointed out, you can also have VSCode automatically do this, by configuring it in your &lt;code&gt;settings.json&lt;/code&gt; file, like so:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	&quot;editor.codeActionsOnSave&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		&quot;source.organizeImports&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;5-ability-to-build-a-custom-extension-for-yourself-or-your-company&quot;&gt;#5: Ability to build a custom extension for yourself or your company&lt;/h2&gt;
&lt;p&gt;One of the best things about VSCode is how extensible it is, and how easy it is to build and test new extensions. Often when this is discussed, the focus is on really flashy and/or impressive extensions, but I want to remind everyone that extensions that solve a singular niche problem can be just as important, especially within certain organizations.&lt;/p&gt;
&lt;p&gt;Within the discussion of “tooling”, you should think if there is a custom extension that could be built for your company (or just yourself) that could improve your developer workflow, help convert legacy code, and/or interop between processes that are specific to your work. It might be easier than you think to build one, and as a bonus, is sure to impress others.&lt;/p&gt;
&lt;h1 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;/h1&gt;
&lt;p&gt;I hope you found at least one of these items helpful! Thank you to the VSCode team and open-source contributors for building a revolutionary code editor!&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Using Google Cloud Functions Permanent URL to Proxy Ngrok Requests</title><link>https://joshuatz.com/posts/2019/using-google-cloud-functions-permanent-url-to-proxy-ngrok-requests/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/using-google-cloud-functions-permanent-url-to-proxy-ngrok-requests/</guid><description>Details on using a Google Cloud Platform Function and NodeJS to maintain a fixed URL that forwards to a dynamic Ngrok host and can be easily redeployed.</description><pubDate>Thu, 28 Nov 2019 20:12:37 GMT</pubDate><content:encoded>&lt;h1 id=&quot;background---why-might-you-need-a-reverse-proxy&quot;&gt;Background - Why might you need a reverse proxy?&lt;/h1&gt;
&lt;p&gt;Web development tooling has come a long way, and most frameworks and SDKs have made previewing your app as you work a breeze; often it is as simple as running something like &lt;code&gt;npm start&lt;/code&gt; from your CLI. But an unexpected hurdle that many web developers hit, and can seem &lt;em&gt;much&lt;/em&gt; more complicated, is “how do I use this &lt;strong&gt;outside&lt;/strong&gt; of my local network?”. Things like screen-sharing, screen recording, and VSCode’s Live Share can solve this issue if all you need to do is show your localhost demo to your remote coworker. But if you need to actually &lt;strong&gt;expose&lt;/strong&gt; your code to the outside world, those tools fall short.&lt;/p&gt;
&lt;p&gt;For example, a very common scenario is trying to test a third-party service (Slack, Github, Twilio, etc.) that talks to your codebase, maybe through a webhook. In this scenario, you can’t run the third party code locally (e.g. running all of Github’s platform on your computer), and if you put something like “localhost:3001/webhook” as the webhook, that third party is going to be unable to reach the code running inside your local network (for good reason!).&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;/media/blocked-entry-crop.jpg&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;blocked entry&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;How do we solve this?&lt;/p&gt;
&lt;h1 id=&quot;reverse-proxies&quot;&gt;Reverse-Proxies&lt;/h1&gt;
&lt;p&gt;The common solution to this issue is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Reverse_proxy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;em&gt;reverse proxy&lt;/em&gt;&lt;/a&gt; combined with &lt;em&gt;ssh tunneling&lt;/em&gt;. You have a public URL, which points to a public server, which uses a reverse proxy to hand-off the request to a different port and/or server, which in turn gets proxied / tunneled via a SSH tunnel between the server and our local development computer. This is much easier to show visually, so here is a simplistic diagram showing this:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;/media/SSH-Reverse-Proxy-For-Development.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;Reverse proxy over SSH&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;There is a lot that goes into this solution. You need to run a public server, set up SSH support on it, have a domain name, DNS entries, SSL certificates; the list goes on and on. It’s not all that complicated (&lt;a href=&quot;https://alexle.net/free-alternative-to-ngrok-with-permanent-subdomain-using-nginx-ssl-and-reverse-ssh-using-your-own-server/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;here is a simple guide&lt;/a&gt;), but it is also not free or quick.&lt;/p&gt;
&lt;h1 id=&quot;quick-simple-and-free---ngrok&quot;&gt;Quick, Simple, and *Free - Ngrok&lt;/h1&gt;
&lt;p&gt;Given the complexity of setting up a full reverse-proxy through SSH pipeline, services have sprung up to simplify this for developers.&lt;/p&gt;
&lt;p&gt;One of the most popular and easiest to use is &lt;a href=&quot;https://ngrok.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ngrok&lt;/a&gt;. With a single command, you can instantly get a public URL that you can use to access your localhost from anywhere - no public server or complicated setup necessary!&lt;/p&gt;
&lt;p&gt;For example, to relay HTTP requests from &lt;em&gt;outside&lt;/em&gt; your home network to your localhost on port 3001, you could use &lt;code&gt;ngrok http 3001&lt;/code&gt; and be up and running in seconds!&lt;/p&gt;
&lt;p&gt;Ngrok basically takes the place of both the SSH tunnel setup (with its CLI client), and the public server with a reverse proxy - the second and third blocks in my diagram up above. It even comes with built-in tools, such as a dashboard, request inspector, and replay functionality.&lt;/p&gt;
&lt;p&gt;Ngrok is also recommended by several well-known third-party platforms (&lt;a href=&quot;https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html&quot;&gt;Twilio&lt;/a&gt;, &lt;a href=&quot;https://developer.github.com/webhooks/configuring/&quot;&gt;Github&lt;/a&gt;).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I am not affiliated with Ngrok in any way; just think they have an impressive service.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-downside-to-ngrok&quot;&gt;The downside to Ngrok&lt;/h2&gt;
&lt;p&gt;Although Ngrok has a very generous free tier, one major restriction is that the public URLs that are generated are both random and not assigned - meaning that each time you start it up, you will get a different random subdomain, like &lt;code&gt;https://df12f52a.ngrok.io&lt;/code&gt;. If you are trying to develop webhooks or something similar, this means that each time you need to restart Ngrok, you would have to login into your third-party platform, find the admin dashboard, and copy and paste your new Ngrok URL into the GUI. Or, if you are sharing URLs with coworkers, you would need to keep sending them updated URLs.&lt;/p&gt;
&lt;p&gt;Ngrok does offer custom subdomains or personalized domains, but these come under the paid option, which starts at $5 a month. Not much, but this is basically the same price as running Ngrok or Nginx reverse proxy yourself on a paid server, like Digital Ocean.&lt;/p&gt;
&lt;h1 id=&quot;my-solution---add-another-proxy-with-cloud-functions&quot;&gt;My Solution - Add Another Proxy with Cloud Functions!&lt;/h1&gt;
&lt;p&gt;This is the kind of solution I am simultaneously both proud and ashamed of. Because I am cheap, and didn’t feel like buying a paid Ngrok plan just to test webhooks a few times a year, or setting up a dedicated server for it, I started thinking of how I might use free, or very low cost workarounds. What I came up with is kind of silly, but it works - just add another proxy, with a fixed URL, in front of the changing Ngrok URL!&lt;/p&gt;
&lt;p&gt;Let me elaborate. As I researched options, I noticed that cloud functions (Google Cloud Functions or AWS Lambda) were often the cheapest to use, in that they start at a scale of zero &lt;em&gt;by design&lt;/em&gt; (they only run when requested). At first I was thinking of replacing Ngrok with them entirely, but this quickly was squashed; they are designed to execute quickly and exit, and would not work for maintaining a SSH tunnel connnection.&lt;/p&gt;
&lt;p&gt;However, I realized two important things; cloud functions often get a semi-permanent PUBLIC url to invoke them, and they can handle both incoming and outbound requests. With this in mind, I realized I could setup a cloud function with a public URL, that simply proxies inbound requests to a re-configurable Ngrok address. Here is what that looks like:&lt;/p&gt;
&lt;p&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;/media/SSH-Reverse-Proxy-For-Development-Ngrok-with-Google-Cloud-Functions.png&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;gcp-proxy-func&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;/p&gt;
&lt;p&gt;In this scenario, &lt;strong&gt;my Cloud Function public URL stays static, even as the Ngrok address it points to changes&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;The only caveat here is that the cloud function has to be redeployed each time that the Ngrok URL changes. However, this is very easy to automate; in fact, I already have a command set up in my &lt;code&gt;package.json&lt;/code&gt;, so if my Ngrok URL has changed, all I need to do to redeploy with the update is type &lt;code&gt;npm run ngrok-deploy&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;gcp&quot;&gt;GCP&lt;/h2&gt;
&lt;p&gt;I ended up going with Google Cloud Platform to provide the serverless function that proxies requests. Their &lt;a href=&quot;https://cloud.google.com/free/&quot; rel=&quot;noopener&quot;&gt;free tier&lt;/a&gt; includes 2 million (!!!) executions per month, plus 5GB of network traffic. This is WAY more than enough for some simple webhook testing.&lt;/p&gt;
&lt;p&gt;You can find all my code here, which proxies inbound requests to a configurable destination: &lt;a target=&quot;_blank&quot; href=&quot;https://github.com/joshuatz/gcp-proxy-func&quot; rel=&quot;noopener&quot;&gt;github.com/joshuatz/gcp-proxy-func&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;another-cheap-alternative&quot;&gt;Another cheap alternative&lt;/h2&gt;
&lt;p&gt;Besides the obvious routes of paying for a pro Ngrok plan or paying for a dedicated server to run your own reverse-proxy, there are some other alternatives I have yet to explore.&lt;/p&gt;
&lt;p&gt;One option could be to wrap a reverse proxy in a container, and deploy it on a platform that supports “scale to zero” container scaling, and have it “wake up” on an SSH connection. Then, you would only have to pay for it while you are actively connected via the SSH tunnel and using it for developing.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Cloud Functions HTTP Proxy Relay with http-proxy-middleware</title><link>https://joshuatz.com/posts/2019/google-cloud-functions-http-proxy-relay-with-http-proxy-middleware/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/google-cloud-functions-http-proxy-relay-with-http-proxy-middleware/</guid><description>Simple example showing how to use http-proxy-middleware with a NodeJS powered Google Cloud Function.</description><pubDate>Sun, 24 Nov 2019 10:35:17 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;Quick post: I just want to say that I was (pleasantly) surprised to find how easy it was to setup and use a NodeJS powered HTTP request proxy, with Google Cloud Functions and &quot;http-proxy-middleware&quot; for Express JS. For example, here is a bare-bones setup:&lt;/p&gt;
&lt;h3&gt;Index.js:&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const express = require(&apos;express&apos;);
const proxy = require(&apos;http-proxy-middleware&apos;);
&lt;p&gt;const app = express();
app.all(’*’, getProxy());&lt;/p&gt;
&lt;p&gt;function getProxy() {
return proxy({
target: ‘&lt;a href=&quot;https://postb.in/123-456&quot;&gt;https://postb.in/123-456&lt;/a&gt;’,
changeOrigin: true,
followRedirects: true,
secure: true
});
}&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-js&quot;&gt;module.exports = {
proxy: app
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Package.json:&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
	&quot;name&quot;: &quot;gcp-proxy-func&quot;,
	&quot;version&quot;: &quot;1.0.0&quot;,
	&quot;description&quot;: &quot;Run http-proxy-middleware to relay requests&quot;,
	&quot;main&quot;: &quot;index.js&quot;,
	&quot;scripts&quot;: {
		&quot;deploy&quot;: &quot;gcloud functions deploy proxy --runtime nodejs8 --trigger-http --memory=128 --timeout=60s&quot;
	},
	&quot;dependencies&quot;: {
		&quot;express&quot;: &quot;^4.17.1&quot;,
		&quot;http-proxy-middleware&quot;: &quot;^0.20.0&quot;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Deploy command:&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;gcloud functions deploy proxy --runtime nodejs8 --trigger-http&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Note:&lt;/h2&gt;
&lt;p&gt;Although this didn&apos;t quite end up fulfilling the need that I had for a project, I thought it was neat how fast the function could spin up, respond to a request, proxy it, and respond.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Docker-Compose: Relative Env Files with Variable Substitution</title><link>https://joshuatz.com/posts/2019/docker-compose-relative-env-files-with-variable-substitution/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/docker-compose-relative-env-files-with-variable-substitution/</guid><description>How to use .env files that are in a different directory than a docker-compose YML file, with variable substitution and relative paths. Options and workarounds.</description><pubDate>Sat, 16 Nov 2019 02:36:06 GMT</pubDate><content:encoded>&lt;p&gt;One of the neat features of the Docker-Compose file syntax is that it allows for &lt;strong&gt;variable substitution&lt;/strong&gt;, where you can put in a placeholder that will get filled in from your &lt;code&gt;.env&lt;/code&gt; file. This is both useful and often necessary because you need to keep your &lt;code&gt;docker-compose.yml&lt;/code&gt; checked into GIT / version-control, but you don’t want to store passwords directly in the code. Variable substitution lets you do exactly that:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;3.1&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;services&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  db&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    image&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;mysql:5.7&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    environment&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      MYSQL_PASSWORD&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${PASSWORD}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; (not checked into version control):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;PASSWORD=&apos;abc123def456&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;problem-docker-compose-does-not-read-env-files-outside-of-the-working-directory&quot;&gt;Problem: Docker-Compose does not read .env files outside of the working directory&lt;/h1&gt;
&lt;p&gt;Here is the issue: all of the above works fine, &lt;em&gt;except&lt;/em&gt; if your .env file lives outside of the directory where the &lt;code&gt;docker-compose.yml&lt;/code&gt; file resides. For example, let’s pretend that our scenario is that we have a &lt;code&gt;.env&lt;/code&gt; file in the project root that we want to share with multiple compose files in subdirectories. Like this:&lt;/p&gt;
&lt;p&gt;Project Root:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; (contains variables)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/backend&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; (consumes variables via substitution)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/frontend&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; (consumes variables via substitution)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, ignoring that there might be ways to optimize our docker setup here and consolidate, how can we get both docker-compose files to use the top level &lt;code&gt;.env&lt;/code&gt; file?&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;best-option-if-it-works-for-you---official-cli-arguments&quot;&gt;Best Option (if it works for you) - Official CLI Arguments&lt;/h2&gt;
&lt;p&gt;This issue / feature has been requested &lt;em&gt;a lot&lt;/em&gt; from users of Docker, and it looks like the Docker dev team has taken a few passes at implementing the ability to use variable substitution with a env file outside of the working directory.&lt;/p&gt;
&lt;h3 id=&quot;project-directory&quot;&gt;project-directory&lt;/h3&gt;
&lt;p&gt;One way that this is supposed to now be possible is through the &lt;code&gt;--project-directory&lt;/code&gt; argument to &lt;code&gt;docker-compose&lt;/code&gt;. The way it is supposed to work, is that you can use it to “specify an alternate working directory”, by passing in a PATH string (CLI ref page &lt;a href=&quot;https://docs.docker.com/compose/reference/overview/&quot;&gt;here&lt;/a&gt;). For example, if I wanted to start the backend while using the parent project directory, theoretically this should work:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;cd&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; backend&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;docker-compose&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --project-directory&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ./../&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; up&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, you can find many instances (&lt;a href=&quot;https://github.com/docker/compose/issues/5986&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://github.com/docker/compose/issues/4841&quot;&gt;2&lt;/a&gt;, &lt;a href=&quot;https://github.com/docker/compose/issues/6310&quot;&gt;3&lt;/a&gt;) where users are saying this straight up &lt;em&gt;&lt;strong&gt;does not work&lt;/strong&gt;&lt;/em&gt;, and I would have to agree from my personal experience. And the official response is essentially “yes, this is usually broken”, despite the argument staying in the docs since Compose 3.2 (&lt;a href=&quot;https://github.com/docker/docker.github.io/pull/2509&quot;&gt;PR here&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id=&quot;env-file&quot;&gt;env-file&lt;/h3&gt;
&lt;p&gt;Another option that is &lt;em&gt;supposed&lt;/em&gt; to be viable for this purpose is the &lt;code&gt;--env-file&lt;/code&gt; argument. However, like the &lt;code&gt;env_file&lt;/code&gt; option, this appears to actually only work for passing environment values &lt;em&gt;directly&lt;/em&gt; to the container, and does not work for &lt;code&gt;docker-compose&lt;/code&gt; variable substitution - in fact, it appears to only work with the &lt;code&gt;run&lt;/code&gt; commands, which would support this hypothesis. This is despite the fact that an issue requesting that &lt;code&gt;--env-file&lt;/code&gt; be useable with the compose commands &lt;a href=&quot;https://github.com/docker/compose/issues/6170&quot;&gt;was closed&lt;/a&gt; - you can see comments on &lt;a href=&quot;https://github.com/docker/compose/pull/6535&quot;&gt;the relevant pull request&lt;/a&gt; that show it still not working (this is my experience as well). If you can get this to work, this would probably be the cleanest solution to this issue.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;workarounds&quot;&gt;Workarounds&lt;/h2&gt;
&lt;p&gt;Since neither of the above “official” approaches actually work for me, and many other users, I want to cover some common workarounds that &lt;em&gt;do&lt;/em&gt; work. Albeit, not in the most optimal solutions.&lt;/p&gt;
&lt;h3 id=&quot;execute-commands-where-the-env-is-located&quot;&gt;Execute commands where the &lt;code&gt;.env&lt;/code&gt; is located&lt;/h3&gt;
&lt;p&gt;Rather running &lt;code&gt;docker-compose&lt;/code&gt; where the YML file is located and getting docker to pull in a .env file in a different directory, you can actually do the reverse; execute the command where the .env file is located, and tell docker the location of a compose file in a different directory. We can do this with the &lt;code&gt;-f&lt;/code&gt; or &lt;code&gt;--file&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;Going back to our original example, here is the example file structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; (contains variables)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/backend&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; (consumes variables)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/frontend&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; (consumes variables)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And here is how we could start up both docker configs.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# In the project root dir&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;docker-compose&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ./backend/docker-compose.yml&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; up&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;docker-compose&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ./frontend/docker-compose.yml&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; up&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -d&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;symlink-the-env-file-in&quot;&gt;Symlink the .env file in&lt;/h3&gt;
&lt;p&gt;This is a really common solution to this issue, and a good solution to remember in general when programs require that files be in the same directory. Instead of copying the file to the directory as a part of a build script, we can create a symbolic link, which we only have to create once, and will make docker think that the .env file really is in the same folder as the compose file.&lt;/p&gt;
&lt;p&gt;For our example scenario, I just have to create the two symlinks in a one time process:&lt;/p&gt;
&lt;p&gt;(Bash example):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ln&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -s&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ../.env&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; backend/.env&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ln&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -s&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ../.env&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; frontend/.env&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Although version control, like GIT, can actually handle storing symlinks, if you are not comfortable with this, you could always have the symlinks generated with a one-time script.&lt;/p&gt;
&lt;h3 id=&quot;copy-the-env-file-around&quot;&gt;Copy the env file around&lt;/h3&gt;
&lt;p&gt;If you are opposed to using symlinks, you could simply copy the top level .env file around to various spots, using some standard shell scripting. I find this method a little messy and less optimal.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id=&quot;simply-passing-env-variable-values-from-host-to-container&quot;&gt;Simply passing env variable values from host to container&lt;/h1&gt;
&lt;p&gt;If you don’t care about variable substitution, and all you’re looking to do is simply pass a bunch of values through to the container, then most of this post is extra information that you don’t need. Passing environment values to the container itself is pretty simple, and has multiple easy-to-use options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/v17.12/compose/environment-variables/#pass-environment-variables-to-containers&quot;&gt;Full details&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;You can use &lt;a href=&quot;https://docs.docker.com/v17.12/compose/environment-variables/#the-env_file-configuration-option&quot;&gt;the &lt;code&gt;env_file&lt;/code&gt; option&lt;/a&gt; to pass an entire &lt;code&gt;.env&lt;/code&gt; file contents to the container&lt;/li&gt;
&lt;li&gt;Put under &lt;code&gt;environment:&lt;/code&gt; but leave off value&lt;/li&gt;
&lt;li&gt;Pass via &lt;code&gt;docker-compose run -e {VAR}={VAL} {imageName}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Chai Testing - Expecting Array To Not Include Members</title><link>https://joshuatz.com/posts/2019/chai-testing---expecting-array-to-not-include-members/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/chai-testing---expecting-array-to-not-include-members/</guid><description>How to test that an array does not include any members of another array with Chai JS and Mocha framework. Explains workaround and why certain tests don&apos;t work.</description><pubDate>Mon, 28 Oct 2019 05:50:10 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;One of the problems with &quot;natural language&quot; testing frameworks, or as I like to think of them, &quot;semantic testing&quot; frameworks, is that language is inherently ambiguous. This can lead to problematic instances where the developer (myself) thinks that, just because a test is written as an English sentence (&quot;expect variable alpha to be a string equal to &apos;abc&apos; and have length of 3&quot;), my interpretation of how this is tested is the same as the frameworks. Often, it is not.&lt;/p&gt;
&lt;p&gt;On to specifics: I hit this issue head on when I couldn&apos;t understand why Chai was letting an array pass a test that it should not have been. &lt;/p&gt;
&lt;h2&gt;Chai - Making sure an Array does NOT include members&lt;/h2&gt;
&lt;p&gt;Click &lt;a href=&quot;#solution&quot;&gt;here&lt;/a&gt; to jump right to the solution, or read on for background and what does *not* work.&lt;/p&gt;
&lt;h3&gt;Problem and attempt:&lt;/h3&gt;
&lt;p&gt;Let&apos;s start with the result we want. We want to ensure that {inputArr} (type of array), does not contain any of {blockArr} (type of array).&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&lt;code&gt;const blockArr = [&apos;&amp;#x26;&apos;, &apos;@&apos;, &apos;!&apos;];&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;One of Chai&apos;s handy chaining functions is &lt;a href=&quot;https://www.chaijs.com/api/bdd/#method_not&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the &quot;not&quot; method&lt;/a&gt;. Per the docs, this &quot;negates all assertions that follow in the chain&quot;. If we want to make sure that an array does not contain of the block elements, this seems like a good starting spot. So, we start with &lt;code&gt;expect(inputArr).to.not&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Next, lets combine this with &lt;a href=&quot;https://www.chaijs.com/api/bdd/#method_members&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the &quot;.members()&quot; method&lt;/a&gt;, which specifies array or object members to check against. So now we have &lt;code&gt;expect(inputArr).to.not.include.members(blockArr)&lt;/code&gt;. However, this will only fail if inputArr has &lt;em&gt;every&lt;/em&gt; element of blockArr in it, so let&apos;s use &lt;a href=&quot;https://www.chaijs.com/api/bdd/#method_any&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the &quot;.any&quot; modifier&lt;/a&gt; to specify that inputArr should not have any element of blockArr in it.&lt;/p&gt;
&lt;p&gt;Here is what we ended up with:&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&lt;code&gt;expect(inputArr).to.not.include.any.members(blockArr);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That looks good, right? It should work? WRONG! &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;It does not work!&lt;/strong&gt;&lt;/span&gt; This is why I mentioned language ambiguity. This reads as a valid sentence, and uses valid methods from Chai, so why is it not working?&lt;/p&gt;
&lt;p&gt;Well, the answer comes in reading more into the docs and realizing that there is a bad combination of requirements.&lt;/p&gt;
&lt;ol&gt;
	&lt;li&gt;We have to use &lt;code&gt;any&lt;/code&gt; in the chain, because otherwise methods like &lt;code&gt;include&lt;/code&gt; or &lt;code&gt;contains&lt;/code&gt; expect that inputArr is a superset of blockArr. In practicality, this means &lt;code&gt;expect([&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]).to.not.include.members([&apos;a&apos;, &apos;b&apos;])&lt;/code&gt; will successfully fail, but if we change it to &lt;code&gt;expect([&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]).to.not.include.members([&apos;a&apos;, &apos;e&apos;])&lt;/code&gt;, it lets the test pass instead of failing it.
&lt;ol&gt;
	&lt;li&gt;This goes back to ambiguity. If I asked you &quot;does the word DOG not contain the letters DB&quot;, you might answer &quot;Yes, it does not contain those letters&quot;, or you might answer &quot;No, it contains the letter D, but not the letter B&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
	&lt;li&gt;We are required by point 1 to use &lt;code&gt;any&lt;/code&gt;, but Chai &lt;em&gt;only&lt;/em&gt; applies this rule to &lt;code&gt;keys&lt;/code&gt;, not to &lt;code&gt;members&lt;/code&gt;!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Because &lt;code&gt;any&lt;/code&gt; only works on keys, including it with negated members actually has zero effect.&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;solution&quot;&gt;&lt;/a&gt;Solution: Workaround to use any with array member values&lt;/h3&gt;
&lt;p&gt;First, the way that Chai seems to recommend is to just keep chaining more precise rules together, like so:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;expect(inputArr).to.not.include(&apos;&amp;#x26;&apos;).and.not.include(&apos;@&apos;).and.not.include(&apos;!&apos;);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;But this starts to get ridiculous with long lists of blocking elements, and is not usable with dynamic tests. Here is a solution that keeps things simple and lets you check that inputArr does not contain ANY element from blockArr:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;expect(inputArr.reduce((running, curr) =&gt; {
	running[curr] = curr;
	return running;
}, {})).to.not.include.any.keys(blockArr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it! All I&apos;ve done is transformed inputArr from an array, to an object where the keys are the values in the array. This lets us use the &lt;code&gt;any&lt;/code&gt; chain method with &lt;code&gt;keys&lt;/code&gt;, so we don&apos;t have any issues like we did with &lt;code&gt;members&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Keep in mind that this removes duplicates, but that shouldn&apos;t really matter if you are trying to check that an array does &lt;em&gt;not&lt;/em&gt; contain elements.&lt;/p&gt;
&lt;p&gt;Another option is to just iterate over the input array and check for blocked elements, then store the result in a boolean, and then check the value of the boolean, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const foundBlocked = inputArr.reduce((blocked, current) =&gt; {
	return blocked || blockArr.indexOf(current) !== -1;
}, false);
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;expect(foundBlocked).to.equal(false);
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Proof and demo:&lt;/h2&gt;
&lt;p&gt;If you want to see how this all works, and maybe play with the results yourself, check out this Repl I set up - &lt;a href=&quot;https://repl.it/@joshuatz/Chai-Array-Does-Not-Include-Any-Members&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://repl.it/@joshuatz/Chai-Array-Does-Not-Include-Any-Members&lt;/a&gt;&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Reddit APIs and Manually Building Reddit Embeds</title><link>https://joshuatz.com/posts/2019/reddit-apis-and-manually-building-reddit-embeds/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/reddit-apis-and-manually-building-reddit-embeds/</guid><description>Information about public Reddit APIs, building post preview embeds, parsing Reddit JSON data, and an example of a custom Reddit embed generator.</description><pubDate>Wed, 09 Oct 2019 19:02:01 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;[Click &lt;a href=&quot;#reddit_api&quot;&gt;here&lt;/a&gt; to jump right to the Reddit APIs documentation]&lt;/p&gt;
&lt;p&gt;First, a warning. Putting together your own dynamic embeds based on Reddit data, rather than using a service, is not a choice that lends itself to an easy or robust solution. Reddit often retires public endpoints, and there can be mixed documentation about those that do exist, which is part of why I am writing this post.&lt;/p&gt;
&lt;h2&gt;An alternative to rolling your own solution:&lt;/h2&gt;
&lt;p&gt;First, before getting into a custom solution, I&apos;ll just point out that &lt;a href=&quot;https://embed.ly/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Embedly&lt;/a&gt;, which is a service that lets you dynamically embed external content on your site, is an &lt;em&gt;official partner&lt;/em&gt; of Reddit. Why does this matter? Well, if Reddit makes a change to their API, you better believe they are going to work with Embedly to make sure their embeds keep working. Some random dev that doesn&apos;t pay to use their API? Not so much.&lt;/p&gt;
&lt;p&gt;If you hit the &quot;share&quot; button below a Reddit post, and select &quot;embed&quot; as the option, the embed code it generates for you to use is actually Embedly code! Or, you can generate the same code using &lt;a href=&quot;https://embed.ly/code&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Embedly&apos;s generator&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Embedly also &lt;a href=&quot;https://embed.ly/providers&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;has &lt;em&gt;hundreds&lt;/em&gt; of providers&lt;/a&gt; other than Reddit. If you are looking to add other external content to your site or app, think about whether it is worth the time to build out a custom connector for each source, rather than one single integration with Embedly.&lt;/p&gt;
&lt;p&gt;I&apos;m not an employee of Embedly or even a user of their service, but just want to make it clear what your options are if you don&apos;t want to reinvent the wheel. Now, for those that do, or are looking for general information on Reddit APIs, read on.&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;reddit_api&quot;&gt;&lt;/a&gt;Reddit API Endpoints, Notes, and Examples:&lt;/h2&gt;
&lt;p&gt;Main docs: &lt;a href=&quot;https://www.reddit.com/dev/api/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://www.reddit.com/dev/api/&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Not advised:&lt;/h3&gt;
&lt;ul&gt;
	&lt;li&gt;https://www.reddit.com/oembed?url={reddit_post_or_comment_link}
&lt;ul&gt;
	&lt;li&gt;Example request: &lt;a href=&quot;https://www.reddit.com/oembed?url=https://www.reddit.com/r/food/comments/60jxzo/homemade_breakfast_sugar_cookies/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;The problem with this endpoint, is that it does not return a proper CORs header
&lt;ul&gt;
	&lt;li&gt;This means the request get blocked in all major browsers&lt;/li&gt;
	&lt;li&gt;This might not be a problem for you if you are building a desktop application or other non-web app&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;The information returned from this is also pretty basic&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Good:&lt;/h3&gt;
&lt;ul&gt;
	&lt;li&gt;The &quot;.json&quot; trick - e.g. {reddit_url}.json
&lt;ul&gt;
	&lt;li&gt;Example: &lt;a href=&quot;https://www.reddit.com/r/food/comments/60jxzo/homemade_breakfast_sugar_cookies/.json?limit=10&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link &lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;This is actually an &lt;strong&gt;insanely cool trick&lt;/strong&gt;. Currently, you can take pretty much &lt;em&gt;any&lt;/em&gt; Reddit permalink URL (post, comment, even boards) and just add &lt;code&gt;.json&lt;/code&gt; to the end of the URL to get back JSON formatted data!&lt;/li&gt;
	&lt;li&gt;This approach even returns a wildcard &lt;code&gt;access-control-allow-origin&lt;/code&gt; header, so you can easily use it with AJAX and not have CORs issues!&lt;/li&gt;
	&lt;li&gt;Downside: The data that is returned from each URL is not really filtered...
&lt;ul&gt;
	&lt;li&gt;This is bad if you simply want to get the title of a post and its preview image, as you add unnecessary latency and bandwidth use &lt;/li&gt;
	&lt;li&gt;However, this is good if you are looking to get tons of data at once (for example, to generate a preview of a popular thread).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;You can limit the number of returned children elements by using the &lt;code&gt;limit={#}&lt;/code&gt; querystring parameter&lt;/li&gt;
	&lt;li&gt;A benefit to this method is that you don&apos;t need to know the type (type prefix) of the object beforehand&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Official &lt;code&gt;/info&lt;/code&gt; API endpoint
&lt;ul&gt;
	&lt;li&gt;Example: &lt;a href=&quot;https://api.reddit.com/api/info/?id=t3_60jxzo&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;This is probably the best API endpoint to use in terms of being documented, likely to be supported for a long time, and returns brief info&lt;/li&gt;
	&lt;li&gt;&lt;em&gt;Actually&lt;/em&gt; documented, &lt;a href=&quot;https://www.reddit.com/dev/api/#GET_api_info&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Parsing API JSON data&lt;/h4&gt;
&lt;p&gt;Both the &lt;code&gt;.json&lt;/code&gt; trick and &lt;code&gt;/info&lt;/code&gt; API endpoints return JSON data.&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;For &lt;code&gt;.json&lt;/code&gt;, it is returned as an array with two elements (objects).
&lt;ul&gt;
	&lt;li&gt;The first element is the &quot;thing&quot; itself (post, comment, etc.)&lt;/li&gt;
	&lt;li&gt;The second element is the children of the thing&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;For &lt;code&gt;/info&lt;/code&gt;, it is returned a single object, of the thing itself&lt;/li&gt;
	&lt;li&gt;Each element contains two sub-objects: &lt;code&gt;data&lt;/code&gt; and &lt;code&gt;kind&lt;/code&gt; (type of thing)
&lt;ul&gt;
	&lt;li&gt;&lt;code&gt;data&lt;/code&gt;contains a &lt;code&gt;children&lt;/code&gt; array, which can contain many sub-elements&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To get the title of a post / article with the &lt;code&gt;.json&lt;/code&gt; trick, you would use `&lt;span class=&quot;message-body-wrapper&quot;&gt;&lt;span class=&quot;message-flex-body&quot;&gt;&lt;span class=&quot;message-body devtools-monospace&quot;&gt;&lt;span class=&quot;cm-variable&quot;&gt;jsonData&lt;/span&gt;[&lt;span class=&quot;cm-number&quot;&gt;0&lt;/span&gt;].&lt;span class=&quot;cm-property&quot;&gt;data&lt;/span&gt;.&lt;span class=&quot;cm-property&quot;&gt;children&lt;/span&gt;[&lt;span class=&quot;cm-number&quot;&gt;0&lt;/span&gt;].&lt;span class=&quot;cm-property&quot;&gt;data&lt;/span&gt;.&lt;span class=&quot;cm-property&quot;&gt;title`, since the post itself is the first child of the first element (itself).&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4&gt;Image 403 Forbidden Error from API:&lt;/h4&gt;
&lt;p&gt;If you grabbed an image URL off the API response and tried to use it, you might have ran into a &quot;Error 403 Forbidden&quot; message and/or HTTP error status. The most likely culprit is that the URL got messed up due to encoding / escaping.&lt;/p&gt;
&lt;p&gt;Whether you did or did not use &lt;code&gt;raw_json=1&lt;/code&gt; option with your API request, you simply need to decode some encoded HTML entities within the image URL. With JavaScript, you can use &lt;a href=&quot;https://gomakethings.com/decoding-html-entities-with-vanilla-javascript/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this code&lt;/a&gt; to decode it.&lt;/p&gt;
&lt;p&gt;If you are not using JS, search for &quot;decoding HTML entities&quot; + your programming language to see if there is a library or pattern you can use. Or simply do a string search and replace of &quot;&amp;#x26;amp;&quot; with &quot;&amp;#x26;&quot;, since that is the usual culprit.&lt;/p&gt;
&lt;h3&gt;For embeds only:&lt;/h3&gt;
&lt;ul&gt;
	&lt;li&gt;If all you need to do is embed a single comment or comment chain in a website, there is a quick trick you can use, no API required:
&lt;ul&gt;
	&lt;li&gt;`redditmedia.com/r/{subred}/{article_id}/{article_name}/{comment_id}/?embed=true`&lt;/li&gt;
	&lt;li&gt;Example: &lt;a href=&quot;https://www.redditmedia.com/r/food/comments/60jxzo/homemade_breakfast_sugar_cookies/df71am8/?embed=true&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;You can stick this directly in an iFrame, no API parsing required.&lt;/li&gt;
	&lt;li&gt;It appears it uses the same querystring params as the options listed for the Comments API endpoint - &lt;a href=&quot;https://www.reddit.com/dev/api/#GET_comments_{article}&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;I can&apos;t find a corresponding trick to generate an embed URL for regular posts instead of comments :(&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Reddit IDs / ID36 / T_&lt;/h2&gt;
&lt;p&gt;One of the things you might see glossed over through API documentation and various help threads is references to a ID36 string, or t_ Reddit ID. What is this? Well, if you look closely at pretty much any Reddit URL, you will start to see a pattern:&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;reddit.com/r/food/comments/&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;60jxzo&lt;/strong&gt;&lt;/span&gt;/homemade_breakfast_sugar_cookies/&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;df71am8&lt;/strong&gt;&lt;/span&gt;/&lt;/p&gt;
&lt;p&gt;See the highlighted parts? Those are Reddit IDs, specifically ID36, or base36 strings. Why the &quot;36&quot;? Well, the IDs will always contain only lowercase letters (26 characters in the alphabet) and/or digits (0-9, which is 10 choices) --&gt; 26 letters + 10 digits = 36 choices!&lt;/p&gt;
&lt;h3&gt;Type prefix (t_) and fullnames&lt;/h3&gt;
&lt;p&gt;A &quot;fullname&quot; in terms of the Reddit API is the type prefix (t1_, t2_, etc.), plus the ID36. So, an example would be &quot;t1_df71am8&quot;, which corresponds to a comment (type t1) with an ID of &quot;df71am8&quot;. You can find the table of type prefixes here and more info about fullnames, &lt;a href=&quot;https://www.reddit.com/dev/api/#fullnames&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Building a Custom Embed - Demo:&lt;/h2&gt;
&lt;p&gt;To put together some of the API basics I&apos;ve covered in this post, and demonstrate how you might use them, I&apos;ve thrown together a quick demo. This is just a little bit of JS + HTML + CSS, that dynamically generates a Reddit post summary card, based on an input URL. It uses the &lt;code&gt;/info&lt;/code&gt; API endpoint, and JavaScript&apos;s &lt;code&gt;fetch()&lt;/code&gt; web API. You can press the &quot;demo&quot; button to test it with the example Reddit thread used throughout this post.&lt;/p&gt;
&lt;p&gt;&lt;iframe style=&quot;width: 100%; height: 350px; max-width: 500px;&quot; src=&quot;https://jsfiddle.net/joshuatz/md0r6hfg/embedded/result,js,html,css&quot;&gt;&amp;#x3C;span data-mce-type=&quot;bookmark&quot; style=&quot;display: inline-block; width: 0px; overflow: hidden; line-height: 0;&quot; class=&quot;mce_SELRES_start&quot;&gt;﻿&amp;#x3C;/span&gt;&lt;/iframe&gt;&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Music Meta DOM Scraper - Simple Meta Info Grabber</title><link>https://joshuatz.com/projects/web-stuff/music-meta-dom-scraper---simple-meta-info-grabber/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/music-meta-dom-scraper---simple-meta-info-grabber/</guid><description>Simple music meta info scraper bookmarklet, for a few sites. Grabs song title, artist, album title, and more, and then lets you copy to clipboard as TSV or JSON.</description><pubDate>Mon, 30 Sep 2019 17:19:39 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;Source code / Repo:&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/joshuatz/music-meta-dom-scraper&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/joshuatz/music-meta-dom-scraper&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Description:&lt;/h2&gt;
&lt;p&gt;There is not a whole lot to this project, and I don&apos;t really have plans to expand on its capabilities, but given how much use I get out of it, I thought it warranted a quick write-up and project page.&lt;/p&gt;
&lt;p&gt;The &quot;Music Meta DOM Scraper&quot; is just a bit of Javascript that lets me quickly grab song meta information (track title, artist, release date, etc.) from a few different sources. Right now, those are just Bing search results and AllMusic discography pages.&lt;/p&gt;
&lt;p&gt;It does not use any APIs or offer bulk functionality beyond scraping an album at a time; its main purpose is to just clean up the data on the webpage, normalize it a little, and make it easy to copy into my clipboard, so I can then paste it into my catalog tracker of choice (Google Sheets, SQL table, etc.).&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;width: 90%; margin: auto; height: auto; display: block;&quot; src=&quot;https://raw.githubusercontent.com/joshuatz/music-meta-dom-scraper/master/demo.gif&quot; alt=&quot;Demo GIF&quot;&gt;&lt;/p&gt;
&lt;p&gt;Right now, I have it built as a simple bookmarklet, which you can install below:&lt;/p&gt;
&lt;h2&gt;Bookmarklet installer:&lt;/h2&gt;
&lt;p&gt;&lt;iframe style=&quot;width: 100%; min-height: 220px;&quot; src=&quot;https://music-meta-dom-scraper-bookmarklet.netlify.app/&quot;&gt;&amp;#x3C;span style=&quot;display: inline-block; width: 0px; overflow: hidden; line-height: 0;&quot; data-mce-type=&quot;bookmark&quot; class=&quot;mce_SELRES_start&quot;&gt;﻿&amp;#x3C;/span&gt;&lt;/iframe&gt;&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>NodeJS: Injecting Values, Sharing Globals, and General Mischief</title><link>https://joshuatz.com/posts/2019/nodejs-injecting-values-sharing-globals-and-general-mischief/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/nodejs-injecting-values-sharing-globals-and-general-mischief/</guid><description>Tips and tricks on sharing global variable values, inject values, capturing variables, and general NodeJS workarounds for if you can&apos;t edit a file.</description><pubDate>Fri, 27 Sep 2019 08:48:46 GMT</pubDate><content:encoded>&lt;p&gt;Warning: A lot of things discussed in this post are “hackish” and not best practice. However, there are some one-off scenarios where they are helpful, and exploring their use gives us some insight into how NodeJS works.&lt;/p&gt;
&lt;h2 id=&quot;example-scenario-executing-a-file-with-side-effects-from-another&quot;&gt;Example Scenario: Executing a file with side-effects from another&lt;/h2&gt;
&lt;p&gt;Here is the setup. We have file &lt;code&gt;as-is.js&lt;/code&gt;, which for some reason, we cannot edit to add imports. Maybe it is third-party vendor code, maybe we need to use it later in an environment that does not allow imports, or maybe we need to execute it with special imported values only for a one off test.&lt;/p&gt;
&lt;p&gt;Example “&lt;code&gt;as-is.js&lt;/code&gt;”:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; mode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (window.location.host &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;localhost&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    mode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;debug mode&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    mode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;production mode&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(mode);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, let’s say that we want to do multiple things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Inject a fake value for “&lt;code&gt;window.location.host&lt;/code&gt;”&lt;/li&gt;
&lt;li&gt;Override “&lt;code&gt;console.log&lt;/code&gt;” with our own method that redirects the output to a file&lt;/li&gt;
&lt;li&gt;Capture the value of “&lt;code&gt;mode&lt;/code&gt;”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We will do all things in a file called “&lt;code&gt;with.js&lt;/code&gt;”:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; fs&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;fs&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Inject window global&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; window &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    location: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        host: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;localhost&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Mutate console.log method&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    fs.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;writeFileSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(__dirname &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/output.txt&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,input,{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        flag: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;a&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Run as-is.js&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(__dirname &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/as-is.js&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Try to read captured `mode` variable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Mode captured from &quot;as-is.js&quot; is ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Right now, that file doesn’t achieve our goals and doesn’t work.&lt;/p&gt;
&lt;p&gt;Here are some options for implementation&lt;/p&gt;
&lt;h3 id=&quot;polluting-the-global-variable-scope&quot;&gt;Polluting the global variable scope&lt;/h3&gt;
&lt;p&gt;In NodeJS, one way to share variables between file is to make them &lt;a href=&quot;https://stackabuse.com/using-global-variables-in-node-js/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;em&gt;&lt;strong&gt;global&lt;/strong&gt;&lt;/em&gt; variables&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One way to do this is to completely remove the variable declaration keyword. If I change “&lt;code&gt;var foo = &apos;bar&apos;;&lt;/code&gt;” to “&lt;code&gt;foo = &apos;bar&apos;;&lt;/code&gt;”, I have now made “&lt;code&gt;foo&lt;/code&gt;” a global variable.&lt;/p&gt;
&lt;p&gt;You can also use “&lt;code&gt;global.foo = &apos;bar&apos;&lt;/code&gt;” as another way to declare a global in node.&lt;/p&gt;
&lt;p&gt;Here is our modified “&lt;code&gt;with.js&lt;/code&gt;”:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; fs&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;fs&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Inject window global&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;window &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    location: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        host: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;localhost&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Mutate console.log method&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    fs.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;writeFileSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(__dirname &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/output.txt&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,input,{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        flag: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;a&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Run as-is.js&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(__dirname &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/as-is.js&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Try to read captured `mode` variable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Mode captured from &quot;as-is.js&quot; is ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only change that has been made is “&lt;code&gt;var window = ...&lt;/code&gt;” to “&lt;code&gt;window = ...&lt;/code&gt;”, making it a global variable. Note that this change does not need to be made to “&lt;code&gt;console&lt;/code&gt;”, since it is &lt;em&gt;already&lt;/em&gt; a global.&lt;/p&gt;
&lt;p&gt;However, there is still an issue with this code. The “&lt;code&gt;mode&lt;/code&gt;” variable is not a global in “&lt;code&gt;as-is.js&lt;/code&gt;”, so it can’t be read by “&lt;code&gt;with.js&lt;/code&gt;”! The last line will of “&lt;code&gt;with.js&lt;/code&gt;” fails!&lt;/p&gt;
&lt;h3 id=&quot;eval-the-file-contents&quot;&gt;Eval the file contents&lt;/h3&gt;
&lt;p&gt;Since we can’t touch “&lt;code&gt;as-is.js&lt;/code&gt;” to change “&lt;code&gt;var mode = ...&lt;/code&gt;” to “&lt;code&gt;mode = ...&lt;/code&gt;” and make it a global, we need some other way to capture its value. A quick hack way to do this is to use “&lt;code&gt;eval()&lt;/code&gt;”. By default, in non-strict-mode, eval runs code in the same lexical scope as where it is called, so variables can be shared across.&lt;/p&gt;
&lt;p&gt;Modified “&lt;code&gt;with.js&lt;/code&gt;”:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; fs&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;fs&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Inject window global&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; window &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    location: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        host: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;localhost&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Mutate console.log method&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    fs.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;writeFileSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(__dirname &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/output.txt&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,input,{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        flag: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;a&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Run as-is.js&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; rawAsIsCode&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; fs.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;readFileSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(__dirname &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/as-is.js&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;eval&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(rawAsIsCode);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Try to read captured `mode` variable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Mode captured from &quot;as-is.js&quot; is ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that we were able to change “&lt;code&gt;window = ...&lt;/code&gt;” back to “&lt;code&gt;var window = ...&lt;/code&gt;”, and it was still able to share its value with “&lt;code&gt;as-is.js&lt;/code&gt;” due to the scope sharing of “&lt;code&gt;eval()&lt;/code&gt;”.&lt;/p&gt;
&lt;p&gt;Note: The value of “&lt;code&gt;mode&lt;/code&gt;” still can’t be read, if the eval’ed code contains the “&lt;code&gt;use strict&lt;/code&gt;” directive; in that case, “&lt;code&gt;as-is.js&lt;/code&gt;” would get its own &lt;em&gt;lexical environment&lt;/em&gt;, which is to say, its own scope.&lt;/p&gt;
&lt;h3 id=&quot;use-nodejs-vms&quot;&gt;Use NodeJS VMs&lt;/h3&gt;
&lt;p&gt;Something that is unique to Node is the &lt;a href=&quot;https://nodejs.org/api/vm.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;vm&lt;/code&gt; module API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With this exposed API, we can create mini &lt;em&gt;sandboxes&lt;/em&gt; with specific context, to run code in. This is pretty similar to “&lt;code&gt;eval()&lt;/code&gt;”, but offers a little more isolation and control.&lt;/p&gt;
&lt;p&gt;Modified “&lt;code&gt;with.js&lt;/code&gt;”:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; fs&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;fs&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; vm&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;vm&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Inject window global&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; window &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    location: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        host: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;localhost&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Mutate console.log method&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    fs.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;writeFileSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(__dirname &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/output.txt&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,input,{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        flag: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;a&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Run as-is.js&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; rawAsIsCode&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; fs.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;readFileSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(__dirname &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/as-is.js&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Create scoped context with shared vars&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; mode;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; vmContext&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; vm.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;createContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    console,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    window,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    mode&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Run code in context&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;vm.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;runInContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(rawAsIsCode,vmContext);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Try to read captured `mode` variable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Mode captured from &quot;as-is.js&quot; is ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;vmContext&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a few things important things to note about this approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The explicit context setup of the vm allows us to capture the value of “&lt;code&gt;mode&lt;/code&gt;”, even if “&lt;code&gt;as-is.js&lt;/code&gt;” is in “&lt;code&gt;strict mode&lt;/code&gt;”&lt;/li&gt;
&lt;li&gt;Like “&lt;code&gt;eval()&lt;/code&gt;”, all the vm run methods expect code as a string.&lt;/li&gt;
&lt;li&gt;Variables have to be explicitly shared via “&lt;code&gt;context&lt;/code&gt;”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;file-concatenation&quot;&gt;File concatenation&lt;/h3&gt;
&lt;p&gt;This is kind of a silly solution, but it works! We will basically pretend that the two files are one, and merge the code together with string concatenation.&lt;/p&gt;
&lt;h4 id=&quot;cli-solution&quot;&gt;CLI Solution&lt;/h4&gt;
&lt;p&gt;The only special part of this solution is that we need to move our final reading of the “&lt;code&gt;mode&lt;/code&gt;” value to the end, by placing it in a separate file and concatenating it last. So the values of the files as they are now are:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;as-is.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;use strict&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; mode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (window.location.host &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;localhost&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    mode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;debug mode&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    mode &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;production mode&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Mode set&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;with.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; fs&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;fs&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; vm&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; require&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;vm&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Inject window global&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; window &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    location: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        host: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;localhost&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Mutate console.log method&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    fs.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;writeFileSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(__dirname &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;/output.txt&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,input,{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        flag: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;a&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;with-final.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Try to read captured `mode` variable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;console.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Mode captured from &quot;as-is.js&quot; is ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;mode&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally, we can concatenate the code from these files and run them from the command line with a single line:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;node&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -e&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;eval(fs.readFileSync(&apos;./with.js&apos;).toString() + fs.readFileSync(&apos;./as-is.js&apos;).toString() + fs.readFileSync(&apos;./with-final.js&apos;).toString());&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap-up&lt;/h2&gt;
&lt;p&gt;These are just some fun examples of messing with NodeJS scope and context; they are far from the only options. Furthermore, depending on what you are looking to do (testing, module mocking, etc), there is likely a library or existing solution to help you out.&lt;/p&gt;
&lt;p&gt;For example, if you are looking to mock the global &lt;code&gt;window&lt;/code&gt; object, since that is a browser API and does not exist in NodeJS, please take a look at &lt;a href=&quot;https://github.com/jsdom/jsdom&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jsdom&lt;/a&gt;. Or for mocking modules, &lt;a href=&quot;https://www.npmjs.com/package/mock-require&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;mock-require&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, if you have the power to actually edit the files you need to share values with, then obviously you should use actual established methods for doing so, such as the module system of exports and imports.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Unix/Linux File Creation Stamps (aka birthtime), and NodeJS</title><link>https://joshuatz.com/posts/2019/unixlinux-file-creation-stamps-aka-birthtime-and-nodejs/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/unixlinux-file-creation-stamps-aka-birthtime-and-nodejs/</guid><description>Figuring out why stat is returning blank birthtimes on Unix/Linux and how NodeJS got around this with an update to fs.stat through its libuv dependency.</description><pubDate>Mon, 23 Sep 2019 09:01:06 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;This learning experience started off because of a test failing on Travis-CI.&lt;/p&gt;
&lt;p&gt;This test used NodeJS &quot;fs&quot; (&quot;filesystem&quot;), or more accurately, &quot;fs-extra&quot;, which extends the &quot;fs&quot; base. It simply created some files, waited a few seconds, modified one of the files to update its modification timestamp (mtime), and then checked to make sure that FS showed that the modification time minus the creation time was the span of the delay. This test passed with no issues on Node v12 and Node v10, but was failing 100% of the time on Node v8. Another very important fact (and clue to the real issue) was that Travis-CI uses Linux, whereas on my own PC, which runs Windows, the test passed on all versions, including Node v8.&lt;/p&gt;
&lt;h2&gt;The exact issue:&lt;/h2&gt;
&lt;p&gt;After adding some logs to my test, it became very clear what was causing the test to fail:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Diff between created and modified should have been 8, but was 0.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In this case, my test had waited 8 seconds to modify the file, so the difference between created time (&lt;code&gt;stats.birthtime&lt;/code&gt;) and modification time (&lt;code&gt;stats.mtime&lt;/code&gt;) should have been 8 seconds, but it was zero, indicating that &lt;strong&gt;modification time was &lt;em&gt;identical&lt;/em&gt; to birthtime&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Since this issue was only appearing on Linux, I fired up a Ubuntu VM with the same version as Travis (16.04.6 LTS Xenial) to see what the actual OS had to say about this. The results were similarly disturbing:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Linux-Ubuntu-File-Stat-Birthtime-Unavailable.png&quot;&gt;&lt;img class=&quot;size-full wp-image-746 aligncenter&quot; src=&quot;/media/Linux-Ubuntu-File-Stat-Birthtime-Unavailable.png&quot; alt=&quot;Linux Ubuntu - File Stat Birthtime Unavailable&quot; width=&quot;599&quot; height=&quot;344&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here you can see that Ubuntu, and more precisely&lt;a href=&quot;http://man7.org/linux/man-pages/man2/stat.2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt; the &lt;code&gt;stat&lt;/code&gt; system call&lt;/a&gt;, is refusing to show the creation time of a file that &lt;strong&gt;it just created&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So obviously NodeJS is not finding a birthtime from the OS, and just using the modified stamp as a fallback.&lt;/p&gt;
&lt;p&gt;But why is birthtime showing as blank? Some quick searches made it sound like getting a file birthtime is either &lt;a href=&quot;https://stackoverflow.com/a/14842384/11447682&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;impossible on Unix&lt;/a&gt;, or it requires &lt;a href=&quot;https://unix.stackexchange.com/a/50184&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;some insane workarounds&lt;/a&gt;. However, knowing that my test was passing on later versions of Node, on the same exact Ubuntu OS, obviously Node had found some sort of solution. What was it?&lt;/p&gt;
&lt;h2&gt;NodeJS fs.stat change and libuv&lt;/h2&gt;
&lt;p&gt;First, to prove that I&apos;m not making this up, here is proof that in older versions of Node, prior to 10.16, fs.stat returns the wrong birthtime, whereas in 10.16 and up, it returns the right one:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/NodeJS-Different-FS-Stat-Birthtimes-on-Different-Versions-GS.png&quot;&gt;&lt;img class=&quot;alignnone size-large wp-image-748&quot; src=&quot;/media/NodeJS-Different-FS-Stat-Birthtimes-on-Different-Versions-GS-1024x580.png&quot; alt=&quot;NodeJS - Different FS Stat Birthtimes on Different Versions - GS&quot; width=&quot;1024&quot; height=&quot;580&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So, how the heck did Node fix this? The answer took some digging, but also gives some insight into how Node versioning and core dependencies work.&lt;/p&gt;
&lt;p&gt;When I first tried to look through Node&apos;s source code to find how &quot;fs&quot; worked, I wasn&apos;t finding much of value. It turns out, that is because Node uses something called &lt;a href=&quot;https://github.com/libuv/libuv&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;libuv&lt;/a&gt; for a lot of the heavy OS stuff, such as filesystem calls, and most of the code for FS stuff is in there.&lt;/p&gt;
&lt;h3&gt;Libuv and statx()&lt;/h3&gt;
&lt;p&gt;Knowing that I should be looking at libuv, I was finally able to get somewhere and found &lt;a href=&quot;https://github.com/libuv/libuv/issues/2152&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a proposal&lt;/a&gt; that explained how the libuv team found out about updates to the Linux system that would improve &lt;code&gt;stat&lt;/code&gt; calls. This was primarily the introduction of the &lt;code&gt;statx()&lt;/code&gt; syscall, which as opposed to &lt;code&gt;stat&lt;/code&gt;, &lt;strong&gt;actually&lt;/strong&gt; *&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;does&lt;/strong&gt;&lt;/span&gt;* &lt;strong&gt;return the birthtime&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;statx()&lt;/code&gt; syscall support was added to the Linux kernel in 4.11 (2017). It was then added to glibc v2.28 in 2018. glibc is the &quot;Gnu C Library&quot;, essentially a wrapper around low-level system stuff that allows you to make calls from C/C++. This is important, because this is how libuv makes system calls from its C powered codebase, and getting &lt;code&gt;statx&lt;/code&gt; support in glibc meant that it could be used in libuv, which also meant it could get used in Node!&lt;/p&gt;
&lt;p&gt;The proposal to use &lt;code&gt;statx&lt;/code&gt; in libuv was accepted, and the code to do it was merged with &lt;a href=&quot;https://github.com/libuv/libuv/pull/2184&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PR #2184&lt;/a&gt;, in Feb 2019, which shortly after made it into the official v1.27.0 (Stable) release in March 2019 (see &lt;a href=&quot;https://github.com/libuv/libuv/blob/v1.x/ChangeLog&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;changelog&lt;/a&gt;). This  update to libuv, in turn, was manually pulled into the Node source code via &lt;a href=&quot;https://github.com/nodejs/node/commit/d6f6d7f8541327b72667d38777c47b9ea675125d#diff-194c5460fc6f1d425ebb0ae0270ead06&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this specific commit&lt;/a&gt;, on March 16th, 2019.&lt;/p&gt;
&lt;p&gt;Here is where it gets a little confusing. That commit is tagged as &quot;v12.10&quot;, so you might expect that the tag, plus how recently it was merged, means that the improved &lt;code&gt;fs.stat&lt;/code&gt; code is only in Node V12.10 and up... but you would be wrong (like I was).&lt;/p&gt;
&lt;p&gt;Node versioning is a little complicated (see &lt;a href=&quot;https://nodejs.org/en/about/releases/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this&lt;/a&gt; and &lt;a href=&quot;https://nodejs.org/en/download/releases/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this&lt;/a&gt;), but the basic summary is that the libuv update was applied to multiple versions of Node based on where they were in their lifecycle:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Node v8.x did not receive the update on any releases, even though it is LTS, since it is on &quot;maintenance LTS&quot; and not &quot;active LTS&quot;&lt;/li&gt;
	&lt;li&gt;Node v9.x did not receive the update on any releases, since it is odd numbered, and thus is not LTS and the update did not happen during its active dev stage&lt;/li&gt;
	&lt;li&gt;Node v10.x received it, starting with 10.16.0, since it is &quot;active LTS&quot;&lt;/li&gt;
	&lt;li&gt;Node v12.x received it on all versions since it is under current development&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Testing for statx support&lt;/h2&gt;
&lt;p&gt;So, back to my original issue, now that I know all the above, how do I detect when the test is going to run on a Linux system where Node&apos;s &lt;code&gt;fs.stat&lt;/code&gt; function will NOT use the improved &lt;code&gt;statx&lt;/code&gt; calls instead of &lt;code&gt;stat&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;The pseudo code is something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(node v &amp;#x3C; 10.16.0 &amp;#x26;&amp;#x26; linux) OR (node v &gt;= 10.16.0 &amp;#x26;&amp;#x26; linux &amp;#x26;&amp;#x26; kernel &amp;#x3C; 4.11)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the above condition is found to be true in my code, I have Travis-CI simply skip the test that relies on an accurate birthtime. You can see how I implemented it if you look at the commits linked to from &lt;a href=&quot;https://github.com/joshuatz/git-date-extractor/issues/1&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this issue&lt;/a&gt;.  No more failing tests! :)&lt;/p&gt;
&lt;h2&gt;Implementing yourself&lt;/h2&gt;
&lt;p&gt;If you are looking to implement the use of &lt;code&gt;statx&lt;/code&gt; in your own program, outside of using it through NodeJS, you have some considerable work set out for you. I would recommend looking at how libuv brought in the changes&lt;a href=&quot;https://github.com/libuv/libuv/pull/2184/files&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt; in their PR&lt;/a&gt;, and also taking a look at &lt;a href=&quot;https://unix.stackexchange.com/a/407305&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this StackExchange answer&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Git-Date-Extractor NPM Package and CLI Tool</title><link>https://joshuatz.com/projects/applications/git-date-extractor-npm-package-and-cli-tool/</link><guid isPermaLink="true">https://joshuatz.com/projects/applications/git-date-extractor-npm-package-and-cli-tool/</guid><description>Automated file timestamp extractor: git-date-extractor NPM package and CLI tool. The tool lets you retrieve file creation and modification dates based on Git history.</description><pubDate>Sat, 21 Sep 2019 04:08:30 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;Github and NPM:&lt;/h2&gt;
&lt;p&gt;Github repo: &lt;a href=&quot;https://github.com/joshuatz/git-date-extractor&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/joshuatz/git-date-extractor&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NPM link: &lt;a href=&quot;https://www.npmjs.com/package/git-date-extractor&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://www.npmjs.com/package/git-date-extractor&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;About this project:&lt;/h2&gt;
&lt;p&gt;In general, OS timestamps for files, such as when a file was created or last modified, are &lt;em&gt;problematic&lt;/em&gt; for developers. They can be unreliable, ambiguous, and are not exactly portable between systems. For example, in many versions of Linux, trying to get the &quot;birthtime&quot; of a file, or when it was created, gives an empty result. Another wrinkle to this issue is that OS timestamps are not tracked as part of Git - so if you clone a repo, all the files that are created will have timestamps that point to the time of cloning, regardless of when the files were actually last modified.&lt;/p&gt;
&lt;p&gt;I built this tool because I needed a solution for retrieving and storing file timestamps, and I realized that although Git does not store timestamps directly on files, they could be pulled programmatically by scraping the Git history of a file and seeing when it was created or modified as part of commits. That is what this tool does; you can give it a list of files (or a directory) and it will use Git as the preferred method for generating timestamps. It can even save the results as a JSON file with the filepaths as keys; this makes it a great alternative solution for storing file modification dates in a system like Gatsby, which has no traditional database for storing that kind of metadata. In fact, I myself use this solution for my own Gatsby site, which is discussed in &lt;a href=&quot;https://joshuatz.com/posts/2019/gatsby-better-last-updated-or-modified-dates-for-posts/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this post&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Demo:&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/media/git_date_extractor-Demo-Advanced_File_Out.gif&quot;&gt;&lt;img class=&quot;alignnone wp-image-742 size-full&quot; src=&quot;/media/git_date_extractor-Demo-Advanced_File_Out.gif&quot; alt=&quot;Git-Date-Extractor - Advanced Demo&quot; width=&quot;1038&quot; height=&quot;588&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Tech stack:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;NodeJS&lt;/li&gt;
	&lt;li&gt;Javascript + TypeScript annotations with JSDoc&lt;/li&gt;
	&lt;li&gt;Initial structure was scaffolded from Yeoman &lt;a href=&quot;https://github.com/sindresorhus/generator-nm&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;generator-nm&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Uses &lt;a href=&quot;https://github.com/sindresorhus/meow&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Meow&lt;/a&gt; for CLI parsing&lt;/li&gt;
	&lt;li&gt;Unit and integration tests use &lt;a href=&quot;https://github.com/avajs/ava&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ava&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Code coverage analysis with &lt;a href=&quot;https://github.com/istanbuljs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Istanbul JS / NYC&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Automated build CI testing with &lt;a href=&quot;https://travis-ci.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Travis CI&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/xojs/xo&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;XO&lt;/a&gt; for linting&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Features:&lt;/h2&gt;
&lt;p&gt;Make sure to checkout &lt;a href=&quot;https://github.com/joshuatz/git-date-extractor&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the Github repo&lt;/a&gt; for the current up-to-date readme, but I&apos;ll list some key features below as well:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Usable from both CLI and JS&lt;/li&gt;
	&lt;li&gt;Very configurable (filter results by filename, directory)&lt;/li&gt;
	&lt;li&gt;Get output in console or save as JSON file&lt;/li&gt;
	&lt;li&gt;Creation timestamps are cached once generated, if using JSON file&lt;/li&gt;
	&lt;li&gt;Automatic re-staging of JSON file if calling script with githook option&lt;/li&gt;
	&lt;li&gt;... and more!&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Gatsby - Better Last Updated or Modified Dates for Posts</title><link>https://joshuatz.com/posts/2019/gatsby---better-last-updated-or-modified-dates-for-posts/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/gatsby---better-last-updated-or-modified-dates-for-posts/</guid><description>Solutions and tips on how to use &quot;created&quot; and &quot;last updated&quot; date and timestamps with Gatsby and Git, including a fully automated approach with git hooks.</description><pubDate>Thu, 12 Sep 2019 04:59:46 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;A common need for documentation or blog posts that cover topics that rapidly change (such as technology) is a &quot;last modified&quot; or &quot;last updated&quot; date and time. It can quickly tell readers how up-to-date your post is and clue them in on how relevant it might be to the information they seek.&lt;/p&gt;
&lt;p&gt;A nasty surprise that I got when I first deployed Gatsby was how it handled file modification stamps. I had setup Gatsby to use a commonly used system for pulling in file timestamps; in GraphQL, I queried &lt;code&gt;file -&gt; modifiedTime&lt;/code&gt; or &lt;code&gt;file -&gt; mtime&lt;/code&gt;, which is when the markdown file was last modified on the disk, and then in in my page template, simply used something like &lt;code&gt;Markdown last updated: {(new Date(node.parent.modifiedTime)).toString()}&lt;/code&gt;. When building and testing locally, everything worked beautifully. But when I deployed to a different location (my actual production server), instead of showing  the true time the file was modified, every page showed the same modification date: when the deploy ran, today!&lt;/p&gt;
&lt;p&gt;I had forgotten a very simple lesson that I had previously learned; &lt;a href=&quot;https://git.wiki.kernel.org/index.php/GitFaq#Why_isn.27t_Git_preserving_modification_time_on_files.3F&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;git does not record or keep file timestamp metadata&lt;/a&gt;. So unless you are syncing files directly from your computer to your server, they are not going to have matching timestamps. In my scenario, my server (Netlify) did a &lt;code&gt;git clone&lt;/code&gt;, which set all the modified stamps to the current time, and subsequent deploys after &lt;code&gt;git push&lt;/code&gt; or &lt;code&gt;git merge&lt;/code&gt; also had incorrect dates. This is not limited to Netlify though, since again, the issue is that git simply does not store this data. In fact, you will lose correct edit timestamps if you delete your project and re-clone from a remote source (Github).&lt;/p&gt;
&lt;h2&gt;Solution A: Storing Dates in Front-Matter or Filenames&lt;/h2&gt;
&lt;p&gt;So how do we get around this? Many people opt to use Gatsby&apos;s support for &quot;front matter&quot; - these are fields that you place at the top of a source Markdown file, and contain metadata about the file that can feed into GraphQL without necessarily being rendered visible to the user. This is basically the official recommendation from Gatsby and Netlify (see &lt;a href=&quot;https://github.com/gatsbyjs/gatsby/issues/9785&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Github Issue&lt;/a&gt; and &lt;a href=&quot;https://stackoverflow.com/questions/48357930/im-sorting-jekyll-posts-by-last-modification-date-but-git-interferes-by-overwri/48427454#48427454&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Stack Overflow&lt;/a&gt;). Here is what that front matter might look like at the top of a markdown file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
date: 2019-08-24
last-modified: 2019-08-26
author: &apos;Liz Lemon&apos;
title: &apos;Favorite Snacks&apos;
tags:
 - food
 - list
 - favs
---
&lt;/code&gt;&lt;p&gt;&lt;code&gt;How to even begin with my favorite snacks! Well, my top 20 are…
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;People also often store the creation date within the filename itself (like &quot;2019-08-24--my-blog-post.md&quot;),  or use that in combination with storing dates in front-matter. If you look at some very popular Gatsby repos on Github (such as &lt;a href=&quot;https://github.com/taniarascia/taniarascia.com/tree/master/content/posts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this&lt;/a&gt;, &lt;a href=&quot;https://github.com/eggheadio/gatsby-starter-egghead-blog/blob/master/content/blog/demo06/index.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this&lt;/a&gt;, or &lt;a href=&quot;https://github.com/greglobinski/gatsby-starter-personal-blog/tree/master/content/posts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this&lt;/a&gt;), either of these solutions are extremely popular.&lt;/p&gt;
&lt;p&gt;Now, I&apos;m about to say something that might be controversial and rather blunt; I think this approach has a bad &quot;code smell&quot;.&lt;/p&gt;
&lt;p&gt;To start with, you&apos;re introducing a non-automated hand-written process into the system; authors have to write out dates by hand as a text string and remember to keep them updated if they edit a file. Furthermore, how are you parsing this string? If you are converting it into a JS Date, you better have error checking in case someone miss-typed a date or suddenly wrote in the wrong syntax! And what about timezones? Do you want to store just the date, or do you want the actual hour modified in there too? If so, that is an extra thing the author has to manually type out!&lt;/p&gt;
&lt;h3&gt;Solution A - part 2&lt;/h3&gt;
&lt;p&gt;It should be noted that, hybrid CMS approaches (aka a &quot;headless CMS&quot; that supports MD) , such as Netlify CMS, do automate the part of generating this frontmatter and saving it in Markdown. However, this is a bit of a moot point if you accept pull-requests from users who are not using a CMS, or hand-edit markdown in addition to using the CMS. Also, this still leaves my second complaint, which is more of a rant:&lt;/p&gt;
&lt;p&gt;The second part of my complaint is partly directed at a tendency to overuse Markdown and SSRs to the point of misuse. Storing separate and accurate post publication and last modified stamps is something that is built-in to pretty much every major CMS that uses a database; it&apos;s just another column in the SQL table. Requiring that authors &lt;strong&gt;hand type&lt;/strong&gt; timestamps or using hacked together code to paste date as text strings into files is going backwards, and is a hint that maybe relying 100% on markdown to generate a site is not &lt;em&gt;always&lt;/em&gt; the best choice. It irks me a bit though that it is so trendy to hate on databases and shove everything in static files, when the truth is that it takes a lot of kludgy fixes to just replicate some simple things that come standard and are more &quot;safe&quot; with a database based CMS. /rant&lt;/p&gt;
&lt;h2&gt;An alternative automated approach: Using git commit timestamps&lt;/h2&gt;
&lt;p&gt;Without using a headless-CMS layer as an editor for our markdown, there is an approach that should work locally, as well as when deployed, since &lt;strong&gt;it will check the timestamps into source control.&lt;/strong&gt; The downside of this approach is that the initial setup is a little complicated, since it relies on combining several advanced tools: git hooks, git formatting (aka &quot;pretty-print&quot;), bash scripting, and using Gatsby&apos;s Node-APIs.&lt;/p&gt;
&lt;p&gt;The way I&apos;m currently setting this up is to use a JSON file to hold all the timestamps; the filepath relative to the project root is used as the lookup key, and the value is an object that holds both a last modified and created timestamp, in UNIX timestamp format.&lt;/p&gt;
&lt;p&gt;The pseudo code / flow looks like this:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;A git &lt;a href=&quot;https://git-scm.com/docs/githooks#_pre_commit&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pre-commit hook&lt;/a&gt; fires when a dev uses &lt;code&gt;git commit&lt;/code&gt;, but before the commit actually finalizes&lt;/li&gt;
	&lt;li&gt;The git hook retrieves the filenames of the files that were changed in the commit, and passes them to a node script, via arguments&lt;/li&gt;
	&lt;li&gt;The node script then does the bulk of the work:
&lt;ul&gt;
	&lt;li&gt;A &quot;created&quot; timestamp is generated once per file by looking up the file history with &lt;code&gt;git log&lt;/code&gt;
&lt;ul&gt;
	&lt;li&gt;A very handy one-liner command to do this is &lt;code&gt;git log --pretty=format:%at --follow -- myFile.txt | tail -n 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;The modified stamp is pulled in with &lt;code&gt;stat&lt;/code&gt; and &lt;code&gt;mtimeMs&lt;/code&gt;, which gives the modification time in a UNIX timestamp&lt;/li&gt;
	&lt;li&gt;Timestamps are updated in the JSON file, and the file is added to the current commit before it finalizes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;After the git hook completes, the updated timestamp JSON file will be lumped in with everything else in the commit&lt;/li&gt;
	&lt;li&gt;To pull the timestamps from the JSON file into Gatsby GraphQL, &lt;code&gt;&lt;a href=&quot;https://www.gatsbyjs.org/docs/node-apis/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;gatsby-node.js&lt;/a&gt;&lt;/code&gt; does the heavy lifting:
&lt;ul&gt;
	&lt;li&gt;Within &lt;code&gt;exports.onCreateNode&lt;/code&gt;, my code checks to see if a timestamp exists for a given file node, and if it does, it uses it with &lt;code&gt;createNodeField&lt;/code&gt; to add it to the node&lt;/li&gt;
	&lt;li&gt;Now within &lt;code&gt;exports.createPages&lt;/code&gt; and within React page templates, I can pull in the newly created node fields with GraphQL queries.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;del&gt;I&apos;m still working on cleaning up this code a bit; once I&apos;m happy with it, I might release it as a NPM package, since I could see it being of a lot of use to many projects, beyond just Gatsby.&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;The node script which pulls the modification and creation times in via git scraping is now published as an NPM package - check it out &lt;a href=&quot;https://joshuatz.com/projects/applications/git-date-extractor-npm-package-and-cli-tool/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So far, it&apos;s working great! Check it how it automatically updated my timestamps in this commit, based on the files updated:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Gatsby-Automated-Markdown-Timestamps-per-Git-Commit.png&quot;&gt;&lt;img class=&quot;alignnone size-large wp-image-734&quot; src=&quot;/media/Gatsby-Automated-Markdown-Timestamps-per-Git-Commit-1024x624.png&quot; alt=&quot;Gatsby - Automated Markdown Timestamps per Git Commit&quot; width=&quot;1024&quot; height=&quot;624&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Cheatsheets and Snippets - Markdown Powered Gatsby Mini Site</title><link>https://joshuatz.com/projects/web-stuff/cheatsheets-and-snippets---markdown-powered-gatsby-mini-site/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/cheatsheets-and-snippets---markdown-powered-gatsby-mini-site/</guid><description>A Markdown-sourced, Gatsby-powered mini-site that displays a bunch of my cheatsheets, code snippets, and miscellaneous programming notes as I collect them.</description><pubDate>Sun, 25 Aug 2019 03:26:02 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;As I&apos;ve been trying to hone my chops as a self-taught developer, one thing that has helped immensely is compiling my own cheatsheets and reference material. I realize that there is a huge amount of programming cheatsheets already out there and my own are not likely to get much traffic, but I am a huge believer that writing things down helps cement them in your own brain. Plus, it is always easier to read your own writing style than someone else&apos;s!&lt;/p&gt;
&lt;p&gt;I used to keep these cheatsheets and snippets in random files in either Dropbox or OneNote, but as the collection started growing, I felt the urge to get them in order. As a project for myself, I converted them all to Markdown, placed them into an organized Github repository, and then integrated Gatsby (&lt;a href=&quot;https://www.gatsbyjs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GatsbyJS&lt;/a&gt;). Gatsby is a ReactJS based static-site generator that can take a variety of inputs, including Markdown, and allows you to customize how it builds the output HTML by integrating custom React components, CSS, and more.&lt;/p&gt;
&lt;p&gt;My Gatsby setup is still pretty boilerplate at the moment, but I have added a few unique features, and I plan on adding more soon. So far, I&apos;ve added:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Dark vs light theme picker
&lt;ul&gt;
	&lt;li&gt;Simple CSS, JS, and persisted state through localStorage&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Auto-generated directory index pages
&lt;ul&gt;
	&lt;li&gt;See &lt;a href=&quot;https://joshuatz.com/posts/2019/gatsby-automatic-directory-listing-pages-beginners-attempt/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this post&lt;/a&gt; for implementation details&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Some custom automated markdown link fixing (needed for my current unique repo setup)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also set this mini-site up to be compiled and served by &lt;a href=&quot;https://www.netlify.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Netlify&lt;/a&gt;, which allows me to have the site auto-rebuild and deploy when ever I push changes to the actual Markdown cheatsheets.&lt;/p&gt;
&lt;p&gt;The Gatsby/React/Netlify powered portal is now live at &lt;a href=&quot;https://cheatsheets.joshuatz.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;cheatsheets.joshuatz.com&lt;/a&gt;, and I plan on continuing to built out my collection.&lt;/p&gt;
&lt;p&gt;I might even add some non-development related cheatsheets, so I don&apos;t have to keep googling things like recipe ingredient equivalents.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Gatsby - Automatic Directory Listing Pages - Beginner&apos;s Attempt</title><link>https://joshuatz.com/posts/2019/gatsby---automatic-directory-listing-pages---beginners-attempt/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/gatsby---automatic-directory-listing-pages---beginners-attempt/</guid><description>A beginner attempt at creating automatic folder directory listing index pages within Gatsby, using createPages, GraphQL, MarkdownRemark and allDirectory.</description><pubDate>Wed, 21 Aug 2019 01:36:21 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I just started building my first Gatsby project yesterday, and I&apos;ve already ran into an issue that I&apos;m having trouble finding existing solutions for. My goal is to have the directory structure of my source directory, which Gatsby is building from, closely mirror the generated site structure. This would include automated directory listing pages.&lt;/p&gt;
&lt;p&gt;The issue is that Gatsby is excellent at creating static files on a 1-to-1 basis, but there is much less information out there on how to create virtual pages, especially when they need to be automated based on directory structure or folder content. This is hard to describe, so I&apos;m just going to show you my exact issue:&lt;/p&gt;
&lt;h2&gt;The Issue:&lt;/h2&gt;
&lt;p&gt;I&apos;m trying to use Gatsby to create a front-end for a Markdown-based cheatsheet repo I&apos;ve been working on. Let&apos;s say that the Gatsby build will be hosted at &quot;output.test&quot;. The repo is a work-in-progress, but currently the structure looks something like this:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Cheatsheets
&lt;ul&gt;
	&lt;li&gt;JavaScript
&lt;ul&gt;
	&lt;li&gt;js-modules.md&lt;/li&gt;
	&lt;li&gt;js-devops.md&lt;/li&gt;
	&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;bash-sh.md&lt;/li&gt;
	&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Random
&lt;ul&gt;
	&lt;li&gt;roku.md&lt;/li&gt;
	&lt;li&gt;index.md&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I follow &lt;a href=&quot;https://www.gatsbyjs.org/docs/adding-markdown-pages/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the Gatsby tutorial on generating a static site based on Markdown&lt;/a&gt; (also see &lt;a href=&quot;https://www.gatsbyjs.org/tutorial/part-seven/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;tutorial part-7&lt;/a&gt;), I&apos;ll end up with a very close-to-usable result, where I can navigate to &quot;output.test/cheatsheets/bash-sh/&quot; or &quot;output.test/random/roku/&quot;. However, what about &quot;output.test/cheatsheets/&quot;? Well, unlike &quot;random&quot;, the &quot;cheatsheets&quot; directory does &lt;strong&gt;not&lt;/strong&gt; have an index.md in it, so no corresponding static index.html file will be generated. On a server with disabled directory listings, this will result in a 404 error or directory listing denied error on trying to access it.&lt;/p&gt;
&lt;p&gt;If you have not disabled directory index listings on your server, one option is that you can just use the default directory listing service offered by your server. For example, this is what it looks like on Apache:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Apache-Default-Directory-Listing-with-Gatsby.png&quot;&gt;&lt;img class=&quot;size-full wp-image-708 aligncenter&quot; src=&quot;/media/Apache-Default-Directory-Listing-with-Gatsby.png&quot; alt=&quot;&quot; width=&quot;438&quot; height=&quot;327&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;However, there are many issue with this. The most important is that it breaks the whole PWA/SPA part of Gatsby - because this &quot;index of&quot; page is not handled by React/Gatsby, it can&apos;t be pre-fetched and rendered by the system, and links to it will be broken. The second issue is that these automatic server directory listing pages are ugly, and not customizable. Here is how it could look a lot nicer, within the Gatsby system, just using default styling:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Gatsby-Directory-Listing-Page.png&quot;&gt;&lt;img class=&quot;size-full wp-image-711 aligncenter&quot; src=&quot;/media/Gatsby-Directory-Listing-Page.png&quot; alt=&quot;Gatsby - Directory Listing Page&quot; width=&quot;560&quot; height=&quot;546&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So how do we create these types of directory listing pages within the Gatsby ecosystem?&lt;/p&gt;
&lt;h2&gt;Gatsby-Node and exports.createPages&lt;/h2&gt;
&lt;p&gt;A good starting point for figuring out how to automatically create &quot;index of&quot; React pages with Gatsby, is with the official tutorial! &lt;a href=&quot;https://www.gatsbyjs.org/tutorial/part-seven/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Step 7 is &quot;programmatically create pages from data&quot;&lt;/a&gt;, and points us towards using &quot;exports.onCreateNode&quot; to listen for updates in source input (such as the filesystem), and &quot;exports.createPages&quot; to tell Gatsby to create new pages programatically.&lt;/p&gt;
&lt;p&gt;That&apos;s all fine and dandy, but again, we aren&apos;t trying to create a 1-to-1 output of static files to input nodes. Here is how I approached this (pseudo code) in gatsby-node.js:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;use &lt;code&gt;exports.onCreateNode&lt;/code&gt; to listen for MarkdownRemark files / nodes
&lt;ul&gt;
	&lt;li&gt;As they come through, add a node field of &quot;slug&quot;&lt;/li&gt;
	&lt;li&gt;So far, this is boilerplate code that matches the tutorial step 7&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;CUSTOM: use &lt;code&gt;exports.createPages&lt;/code&gt; to create directory listing pages
&lt;ul&gt;
	&lt;li&gt;Use GraphQL to get allMarkdownRemark and allDirectory nodes&lt;/li&gt;
	&lt;li&gt;Iterate through all the markdown files:
&lt;ul&gt;
	&lt;li&gt;Create a page for the markdown file itself (same as tutorial)&lt;/li&gt;
	&lt;li&gt;Record what directory the page belongs to, and mark that this file is a child of it&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Iterate through all the directories that hold markdown files
&lt;ul&gt;
	&lt;li&gt;Record which directory the current one is a sub-dir of, and mark as child of it&lt;/li&gt;
	&lt;li&gt;Check if the current directory already has an index page, and if it does not, add it to a &quot;queue&quot; of index pages that need to be generated&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Iterate through the queue of index pages that need to be created
&lt;ul&gt;
	&lt;li&gt;use &lt;code&gt;actions.createPage&lt;/code&gt; to generate the page, and pass a template file, and tell Gatsby to also send along &quot;meta&quot; information about the children of the index page, through context (which gets passed as &lt;code&gt;props.pageContext&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Directory Listing Template - &lt;code&gt;directory-index.js&lt;/code&gt;:
&lt;ul&gt;
	&lt;li&gt;Combine the child directories and child file lists that were passed via &lt;code&gt;props.pageContext&lt;/code&gt; into a combined array.&lt;/li&gt;
	&lt;li&gt;Use the combined listing array to generate links and icons on directory vs file&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here it is in action:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Gatsby-Generated-Folder-Directory-Listing-Index-Pages.gif&quot;&gt;&lt;img class=&quot;size-full wp-image-710 aligncenter&quot; src=&quot;/media/Gatsby-Generated-Folder-Directory-Listing-Index-Pages.gif&quot; alt=&quot;Gatsby - Generated Folder Directory Listing Index Pages&quot; width=&quot;595&quot; height=&quot;582&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Not bad! And that is with very minimal styling or tweaking beyond the Gatsby defaults.&lt;/p&gt;
&lt;h3&gt;The Code:&lt;/h3&gt;
&lt;p&gt;This is not the prettiest or most efficient code, but it is a good jumping off point for some approaches on how to handle this. If you are going to try to replicate these result for your own needs, you will probably need to tweak both these files to get things how you want them.&lt;/p&gt;
&lt;p&gt;gatsby-node.js:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const path = require(&apos;path&apos;);
const { createFilePath } = require(`gatsby-source-filesystem`)
const AUTOBUILD_INDEXES = true;
&lt;p&gt;/**&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement Gatsby’s Node APIs in this file.&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;See: &lt;a href=&quot;https://www.gatsbyjs.org/docs/node-apis/&quot;&gt;https://www.gatsbyjs.org/docs/node-apis/&lt;/a&gt;
*/&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;exports.onCreateNode = ({node, getNode, actions}) =&gt; {
if (node.internal.type === ‘MarkdownRemark’){
const fileNode = getNode(node.parent);
// Programmatically create slug field and value, and append to node to be consumed by page creator
// path should mirror md directory, and should be extracted
let slug = createFilePath({node, getNode, basePath: ”});
if (node.frontmatter &amp;#x26;&amp;#x26; node.frontmatter.customPageSlug){
// replace parsedName with customPageSlug
slug = slug.replace((new RegExp(&lt;code&gt;\\/${fileNode.name}\\/$&lt;/code&gt;)),&lt;code&gt;/${node.frontmatter.customPageSlug}/&lt;/code&gt;);
}
actions.createNodeField({
node,
name: ‘slug’,
value: slug
});
}
}
exports.createPages = async ({graphql, actions}) =&gt; {
let subdirsWithIndexPages = [];
let subdirIndexesToCreate = [];
let subdirIndexPages = {};&lt;/p&gt;
&lt;p&gt;const result = await graphql(&lt;code&gt;    query {       allMarkdownRemark {         edges {           node {             fileAbsolutePath             fields {               slug             }             parent {               ... on File {                 relativePath                 base                 name               }             }           }         }       }       allDirectory(filter: {sourceInstanceName: {eq: &quot;cheatsheets-md&quot;}}, sort: {fields: name, order: ASC}) {         nodes {           absolutePath           base           relativeDirectory           relativePath           name         }       }     }  &lt;/code&gt;);&lt;/p&gt;
&lt;p&gt;function recordAsChild(subdir, child, isDir){
subdirIndexPages[subdir] = typeof(subdirIndexPages[subdir])===‘object’ ? subdirIndexPages[subdir] : {children:{
dirs: [],
md: []
}}
const target = isDir ? subdirIndexPages[subdir].children.dirs : subdirIndexPages[subdir].children.md;
target.push(child);
}&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;result.data.allMarkdownRemark.edges.forEach(async ({node}) =&gt; {
// Get dir of file
const subdirAbs = path.dirname(node.fileAbsolutePath);
// Note that this md file is child of dir
recordAsChild(subdirAbs,node,false);
// Create page for file itself
actions.createPage({
path: node.fields.slug,
component: path.resolve(&lt;code&gt;./src/templates/generic.js&lt;/code&gt;),
context: {
// Becomes available as GraphQL variables within page queries
slug: node.fields.slug
}
});
});
// Iterate over directories that contain MD, and check if they need an index page created
let directoryIteratorPromise = new Promise((resolve,reject) =&gt;{
result.data.allDirectory.nodes.forEach(async (node, index, arr)=&gt;{
const subdirAbs = node.absolutePath;
const subdirRel = node.relativePath;
const parentDir = path.posix.dirname(subdirAbs);
// Note child of dir
recordAsChild(parentDir,node,true);
// Create page for subdir that file is in, if it is missing an index page. Skip for homepage (’/’), or top level page (/test.md)
const alreadyHasIndexPage = (subdirsWithIndexPages.indexOf(subdirAbs)!==-1 || subdirRel === ”);
if (!alreadyHasIndexPage){
// Check for index.md
const indexPath = path.posix.join(subdirAbs,‘index.md’);
const existResult = await graphql(&lt;code&gt;         {           allMarkdownRemark(filter: {fileAbsolutePath: {eq: &quot;${indexPath}&quot;}}) {             totalCount           }         }&lt;/code&gt;);
if (existResult.data.allMarkdownRemark.totalCount &amp;#x3C; 1) {
console.log(&lt;code&gt;There is no index for ${indexPath}&lt;/code&gt;);
subdirIndexesToCreate.push({
subdirAbs: subdirAbs,
subdirRel: subdirRel,
parentDir: parentDir
});
}
subdirsWithIndexPages.push(subdirAbs);
}
if (index === arr.length-1) resolve();
});
});
if (AUTOBUILD_INDEXES){
directoryIteratorPromise.then(()=&gt;{
console.log(’=============== BUILDING SUBDIR INDEX PAGES! ======================’);
for (let x=0; x&amp;#x3C;subdirIndexesToCreate.length; x++){
const subdirPaths = subdirIndexesToCreate[x];
// Create index page!
actions.createPage({
path: subdirPaths.subdirRel,
component: path.resolve(&lt;code&gt;./src/templates/directory-index.js&lt;/code&gt;),
context: {
slug: subdirPaths.subdirRel,
hasIndex: false,
meta: subdirIndexPages[subdirPaths.subdirAbs]
}
});
}
});
}
}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;src/templates/directory-index.js:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import React from &quot;react&quot;
import { Link, graphql } from &quot;gatsby&quot;
&lt;p&gt;import Layout from ”../components/layout”
import Image from ”../components/image”
import SEO from ”../components/seo”
import {strMethods} from ”../helpers/helpers”&lt;/p&gt;
&lt;p&gt;function makeDisplayName(slug){
slug = slug.replace(/-/gim,’ ’);
slug = slug.split(///).map((e)=&gt;{return strMethods.toTitleCase(e)}).join(’/’);
return slug;
}&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;export default (props) =&gt; {
const displayName = makeDisplayName(props.pageContext.slug);
const children = props.pageContext.meta.children;
// Compile listings - subdirectories and files
let listings = [];
listings = listings.concat(children.dirs.map(function(d){return {
path: ’/’ + d.relativePath,
display: ’/’ + d.name,
isDir: true
}}));
listings = listings.concat(children.md.map(function(m){return {
path: m.fields.slug,
display: m.parent.name,
isDir: false
}}));
return (
&amp;#x3C;Layout&gt;
&amp;#x3C;SEO title={&lt;code&gt;${props.pageContext.slug}&lt;/code&gt;} /&gt;
&amp;#x3C;h1&gt;Generated Index Page - {&lt;code&gt;${displayName}&lt;/code&gt;}&amp;#x3C;/h1&gt;
&amp;#x3C;div className=“directoryListingWrapper”&gt;
&amp;#x3C;div className=“directoryListingRow”&gt;
&amp;#x3C;i className=“material-icons”&gt;folder&amp;#x3C;/i&gt;
&amp;#x3C;Link to={&lt;code&gt;${document.location.pathname + &apos;/../&apos;}&lt;/code&gt;}&gt;..&amp;#x3C;/Link&gt;
&amp;#x3C;/div&gt;
{listings.map((listing)=&gt;{
return (
&amp;#x3C;div className=“directoryListingRow” key={&lt;code&gt;${listing.path + ((new Date()).getTime())}&lt;/code&gt;}&gt;
&amp;#x3C;i className=“material-icons”&gt;{listing.isDir ? ‘folder’ : ‘description’}&amp;#x3C;/i&gt;
&amp;#x3C;Link to={&lt;code&gt;${listing.path}&lt;/code&gt;}&gt;
{listing.display}
&amp;#x3C;/Link&gt;
&amp;#x3C;/div&gt;
)
})}
&amp;#x3C;/div&gt;
&amp;#x3C;/Layout&gt;
)
}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>WordPress - Relative Stylesheets and Scripts, Plus wp-config.php Order</title><link>https://joshuatz.com/posts/2019/wordpress---relative-stylesheets-and-scripts-plus-wp-configphp-order/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/wordpress---relative-stylesheets-and-scripts-plus-wp-configphp-order/</guid><description>&lt;!-- WP HTML export, not converted to MD --&gt;

&lt;p&gt;I recently wanted to force WordPress to make all my &amp;lt;script&amp;gt; and stylesheet (&amp;lt;link&amp;gt;) tags load relative URLs instead of absolute URLs, and ended up learning a bit about the internals of WordPress (pr...</description><pubDate>Sun, 18 Aug 2019 15:00:38 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I recently wanted to force WordPress to make all my &amp;#x3C;script&gt; and stylesheet (&amp;#x3C;link&gt;) tags load relative URLs instead of absolute URLs, and ended up learning a bit about the internals of WordPress (probably more than I wanted to know). For reference, I wanted to turn this:&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&amp;#x3C;link rel=&apos;stylesheet&apos; id=&apos;joshuatzwp-style-css&apos; href=&apos;&lt;strong&gt;http://joshuatz-wp.test/wp-content/themes/joshuatzwp/style.css&lt;/strong&gt;&apos; type=&apos;text/css&apos; media=&apos;all&apos; /&gt;&lt;/p&gt;
&lt;p&gt;Into this:&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&amp;#x3C;link rel=&apos;stylesheet&apos; id=&apos;joshuatzwp-style-css&apos; href=&apos;&lt;strong&gt;/wp-content/themes/joshuatzwp/style.css&lt;/strong&gt;&apos; type=&apos;text/css&apos; media=&apos;all&apos; /&gt;&lt;/p&gt;
&lt;p&gt;I wanted to do this because I was working with a local development environment, using Laragon on Windows as an Apache host, and proxying through Ngrok or something similar was not working with absolute domain-tied links, for a number of reasons. Furthermore, relative paths that were being passed to &lt;code&gt;wp_enqueue_style&lt;/code&gt; were not being preserved.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#solution&quot;&gt;Click here&lt;/a&gt; to jump right to the solution, or read on if you are interested in the details of how WordPress forces absolute paths.&lt;/p&gt;
&lt;h2&gt;First wrinkle: WP forces absolute links&lt;/h2&gt;
&lt;p&gt;The first issue I ran into was WordPress does not respect the format you use to pass links into the enqueue system, even if you make 100% sure you pass in a relative link. For example, look at this code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;wp_enqueue_style(&apos;joshuatzwp-style&apos;,wp_make_link_relative(get_stylesheet_uri()),array(),$cacheBustStamp,&apos;all&apos;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You would expect that the use of &lt;code&gt;wp_make_link_relative&lt;/code&gt; would ensure a relative link, but the style tag that this generates actually is absolute with the domain:&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&amp;#x3C;link rel=&apos;stylesheet&apos; id=&apos;joshuatzwp-style-css&apos; href=&apos;http://joshuatz-wp.test/wp-content/themes/joshuatzwp/style.css&apos; type=&apos;text/css&apos; media=&apos;all&apos; /&gt;&lt;/p&gt;
&lt;p&gt;Why is this happening? Well, it is not a fun answer:&lt;/p&gt;
&lt;h2&gt;WordPress do_item complexity&lt;/h2&gt;
&lt;p&gt;To figure out what was going on, I ended up tracing backwards from where the &amp;#x3C;script&gt; and &amp;#x3C;link&gt; tags are actually echoed out into the page, to see how that HTML is generated. Here is a rough overview of what that looks like:&lt;/p&gt;
&lt;ul&gt;
    &lt;li&gt;Both the script class (wp-includes/class.wp-scripts.php) and the style class (wp-includes/class.wp-styles.php) have a method called &lt;code&gt;do_item()&lt;/code&gt; that is responsible for actually processing each enqueued &quot;dependency&quot; and echoing it out
&lt;ul&gt;
    &lt;li&gt;In class.wp-scripts.php, there is &lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4803fc405e3448e825bbc63b23cde2a6a86225a9/wp-includes/class.wp-scripts.php#L341&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;inline code that prefixes any relative URL&lt;/a&gt; with &lt;code&gt;$base_url&lt;/code&gt;&lt;/li&gt;
    &lt;li&gt;In class.wp-styles.php, &lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4803fc405e3448e825bbc63b23cde2a6a86225a9/wp-includes/class.wp-styles.php#L194&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;it uses a function called &lt;code&gt;_css_href()&lt;/code&gt;&lt;/a&gt;, which also &lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4803fc405e3448e825bbc63b23cde2a6a86225a9/wp-includes/class.wp-styles.php#L342&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;prefixes relative URLs&lt;/a&gt; with &lt;code&gt;$base_url&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
    &lt;li&gt;The &lt;code&gt;$base_url&lt;/code&gt;, which is a member of the shared extended class for both scripts and styles, and used to prefix URLs above, is actually &lt;a href=&quot;https://github.com/WordPress/WordPress/blob/6da93732e770f18493ca260cea7bbdd875b2ec55/wp-includes/script-loader.php#L854&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;filled with a value in wp-includes/script-loader.php&lt;/a&gt;
&lt;ul&gt;
    &lt;li&gt;This value ends up either being the result of &lt;code&gt;site_url()&lt;/code&gt;, or &lt;code&gt;wp_guess_url()&lt;/code&gt;
&lt;ul&gt;
    &lt;li&gt;&lt;code&gt;site_url()&lt;/code&gt; pulls it from &lt;a href=&quot;https://github.com/WordPress/WordPress/blob/e6a4b884f5e3be89c21bae512923e3e8466e4162/wp-includes/link-template.php#L3186&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;code&gt;get_site_url()&lt;/code&gt;&lt;/a&gt;, which pulls it from &lt;code&gt;get_option(&apos;siteurl&apos;)&lt;/code&gt;, which comes from your admin settings&lt;/li&gt;
    &lt;li&gt;&lt;code&gt;wp_guess_url()&lt;/code&gt; &lt;a href=&quot;https://github.com/WordPress/WordPress/blob/282dfee8e0f28b72a8507e5166c03e289f1ac563/wp-includes/functions.php#L4973&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;checks the global constant defined &apos;WP_SITEURL&apos;&lt;/a&gt;, and if that fails, tries to scrape it off the incoming request&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this knowledge was strictly necessary to force relative paths, but it gives some insight into how the whole process works. I wouldn&apos;t actually want to change any of these files, since they are part of the core of WP, and would be overwitten on a system update anyways.&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;solution&quot;&gt;&lt;/a&gt;The solution for relative paths:&lt;/h2&gt;
&lt;p&gt;The solution here is to hook into &lt;a href=&quot;https://developer.wordpress.org/plugins/hooks/filters/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a WordPress filter&lt;/a&gt;. I&apos;ve written about this before, when trying to add async/defer attributes to scripts and style tags, in &lt;a href=&quot;https://joshuatz.com/posts/2019/wordpress-script-and-style-tags-adding-defer-async-and-lazy-load/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this post&lt;/a&gt;. In fact, our solution is going to look pretty similar.&lt;/p&gt;
&lt;p&gt;Basically, we will hook into the filter for &apos;script_loader_tag&apos;, which is the filter for &amp;#x3C;script&gt; tags, and &apos;style_loader_tag&apos;, which is the filter for stylesheet &amp;#x3C;link&gt; tags. In our hook, we will take the source that WordPress has prefixed and forced into an absolute URL, and remove the domain part that they added.&lt;/p&gt;
&lt;p&gt;Here is the code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;function makeInternalLinkRelative($src){
    return str_replace(get_option(&apos;siteurl&apos;),&apos;&apos;,$src);
}
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-php&quot;&gt;add_filter(‘style_loader_src’,function($src, $handle){
return makeInternalLinkRelative($src);
},10,4);
add_filter(‘script_loader_src’,function($src, $handle){
return makeInternalLinkRelative($src);
},10,4);
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Pretty simple, huh! Just add this code to your theme&apos;s (or child theme&apos;s) function.php file, and you should be good to go!&lt;/p&gt;
&lt;h2&gt;The Solution for Dynamic Domains&lt;/h2&gt;
&lt;p&gt;If you don&apos;t want to use relative paths, but just want WordPress to know when you are serving it through a different domain (such as when using Ngrok or swapping domains) and automatically adjust all the links, there is another solution we can employ. We can modify the &lt;code&gt;wp-config.php&lt;/code&gt; file in the root of our WordPress install to dynamically set the &quot;siteurl&quot;, which as my earlier research pointed out, is used internally by WP to auto-prefix relative links.&lt;/p&gt;
&lt;p&gt;Here is what I have added to my wp-config.php file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$host = $_SERVER[&apos;HTTP_HOST&apos;];
$protocol = ((!empty($_SERVER[&apos;HTTPS&apos;]) &amp;#x26;&amp;#x26; $_SERVER[&apos;HTTPS&apos;] !== &apos;off&apos;) || (isset($_SERVER[&apos;SERVER_PORT&apos;]) &amp;#x26;&amp;#x26; $_SERVER[&apos;SERVER_PORT&apos;]===443)) ? &apos;https://&apos; : &apos;http://&apos;;
define(&apos;WP_SITEURL&apos;, $protocol . $host);
define(&apos;WP_HOME&apos;, $protocol . $host);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, here is the &lt;em&gt;really&lt;/em&gt; important thing. &lt;strong&gt;Make sure this code comes &lt;span style=&quot;text-decoration: underline;&quot;&gt;before&lt;/span&gt; this line of code that should already be in the file:&lt;/strong&gt;&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&lt;code&gt;require_once(ABSPATH . &apos;wp-settings.php&apos;);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This matters because wp-settings.php is responsible for the chain of events that fills in the $base_url variable we were looking at earlier, and is ultimately responsible for prefixing URLs. If that runs before you set the &quot;siteurl&quot; global, it will have the domain from your admin settings, not from our dynamic override. I found this out the hard way!&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Coding a CSS Theme Switcher - a Multitude of Web Dev Options</title><link>https://joshuatz.com/posts/2019/coding-a-css-theme-switcher---a-multitude-of-web-dev-options/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/coding-a-css-theme-switcher---a-multitude-of-web-dev-options/</guid><description>So many options for building a theme switcher or customization - CSS, React, Vue, Sass, etc. Let&apos;s break these down into examples and simple descriptions!</description><pubDate>Fri, 16 Aug 2019 15:00:36 GMT</pubDate><content:encoded>&lt;!-- Dev URL: https://dev.to/joshuatz/coding-a-css-theme-switcher-so-many-options-1cmn --&gt;
&lt;h1 id=&quot;table-of-contents&quot;&gt;Table of Contents:&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Intro and “What is theming?”&lt;/li&gt;
&lt;li&gt;Options:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#method-a-using-css-custom-properties--variables&quot;&gt;Method A: CSS Custom Properties&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#method-a-1-using-specificity-to-change-variable-inheritance&quot;&gt;Method A-1: Using specificity to modify variable inheritance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-a-2-re-assign-variable-values-with-js&quot;&gt;Method A-2: Reassigning variable values with Javascript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-b-using-sass-sassscss&quot;&gt;Method B: Using SASS/SCSS&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#method-b-1-themes-as-nested-maps&quot;&gt;Method B-1: Themes as nested maps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-b-2-simple-mixin-dump&quot;&gt;Method B-2: Mixing dumping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-b-3-css-custom-properties-to-sass&quot;&gt;Method B-3: Pulling CSS variables into SASS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-c-mutating-state&quot;&gt;Method C: Mutating State (React, Vue, etc.)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-d-crazy-old-school&quot;&gt;Method D: Crazy Old-School&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#wrap-up--final-thoughts&quot;&gt;Wrap up / Final Notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Dev.to table of contents:&lt;/p&gt;
&lt;h1 id=&quot;table-of-contents-1&quot;&gt;Table of Contents:&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Intro and “What is theming?”&lt;/li&gt;
&lt;li&gt;Options:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#method-a-using-css-custom-properties-variables&quot;&gt;Method A: CSS Custom Properties&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#method-a1-using-specificity-to-change-variable-inheritance&quot;&gt;Method A-1: Using specificity to modify variable inheritance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-a2-reassign-variable-values-with-js&quot;&gt;Method A-2: Reassigning variable values with Javascript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-b-using-sass-sassscss&quot;&gt;Method B: Using SASS/SCSS&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#method-b1-themes-as-nested-maps&quot;&gt;Method B-1: Themes as nested maps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-b2-simple-mixin-dump&quot;&gt;Method B-2: Mixing dumping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-b3-css-custom-properties-to-sass&quot;&gt;Method B-3: Pulling CSS variables into SASS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-c-mutating-state&quot;&gt;Method C: Mutating State (React, Vue, etc.)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#method-d-crazy-oldschool&quot;&gt;Method D: Crazy Old-School&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#wrapup-final-thoughts&quot;&gt;Wrap up / Final Notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id=&quot;introduction&quot;&gt;Introduction:&lt;/h1&gt;
&lt;p&gt;Front-end web theming seems to be a current hot topic, and I have to admit that I felt the itch to jump on the bandwagon and add a dark theme to a Single-Page Application I’m working on. But where to get started? There are so many guides out there, and yet I couldn’t find many that I felt adequately talked about CSS theming in a generic way that covers many different approaches, rather than a tutorial that is essentially “copy and paste this into your code”.&lt;/p&gt;
&lt;p&gt;So I set out to make one (this post), a guide that briefly discusses each of the many available options in a broad manner, with some short snippets of example code.&lt;/p&gt;
&lt;p&gt;First though…&lt;/p&gt;
&lt;h1 id=&quot;what-is-theming&quot;&gt;What is theming?&lt;/h1&gt;
&lt;p&gt;In this context, CSS theming or CSS theme switching refers to a set of shared styles (colors, etc) that are grouped as a theme, and being able to switch between themes instantly on a webpage or within a SPA, without refreshing the page.&lt;/p&gt;
&lt;p&gt;A very simple example of a theme, that I use throughout this post, is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dark theme:
&lt;ul&gt;
&lt;li&gt;Primary Color: Black&lt;/li&gt;
&lt;li&gt;Secondary Color: White&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Light Theme:
&lt;ul&gt;
&lt;li&gt;Primary Color White&lt;/li&gt;
&lt;li&gt;Secondary Color: Black&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ok, now lets look at different options for implementation:&lt;/p&gt;
&lt;h1 id=&quot;method-a-using-css-custom-properties--variables&quot;&gt;Method A: Using CSS custom properties / variables&lt;/h1&gt;
&lt;p&gt;This is fast becoming one of the most common current approach to implementing switchable themes, and uses a newer CSS feature called “&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties&quot;&gt;Custom Properties&lt;/a&gt;”, which are often referred to as “CSS variables”. In short, CSS custom properties allow you to define a variable, such as “primaryColor”, which can then be referenced in hundreds of other spots throughout your CSS.&lt;/p&gt;
&lt;p&gt;This is crucial for theming, because it means that you can share a property among thousands of elements, and to change it, you just need to update that one variable value, rather than needing to individually update each element’s CSS.&lt;/p&gt;
&lt;p&gt;To get into the details of how to use CSS variables to deliver switchable CSS themes, let’s break this method into a few different approaches:&lt;/p&gt;
&lt;h2 id=&quot;method-a-1-using-specificity-to-change-variable-inheritance&quot;&gt;Method A-1: Using specificity to change variable inheritance&lt;/h2&gt;
&lt;p&gt;Ok, long title, but I promise you that this method is actually pretty simple. In fact, this is one of the most common methods, and can be seen in current use across several &lt;em&gt;major&lt;/em&gt; websites. Basically, you define the default theme as a set of variables, and then override the exact same variables through another block of CSS. The override will work because we will add something specific (class or attribute) to a top level node (like &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt;) to trigger the more specific rule.&lt;/p&gt;
&lt;p&gt;Here is some example CSS, without using custom properties:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; .myElement&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    background-color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;white&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;.dark&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; .myElement&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;data-theme&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;dark&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;.myElement&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    background-color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;black&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is the same output, but using CSS variables:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    --primaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;white&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    --secondaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;black&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; .myElement&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    background-color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;--primaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;.dark&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;body&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;data-theme&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;dark&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    --primaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;black&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    --secondaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;white&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you can simply toggle the class or attribute with JS, either directly with something like &lt;code&gt;.setAttribute(‘data-theme’,’dark’)&lt;/code&gt;, or with a reactive binding within something like React or Vue.&lt;/p&gt;
&lt;p&gt;Here is a quick demo using setAttribute on &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt; to switch themes:&lt;/p&gt;
&lt;iframe style=&quot;width: 100%;&quot; title=&quot;Method A-1&quot; src=&quot;//codepen.io/joshuatz/embed/xxKOZVr/?height=320&amp;#x26;theme-id=0&amp;#x26;default-tab=css,result&quot; height=&quot;320&quot; frameborder=&quot;no&quot; scrolling=&quot;no&quot; allowfullscreen&gt;See the Pen &amp;#x3C;a href=&quot;https://codepen.io/joshuatz/pen/xxKOZVr/&quot;&gt;Method A-1&amp;#x3C;/a&gt; by Joshua T(&amp;#x3C;a href=&quot;https://codepen.io/joshuatz&quot;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&quot;https://codepen.io&quot;&gt;CodePen&amp;#x3C;/a&gt;.&lt;/iframe&gt;
&lt;p&gt;You could technically do this without using CSS custom variables, but you end up with a lot of handcoding and repeating rules. For every rule/element that has different styling for a specific theme, you only have to write that block once if you are using variables, but if not, you have to write it 1 x as many themes as you have. Example below, where you can see that with three elements and three themes, the minimum number of CSS blocks without custom properties is 9 (3×3), whereas with variables, we only need 3 (one for each element):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://thepracticaldev.s3.amazonaws.com/i/8mh80wjt5kyejy095n0d.png&quot; alt=&quot;Showing handcoding custom themed elements vs using variables&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;method-a-2-re-assign-variable-values-with-js&quot;&gt;Method A-2 Re-Assign variable values with JS&lt;/h2&gt;
&lt;p&gt;This method usually is used when you aren’t just theme switching, but also allowing for live theme property changing by the user, through something like a color picker. Let’s pretend that the user has just picked a new custom primary color for their theme, and they want to preview how it will look. Here is the initial CSS:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;:root&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    --primaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;red&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    --secondaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;blue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With just a little bit of JavaScript, we can easy modify any of these values. Here we go changing the primary color:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// This will target :root variables&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;document.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;:root&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).style.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setProperty&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;--primaryColor&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,newColor);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Or, same thing...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;document.documentElement.style.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setProperty&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;--primaryColor&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,newColor);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can take this a step further and easily implement theming logic to swap out entire themes and switch between multiple:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; themes&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    light: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;--primaryColor&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;white&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;--secondaryColor&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;black&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    dark: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;--primaryColor&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;black&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;--secondaryColor&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;white&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	red: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;		&apos;--primaryColor&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;red&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;		&apos;--secondaryColor&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;white&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; activateTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;theme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; prop &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; theme){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        document.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;querySelector&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;:root&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).style.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setProperty&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(prop, theme[prop]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Switch to the dark theme:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;activateTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(themes.dark);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Demo:&lt;/p&gt;
&lt;iframe style=&quot;width: 100%;&quot; title=&quot;Method A-2&quot; src=&quot;//codepen.io/joshuatz/embed/yLBJeJY/?height=265&amp;#x26;theme-id=0&amp;#x26;default-tab=js,result&quot; height=&quot;265&quot; frameborder=&quot;no&quot; scrolling=&quot;no&quot; allowfullscreen&gt;See the Pen &amp;#x3C;a href=&quot;https://codepen.io/joshuatz/pen/yLBJeJY/&quot;&gt;Method A-2&amp;#x3C;/a&gt; by Joshua T(&amp;#x3C;a href=&quot;https://codepen.io/joshuatz&quot;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&quot;https://codepen.io&quot;&gt;CodePen&amp;#x3C;/a&gt;.&lt;/iframe&gt;
&lt;p&gt;By default, when you change a CSS property via Javascript, the change is not persisted when the user reloads the page. To save their changes to a theme, you have a bunch of different options, which is beyond the scope of this post. However, here are some generic options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On value change, also AJAX to server and save with User data
&lt;ul&gt;
&lt;li&gt;Then, when user logs in, send back theme data either as on-the-fly generated CSS, or JSON which gets parsed and mapped back to CSS custom vars&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Persist theme to localStorage and map back on page load
&lt;ul&gt;
&lt;li&gt;If you are using a state management system and storing the user’s theme config there, this should be very easy to do with something like &lt;a href=&quot;https://github.com/rt2zz/redux-persist&quot;&gt;“redux-persist”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;You can also just inject a &lt;code&gt;&amp;#x3C;style&gt;&lt;/code&gt; tag into the page with overriding variable values; this is how dev.to is currently handling theming — on page load, it injects a different set of variable values through a &lt;code&gt;&amp;#x3C;style&gt;&lt;/code&gt; tag, depending on the value of a localStorage value&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;method-b-using-sass-sassscss&quot;&gt;Method B: Using SASS (Sass/Scss)&lt;/h1&gt;
&lt;p&gt;Technically, you could use Sass to accomplish both methods A &amp;#x26; B, since Sass transpiles to CSS, and you could also just stick both methods, as CSS, into Sass, and it should be valid.&lt;/p&gt;
&lt;p&gt;However, there are some Sass specific approaches for theme switching, which I’ve outlined below.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sidenote: Throughout this post, my demos all use the SCSS style of Sass, but all of this should be possible with indented SASS; you would just need to reformat.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;method-b-1-themes-as-nested-maps&quot;&gt;Method B-1: Themes as Nested Maps&lt;/h2&gt;
&lt;p&gt;One way to approach this in Sass is with nested maps. The inspiration for this code, and one of the best guides on this method is &lt;a href=&quot;https://medium.com/@dmitriy.borodiy/easy-color-theming-with-scss-bc38fd5734d1&quot;&gt;this post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Nested maps in Sass resemble JSON, and allow nesting of key-pair maps that can hold any valid Sass value. This makes them a great way to hold multiple named themes, and sub-properties for each theme. Here are my two example themes as a nested map saved to $themes:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;scss&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$themes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;dark&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;primary&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;black&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;secondary&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;white&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;light&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;primary&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;white&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;secondary&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;black&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This by itself does not generate any CSS, so you have to write your own function/code to loop through your themes and generate the CSS you want. It is best to define re-usable helper functions to handle this, that use mixins:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;scss&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;* Mixin to use to generate blocks for each theme&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;* Automatically takes @content&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;*/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$scopedTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: null;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@mixin&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; themeGen&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$allThemesMap&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$themes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    @each&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt; $themeName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$themeMap&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt; $allThemesMap&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        .theme-&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;#{$themeName}&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            // Creating a map that contains values specific to theme.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            // Global is necessary since in mixin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            $scopedTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: () !global;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            @each&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt; $variableName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$variableValue&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt; $themeMap&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;                // Merge each key-value pair into the theme specific map&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                $scopedTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;map-merge&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$scopedTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$variableName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$variableValue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)) !global;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            // The original content passed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            @content&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            // Unset&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            $scopedTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: null !global;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;* Function to call within themeGen mixin, to get value from the current theme in the iterator&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;*/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@function&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; getThemeVal&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$themeVar&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    @return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; map-get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$scopedTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$themeVar&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, using this with an element:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;scss&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;* Actually using theme values to generate CSS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;*/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;.myComponent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    @include&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; themeGen&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        background-color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;getThemeVal&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;primary&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;getThemeVal&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;secondary&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is a demo showing the final CSS this can generate:&lt;/p&gt;
&lt;iframe style=&quot;width: 100%;&quot; title=&quot;Method B-1&quot; src=&quot;//codepen.io/joshuatz/embed/aboNPLV/?height=500&amp;#x26;theme-id=0&amp;#x26;default-tab=css,result&quot; height=&quot;500&quot; frameborder=&quot;no&quot; scrolling=&quot;no&quot; allowfullscreen&gt;See the Pen &amp;#x3C;a href=&quot;https://codepen.io/joshuatz/pen/aboNPLV/&quot;&gt;Method B-1&amp;#x3C;/a&gt; by Joshua T(&amp;#x3C;a href=&quot;https://codepen.io/joshuatz&quot;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&quot;https://codepen.io&quot;&gt;CodePen&amp;#x3C;/a&gt;.&lt;/iframe&gt;
&lt;p&gt;Technically, you could hand code the CSS that this generates (and some old-school devs do), but there is not a good reason to do so, and if anything, you are more likely to write error-ridden CSS.&lt;/p&gt;
&lt;p&gt;A related, but slightly different approach (avoids global vars) can be found &lt;a href=&quot;https://www.sitepoint.com/sass-theming-neverending-story/&quot;&gt;here&lt;/a&gt;. And &lt;a href=&quot;https://stackoverflow.com/a/47873911/11447682&quot;&gt;here’s&lt;/a&gt; another variation.&lt;/p&gt;
&lt;h2 id=&quot;method-b-2-simple-mixin-dump&quot;&gt;Method B-2: Simple Mixin “Dump”&lt;/h2&gt;
&lt;p&gt;This method is not as elegant as C-1, but less complicated. Essentially you dump a huge block of CSS into a mixin, which then repeats it, but with a named parent theme class.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;scss&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;@mixin&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; scopeToTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$themeName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$prop&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$val&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;#{$themeName}&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        #{$prop}&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;#{$val}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  background-color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;red&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  padding&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;20&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  margin-bottom&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  border-radius&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  @include&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; scopeToTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;dark-theme&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;background-color&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;black&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  @include&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; scopeToTheme&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;light-theme&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;background-color&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;white&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is the CSS that this generated:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;button&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  background-color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;red&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  padding&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;20&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  margin-bottom&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  border-radius&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;px&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;.dark-theme&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt; button&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  background-color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;black&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;.light-theme&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt; button&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  background-color&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;white&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;method-b-3-css-custom-properties-to-sass&quot;&gt;Method B-3: CSS Custom Properties to Sass&lt;/h2&gt;
&lt;p&gt;I’m not sure I’ve seen this done anywhere, but an interesting approach could be to combine Method B with Sass by &lt;em&gt;pulling&lt;/em&gt; the CSS variables into Sass. Like so – CSS:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;:root&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    --primaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;red&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    --secondaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;blue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sass:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;scss&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$primaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;--primaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$secondaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;--secondaryColor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;method-c-mutating-state&quot;&gt;Method C: Mutating State&lt;/h1&gt;
&lt;p&gt;I won’t get into the specific of how to code this out, but this is pretty easy to think about conceptually. In a reactive framework, you can hold CSS properties, such as colors, as part of &lt;code&gt;state&lt;/code&gt;. Then, using a CSS-in-JS framework, or even just sticking state in inline css, you can plug the value from state into your scoped CSS.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React Demos:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://codepen.io/elainehuang/pen/yPWxRX&quot;&gt;Simple inline style binding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://codesandbox.io/s/x26q7l9vyq&quot;&gt;Styled components + ThemeProvider&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;React Native Web
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/necolas/react-native-web/blob/master/docs/guides/style.md&quot;&gt;Inline style binding with “atomic” CSS classes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Vue Demos:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://codesandbox.io/s/3no663nv6&quot;&gt;Simple inline style binding&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It looks like Twitter uses this method, with React Native Web.&lt;/p&gt;
&lt;h1 id=&quot;method-d-crazy-old-school&quot;&gt;Method D: Crazy Old-School&lt;/h1&gt;
&lt;p&gt;There are so many reasons not to do it this way, but I feel obligated to share this hackish idea anyways. If you really wanted to insist on using CSS, but not using any variables or touching of the &lt;code&gt;&amp;#x3C;body&gt;&lt;/code&gt; class or attributes, you could hand code different themes as completely separate CSS files. Like &lt;code&gt;dark-theme.css&lt;/code&gt; and &lt;code&gt;light-theme.css&lt;/code&gt;. Then, to switch the theme, you could use AJAX to pull in the new theme file. Or, if you wanted to support browsers without Javascript, you could even use server side code to change which theme gets included and force a page refresh.&lt;/p&gt;
&lt;p&gt;As a side note, an effect of this approach is actually better utilization of bandwidth, since you are only sending what theme is needed.&lt;/p&gt;
&lt;h1 id=&quot;wrap-up--final-thoughts&quot;&gt;Wrap-Up / Final Thoughts:&lt;/h1&gt;
&lt;p&gt;First, although this was an extensive look at theming options, it should not be considered fully comprehensive; there is an endless number of ways you can implement custom theming.&lt;/p&gt;
&lt;p&gt;Second, if you have made it all the way through the post, you might still be wondering “well, which option is the best one?”. The truth is that &lt;strong&gt;it really matters what your objective is&lt;/strong&gt;. If your goal is to support as many browsers as possible and/or have functions within styling (like converting hex to RGBA and then darkening), then SASS is probably the way forward (Method B). Or, if you want to have the smallest CSS filesize and/or binding of CSS values to JS values, then CSS custom properties are likely the best fit (Method A and Method C, or Method B with special setup).&lt;/p&gt;
&lt;p&gt;The short answer is use what fits best with the framework or existing structure of your site/SPA/App. And have fun theming!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This writeup was first published at: &lt;a href=&quot;https://joshuatz.com/posts/2019/coding-a-css-theme-switcher-a-multitude-of-web-dev-options/&quot;&gt;https://joshuatz.com/posts/2019/coding-a-css-theme-switcher-a-multitude-of-web-dev-options/&lt;/a&gt;&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Sass-Embed: Dynamic embedded SASS/SCSS-to-CSS Playground</title><link>https://joshuatz.com/projects/web-stuff/sass-embed-dynamic-embedded-sassscss-to-css-playground/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/sass-embed-dynamic-embedded-sassscss-to-css-playground/</guid><description>An embeddable and interactive SASS-to-CSS playground, which can be dynamically generated and modified with custom settings and preloaded SASS/SCSS input.</description><pubDate>Thu, 15 Aug 2019 09:11:38 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;Github repo: &lt;a href=&quot;https://github.com/joshuatz/sass-embed&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/joshuatz/sass-embed&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Purpose:&lt;/h2&gt;
&lt;p&gt;I needed to embed SASS/SCSS side-by-side into a blog post as a demonstration of some code, and realized that it was extremely difficult to find any &quot;live&quot; SASS-to-CSS playgrounds that supported embedding into another website. There are code playgrounds that support SASS conversion and embeds, but the conversion is usually done server side when the playground is saved, which is incompatible with an anonymous iframe embed. For examples of those types of solutions, see &lt;a href=&quot;#alternatives&quot;&gt;the section on alternative (easier) solutions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This project is the end result of roughly a day and half of work, to see how much functionality I could shoehorn into a single-page that could be embedded as an iframe.&lt;/p&gt;
&lt;h2&gt;What it is:&lt;/h2&gt;
&lt;p&gt;The whole point of this project is being able to embed it into posts like this, so why don&apos;t I just show you! Here is a live embedded demo:&lt;/p&gt;
&lt;p&gt;&lt;iframe id=&quot;mainEmbedDemo&quot; src=&quot;https://sass-embed.netlify.app/?sassString=%24myColor%3A%20red%3B%0A.myComponent%20%7B%0A%20%20button%20%7B%0A%20%20%20%20background-color%3A%20%24myColor%3B%0A%20%20%7D%0A%7D&amp;#x26;autorun=true&quot; width=&quot;100%&quot; height=&quot;350&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;As you can see, above is a two-pane embed, showing SCSS input side-by-side with CSS output. What is unique about this though, is a few things:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;It is using in-browser SASS-to-CSS conversion&lt;/li&gt;
	&lt;li&gt;It is a live playground
&lt;ul&gt;
	&lt;li&gt;With &quot;autorun&quot; on, if you type in the left panel and then pause for a second or two, it will automatically be converted&lt;/li&gt;
	&lt;li&gt;You can manually trigger conversion by clicking the &quot;convert&quot; button&lt;/li&gt;
	&lt;li&gt;You can toggle indented syntax (SCSS vs indented SASS)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;You can adjust the width of each panel by sliding the divider in the middle&lt;/li&gt;
	&lt;li&gt;Even though the playground is embedded within an iframe, you can modify the contents from the parent window by sending messages. For example, &lt;a id=&quot;postMessageLink&quot; href=&quot;#&quot;&gt;try clicking here&lt;/a&gt; and looking at the above embed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Embed-Gen&lt;/h2&gt;
&lt;p&gt;Since the embed is a simple single-page application without any sort of database, in order to preload SASS and various options, you have to either use special querystring parameters (part of the URL of the embed), inline tags placed into the &lt;code&gt;index.html&lt;/code&gt; file, and/or postMessage events.&lt;/p&gt;
&lt;p&gt;Embed-Gen is a simple tool that I built to simplify crafting the necessary embed codes to do all this. You find it hosted up &lt;a href=&quot;https://joshuatz.com/static-for-wp-iframes/sass-embed/embed-gen.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;How it works:&lt;/h2&gt;
&lt;p&gt;For how it works, and more details on how to use it, please refer to &lt;a href=&quot;https://github.com/joshuatz/sass-embed/blob/master/README.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the readme file in the project repo&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;alternatives&quot;&gt;&lt;/a&gt;Alternative (easier) Solutions:&lt;/h2&gt;
&lt;p&gt;If you just want to embed a side-by-side display of SASS input and CSS output, there are some easier solutions. One of the easiest is to use an embeddable code playground tool, like JSFiddle, use the SASS input panel, and then force the editor to show the generated CSS. My first attempt at this used Javascript, which you check out &lt;a href=&quot;https://jsfiddle.net/joshuatz/epu0n7vg/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is already pretty simple, but I was able to simplify it even further thanks to this tweet from Matt Stow:&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;
&lt;p dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;There&apos;s an even simpler, faster way of making a Sassmeister clone in &lt;a href=&quot;https://twitter.com/CodePen?ref_src=twsrc%5Etfw&quot;&gt;@CodePen&lt;/a&gt;: CSS! &lt;a href=&quot;https://t.co/lg6DpAvIFR&quot;&gt;pic.twitter.com/lg6DpAvIFR&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;— Matt Stow ✨🦄 (@stowball) &lt;a href=&quot;https://twitter.com/stowball/status/1119484043685285888?ref_src=twsrc%5Etfw&quot;&gt;April 20, 2019&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;script async src=&quot;https://platform.twitter.com/widgets.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt;&lt;/p&gt;
&lt;p&gt;That snippet of CSS is not only a more simple solution than the JS solution, but it also easily works across different code playgrounds (with a slight tweak), since most tend to inject the generated CSS through an inline &amp;#x3C;style&gt;&amp;#x3C;/style&gt; tag, which is what this snippet relies on to work.&lt;/p&gt;
&lt;p&gt;Here are examples using this method, which you can easily clone and paste your SASS into before generating an embed code:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;JSFiddle - &lt;a href=&quot;https://jsfiddle.net/joshuatz/xe2pnoat/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;CodePen - &lt;a href=&quot;https://codepen.io/joshuatz/pen/rNBeyaW&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And here is a nice demo:&lt;/p&gt;
&lt;p&gt;&lt;iframe style=&quot;width: 100%;&quot; title=&quot;SASS Embed - Show CSS - With CSS&quot; src=&quot;//codepen.io/joshuatz/embed/rNBeyaW/?height=265&amp;#x26;theme-id=0&amp;#x26;default-tab=css,result&quot; height=&quot;265&quot; frameborder=&quot;no&quot; scrolling=&quot;no&quot; allowfullscreen&gt;&amp;#x3C;span data-mce-type=&quot;bookmark&quot; style=&quot;display: inline-block; width: 0px; overflow: hidden; line-height: 0;&quot; class=&quot;mce_SELRES_start&quot;&gt;﻿&amp;#x3C;/span&gt;
  See the Pen &amp;#x3C;a href=&quot;https://codepen.io/joshuatz/pen/rNBeyaW/&quot;&gt;SASS Embed - Show CSS - With CSS&amp;#x3C;/a&gt; by Joshua T
  (&amp;#x3C;a href=&quot;https://codepen.io/joshuatz&quot;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&quot;https://codepen.io&quot;&gt;CodePen&amp;#x3C;/a&gt;.&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;Or, if you don&apos;t need the SASS/SCSS input side-by-side and are OK simply toggling between, create a CodePen with nothing but SASS input and un-select the result pane when generating the embed code. Then the embed will look like the below and you can simply toggle between the input SASS and the generated CSS by clicking the &quot;View Compiled&quot; button in the bottom right:&lt;/p&gt;
&lt;p&gt;&lt;iframe style=&quot;width: 100%;&quot; title=&quot;SASS Embed - CodePen Toggle Between&quot; src=&quot;//codepen.io/joshuatz/embed/qBWZrdp/?height=265&amp;#x26;theme-id=0&amp;#x26;default-tab=css&quot; height=&quot;265&quot; frameborder=&quot;no&quot; scrolling=&quot;no&quot; allowfullscreen&gt;
  See the Pen &amp;#x3C;a href=&quot;https://codepen.io/joshuatz/pen/qBWZrdp/&quot;&gt;SASS Embed - CodePen Toggle Between&amp;#x3C;/a&gt; by Joshua T
  (&amp;#x3C;a href=&quot;https://codepen.io/joshuatz&quot;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&quot;https://codepen.io&quot;&gt;CodePen&amp;#x3C;/a&gt;.&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;If you don&apos;t need to embed at all, you can use one of these live Sass-to-CSS playground:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://www.sassmeister.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Sassmeister&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;http://sass.js.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Sass.js Playground&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Wordpress - Script and Style Tags - Adding Defer, Async, and Lazy Load</title><link>https://joshuatz.com/posts/2019/wordpress---script-and-style-tags---adding-defer-async-and-lazy-load/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/wordpress---script-and-style-tags---adding-defer-async-and-lazy-load/</guid><description>An exercise in modifying the script and style tag HTML output by WordPress from enqueue, in order to add async, defer, and lazy loading attributes to resources.</description><pubDate>Sun, 11 Aug 2019 09:29:24 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;One of the downsides to WordPress is that they don&apos;t always have the best exposed methods / APIs for modifying how HTML generated by their own system (rather than your content like posts or pages) gets echoed out. For example, WP devs are familiar with &apos;&lt;a href=&quot;https://developer.wordpress.org/reference/functions/wp_enqueue_script/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;wp_enqueue_script()&lt;/a&gt;&apos; in PHP, which will generate a &lt;code&gt;&amp;#x3C;script&gt;&amp;#x3C;/script&gt;&lt;/code&gt; tag output in HTML, but there are no arguments on that method to add attributes or even to simply grab the raw text output of it. To do anything like this requires using &lt;a href=&quot;https://www.wpbeginner.com/glossary/filter/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;WordPress Filter&lt;/a&gt;, which is not always straightforward.&lt;/p&gt;
&lt;p&gt;I started looking into this since I wanted to be able to add defer or async loading to scripts and styles, which requires modifying attributes of the outputted tags.&lt;/p&gt;
&lt;h2&gt;Where are the filter calls actually generated&lt;/h2&gt;
&lt;p&gt;If you start to search for how to modify the HTML that is generated by &quot;wp_enqueue_style&quot; and &quot;wp_enqueue_script&quot;, you will quickly come across posts recommending that you hook into filters, &quot;style_loader_tag&quot; and &quot;script_loader_tag&quot; respectively. But why does this work? How?&lt;/p&gt;
&lt;p&gt;Since WordPress is open source, we can find out!&lt;/p&gt;
&lt;p&gt;After a little digging, we can see that the function that actually turns queued scripts and styles into echoed out &amp;#x3C;link&gt; and &amp;#x3C;script&gt; tags is do_item($handle), which is a public method on the WP_Scripts and WP_Styles classes. Both methods also call a named filter. Here is a summary:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 63px;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 9.6516%; height: 21px;&quot;&gt;Type&lt;/td&gt;
&lt;td style=&quot;width: 26.9774%; height: 21px;&quot;&gt;HTML Generated by&lt;/td&gt;
&lt;td style=&quot;width: 23.7994%; height: 21px;&quot;&gt;Uses filter:&lt;/td&gt;
&lt;td style=&quot;width: 14.5716%; height: 21px;&quot;&gt;Actual filter call:&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;Arguments passed to filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 9.6516%; height: 21px;&quot;&gt;Script&lt;/td&gt;
&lt;td style=&quot;width: 26.9774%; height: 21px;&quot;&gt;&lt;a href=&quot;https://developer.wordpress.org/reference/classes/wp_scripts/do_item/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;WP_Scripts::do_item($handle, $group)&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.7994%; height: 21px;&quot;&gt;&lt;a href=&quot;https://developer.wordpress.org/reference/hooks/script_loader_tag/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;script_loader_tag&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.5716%; height: 21px;&quot;&gt;&lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4803fc405e3448e825bbc63b23cde2a6a86225a9/wp-includes/class.wp-scripts.php#L366&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;$tagHtml, $handle, $src&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 9.6516%; height: 21px;&quot;&gt;Style&lt;/td&gt;
&lt;td style=&quot;width: 26.9774%; height: 21px;&quot;&gt;&lt;a href=&quot;https://developer.wordpress.org/reference/classes/wp_styles/do_item/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;WP_Styles::do_item($handle)&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.7994%; height: 21px;&quot;&gt;&lt;a href=&quot;https://developer.wordpress.org/reference/hooks/style_loader_tag/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;style_loader_tag&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.5716%; height: 21px;&quot;&gt;&lt;a href=&quot;https://github.com/WordPress/WordPress/blob/4803fc405e3448e825bbc63b23cde2a6a86225a9/wp-includes/class.wp-styles.php#L214&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 21px;&quot;&gt;$tagHtml, $handle, $src, $media&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Modifying the output of do_item()&lt;/h2&gt;
&lt;p&gt;The way both of the do_item() filters work is that it passes a bunch of arguments to a function you hook into the filter with add_filter(), and it expects that your function returns a string that represents the new &amp;#x3C;link&gt; or &amp;#x3C;script&gt; tag that it should use. For example, if you wanted to change all non-https scripts to https, just in case a plugin tries to add one, you could do something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#x3C;?php
add_filter(&apos;script_loader_tag&apos;, function($tag, $handle, $src){
    $tag = preg_replace(&apos;/^http:\/\//&apos;, &apos;https://&apos;, $tag);
    return $tag;
},10,4);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can do pretty much anything you want to the tag HTML string; this is both beneficial and dangerous. On one hand, this gives a great deal of flexibility, but on the other hand, it is very easy to craft malformed HTML. Plus, like I mentioned, all you can return is the string of HTML for WP to embed - there is not a method for adding or removing a particular attribute, which means most of the time relying on Regex to modify the tag passed by WP.&lt;/p&gt;
&lt;h2&gt;Putting it together in a reusable way&lt;/h2&gt;
&lt;p&gt;There are a lot of hackish ways you can add filters all over your functions.php to file modify the output of queued scripts and styles, but there are also some ways to craft reusable methods that will make modifying script and style tags easier. For one, since both filters have almost identical arguments, we can add a single function as the callback if we want to. Here is how I have done that:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#x3C;?php
/**
 * Callback for WP to hit before echoing out an enqueued resource
 * @param {string} $tag - Will be the full string of the tag (`&amp;#x3C;link&gt;` or `&amp;#x3C;script&gt;`)
 * @param {string} $handle - The handle that was specified for the resource when enqueuing it
 * @param {string} $src - the URI of the resource
 * @param {string|null} $media - if resources is style, should be the target media, else null
 * @param {boolean} $isStyle - If the resource is a stylesheet
 */
function scriptAndStyleTagCallback($tag, $handle, $src, $media, $isStyle){
    // ...
    // Function body omitted
}
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-php&quot;&gt;add_filter(‘script_loader_tag’,function($tag, $handle, $src){
return scriptAndStyleTagCallback($tag, $handle, $src, null, false);
},10,4);
add_filter(‘style_loader_tag’,function($tag, $handle, $src, $media){
return scriptAndStyleTagCallback($tag, $handle, $src, $media, true);
},10,4);
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Configurable defer, async, and lazy CSS loading&lt;/h3&gt;
&lt;p&gt;Next, I wanted to be able to easily enqueue certain scripts and styles in a way that would generate output HTML with async or defer loading. My current approach is as follows:&lt;/p&gt;
&lt;p&gt;First, have a variable that holds the handles (unique identifiers) of any scripts or styles that need to be given special treatment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$specialLoadHnds = (object) array(
    &apos;scripts&apos; =&gt; (object) array(
        &apos;async&apos; =&gt; array(),
        &apos;defer&apos; =&gt; array()
    ),
    &apos;styles&apos; =&gt; (object) array(
        &apos;async&apos; =&gt; array(),
        &apos;asyncPreload&apos; =&gt; array()
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Items are added to this variable through helper functions I&apos;ve added:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#x3C;?php
// Same signature as wp_enqueue_style, + $loadMethod as last arg
function wp_enqueue_style_special($handle, $srcString, $depArray, $version, $media, $loadMethod){
    global $specialLoadHnds;
    array_push($specialLoadHnds-&gt;styles-&gt;{$loadMethod},$handle);
    wp_enqueue_style($handle, $srcString, $depArray, $version, $media);
}
// Same signature as wp_enqueue_script, + $loadMethod as last arg
// Reminder - $inFooter should probably be false for both async and defer
function wp_enqueue_script_special($handle, $srcString, $depArray, $version, $inFooter, $loadMethod){
    global $specialLoadHnds;
    array_push($specialLoadHnds-&gt;scripts-&gt;{$loadMethod},$handle);
    wp_enqueue_script($handle, $srcString, $depArray, $version, $inFooter);
}
&lt;p&gt;/**&lt;/p&gt;
&lt;/code&gt;&lt;ul&gt;&lt;code class=&quot;language-php&quot;&gt;
&lt;li&gt;Example: Adding a script with ‘async’&lt;/li&gt;
&lt;/code&gt;&lt;li&gt;&lt;code class=&quot;language-php&quot;&gt;wp_enqueue_script_special(‘jquery’,‘&lt;a href=&quot;https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js&amp;#x27;,array(),false,false,&amp;#x27;async&quot;&gt;https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js’,array(),false,false,‘async&lt;/a&gt;’);
*/
&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/pre&gt;

&lt;p&gt;Next, within scriptAndStyleTagCallback(), I check the incoming $handle variable and see if it is contained in any of the special arrays. If it is, I apply a treatment as follows:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Scripts:
&lt;ul&gt;
	&lt;li&gt;&apos;async&apos;
&lt;ul&gt;
	&lt;li&gt;add &apos;async=&quot;true&quot;&apos; to &amp;#x3C;script&gt; tag&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&apos;defer&apos;
&lt;ul&gt;
	&lt;li&gt;add &apos;defer&apos; attribute to &amp;#x3C;script&gt; tag&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Styles
&lt;ul&gt;
	&lt;li&gt;&apos;async&apos;
&lt;ul&gt;
	&lt;li&gt;set &apos;media=&quot;none&quot;&apos; and &apos;onload=&quot;if(media!=&apos;all&apos;)media=&apos;all&apos;&quot;&apos;&lt;/li&gt;
	&lt;li&gt;Add &amp;#x3C;noscript&gt;&amp;#x3C;/noscript&gt; block that contains raw $tag, in case user has JS disabled&lt;/li&gt;
	&lt;li&gt;Based on &lt;a href=&quot;https://keithclark.co.uk/articles/loading-css-without-blocking-render/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&apos;asyncPreload&apos;
&lt;ul&gt;
	&lt;li&gt;set &apos;rel=&quot;preload&quot;&apos; and &apos;as=&quot;style&quot;&apos; and onload=&quot;this.onload=null;this.rel=&apos;stylesheet&apos;&quot;&lt;/li&gt;
	&lt;li&gt;Add &amp;#x3C;noscript&gt;&amp;#x3C;/noscript&gt; block that contains raw $tag, in case user has JS disabled&lt;/li&gt;
	&lt;li&gt;Based on &lt;a href=&quot;https://github.com/filamentgroup/loadCSS/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;loadCSS approach&lt;/a&gt;, and should be used with their &lt;a href=&quot;https://github.com/filamentgroup/loadCSS/blob/master/src/cssrelpreload.js&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;preload polyfill&lt;/a&gt; for browsers that don&apos;t support the &apos;preload&apos; attribute&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is everything all put together, into a single file that I require() into my functions.php file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot; data-jptremote=&quot;https://github.com/joshuatz/joshuatz-wp-theme/blob/71a12bca12c5fa5b25b6eb3a70cd498845076834/inc/special-loader.php&quot;&gt;https://github.com/joshuatz/joshuatz-wp-theme/blob/71a12bca12c5fa5b25b6eb3a70cd498845076834/inc/special-loader.php&lt;/code&gt;&lt;/pre&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>How to Get Github to Recognize a Pure Markdown Repo</title><link>https://joshuatz.com/posts/2019/how-to-get-github-to-recognize-a-pure-markdown-repo/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/how-to-get-github-to-recognize-a-pure-markdown-repo/</guid><description>How to get Github&apos;s language statistics breakdown bar to properly show Markdown stats for a repository, using Linguist and gitattributes.</description><pubDate>Wed, 31 Jul 2019 15:00:44 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;File this under &quot;not really necessary, but nice to have&quot;. If you have ever used Github, you have probably noticed the language details statistics bar (or &quot;language breakdown bar&quot;):&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Github-Language-Statistics-Breakdown-Bar.png&quot;&gt;&lt;img class=&quot;alignnone size-large wp-image-652&quot; src=&quot;/media/Github-Language-Statistics-Breakdown-Bar-1024x63.png&quot; alt=&quot;Github Language Statistics Breakdown Bar&quot; width=&quot;1024&quot; height=&quot;63&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It doesn&apos;t really have an impact on anything, but it can tell other devs at a glance what kind of tech is likely in your repo, and it does influence the search results when searching for a repo across Github.&lt;/p&gt;
&lt;p&gt;Unfortunately, if you create a repo and fill it with nothing but markdown files (.md), the language breakdown bar will either not appear at all, or if you introduce some non-markdown files, it will only show those. What gives?&lt;/p&gt;
&lt;p&gt;Both the problem and &lt;a href=&quot;#solution-start&quot;&gt;the solution&lt;/a&gt; are revealed with a little bit of background knowledge.&lt;/p&gt;
&lt;h2&gt;How is the language stats calculated?&lt;/h2&gt;
&lt;p&gt;Internally, Github uses a tool called &lt;a href=&quot;https://github.com/github/linguist&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Linguist&lt;/a&gt; to detect and classify code files. What you might not know though, is that Linguist also has a lot of rules for &lt;em&gt;preventing&lt;/em&gt; a file from contributing towards statistics. A file is excluded from stats if it is classified as any of these:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Vendored-code&lt;/li&gt;
	&lt;li&gt;Generated-code&lt;/li&gt;
	&lt;li&gt;Documentation&lt;/li&gt;
	&lt;li&gt;Data&lt;/li&gt;
	&lt;li&gt;Prose&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For details, see &lt;a href=&quot;https://github.com/github/linguist#my-repository-isnt-showing-my-language&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this section&lt;/a&gt; of the readme.&lt;/p&gt;
&lt;h2&gt;Why does Markdown not show up?&lt;/h2&gt;
&lt;p&gt;The first reason why Markdown does not show up is because it is immediately classified as &quot;Prose&quot;, which we know from above, is ignored when it comes to repo language statistics.&lt;/p&gt;
&lt;p&gt;The second reason might or might not apply to you; filename and file structure. For example, if all your markdown files are named &quot;readme.md&quot; or are just in a folder called &quot;Documentation&quot;, they would be classified as &quot;Documentation&quot; and excluded. You can see this from the Regular Expressions patterns in the &lt;a href=&quot;https://github.com/github/linguist/blob/master/lib/linguist/documentation.yml&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;documentation.yml&quot; file&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Solution:&lt;/h2&gt;
&lt;p&gt;It used to be that there was not a solution to this issue. However, over time, Github added &quot;overrides&quot; to Linguist, so now users can modify the default classification of files by overriding with git attributes (see &lt;a href=&quot;https://github.com/github/linguist/issues/3964&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;issue #3964&lt;/a&gt;, which was solved by &lt;a href=&quot;https://github.com/github/linguist/pull/3807&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;PR #3807&lt;/a&gt;, and example given in &lt;a href=&quot;https://github.com/github/linguist/pull/3806&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#3806&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;How do we implement overrides? It is actually surprisingly pain-free.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;&lt;a id=&quot;solution-start&quot;&gt;&lt;/a&gt;First&lt;/strong&gt;&lt;/span&gt;, create a &quot;.gitattributes&quot; file in the root of your repo. Then, use &lt;a href=&quot;https://git-scm.com/docs/gitattributes&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the standard git attributes syntax&lt;/a&gt; within that file to modify file attributes based on filename or pattern. Git Attributes are a way to assign specific, custom, and arbitrary attributes to specific files.&lt;/p&gt;
&lt;p&gt;The linguist attributes you can override are:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;linguist-documentation&lt;/li&gt;
	&lt;li&gt;linguist-langauge&lt;/li&gt;
	&lt;li&gt;linguist-vendored&lt;/li&gt;
	&lt;li&gt;linguist-generated&lt;/li&gt;
	&lt;li&gt;linguist-detectable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, here is a .gitattributes file where I have forced Github to count all Markdown files towards my language stats, *except* for readme.md:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;# Linguist overrides
*.md linguist-detectable=true
README.md linguist-detectable=false
readme.md linguist-detectable=false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Technically, I believe toggling &quot;linguist-detectable&quot; should be all it takes to change whether or not a file is ignored, since it acts as a final override. If you wanted to be 100% sure that markdown is not excluded, you could also override any attribute that might prevent it from being recognized, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markup&quot;&gt;*.md linguist-vendored=false
*.md linguist-generated=false
*.md linguist-documentation=false
*.md linguist-detectable=true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Final notes:&lt;/h2&gt;
&lt;p&gt;Keep in mind that the solution found here is applicable towards many issues with Github stats; you can easily override any Linguist classifications with Git attributes.&lt;/p&gt;
&lt;p&gt;Also, &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt;&lt;/span&gt; You can easily check if your file patterns are correct in your .gitattributes file by using the &quot;git check-attr&quot; command. For example, to check the attributes of a markdown file with the filename &quot;web-dev.md&quot;, in my folder &quot;cheatsheets&quot;, I would use:&lt;/p&gt;
&lt;pre&gt;git check-attr -a cheatsheets/web-dev.md&lt;/pre&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Vue - Mixing SASS with SCSS, with Vuetify as an Example</title><link>https://joshuatz.com/posts/2019/vue---mixing-sass-with-scss-with-vuetify-as-an-example/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/vue---mixing-sass-with-scss-with-vuetify-as-an-example/</guid><description>A guide on mixing Sass-syntax and SCSS-syntax within a single Vue project, and how it can conflict with vendor Sass files, such as Vuetify.</description><pubDate>Tue, 30 Jul 2019 09:34:28 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;The Vue CLI is an impressive tool that, similar to create-react-app, boostraps and automates a bunch of the Vue setup process. Unfortunately, the &quot;magic&quot; that makes the CLI and Vue so easy to use, also abstracts away a lot of what it is doing under the hood and makes it a little difficult to understand how to deal with unexpected issues, such as a SASS vs SCSS conflict when trying to use SCSS with Vuetify:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Error: Semicolons aren&apos;t allowed in the indented syntax.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I got this error when trying to load a scss file through vue.config.js, which is the method that Vue &lt;a href=&quot;https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;officially recommends&lt;/a&gt;. If you search around, you can find that this is a known issue with Vue and how it uses webpack. If you selected SASS as one of the options while setting up the Vue CLI, under the hood Vue has installed and configured both &quot;&lt;a href=&quot;https://vue-loader.vuejs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;vue-loader&lt;/a&gt;&quot; and  &lt;a href=&quot;https://webpack.js.org/loaders/sass-loader/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;sass-loader&quot;&lt;/a&gt; to handle the Sass-&gt;CSS conversion. The specific issue is that &quot;vue-loader&quot;, using &quot;sass-loader&quot;, can handle both SASS (indented syntax) and SCSS (CSS3 superset) formatted Sass files, but will run into a conflict if trying to mix them at a global level with loaderOptions.sass.data. And in my situation, I&apos;m trying to combine Vuetify, which uses the SASS syntax, with my own style file, which uses SCSS syntax.&lt;/p&gt;
&lt;p&gt;If you want to jump right to the solution click &lt;a href=&quot;#easy-solution&quot;&gt;here&lt;/a&gt;, otherwise, keep reading as I dig into the specifics of this issue.&lt;/p&gt;
&lt;h2&gt;What this issues is not about:&lt;/h2&gt;
&lt;p&gt;First, let&apos;s try to make it clear what this issue is &lt;strong&gt;not. &lt;/strong&gt;This issue is &lt;strong&gt;not &lt;/strong&gt;simply about getting SASS/SCSS to work with Vue. That is pretty simple, assuming you have &quot;sass-loader&quot; installed. There are a few options, but the easiest is to simply stick your Sass into a style tag, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;template&gt;
    &amp;#x3C;div class=&quot;login&quot;&gt;
        &amp;#x3C;button&gt;Click to Login&amp;#x3C;/button&gt;
    &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;
&lt;p&gt;&amp;#x3C;script&gt;
&amp;#x3C;/script&gt;&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;style lang=“scss”&gt;
.login {
button {
color: green;
}
}
&amp;#x3C;/style&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Or, you could &quot;import&quot; the Sass style file in your main JS file that loads Vue. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import Vue from &apos;vue&apos;;
import App from &apos;./App.vue&apos;;
import router from &apos;./router&apos;;
import store from &apos;./store&apos;;
import &apos;./registerServiceWorker&apos;;
import vuetify from &apos;./plugins/vuetify&apos;;
&lt;p&gt;import ’./styles/App.scss’;&lt;/p&gt;
&lt;p&gt;Vue.config.productionTip = false;&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;new Vue({
router,
store,
vuetify,
render: (h) =&gt; h(App),
}).$mount(‘#app’);
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;The real issue: Global vs Non-Global&lt;/h2&gt;
&lt;p&gt;The real issue here is how to inject global styles across your entire Vue app, using Sass. The common reason why this is desired is Sass variables. For example, a common dev practice is to create a Sass variable file, maybe &quot;&lt;em&gt;_variables.scss&lt;/em&gt;&quot;, which has variables such as &quot;&lt;em&gt;$darkColor: #222222;&lt;/em&gt;&quot;, and then in a Vue component file (SFC), reference that variable value in the style tag, such as with:&lt;/p&gt;
&lt;pre style=&quot;padding-left: 40px;&quot;&gt;button { background-color: $darkColor; }&lt;/pre&gt;
&lt;p&gt;This is deceptively complicated, as evidenced by &lt;a href=&quot;https://github.com/vuejs/vue-loader/issues/328&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this massive Github issue thread on vue-loader&lt;/a&gt;, which explores different approaches and issues. The solution that most people have landed on is modifying the vue.config.js file and tweaking the loaderOptions for CSS - there is a great guide by CSS-Tricks on how to do this &lt;a href=&quot;https://css-tricks.com/how-to-import-a-sass-file-into-every-vue-component-in-an-app/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;, and another guide by VueSchool &lt;a href=&quot;https://vueschool.io/articles/vuejs-tutorials/globally-load-sass-into-your-vue-js-applications/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;. Here is a sample vue.config.js file that follows their guide:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const path = require(&apos;path&apos;);
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;module.exports = {
css: {
loaderOptions: {
sass: {
data: &lt;code&gt;@import &quot;@/styles/_variables.scss&quot;;&lt;/code&gt;
}
}
},
}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;This solution works for many users, but those using third-party libraries formatted with Sass syntax, such as Vuetify, will likely end up with this dreaded error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Error: Semicolons aren&apos;t allowed in the indented syntax.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What is happening is that vue-loader/sass-loader is essentially trying to inject your import code, which is formatted as SCSS syntax (with a semicolon), into your third-party library, which uses SASS syntax (semicolons are not allowed.&lt;/p&gt;
&lt;p&gt;Now what? We need some way to tell sass-loader to remove the semicolon when using with SASS syntax, and/or don&apos;t inject it at all into .sass if possible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;August 2019 Update: There is now a much easier way to handle this thanks to an update to the Vue CLI. See &quot;the easy solution&quot; below!&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Mixing Sass-syntax and SCSS-syntax in Vue&lt;/h2&gt;
&lt;p&gt;If you don&apos;t need to use variables, and simply want some SCSS converted to CSS and applied across globally, across your entire app, you have a few options, some of which I&apos;ve already mentioned:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Import in your main JS file (with &quot;import &apos;./styles/myStyle.scss&apos;&quot;)&lt;/li&gt;
	&lt;li&gt;Import in your root Vue template file (e.g. App.vue), either through the script section, the style section, or a style link tag.&lt;/li&gt;
	&lt;li&gt;Use your own pre-processor of choice to process your files and push them into the right file locations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, if you want to use variables, mixins, or anything else that needs to be &quot;reference-able&quot; across components, this won&apos;t cut it.&lt;/p&gt;
&lt;p&gt;To figure out how to get this to work, I started combing through a few relevant threads, starting with &lt;a href=&quot;https://github.com/vuejs/vue-cli/issues/4116&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this Github issue for Vue-CLI&lt;/a&gt;. Some extra hints came from &lt;a href=&quot;https://github.com/vuejs/vue-loader/issues/9&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this&lt;/a&gt;, and especially &lt;a href=&quot;https://vue-loader.vuejs.org/guide/pre-processors.html#sass-vs-scss&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;easy-solution&quot;&gt;&lt;/a&gt;The easy solution:&lt;/h3&gt;
&lt;p&gt;Thanks to the v3.11.0 update to the Vue CLI, there is now a built-in solution for handling mixed SCSS and SASS syntax imports within the same project. Make sure your CLI is updated, and then you can use the following format within your &lt;code&gt;vue.config.js&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;module.exports = {
  css: {
    loaderOptions: {
      sass: {
        // Here we can specify the older indent syntax formatted SASS
        // Note that there is *not* a semicolon at the end of the below line
        data: `@import &quot;@/styles/_variables.sass&quot;`
      },
      scss: {
        // Here we can use the newer SCSS flavor of Sass.
        // Note that there *is* a semicolon at the end of the below line
        data: `@import &quot;@/styles/_variables.scss&quot;;`
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That was easy! If you are curious, &lt;a href=&quot;https://github.com/vuejs/vue-cli/pull/4386&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here is the PR&lt;/a&gt; that fixed this issue, and &lt;a href=&quot;https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here is the updated guide section&lt;/a&gt; showing the new usage and examples.&lt;/p&gt;
&lt;p&gt;If the above did not work for you, or you are stuck on an old version of the Vue CLI, you might have to use one of the hard solutions below:&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;the-solutions&quot;&gt;&lt;/a&gt;The hard solution:&lt;/h3&gt;
&lt;p&gt;We can tap into webpack settings, by using the &quot;chainWebpack&quot; function in our vue.config.js file, which is a way to modify the default behavior of Vue&apos;s &quot;behind-the-scenes&quot; webpack configuration. Here are some relevant links:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://cli.vuejs.org/config/#chainwebpack&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Documentation for Vue config file&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://cli.vuejs.org/guide/webpack.html#chaining-advanced&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vue&apos;s Documentation for using chainWebpack&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/neutrinojs/webpack-chain&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Documentation for &quot;webpack-chain&quot;&lt;/a&gt;, which is the module that powers the chainWebpack function.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&apos;s get to work:&lt;/p&gt;
&lt;h4&gt;First: Make sure vue.config.js exists&lt;/h4&gt;
&lt;p&gt;Depending on how you use the Vue CLI to create your project, the vue.config.js file might not actually exist yet. If it does not, go ahead and create it in the &lt;strong&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;root&lt;/span&gt;&lt;/strong&gt; of your project.&lt;/p&gt;
&lt;p&gt;Now that we have a config file, let&apos;s move onto the solutions:&lt;/p&gt;
&lt;h4&gt;Solution - Option B - &quot;Tap&quot; into webpack settings using chainWebpack&lt;/h4&gt;
&lt;p&gt;This solution is provided by a Vuetify dev &lt;a href=&quot;https://github.com/vuejs/vue-cli/issues/4116#issuecomment-510288266&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;on a Github issue opened about this specific problem&lt;/a&gt;. I&apos;ve modified it just a tiny bit to accept an array of files:&lt;/p&gt;
&lt;p&gt;vue.config.js:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const globalSassFiles = [
    &apos;~@/styles/_variables.scss&apos;,
    &apos;~@/styles/App.scss&apos;
]
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;module.exports = {
css: {
loaderOptions: {
sass: {
data: globalSassFiles.map((src)=&gt;‘@import ”’ + src + ’”;‘).join(‘\n’)
}
}
},
chainWebpack: config =&gt; {
[“vue-modules”, “vue”, “normal-modules”, “normal”].forEach((match) =&gt; {
config.module.rule(‘sass’).oneOf(match).use(‘sass-loader’)
.tap(opt =&gt; {
return Object.assign(opt, {
data: globalSassFiles.map((src)=&gt;‘@import ”’ + src + ’”‘).join(‘\n’)
});
});
});
}
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;This solution should work for most users, although a couple issues with it should be noted. For one, it injects your files into *every* used sass file, including all Vuetify files. This means that if you put something into Sass that is going to generate actual CSS code, it also ends up in all vendor CSS files. This also means an increased rebuild time for hot-reload, as touching one of those global sass files means all vendor files have to be reprocessed to re-inject your imports.&lt;/p&gt;
&lt;p&gt;If you just need your Sass variables available to your own Vue template code, a better solution might be something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const globalSassFiles = [
    &apos;~@/styles/_variables.scss&apos;,
    &apos;~@/styles/App.scss&apos;
]
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;module.exports = {
css: {
loaderOptions: {
sass: {
data: globalSassFiles.map((src)=&gt;‘@import ”’ + src + ’”;‘).join(‘\n’)
}
}
},
chainWebpack: config =&gt; {
[“vue-modules”, “normal-modules”, “normal”].forEach((match) =&gt; {
config.module.rule(‘sass’).oneOf(match).use(‘sass-loader’)
.tap(opt =&gt; {
delete opt.data;
return opt;
});
});
}
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;As you can see, &quot;vue&quot; has been removed from the match array, and for anything other than &quot;vue&quot; (template files), the import data is removed.&lt;/p&gt;
&lt;h4&gt;Solution - Option C - Use &quot;Sass-Resource-Loader&quot;&lt;/h4&gt;
&lt;p&gt;&quot;Sass-Resource-Loader&quot; is a loader plugin for Webpack that is specifically designed for injecting SASS variables as globals across imports. It even has a guide for using with vue.config.js and the Vue CLI 3+, which you can find &lt;a href=&quot;https://github.com/shakacode/sass-resources-loader#vuejs-webpack-templatevue-cli3&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To get the same results as above, but with this solution, this is what my vue.config.js looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const globalSassFiles = [
    &apos;./src/styles/_variables.scss&apos;,
    &apos;./src/styles/App.scss&apos;
]
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;module.exports = {
chainWebpack: config =&gt; {
const oneOfsMap = config.module.rule(‘scss’).oneOfs.store
oneOfsMap.forEach(item =&gt; {
item
.use(‘sass-resources-loader’)
.loader(‘sass-resources-loader’)
.options({
resources: globalSassFiles
})
.end()
})
}
}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Final notes:&lt;/h2&gt;
&lt;p&gt;If none of this worked for you, my advice would probably be to either open a Github issue with Vue or Vue-loader, or use webpack outside of vue.config.js (either by switching to Vue CLI 2, which exposes more webpack stuff, or by setting up webpack from scratch outside of the Vue CLI). A great tool to use to try and determine how Vue is internally using Webpack is &lt;a href=&quot;https://cli.vuejs.org/guide/webpack.html#modifying-options-of-a-plugin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the &quot;vue inspect&quot; command&lt;/a&gt;.  For example, to see what webpack config is used for production builds, and save it to output.txt, use:&lt;/p&gt;
&lt;pre&gt;vue inspect -v --mode production &gt; output.txt&lt;/pre&gt;
&lt;p&gt;Finally, on a personal note, I would have to say that this whole post exemplifies one of the problems with &quot;magic&quot; tools like the Vue CLI, which abstract away how things are actually working below the surface. It really shouldn&apos;t be this difficult to tweak behavior, but it is. There is a great related thread on the balance between ease of use and exposing controls when it comes to the Vue CLI, and &lt;a href=&quot;https://github.com/vuejs/vue-cli/issues/2796#issuecomment-456300832&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this reponse in particular&lt;/a&gt; is something that resonates with me.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Data Studio - Custom Toggl Time Tracker Connector</title><link>https://joshuatz.com/projects/marketing/google-data-studio---custom-toggl-time-tracker-connector/</link><guid isPermaLink="true">https://joshuatz.com/projects/marketing/google-data-studio---custom-toggl-time-tracker-connector/</guid><description>A custom Google Data Studio connector, built from scratch with TypeScript, to pull in data from Toggl Time Tracker via API.</description><pubDate>Fri, 05 Jul 2019 16:01:05 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;&lt;a href=&quot;/media/Google-Data-Studio-Toggl-Time-Tracker-Connector-joshuatz.gif&quot;&gt;&lt;img class=&quot;size-full wp-image-631 aligncenter&quot; src=&quot;/media/Google-Data-Studio-Toggl-Time-Tracker-Connector-joshuatz.gif&quot; alt=&quot;Google Data Studio - Toggl Time Tracker Connector - joshuatz&quot; width=&quot;1000&quot; height=&quot;698&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Project status: Final stages&lt;/h2&gt;
&lt;p&gt;Source code: &lt;a href=&quot;https://github.com/joshuatz/gds-toggl-connector&quot;&gt;https://github.com/joshuatz/gds-toggl-connector&lt;/a&gt;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Installation:&lt;/h2&gt;
&lt;p&gt;This is not yet live; when it is published I will update this post with the link to install it. In the meantime, if you would like to notified when this goes live, you can fill out a form &lt;a href=&quot;https://docs.google.com/forms/d/e/1FAIpQLScLlLqQ_P_K06sx_ruvrmwkquuf0QtOAsAZlGM9J65lfKgiyA/viewform&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once this connector is live, the installation process should be as easy as clicking a link, and then copying and pasting your Toggl API user token into the setup page.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Data-Studio-Toggl-Connector-Installation-Page.png&quot;&gt;&lt;img class=&quot;size-full wp-image-633 aligncenter&quot; src=&quot;/media/Google-Data-Studio-Toggl-Connector-Installation-Page.png&quot; alt=&quot;Google Data Studio - Toggl Connector - Installation Page&quot; width=&quot;805&quot; height=&quot;568&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Background:&lt;/h2&gt;
&lt;p&gt;I currently use a service called &lt;a href=&quot;https://toggl.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;Toggl&quot;&lt;/a&gt;, an online time tracking software, to keep tabs on how I spend my time; both for personal and business purposes. I&apos;m not alone; with 1.6+ million users and over 70,000 businesses enrolled, Toggl is definitely a popular time tracker. As a detail oriented, technology-obsessed person, I also have been milling around the idea of building a personal &quot;dashboard&quot; using Google Data Studio (GDS), where at a glance I could see how much time I had worked on various projects in the current week, how many website visitors this site got, and things like that. For those unfamiliar, Google Data Studio is a (free) online reporting and visualization platform, similar to Tableau or Microsoft Power BI. It lets you create and share interactive reports and widgets that pull from multiple massive data sources.&lt;/p&gt;
&lt;p&gt;Toggl would be a big part of this personal dashboard, but I was disappointed to find that they did not have a &quot;connector&quot; available to pull in data from Toggl into Google Data Studio. However, being who I am, and loving a challenge, I thought this was a great opportunity to see what went into creating a data connector, and try my hand at creating it from scratch.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Implementation:&lt;/h2&gt;
&lt;p&gt;Having just recently tried out TypeScript and being impressed with how it worked and kept large complex codebases tidy, I figured this would make a good first project to try it with. So the first part of this project was simply to get TypeScript to play nicely with Google Apps Script (the engine that powers GDS connectors), which was its own ordeal, and has a write-up &lt;a href=&quot;https://joshuatz.com/posts/2019/using-typescript-with-google-apps-script-and-google-ads-scripting/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;. After I had figured that out, it wasn&apos;t actually all that difficult to build out, although I ran into a lot of &quot;gotchas&quot; and strange undocumented bugs of the GDS connector library, which I have written up &lt;a href=&quot;https://joshuatz.com/posts/2019/developing-a-google-data-studio-connector-tips-and-gotchas/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Data-Studio-Toggl-Connector-Side-by-Side-with-Code.png&quot;&gt;&lt;img class=&quot;size-large wp-image-632 aligncenter&quot; src=&quot;/media/Google-Data-Studio-Toggl-Connector-Side-by-Side-with-Code-1024x576.png&quot; alt=&quot;Google Data Studio - Toggl Connector - Side by Side with Code&quot; width=&quot;1024&quot; height=&quot;576&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Of course, the main crux of the connector is fetching results from the Toggl API and then translating the results into GDS formatted rows. The Toggl API is &lt;a href=&quot;https://github.com/toggl/toggl_api_docs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;well documented&lt;/a&gt;, but unfortunately does not have TypeScript definitions available, so that was another thing that I had to code out from scratch.&lt;/p&gt;
&lt;p&gt;I have not yet deployed this connector in an &quot;official&quot; capacity, but once I have and it has been approved, I will update this post.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Marketing Applications:&lt;/h2&gt;
&lt;p&gt;I filed this project under &quot;marketing&quot; for several reasons. For one, this shows my skills working with data sets and data transformation, both of which are a key part of the digital marketing eco-system. Google Data Studio is also, primarily, a marketing dashboard service, and I could have just as easily have built this as a connector for Tableau instead of GDS.&lt;/p&gt;
&lt;p&gt;Finally, Toggl itself can be very useful for marketing-specific businesses. For example, an advertising agency might use the &quot;client&quot; grouping feature to track their billable time per client, then pull the data into a GDS report that combines the time spent per client with KPIs, to prove the value of their time and encourage clients to request more billable hours. Or, for a business that does not bill by the hour, or an in-house department, you could use it to track what type of tasks are burning more time and pull insights like &quot;QA for creatives is taking 2x as long this month as last month; is that something we need to look into with our design department?&quot;.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Custom WordPress Theme and PHP Helpers: joshuatz-wp-theme</title><link>https://joshuatz.com/projects/web-stuff/custom-wordpress-theme-and-php-helpers-joshuatz-wp-theme/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/custom-wordpress-theme-and-php-helpers-joshuatz-wp-theme/</guid><description>The custom WordPress theme that powers both the styling behind this site, as well as a lot of back-end logic that controls indexing, redirects, and more.</description><pubDate>Mon, 01 Jul 2019 23:36:31 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;Source Code:&lt;/h2&gt;
&lt;p&gt;You can find the full theme here: &lt;a href=&quot;https://github.com/joshuatz/joshuatz-wp-theme&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/joshuatz/joshuatz-wp-theme&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NOTE: This repo serves mainly as a host for deployments and to showcase my theme building abilities. I&apos;m not planning on releasing this theme into the WordPress theme directory, as I really only built it with myself in mind, and continue to constantly tweak it to suit my own personal needs.&lt;/p&gt;
&lt;h2&gt;Details:&lt;/h2&gt;
&lt;p&gt;I&apos;ve been meaning to post this for a while now; this custom WordPress theme has actually been in use for a while now, for over half a year. Originally I built this site on the Blogger platform (Google&apos;s point-and-click blog platform), then moved to static HTML  files, then to some simple PHP, and finally to WordPress, where I built and implemented a custom theme that would allow me to do whatever I like with the site.&lt;/p&gt;
&lt;p&gt;Although WordPress is not necessarily liked by all developers, and I understand some of their frustrations, I enjoy the flexibility that the platform allows. For example, I&apos;ve built a lot of custom code into my theme that tries to help SEO by keeping low quality posts from being indexed; if my code detects that a post is a placeholder (less than x number of characters) or  a &quot;virtual&quot; post that immediately redirects, it will set a flag to stop Google and other search engines from indexing it. Another feature I implemented that I really like is automatic disclaimers based on date; if a post was created longer than [x] number of years ago, my code will automatically add a disclaimer at the top notifying users that the post content might be out of date.&lt;/p&gt;
&lt;p&gt;I&apos;ve also created a &quot;helpers&quot; PHP class, that abstracts a lot of code to solve annoyances and peculiarities with WP into helpful methods. For example, a single function to get the age of a post. You can find that file, helpers.php, &lt;a href=&quot;https://github.com/joshuatz/joshuatz-wp-theme/blob/master/inc/helpers.php&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Developing a Google Data Studio Connector - Tips and Gotchas</title><link>https://joshuatz.com/posts/2019/developing-a-google-data-studio-connector---tips-and-gotchas/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/developing-a-google-data-studio-connector---tips-and-gotchas/</guid><description>Tips, tricks, and gotchas, for building a Google Data Studio Community Connector on Google Apps Script. Covers some common issues and recommended approaches.</description><pubDate>Mon, 01 Jul 2019 05:11:58 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I&apos;m working on building a Google Data Studio (GDS) Connector; for those unfamiliar, &lt;a href=&quot;https://datastudio.google.com/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Google Data Studio&lt;/a&gt; is a free BI (Business Intelligence) reporting platform, similar to Microsoft BI or Tableau. A connector, or &quot;&lt;a href=&quot;https://developers.google.com/datastudio/connector/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Community Connector&lt;/a&gt;&quot; (a connector not built by Google) is an Apps Script powered bundle of code (Javascript) that enables Google Data Studio to connect to any third part service, by acting as the relay in-between.&lt;/p&gt;
&lt;p&gt;As I&apos;ve been working, I&apos;ve been more than a little frustrated by the lack of in-depth documentation and troubleshooting guides available, so this post is both a way for me to fill in some of those gaps for others, as well as a way to vent my anger :)&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Tip: Review the community connectors repo&lt;/h2&gt;
&lt;p&gt;If you get lost or stuck, and are wondering &quot;how have others done this?&quot;, you might find it helpful to check out &lt;a href=&quot;https://github.com/googledatastudio/community-connectors&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the &quot;community-connectors&quot; repo on Github&lt;/a&gt;. These are connectors that various people have developed, that are open-sourced and varied in usage.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Tip: Testing new code without deploys&lt;/h2&gt;
&lt;p&gt;If you want to test how new code actually works with GDS, you might be tempted to either reconnect to Data Studio or create a brand new deployment. Both are actually unnecessary if you just want to test for just yourself and are not pushing to production. When you click &quot;Deploy from manifest...&quot; in the &quot;Publish&quot; menu dropdown, that alone will actually push your code to any connectors that were setup by clicking the special link under &quot;Latest Version (Head)&quot;.&lt;/p&gt;
&lt;p&gt;So, for example, if I&apos;ve just added a new &quot;console.log()&quot; line to my code and want to instantly see the new message appear in StackDriver, I would add the line of code, save, then go to &quot;Menu -&gt; Publish -&gt; Deploy from manifest&quot;, and then, in another tab, simply refresh a Google Data Studio report that uses the connector.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Tip: Use TypeScript&lt;/h2&gt;
&lt;p&gt;Due to the complexity of writing a GDS connector and the amount of different types involved, I recommend using TypeScript, especially if you are used to using statically typed languages. I started writing my connector in vanilla Javascript, realized how complex things were getting, and rewrote it quickly into TS. During the rewrite process, I caught several bugs in my code purely through looking at TypeScripts type-checking errors. That is not to say that TS will prevent you from writing broken code, but I find it helps to keep things orderly and avoid what I would call &quot;silly&quot; programming mistakes. Part of this is personal preference as well.&lt;/p&gt;
&lt;p&gt;I would only advise this if you have at least a little familiarity with TypeScript, or are looking to learn and have the extra time for it.&lt;/p&gt;
&lt;p&gt;For how to use TypeScript with GDS: I&apos;ve already written &lt;a href=&quot;https://joshuatz.com/posts/2019/using-typescript-with-google-apps-script-and-google-ads-scripting/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a guide for using TypeScript with GAS projects&lt;/a&gt;, and it applies to Google Data Studio Connectors as well.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Tip: Use TypeScript definition files&lt;/h2&gt;
&lt;p&gt;Even if you are not using TypeScript, one of the awesome things about VSCode is that you can still get the intellisense / auto-suggest capability to take advantage of TypeScript definition files. Just make sure you have a package.json file, and &lt;a href=&quot;https://www.npmjs.com/package/@types/google-apps-script&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;install &quot;@types/google-apps-script&quot;&lt;/a&gt;. Once you do that, even if you are writing your connector in vanilla JS, you should be able to take advantage of the improved intellisense:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/VSCode-Google-Data-Studio-Connector-Using-Google-Apps-Script-Type-Definition.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-627&quot; src=&quot;/media/VSCode-Google-Data-Studio-Connector-Using-Google-Apps-Script-Type-Definition.png&quot; alt=&quot;VSCode - Google Data Studio Connector - Using Google-Apps-Script Type Definition&quot; width=&quot;928&quot; height=&quot;310&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Tip: Additional resources:&lt;/h2&gt;
&lt;p&gt;There are a bunch of great resources out there for help on building a connector, beyond &lt;a href=&quot;https://developers.google.com/datastudio/connector/build&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the official guides&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here are some in particular that stood out to me:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Looking at code in &lt;a href=&quot;https://github.com/googledatastudio/community-connectors&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the &quot;community-connectors&quot; repo&lt;/a&gt;.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://itnext.io/building-a-custom-google-data-studio-connector-from-a-z-b4d711a5cf58&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;Building a custom Google Data Studio connector from A-Z&quot;&lt;/a&gt; by Jan Bajena&lt;/li&gt;
	&lt;li&gt;This &lt;a href=&quot;https://datastudio.google.com/u/0/reporting/0B2lgFyX5qOqhbFE5RllsdFdtMXc/page/0DlG&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;massive compilation of GDS resources&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/helpfullee&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;@Helpfullee&lt;/a&gt;.&lt;/li&gt;
	&lt;li&gt;The &lt;a href=&quot;https://www.youtube.com/watch?v=h9HgfWyaPPU&amp;#x26;list=PLIivdWyY5sqLNJttHVnNtjKVgt2PGF4Js&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;DataVis DevTalk&quot; series on Youtube&lt;/a&gt; by Google Cloud Platform (especially talks by &lt;a href=&quot;https://twitter.com/_mkazi_&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;@_mkazi_&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Gotcha: Unexpected data formatting rules&lt;/h2&gt;
&lt;p&gt;I&apos;ll just go ahead and say it: the way Google Data Studio expects data to be formatted is... well... a little strange. The format of the data in a field that you return to GDS is usually referred to as a &quot;semantic type&quot;. The GDS docs have a nice table of the possible formats &lt;a href=&quot;https://developers.google.com/datastudio/connector/reference#semantictype&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Noticed anything odd about that table? I sure did! There are several random semantic types that, despite always holding an integer value with no zero-padding, such as &quot;QUARTER&quot; and &quot;DAY_OF_WEEK&quot;, expect a string.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;Furthermore, there is no field to hold a full datetime/timestamp value! The date field with the highest precision is &quot;YEAR_MONTH_DAY_HOUR&quot;, and that only gets as precise as the hour. If you really wanted to pass a full timestamp to GDS, you could pass an epoch / unix timestamp in a NUMBER field, and then leave it up to the user to format it into any date format they would like with a &quot;&lt;a href=&quot;https://support.google.com/datastudio/answer/6299685&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;calculated field&lt;/a&gt;&quot; and the &quot;&lt;a href=&quot;https://support.google.com/datastudio/answer/7037300&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TODATE()&lt;/a&gt;&quot; function.This solution also allows you to ignore timezone discrepancies, as the epoch does not need to take those into account (at least not for the purpose of passing it to GDS for their TODATE to handle).&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;EDIT: On 09/09/2019, they added support for &quot;YEAR_MONTH_DAY_SECOND&quot; (format: &lt;code&gt;YYYYMMDDHHMMSS&lt;/code&gt;), which is a massive improvement considering that previously the highest precision date-time field was a specific hour (YEAR_MONTH_DAY_HOUR). Thanks GDS!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Gotcha: Debugging, Logging messages and errors&lt;/h2&gt;
&lt;h3&gt;Debugging:&lt;/h3&gt;
&lt;p&gt;There isn&apos;t really a way to use the built-in GAS debugger as the connector runs, such as when a user navigates a report that uses the connector within Google Data Studio, or runs through the configuration setup. However, you could create functions that &quot;mock&quot; those actions, and then debug them by selecting the function in the &quot;Select function&quot; dropdown, and hitting the &quot;debug&quot; button.&lt;/p&gt;
&lt;h3&gt;Logging:&lt;/h3&gt;
&lt;p&gt;Since there is no real way to &quot;debug&quot; a community connector as it runs, you will likely find yourself putting in lots of print statements when you are trying to track down an error. If you are not seeing your messages show up, check how you are logging them.&lt;/p&gt;
&lt;p&gt;The old school way is to use &quot;Logger.log(message)&quot;, but that only works if your code is initiated directly within the Apps Script IDE. In general, you should avoid Logger, and instead use &quot;console.log()&quot;. Calls to console methods log to StackDriver, which you can find by going to &quot;view -&gt; StackDriver Logging&quot; inside the Apps Script IDE.&lt;/p&gt;
&lt;p&gt;Also, note that the syntax for &quot;console.log()&quot; in GAS is not exactly the same as in your browser. &lt;a href=&quot;https://developers.google.com/apps-script/reference/base/console#log(Object,Object...)&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Here is the reference page&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Gotcha: Timezones&lt;/h2&gt;
&lt;p&gt;There are a few things to note about using Timezones with GDS. First, both the GDS platform and the Apps Script environment &lt;a href=&quot;https://support.google.com/datastudio/answer/6401549&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;use UTC standard time&lt;/a&gt; (aka GMT) as the default. So you will need to take that into account when returning date/time fields.&lt;/p&gt;
&lt;p&gt;Timezones are complicated (and annoying), so I won&apos;t even attempt to try to pretend I can solve this for you, but I will give some possible approaches:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Always return everything in UTC-0. This is probably the safest solution, but requires coding offsets for every piece of data you are relaying from another API to GDS.
&lt;ul&gt;
	&lt;li&gt;Worse yet, if the API does not have a specific endpoint to query what the users timezone is, you might have to &quot;sniff&quot; it based on date/time strings...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Offer users a configuration setting where they can enter their timezone in hours offset from UTC, then inside your connector, add or subtract that number from every date field. This is the approach used by the &lt;a href=&quot;https://github.com/googledatastudio/community-connectors/blob/master/community-connectors/openweathermap-current/main.js&quot;&gt;openweathermap community connector&lt;/a&gt;.&lt;/li&gt;
	&lt;li&gt;If the API  you are requesting data from allows for you to specify a timezone, give it UTC-0 as the desired format, and then you won&apos;t need to offset the data it returns before passing to GDS.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Gotcha: Promises, async/await&lt;/h2&gt;
&lt;p&gt;Here is the deal; yes, you can &lt;em&gt;actually&lt;/em&gt; use promises and async/await inside Google Apps Script (and thus Google Data Studio connect0rs), but, &lt;strong&gt;you probably don&apos;t want to.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The issue is that pretty much all required functions are synchronous in nature, and are expecting to be returned a non-promise value. For example, if you are wrapping API requests in promises, there is no way for you to return the value inside the promise to getData, and then return it to GDS. You can&apos;t make getData async to use await inside it, so you might be tempted to write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getData(request){
    let apiResponse;
    myApiPromise.then((result)=&gt;{
        apiResponse = result;
    });
    return apiResponse;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...but in this case, your promise would start to execute, and then while it is still executing, your return statement would hit the return line and instantly return &quot;undefined&quot; to GDS. This would actually trigger a &quot;column number does not match request&quot; error (see &lt;a href=&quot;#number_of_columns_error&quot;&gt;this gotcha item&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;If you insist on using promises, there are just a few steps to take to get them working.&lt;/p&gt;
&lt;ol&gt;
	&lt;li&gt;Add the &lt;a href=&quot;https://github.com/stefanpenner/es6-promise&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;es6-promise polyfill&lt;/a&gt; to your project. The easiest way is to simply copy and paste from &lt;a href=&quot;https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the minified source&lt;/a&gt; into an Apps Script code tab.&lt;/li&gt;
	&lt;li&gt;Since es6-promise relies on setTimeout, which does not exist in the GAS environment, you need to manually polyfill that as well. Copy and paste this code into your project:
&lt;ol&gt;
	&lt;li&gt;
&lt;pre&gt;&lt;code&gt;function setTimeout(cb, ms) {
    Utilities.sleep(ms);
    cb();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
	&lt;li&gt;Done! You should now be able to use polyfill and async/await just like normal! Just don&apos;t use them in places where they are not expected.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;Gotcha: The order of requested fields&lt;/h2&gt;
&lt;h3&gt;FIXED! You can now ignore this section.&lt;/h3&gt;
&lt;p&gt;This issue has been addressed and actually fixed by GDS :)&lt;/p&gt;
&lt;p&gt;You can find their PR mentioning a patch &lt;a href=&quot;https://github.com/googledatastudio/community-connectors/pull/255&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&apos;ll leave the below section for historical reasons, or if this issue ever resurfaces.&lt;/p&gt;
&lt;h3&gt;Previously encountered issue:&lt;/h3&gt;
&lt;p&gt;When GDS requests a set of data, there are three places where field order has the potential to change; in the request.fields, in the order that you return schema in the getData() return object schema property, and the order you return columns in the return object rows-&gt;values array. The first one does not really matter, but what does matter is that your schema field order and rows column order match, or else it will throw an error, usually &quot;Failed to fetch data from the underlying data set&quot;.&lt;/p&gt;
&lt;p&gt;Here is the &quot;gotcha&quot;; the way that Google recommends you build schema in response to a getData request is actually very likely to cause an issue with this. They recommend that you use getFields().forIds(), and pass in the array of field IDs that were requested by GDS. The catch? This is not documented, but I caught forIds() returning fields in an order that did not match the order of the array passed to it!&lt;/p&gt;
&lt;p&gt;Here is an example. Take a look at this code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getData(request) {
    // Raw request field ids - [&apos;day&apos;,&apos;time&apos;,&apos;cost&apos;,...]
    var requestedFieldIds = request.fields.map(function (field) { return field.name; });
    // Should return special fields obj collection
    var requestedFields = getFields().forIds(requestedFieldIds);
    // Map back to an array of field ids - [&apos;day&apos;,&apos;time&apos;,&apos;cost&apos;,...]
    var requestedFieldsMappedBackToIds = requestedFields.asArray().map(function (field) { return field.getId(); });
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;myConsole.log({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;Raw Request Ids&quot;: requestedFieldIds,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;GDS getFields().forIds() mapped back to IDs&quot;: requestedFieldsMappedBackToIds&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// ... rest of getData() left out&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;One might assume that both arrays of field IDs should be identical in my console.log() result. After all, to generate the second array, the first ordered array was used as the input. But...&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;Raw Request Ids&quot;: [
        &quot;billableMoneyTotal&quot;,
        &quot;entryDescription&quot;,
        &quot;time&quot;
    ],
    &quot;GDS getFields().forIds() mapped back to IDs&quot;: [
        &quot;entryDescription&quot;,
        &quot;billableMoneyTotal&quot;,
        &quot;time&quot;
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Argh! forIds() randomly swapped the order of two of the columns! Because I was using the variable requestedFieldIds to generate a response to getData() in later code, my connector was failing because my schema had the flipped order, but my rows did not. Definitely something to be aware of (and would be great if Google could document somewhere). So basically, the rule of thumb should be to not rely on the order of fields/columns requested in getData(request), but instead focus on your result.schema and rows.rows[{values[]}] columns matching.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Gotcha: Unhelpful errors&lt;/h2&gt;
&lt;h3&gt;&lt;a id=&quot;number_of_columns_error&quot;&gt;&lt;/a&gt;&quot;The number of columns received in the data returned from the community connector does not match the number of columns requested by Data Studio&quot;&lt;/h3&gt;
&lt;p&gt;Despite this error being one that you are most likely to encounter, there is very little detail about it on the internet, and virtually none from Google&apos;s own documentation. &lt;a href=&quot;https://stackoverflow.com/questions/47127604/datastudio-returns-random-error-id-when-using-custom-connector?rq=1&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;This StackOverflow question&lt;/a&gt; is one of the few search results (although maybe now this page will come up!).&lt;/p&gt;
&lt;p&gt;This error message is directly linked to whatever you are returning from &quot;getData()&quot;. As the error text would imply, Google is saying that the number of columns returned by your code in getData does not match the number of columns that it requested, which you are supposed to read by checking the contents of the &quot;request&quot;, the only argument to getData.&lt;/p&gt;
&lt;p&gt;Let&apos;s walk through an example scenario. If I use &quot;console.log(request)&quot; inside &quot;getData(request)&quot;, this is what the request object looks like in our example case:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;configParams&quot;: {
        &quot;workspaceId&quot;: &quot;9234225&quot;
    },
    &quot;dateRange&quot;: {
        &quot;endDate&quot;: &quot;2019-06-29&quot;,
        &quot;startDate&quot;: &quot;2019-06-29&quot;
    },
    &quot;fields&quot;: [
        {
            &quot;name&quot;: &quot;day&quot;
        },
        {
            &quot;name&quot;: &quot;time&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important thing to note in the above request from GDS is that there are exactly &lt;strong&gt;two&lt;/strong&gt; fields requested, &quot;day&quot; and &quot;time&quot;.  Now, in our return object, both returnObject.schema and returnObject.rows[x].values, there should be exactly two columns returned by our code. Here is an example that would fail with the &quot;number of columns does not match&quot; error for the above request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getData(request){
    return {
        &quot;cachedData&quot;: false,
        &quot;schema&quot;: [
            {
                &quot;dataType&quot;: &quot;STRING&quot;,
                &quot;name&quot;: &quot;day&quot;,
                &quot;label&quot;: &quot;Date&quot;,
                &quot;semantics&quot;: {
                    &quot;conceptType&quot;: &quot;DIMENSION&quot;,
                    &quot;semanticType&quot;: &quot;YEAR_MONTH_DAY&quot;
                }
            },
            {
                &quot;dataType&quot;:&quot;STRING&quot;,
                &quot;name&quot;:&quot;entryDescription&quot;,
                &quot;label&quot;:&quot;Entry Description / Title&quot;,
                &quot;semantics&quot;: {
                    &quot;conceptType&quot;:&quot;DIMENSION&quot;,
                    &quot;semanticType&quot;:&quot;TEXT&quot;
                }
            },
            {
                &quot;defaultAggregationType&quot;: &quot;SUM&quot;,
                &quot;dataType&quot;: &quot;STRING&quot;,
                &quot;name&quot;: &quot;time&quot;,
                &quot;description&quot;: &quot;Logged Time&quot;,
                &quot;label&quot;: &quot;Time&quot;,
                &quot;semantics&quot;: {
                    &quot;isReaggregatable&quot;: true,
                    &quot;conceptType&quot;: &quot;METRIC&quot;,
                    &quot;semanticType&quot;: &quot;DURATION&quot;
                }
            }
        ],
        &quot;rows&quot;: [
            {
                &quot;values&quot;: [
                    &quot;20190629&quot;,
                    &quot;Entry Alpha&quot;,
                    &quot;1245&quot;
                ]
            },
            {
                &quot;values&quot;: [
                    &quot;20190629&quot;,
                    &quot;Entry Bravo&quot;,
                    &quot;1981&quot;
                ]
            },
            {
                &quot;values&quot;: [
                    &quot;20190629&quot;,
                    &quot;Entry Charlie&quot;,
                    &quot;1570&quot;
                ]
            }
        ]
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Did you spot the error? I returned an extra field, &quot;entryDescription&quot;, which was not requested! Here is a different returned object that should satisfy the request and make the error go away.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getData(request){
    return {
        &quot;cachedData&quot;: false,
        &quot;schema&quot;: [
            {
                &quot;dataType&quot;: &quot;STRING&quot;,
                &quot;name&quot;: &quot;day&quot;,
                &quot;label&quot;: &quot;Date&quot;,
                &quot;semantics&quot;: {
                    &quot;conceptType&quot;: &quot;DIMENSION&quot;,
                    &quot;semanticType&quot;: &quot;YEAR_MONTH_DAY&quot;
                }
            },
            {
                &quot;defaultAggregationType&quot;: &quot;SUM&quot;,
                &quot;dataType&quot;: &quot;STRING&quot;,
                &quot;name&quot;: &quot;time&quot;,
                &quot;description&quot;: &quot;Logged Time&quot;,
                &quot;label&quot;: &quot;Time&quot;,
                &quot;semantics&quot;: {
                    &quot;isReaggregatable&quot;: true,
                    &quot;conceptType&quot;: &quot;METRIC&quot;,
                    &quot;semanticType&quot;: &quot;DURATION&quot;
                }
            }
        ],
        &quot;rows&quot;: [
            {
                &quot;values&quot;: [
                    &quot;20190629&quot;,
                    &quot;1245&quot;
                ]
            },
            {
                &quot;values&quot;: [
                    &quot;20190629&quot;,
                    &quot;1981&quot;
                ]
            },
            {
                &quot;values&quot;: [
                    &quot;20190629&quot;,
                    &quot;1570&quot;
                ]
            }
        ]
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, obviously, you would not want to be blindly returning hardcoded objects like this; your getData function should inspect every request, check what fields are needed, and dynamically construct the proper return object based on those fields. As such, your getData function is likely to be the most complicated method in your project&lt;/p&gt;
&lt;h4&gt;If the above did not solve your problem:&lt;/h4&gt;
&lt;p&gt;Now, the reason why I put this error in the &quot;Gotcha&quot; section is that the above example is &lt;em&gt;usually&lt;/em&gt; why this error appears, but it will actually appear if getData() returns &lt;em&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;ANYTHING other than what matches the request&lt;/strong&gt;&lt;/span&gt;&lt;/em&gt;. What I mean by this, is that this code will trigger the error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getData(request){
    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Although Google&apos;s error about the column number not matching is &lt;em&gt;technically&lt;/em&gt; correct in this case (0 columns !== number &gt; 0 in request), it is not helpful. They really should have a separate error for when getData returns a completely malformed response, that has 0 columns, is not an object, is lacking the &quot;rows&quot; object property, etc. Something like &quot;getData response does not match expected format&quot;.&lt;/p&gt;
&lt;p&gt;The above code is an extremely simplified example; I actually made this mistake myself, and here are a bunch of ways you can end up accidentally returning the wrong thing in getData and triggering this error:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Using promises or other async code in getData and trying to return from inside them. GDS will not see the final result, even if your promise succeeds, and it will see null/undefined/void as the returned object&lt;/li&gt;
	&lt;li&gt;Using a bunch of if/else blocks to return different objects based on the request, but forgetting to have a &quot;default&quot; or missing a scenario not covered by your if/else blocks&lt;/li&gt;
	&lt;li&gt;Not returning due to an error case (maybe in a try/catch set of blocks), but not throwing an exception to replace the &quot;column number&quot; error.
&lt;ul&gt;
	&lt;li&gt;You should use &quot;cc.newUserError()&quot; to throw a user-facing error. See &lt;a href=&quot;https://developers.google.com/datastudio/connector/error-handling&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the Google Data Studio guide on handling exceptions&lt;/a&gt; for details.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Gotcha: Publishing and sharing your connector&lt;/h2&gt;
&lt;p&gt;So, your connector is now functional and you are ready to share it with others. Deploying to your own account has been as easy a few clicks, so publishing and sharing the connector with others should be relatively straightforward too, right? Not so fast...&lt;/p&gt;
&lt;h3&gt;&quot;Open Source&quot; Community Connector vs Partner Connector&lt;/h3&gt;
&lt;p&gt;The first thing to note is that GDS offers two very distinct paths forward for &quot;publishing&quot; your connector in a way that it will show up in the public connector gallery (&lt;a href=&quot;https://datastudio.google.com/data&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;). A table comparing these two options can be found on the &lt;a href=&quot;https://developers.google.com/datastudio/connector/publish-connector&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;Publishing Overview&quot; connector guide page&lt;/a&gt;. However, I would like to add some additional commentary here that I feel obligated to share.&lt;/p&gt;
&lt;h4&gt;Appsscript.json sources array - requirement for both options&lt;/h4&gt;
&lt;p&gt;Regardless of which publishing option you pick, there is a requirement that is easy to miss at first glance. In your appsscript.json meta file, there is a property called &quot;sources&quot;, which should have an array of strings corresponding to various sources that your connector uses. The catch here is that you should actually be only using source strings that exist as part of a source enum, which is maintained in a separate repository, the &quot;&lt;a href=&quot;https://github.com/googledatastudio/ds-data-registry&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Data Registry Repository&lt;/a&gt;&quot;. So if you are building a connector that uses a brand new source that no other connector uses, you will need to open a PR into that repo to add the new source, &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;before&lt;/strong&gt;&lt;/span&gt; you publish your connector. See &lt;a href=&quot;https://developers.google.com/datastudio/connector/manifest#sources&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this page&lt;/a&gt; for details.&lt;/p&gt;
&lt;h4&gt;The &quot;Open Source&quot; Community Connector Option is... unusual...&lt;/h4&gt;
&lt;p&gt;First, you can find the full list of requirements for publishing the connector as an open source community connector &lt;a href=&quot;https://developers.google.com/datastudio/connector/oscc-requirements&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;. What I would like to add, is that it is a little odd how this process works.&lt;/p&gt;
&lt;p&gt;Community Connectors are maintained in a single mono-repo, &lt;a href=&quot;https://github.com/googledatastudio/community-connectors&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;googledatastudio/community-connectors&lt;/a&gt;, which are apparently synced across automatically to the connector gallery. To add your connector, you would first make sure it meets all the requirements, then make a pull-request, &lt;a href=&quot;https://github.com/googledatastudio/community-connectors/pull/208&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;like this one&lt;/a&gt;, to add your connectors code to the repo. It also looks like one of the requirements to get your PR approved is to have signed the Google CLA (Contributor License Agreement).&lt;/p&gt;
&lt;p&gt;Once the PR is approved, and your connector is in their repo, Google will start handling pretty much all parts of your connector; they are responsible for deployments, users are instructed to open issues with the connector on their repo instead of yours, and the only way to update the code is to make a new PR against the repo and wait for Google to merge and then redeploy. Another strange thing is that this whole workflow &lt;a href=&quot;https://github.com/googledatastudio/community-connectors/graphs/contributors&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;seems to be currently handled by just one Google employee&lt;/a&gt; (&quot;diminishedprime&quot;), but it looks like he is doing an impressive job responding to all the PRs and even taking time to give pointers to devs who are missing things on their PRs. Still, it seems like Google can afford to lend some more employees to cover the GDS connector repo.&lt;/p&gt;
&lt;p&gt;The main downside to this publishing route is that you are relinquishing a huge amount of control to Google; they control the deploys, code updates, or could &lt;a href=&quot;https://github.com/googledatastudio/community-connectors/issues/123&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;remove your connector entirely&lt;/a&gt; if you fail to update it to address issues. It looks like this might not always be apparent to everyone, as &lt;a href=&quot;https://github.com/googledatastudio/community-connectors/pull/208&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Google has taken to warning devs of new PRs&lt;/a&gt; about the downside to publishing as a community connector:&lt;/p&gt;
&lt;blockquote&gt;This connector might be better served as a partner connector. Please see Publish A Community Connector for an outline of why you might pick open-source vs Partner. &lt;br&gt;
&lt;br&gt;
We can certainly merge this is an an open-source connector, but keep in mind that our support for this is limited. &lt;br&gt;
&lt;br&gt;
If you need to make a fix/update to the connector, you&apos;ll have to make a PR to this repo and we can only review those as we have time. If (potentially) long delays are okay with you (and your customers), we can move ahead with the review, but our guidance is anything business critical should go through the partner-path, or share deployment links directly.&lt;/blockquote&gt;
&lt;h4&gt;Partner Connector Option&lt;/h4&gt;
&lt;p&gt;The partner connection option is more straightforward than the Open Source Community Connector option. The page of requirements and steps to take can be found &lt;a href=&quot;https://developers.google.com/datastudio/connector/pscc-requirements&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;What about private publishing?&lt;/h3&gt;
&lt;p&gt;If you have built a connector that you just want to share with a few other people (or maybe internally at the company you work for), and not publish to the connector gallery, you might be tempted to just copy and paste the install link from the &quot;deploy from manifest&quot; popup in Apps Script editor. However, if you do this after starting from the default Apps Script environment, and send it to another user, they will likely get this error when they try to open the link:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The linked connectorId was invalid. Check the connectorId and try again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OK, so how do we share a connector privately with other users? You might try next having the other use try adding the connector by ID instead of hyperlink, by going to the &lt;a href=&quot;https://datastudio.google.com/datasources/create&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;select connector&quot; page&lt;/a&gt; and then hitting the &quot;DEVELOPERS&quot; button on the upper right and pasting your deployment ID. But, if they did that, they would likely get this message:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The connector manifest could not be retrieved or is invalid. Check the connector and try again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hmm. Luckily &lt;a href=&quot;https://www.en.advertisercommunity.com/t5/Data-Studio/quot-Connector-ID-invalid-quot-error-when-trying-to-run-my-Head/td-p/1834210&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this Google community post&lt;/a&gt; points us in the right direction. For both these errors, the issue is the permissions setting on the actual Apps Script project code. In order for other uses to &quot;see&quot; our manifest file, and actually load the connector, the permissions for the connector project needs to be set to give those users &quot;view&quot; privileges, at a minimum. You can change sharing permissions by clicking the &quot;share&quot; button in the upper right of the Apps Script IDE, or going to &quot;File -&gt; Share&quot;.&lt;/p&gt;
&lt;p&gt;If you work at a company, and want to keep the connector internal, you could set the permission to share with &quot;people at this organization&quot;. Otherwise, you should set it to &quot;Anyone who has the link can view&quot;, or, individually add the email addresses of everyone you want to share the connector with, and give them view access. Note that giving users view access technically means that they can view the source code of the connector, but they would need to know the URL of the script, whereas you will be sharing a deployment ID or deployment URL with them.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Share-Data-Studio-Connector-With-Anyone-With-Link-View-Access.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-624&quot; src=&quot;/media/Google-Share-Data-Studio-Connector-With-Anyone-With-Link-View-Access.png&quot; alt=&quot;Google - Share Data Studio Connector With Anyone With Link - View Access&quot; width=&quot;432&quot; height=&quot;334&quot;&gt;&lt;/a&gt;&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Sheets: UNICODE() Char Code to UTF-8, Decimal and Binary</title><link>https://joshuatz.com/posts/2019/google-sheets-unicode-char-code-to-utf-8-decimal-and-binary/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/google-sheets-unicode-char-code-to-utf-8-decimal-and-binary/</guid><description>How to get the UTF-8 Character Code (aka codepoint) for a given character in Google Sheets. Custom formula converts from UTF-16 decimal to UTF-8.</description><pubDate>Tue, 25 Jun 2019 23:58:28 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD (but some conversion efforts on 2/28/2021 during update) --&gt;
&lt;p&gt;Warning: This post is going to get into the &quot;dangerously nerdy&quot; territory :)&lt;/p&gt;
&lt;h2&gt;Quick links to solution:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#char_to_utf8_binary&quot;&gt;Convert to UTF-8 Binary&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Convert to UTF-8 Decimal Char Code
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#convert_to_utf8_decimal_formula&quot;&gt;Pure formula&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#convert_to_utf8_decimal_script_formula&quot;&gt;Custom Script-based Formula&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The issue - Google Sheets does not use UTF-8&lt;/h2&gt;
&lt;p&gt;I recently ran into a head-scratcher of an issue with Google Sheets, where a formula that used the output of the &lt;a href=&quot;https://support.google.com/docs/answer/9149523&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;UNICODE()&lt;/a&gt; and/or &lt;a href=&quot;https://support.google.com/docs/answer/3094122&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CODE()&lt;/a&gt; functions was not matching with the expected value. After a bit of digging, I discovered that, although this is not really documented, most of the Google Sheets functions use UTF-32 (UTF-32BE) as the encoding scheme, whereas I was expecting UTF-8. This is made even more confusing by the fact that the Apps Script part of Google Sheets &lt;a href=&quot;https://developers.google.com/apps-script/reference/utilities/charset&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;has an enum specifically for the UTF-8 charset&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To show this more clearly, here is table directly from Google Sheets, that show for various input characters, the &lt;code&gt;CODE()&lt;/code&gt; function always returns the &lt;code&gt;UTF-32BE&lt;/code&gt; value:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Sheets-Unicode-Functions-Use-UTF-32BE.png&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto; text-align:center;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/Google-Sheets-Unicode-Functions-Use-UTF-32BE.png&quot; alt=&quot;Screenshot showing that, for a variety of single character inputs, the Sheets UNICODE() functions all return the UTF-32BE value for each character.&quot; style=&quot;width:100%;max-width: 786px;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Why is this a problem? Well, most of the time it is not; as long as you use the same encoding scheme on both ends (input and output) it really doesn&apos;t matter. However, when you are feeding the result of UNICODE() into something where you don&apos;t control the decoder part, it becomes an issue. To be specific, I was running into this with writing a custom Base64 encoder function (see full details on that &lt;a href=&quot;https://joshuatz.com/posts/2019/google-sheets-quick-start-with-base64/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Usually, when people talk about converting text to Base64, without even saying it, the implication is that we are going to be using UTF-8 as the encoding scheme. In fact, the default charset used in Apps Script with Utilities.base64Decode() is UTF-8, and specifying UTF-32 for decoding is&lt;strong&gt; not an option!&lt;/strong&gt; If you pull up various online &quot;text to base64&quot; converters online, chances are that 99% of them use either plain ASCII or UTF-8 for the encoding/decoding of text.&lt;/p&gt;
&lt;p&gt;In my case, I was feeding the result of UNICODE() into my base64 generator, which was causing it to spit out a base64 encoded string that could only be properly decoded with a decoder that uses UTF-32, which most decoders do not use.&lt;/p&gt;
&lt;h3&gt;Sidenote: JavaScript suffers the same issue!&lt;/h3&gt;
&lt;p&gt;This might surprise some people, but most JS engines do not use UTF-8 as the encoding scheme for strings - they use UTF-16!!! Similar to how I tested Google Sheets, this is easy to verify by checking the char code of a non-ASCII character:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Chrome-UTF16-Charcode-Example.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-593&quot; src=&quot;/media/Google-Chrome-UTF16-Charcode-Example.png&quot; alt=&quot;Google Chrome - UTF16 Charcode Example&quot; width=&quot;1487&quot; height=&quot;565&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And guess where this gets special mention... &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the MDN page on base64&lt;/a&gt;! I am not alone in realizing the UTF-* issue with Base64!&lt;/p&gt;
&lt;h2&gt;Why is UTF-8 so different?&lt;/h2&gt;
&lt;p&gt;The reason why UTF-8 is so different, and also why it is so popular, is because it is a variable length encoding scheme, with a minimum length of just one byte, and a maximum of 4 bytes wide. In comparison, UTF-16 is also variable, but has a fixed minimum of 2 bytes in width, and UTF-32 is not variable, and fixed at 4 bytes in width. The variable nature of UTF-8 means that each byte also has to have information in it on whether or not it is the last byte, which is the reason why you can&apos;t just plug the binary from a 2-byte-wide UTF-8 char into UTF-32, and vice-versa.&lt;/p&gt;
&lt;h2&gt;The solutions!&lt;/h2&gt;
&lt;p&gt;There is a lot to this, so I’m going to break down my overall solution (Text -&gt; UTF-16 Unicode Decimal -&gt; UTF-8 Decimal -&gt; UTF-8 Binary) into smaller chunks, in case you need just one part of it.&lt;/p&gt;
&lt;h3 id=&quot;google-sheets-text-character-to-utf-8-array&quot;&gt;Google Sheets: Text Character to UTF-8 Array&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;This section talks about converting UTF-16 to UTF-8. Originally, I thought Sheets was using UTf-16, but I am now aware it is UTF-32BE. However, the approach below still seems to work.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My starting point for this project was to try and convert a single UTF-16 charcode/codepoint to a UTF-8 codepoint. Luckily, the MDN page on base64 encoding and decoding &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#Solution_2_%E2%80%93_JavaScript&amp;#x27;s_UTF-16_%3E_UTF-8_%3E_base64&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;had a very clean Javascript function&lt;/a&gt; I could use as a base.&lt;/p&gt;
&lt;p&gt;Here is the relevant part of their Javascript code, that converts a UTF8 charcode to UTF-16:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sDOMStr.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;charCodeAt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(nChrIdx);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    /* one byte */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nChr;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x800&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    /* two bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 192&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x10000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    /* three bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 224&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x200000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    /* four bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 240&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 18&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x4000000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    /* five bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 248&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 24&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 18&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; /* if (nChr &amp;#x3C;= 0x7fffffff) */&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    /* six bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 252&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 24&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 18&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, here it is replicated in Google Sheets, as a formula:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;=IF(UNICODE(A2)&amp;#x3C;128,UNICODE(A2),IF(UNICODE(A2)&amp;#x3C;2048,{(192+BITRSHIFT(UNICODE(A2),6)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;65536,{(224+BITRSHIFT(UNICODE(A2),12)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;2097152,{(240+BITRSHIFT(UNICODE(A2),18)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;67108864,{(248+BITRSHIFT(UNICODE(A2),24)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},{(248+BITRSHIFT(UNICODE(A2),30)),(128+BITAND(BITRSHIFT(UNICODE(A2),24),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))})))))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, if Google Sheets doesn’t like the nested IFS returning arrays of different sizes, this slightly longer formula uses SPLIT() to produce the arrays:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;=IF(UNICODE(A2)&amp;#x3C;128,UNICODE(A2),IF(UNICODE(A2)&amp;#x3C;2048,SPLIT(JOIN(&quot;|&quot;,(192+BITRSHIFT(UNICODE(A2),6)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;),IF(UNICODE(A2)&amp;#x3C;65536,SPLIT(JOIN(&quot;|&quot;,(224+BITRSHIFT(UNICODE(A2),12)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;),IF(UNICODE(A2)&amp;#x3C;2097152,SPLIT(JOIN(&quot;|&quot;,(240+BITRSHIFT(UNICODE(A2),18)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;),IF(UNICODE(A2)&amp;#x3C;67108864,SPLIT(JOIN(&quot;|&quot;,(248+BITRSHIFT(UNICODE(A2),24)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;),SPLIT(JOIN(&quot;|&quot;,(248+BITRSHIFT(UNICODE(A2),30)),(128+BITAND(BITRSHIFT(UNICODE(A2),24),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;))))))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is confirmation that it works:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Sheets-Text-to-UTF-8-Decimal-Array.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-594&quot; src=&quot;/media/Google-Sheets-Text-to-UTF-8-Decimal-Array.png&quot; alt=&quot;Google Sheets - Text to UTF-8 Decimal Array&quot; width=&quot;1546&quot; height=&quot;525&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;char_to_utf8_binary&quot;&gt;&lt;/a&gt;Google Sheets: Text to UTF-8 Binary&lt;/h3&gt;
&lt;p&gt;The formula above is cool, but it spits out the code points as an array, chunked in bytes, represented in decimal. What if we want it in binary? And how about as a single cell instead of as an array? That would be neat!&lt;/p&gt;
&lt;p&gt;OK! Let&apos;s do it! First, lets convert the decimal array into binary. To do that, the easiest way will be to apply the DEC2BIN() function to each element of the array, converting from decimal to binary, which can be done by wrapping the above formula in =ARRAYFORMULA(DEC2BIN(...)). Easy!&lt;/p&gt;
&lt;p&gt;And if we want it as a single cell output instead of an array? Just use JOIN()! Here is the final massive formula, to convert a text cell into UTF-8 binary:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;=JOIN(&quot;&quot;,&quot;&quot;,ARRAYFORMULA(DEC2BIN(IF(UNICODE(A2)&amp;#x3C;128,UNICODE(A2),IF(UNICODE(A2)&amp;#x3C;2048,SPLIT(JOIN(&quot;|&quot;,(192+BITRSHIFT(UNICODE(A2),6)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;),IF(UNICODE(A2)&amp;#x3C;65536,SPLIT(JOIN(&quot;|&quot;,(224+BITRSHIFT(UNICODE(A2),12)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;),IF(UNICODE(A2)&amp;#x3C;2097152,SPLIT(JOIN(&quot;|&quot;,(240+BITRSHIFT(UNICODE(A2),18)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;),IF(UNICODE(A2)&amp;#x3C;67108864,SPLIT(JOIN(&quot;|&quot;,(248+BITRSHIFT(UNICODE(A2),24)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;),SPLIT(JOIN(&quot;|&quot;,(248+BITRSHIFT(UNICODE(A2),30)),(128+BITAND(BITRSHIFT(UNICODE(A2),24),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))),&quot;|&quot;)))))))))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id=&quot;convert_to_utf8_decimal_formula&quot;&gt;&lt;/a&gt;Google Sheets: Text to UTF-8 Decimal (Pure Formula)&lt;/h3&gt;
&lt;p&gt;Based on what we have so far, we should just be able to shove the final large binary block generated by the above formula in to &lt;a href=&quot;https://support.google.com/docs/answer/3092991&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;BIN2DEC()&lt;/a&gt;, right?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Error: Function BIN2DEC parameter 1 is too long. It is 24 characters; the maximum length is 10 characters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Uh-oh! Well, that is disappointing. But here is an alternative; instead of using &quot;character -&gt; utf-16 -&gt; utf-8 split into bytes as decimal -&gt; split into bytes as binary -&gt; binary&quot;, we can stop at the 3rd step, where we have an array of bytes as decimals, and add them together into a binary product. I have to admit that binary math is a bit beyond the scope of what I normally am comfortable with, but I managed to hack this together with &lt;a href=&quot;https://groups.google.com/d/msg/microsoft.public.excel.worksheet.functions/jQJuwCNL-6o/W7STyUB1iB0J&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a related answer&lt;/a&gt; I found for working around the BIN2DEC limit with Excel. Here is the final formula!&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;=SUMPRODUCT(--MID(JOIN(&quot;&quot;,&quot;&quot;,ARRAYFORMULA(DEC2BIN(IF(UNICODE(A2)&amp;#x3C;128,UNICODE(A2),IF(UNICODE(A2)&amp;#x3C;2048,{(192+BITRSHIFT(UNICODE(A2),6)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;65536,{(224+BITRSHIFT(UNICODE(A2),12)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;2097152,{(240+BITRSHIFT(UNICODE(A2),18)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;67108864,{(248+BITRSHIFT(UNICODE(A2),24)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},{(248+BITRSHIFT(UNICODE(A2),30)),(128+BITAND(BITRSHIFT(UNICODE(A2),24),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))})))))))),LEN(JOIN(&quot;&quot;,&quot;&quot;,ARRAYFORMULA(DEC2BIN(IF(UNICODE(A2)&amp;#x3C;128,UNICODE(A2),IF(UNICODE(A2)&amp;#x3C;2048,{(192+BITRSHIFT(UNICODE(A2),6)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;65536,{(224+BITRSHIFT(UNICODE(A2),12)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;2097152,{(240+BITRSHIFT(UNICODE(A2),18)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;67108864,{(248+BITRSHIFT(UNICODE(A2),24)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},{(248+BITRSHIFT(UNICODE(A2),30)),(128+BITAND(BITRSHIFT(UNICODE(A2),24),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))})))))))))+1-ROW(INDIRECT(&quot;1:&quot;&amp;#x26;LEN(JOIN(&quot;&quot;,&quot;&quot;,ARRAYFORMULA(DEC2BIN(IF(UNICODE(A2)&amp;#x3C;128,UNICODE(A2),IF(UNICODE(A2)&amp;#x3C;2048,{(192+BITRSHIFT(UNICODE(A2),6)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;65536,{(224+BITRSHIFT(UNICODE(A2),12)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;2097152,{(240+BITRSHIFT(UNICODE(A2),18)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;67108864,{(248+BITRSHIFT(UNICODE(A2),24)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},{(248+BITRSHIFT(UNICODE(A2),30)),(128+BITAND(BITRSHIFT(UNICODE(A2),24),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))}))))))))))),1),(2^(ROW(INDIRECT(&quot;1:&quot;&amp;#x26;LEN(JOIN(&quot;&quot;,&quot;&quot;,ARRAYFORMULA(DEC2BIN(IF(UNICODE(A2)&amp;#x3C;128,UNICODE(A2),IF(UNICODE(A2)&amp;#x3C;2048,{(192+BITRSHIFT(UNICODE(A2),6)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;65536,{(224+BITRSHIFT(UNICODE(A2),12)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;2097152,{(240+BITRSHIFT(UNICODE(A2),18)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},IF(UNICODE(A2)&amp;#x3C;67108864,{(248+BITRSHIFT(UNICODE(A2),24)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))},{(248+BITRSHIFT(UNICODE(A2),30)),(128+BITAND(BITRSHIFT(UNICODE(A2),24),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),18),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),12),63)),(128+BITAND(BITRSHIFT(UNICODE(A2),6),63)),(128+BITAND(UNICODE(A2),63))})))))))))))-1)))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is the confirmation it works:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Sheets-Text-to-UTF-8-Char-Code.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-599&quot; src=&quot;/media/Google-Sheets-Text-to-UTF-8-Char-Code.png&quot; alt=&quot;Google Sheets - Text to UTF-8 Char Code&quot; width=&quot;1267&quot; height=&quot;501&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Pretty neat, huh?&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;convert_to_utf8_decimal_script_formula&quot;&gt;&lt;/a&gt;Google Sheets: Text to UTF-8 Decimal (Javascript)&lt;/h3&gt;
&lt;p&gt;As a &quot;cleaner&quot; solution, that does not use a huge formula, we can take advantage of Google Sheets custom formulas, which we can setup with Javascript in &quot;Tools -&gt; Script editor&quot;. Here is the code to use:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; utf8Char&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; decArr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; strToUTF8Arr&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(input);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; binString &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; x &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; x &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; decArr.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; x&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        binString &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (decArr[x]).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; parseInt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(binString, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Adapted from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#Solution_2_%E2%80%93_JavaScript&apos;s_UTF-16_%3E_UTF-8_%3E_base64&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; strToUTF8Arr&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; aBytes, nChr, nStrLen &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, nArrLen &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nMapIdx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; nMapIdx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nStrLen; nMapIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;charCodeAt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(nMapIdx);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        nArrLen &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x80&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x800&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x10000&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x200000&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x4000000&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 5&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    aBytes &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; x &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; x &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nArrLen.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; x&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        aBytes.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(nArrLen[x] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nArrLen[x] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 256&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nArrLen[x]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nIdx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, nChrIdx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; nIdx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nArrLen; nChrIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; input.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;charCodeAt&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(nChrIdx);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            /* one byte */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nChr;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x800&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            /* two bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 192&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x10000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            /* three bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 224&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x200000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            /* four bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 240&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 18&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0x4000000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            /* five bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 248&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 24&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 18&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; /* if (nChr &amp;#x3C;= 0x7fffffff) */&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            /* six bytes */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 252&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 24&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 18&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            aBytes[nIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 128&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (nChr &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 63&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; aBytes;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simply copy and paste this code into the script editor, save, and reload your spreadsheet. After that, you can use:&lt;/p&gt;
&lt;pre&gt;=utf8Char(A2)&lt;/pre&gt;
&lt;p&gt;...and get back the UTF-8 charcode in decimal!&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;2/28/2021 Update: I previously thought that Sheets was using &lt;code&gt;UTF-16&lt;/code&gt;, but after a revisit to this post, I realized it is actually &lt;code&gt;UTF-32BE&lt;/code&gt;; the post has been updated to reflect this.&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Apps Script - Authorization in a cross-origin iframe</title><link>https://joshuatz.com/posts/2019/google-apps-script---authorization-in-a-cross-origin-iframe/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/google-apps-script---authorization-in-a-cross-origin-iframe/</guid><description>How to embed your Google Apps Script in an iframe when a user needs to authenticate it first before it can work. Solutions uses JSONP to inform parent page.</description><pubDate>Sun, 16 Jun 2019 00:14:10 GMT</pubDate><content:encoded>&lt;!-- @TODO - Reformat to Markdown --&gt;
&lt;p&gt;I&apos;ve noticed more than a few questions on StackOverflow that have to do with using Google Apps Script or other authenticated google embeds in combination with iframes, and it has prompted me to do some digging. First and foremost, if you want to publish a Google Apps Script that can render within an iframe, that is actually surprisingly simple. To avoid any X-Frame-Options errors, simply use &quot;&lt;a href=&quot;https://developers.google.com/apps-script/migration/iframe&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;HtmlService.XFrameOptionsMode.ALLOWALL&lt;/a&gt;&quot;. For example:&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;ul class=&quot;collapsible&quot;&gt;
	&lt;li class=&quot;active&quot;&gt;
&lt;div class=&quot;collapsible-header&quot;&gt;Code.gs&lt;/div&gt;
&lt;div class=&quot;collapsible-body&quot;&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
function doGet() {
    var template = HtmlService.createTemplateFromFile(&apos;output&apos;);
    return template.evaluate().setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
            &lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/li&gt;
	&lt;li&gt;
&lt;div class=&quot;collapsible-header&quot;&gt;output.html&lt;/div&gt;
&lt;div class=&quot;collapsible-body&quot;&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;
&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html lang=&quot;en&quot;&gt;
&lt;p&gt;&amp;#x3C;head&gt;
&amp;#x3C;meta charset=“UTF-8”&gt;
&amp;#x3C;meta name=“viewport” content=“width=device-width, initial-scale=1.0”&gt;
&amp;#x3C;meta http-equiv=“X-UA-Compatible” content=“ie=edge”&gt;
&amp;#x3C;title&gt;Document&amp;#x3C;/title&gt;
&amp;#x3C;/head&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;body&gt;
&amp;#x3C;p&gt;Hello World!&amp;#x3C;/p&gt;
&amp;#x3C;/body&gt;&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And then simply load it as the source of an iframe:&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&amp;#x3C;iframe src=&quot;https://script.google.com/macros/s/SCRIPT_ID/exec&quot;&gt;&lt;/p&gt;
&lt;p&gt;Note that the above will only work if&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;A) The script is set to be authorized by your own account (executing as yourself), not the users,&lt;/p&gt;
&lt;p style=&quot;padding-left: 80px;&quot;&gt;Or:&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;B) The script is set to be authorized by the end user (&quot;execute the app as user accessing the web app&quot;), but they are logged in and already authorized by the time they load the page the iframe embed is on.&lt;/p&gt;
&lt;h2&gt;Dealing with Authentication / Logged Out Users - Intro&lt;/h2&gt;
&lt;p&gt;If your script touches any other Google product (email, Google sheets, etc.) there is a very good chance you are using the user&apos;s authentication to perform actions on their behalf. If you are using your user&apos;s authorization, your &quot;deploy web app&quot; dialog should look like this:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Apps-Script-Authorizing-as-End-User-Deploy-as-Web-App.png&quot;&gt;&lt;img class=&quot;size-full wp-image-561 aligncenter&quot; src=&quot;/media/Google-Apps-Script-Authorizing-as-End-User-Deploy-as-Web-App.png&quot; alt=&quot;&quot; width=&quot;429&quot; height=&quot;455&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The problem with this, is that if you embed your App Script into an iframe, and a user visits the page it is embedded into and they are logged out of Google, then this happens:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Login-Page-Blocked-in-Iframe-X-Frame-Options-Deny.png&quot;&gt;&lt;img class=&quot;size-full wp-image-562 aligncenter&quot; src=&quot;/media/Google-Login-Page-Blocked-in-Iframe-X-Frame-Options-Deny.png&quot; alt=&quot;Google Login Page Blocked in Iframe - X-Frame-Options Deny&quot; width=&quot;368&quot; height=&quot;248&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you open your devtools when this happens, you will also see this descriptive error message:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div&gt;Refused to display &apos;https://accounts.google.com/ServiceLogin?passive=1209600&amp;#x26;continue=https...&apos; in a frame because it set &apos;X-Frame-Options&apos; to &apos;deny&apos;.&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is because Google realizes that your script is going to require an authenticated Google account to function, and redirects the iframe to the Google Login page. However, the Google Login page is set to not allow itself to be iframed into any site (thus the X-Frame-Options value of &quot;deny&quot;), so the browser blocks the iframe from loading the login page and errors out.&lt;/p&gt;
&lt;p&gt;Obviously, this leads a poor user experience; how is the end user supposed to know what the grey sad face means, or how they should proceed?&lt;/p&gt;
&lt;h3&gt;Easiest solution - warn in advance:&lt;/h3&gt;
&lt;p&gt;The simplest solution is to just add some sort of warning message above or near where the script is embedded. Something like: &quot;If the widget below appears as a grey box, please login to your Google account here [link to Google Login page] and then reload this page and accept permissions request.&quot;&lt;/p&gt;
&lt;h2&gt;Dealing with Logged Out Users with iframes - Advanced&lt;/h2&gt;
&lt;p&gt;Obviously, a lot of devs are not going to like just having a warning message about &quot;seeing a grey box appear&quot;, and even I would have to agree that there feels like there should be a better way. Is there a way to detect if a user is not logged in and is going to get redirected inside the iframe to the non-functioning login page? Can we preemptively redirect them? This is where things started to get a little complicated and I tried some creative approaches...&lt;/p&gt;
&lt;p&gt;If you don&apos;t care about what doesn&apos;t work, and are just looking for what I consider to be &quot;the solution&quot;, which I list last,  you can jump right to it by &lt;a href=&quot;#solution&quot;&gt;clicking here&lt;/a&gt;. Otherwise, read on to see all the ways I attempted this before getting to the solution!&lt;/p&gt;
&lt;h3&gt;Using postMessage()&lt;/h3&gt;
&lt;p&gt;One of my first ideas was to pass info about the success of the user getting authenticated, from the child iframe embedded App Script, to the parent window, which is wherever the widget is embedded. Although iframes are sandboxed so that the content can&apos;t be accessed from the parent (or vice-versa), &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;postMessage&lt;/a&gt; is a method you can use for passing small snippets of data back and forth. My idea was to have my App Script immediately send a message to the parent window saying something like &quot;Successfully loaded&quot;, and then the parent could start a timer, and if it does not receive that message within x # of seconds after the page loads, it knows the script got blocked from loading due to a login redirect. However, trying to implement even just the most basic of postMessage tests, I noticed none of the events were coming through, and I saw this in the console:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;dropping postMessage.. was from unexpected window&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What? What is that?&lt;/p&gt;
&lt;p&gt;Oh... So it looks like Google Apps Script &lt;strong&gt;double&lt;/strong&gt; iframes your content in. So really my custom HTML code is nested inside of Google proprietary wrapper code, which then loads into the iframe I actually embedded into my website. Google&apos;s custom wrapper iframe listens for postMessages, but only ones that match its own origin, so they are ignoring mine. Dang. I don&apos;t think there is an easy way around this. Let&apos;s try something else...&lt;/p&gt;
&lt;h3&gt;Create our own Ajax endpoint&lt;/h3&gt;
&lt;p&gt;My next idea: instead of only returning HTML, what if we set up our app script so it could respond with JSON in response to an ajax request? Then, from our website, we can use fetch(), and if the response is anything other than the JSON we are expecting, such as a redirect, we know the user has not been authenticated. Let&apos;s set this up:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function doGet(e) {
    if (e.queryString &amp;#x26;&amp;#x26; e.queryString.indexOf(&apos;ajaxCheck&apos;)!==-1){
        // Respond back! With JSON.
        var response = {
            success: true
        }
        return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
    }
    else {
        // Display HTML in iframe
        var email = Session.getActiveUser().getEmail();
        var template = HtmlService.createTemplateFromFile(&apos;output&apos;);
        template.authedEmail = email;
        return template.evaluate().setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, we can even reuse the same script, but change the response type (HTML vs JSON) based on the querystring used to request the script. This seems like it might work, but in practice...&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Apps-Script-Automatic-302-Redirect-to-Return-JSON-Content.png&quot;&gt;&lt;img class=&quot;size-full wp-image-564 aligncenter&quot; src=&quot;/media/Google-Apps-Script-Automatic-302-Redirect-to-Return-JSON-Content.png&quot; alt=&quot;Google Apps Script - Automatic 302 Redirect to Return JSON Content&quot; width=&quot;539&quot; height=&quot;93&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What the heck? The request redirected? Well, it looks like for some reason (likely to do with character escaping or something like that), Google will not serve JSON from our script &lt;em&gt;directly&lt;/em&gt;, instead it redirects the request to a shared common endpoint for echoing content, but with a unique querystring to tell the server what to serve:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;https://script.googleusercontent.com/macros/echo?user_content_key=[unique-key]&amp;#x26;lib=[?]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OK, well this is another wrench in the works. This would not be a deal-breaker on its own, but this is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Access to fetch at &apos;https://script.google.com/macros/s/.../exec?ajaxCheck&apos; from origin &apos;https://joshuatz.com&apos; has been blocked by CORS policy: No &apos;Access-Control-Allow-Origin&apos; header is present on the requested resource. If an opaque response serves your needs, set the request&apos;s mode to &apos;no-cors&apos; to fetch the resource with CORS disabled.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We just can&apos;t catch a break! Apps Script is not sending back the proper CORs header, so our requests are being outright blocked. There is basically no workaround for this - if we set fetch to use a mode of &apos;no-cors&apos;, than all we get back is an opaque response, which really tells us nothing. We also can&apos;t use a CORs proxy, like &lt;a href=&quot;https://cors-anywhere.herokuapp.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;cors-anywhere&lt;/a&gt;, because we need to include credentials with our request, so Google can see if they are a logged in Google user or not, and that obviously won&apos;t work if the domains don&apos;t match.&lt;/p&gt;
&lt;p&gt;This is starting to get a little frustrating, but trying to get JSON responses working actually made me stumble onto something promising...&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;solution&quot;&gt;&lt;/a&gt;The Solution!!! Using &amp;#x3C;script&gt; tag with/without JSONP pattern&lt;/h2&gt;
&lt;p&gt;Script tags are not subject to the same CORs/CORB restrictions that are placed on AJAX, and I stumbled upon Google actually suggesting this method as a way to get script data into webpages &lt;a href=&quot;https://developers.google.com/apps-script/guides/content#serving_jsonp_in_web_pages&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;. They recommend using the JSONP pattern, but you could also use it to inject whatever JS content you want, which I&apos;ll explore as an option.&lt;/p&gt;
&lt;h3&gt;Option A) Recommended JSONP Pattern:&lt;/h3&gt;
&lt;p&gt;There are only a few unique things about the JSONP pattern and how it applies here:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;You must define a callback function that is a window global, that will receive the response from the app script&lt;/li&gt;
	&lt;li&gt;Pass the string literal name of the callback function as a querystring key/value pair&lt;/li&gt;
	&lt;li&gt;In your app script, return JS that will call the function by name and pass the results&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can check out the example from the Google docs &lt;a href=&quot;https://developers.google.com/apps-script/guides/content#serving_jsonp_in_web_pages&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;, or I have prepared a complete working solution to the original problem we were trying to solve; detecting a completely logged out user and redirecting. My solution basically works as follows:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;In HTML, have an overlay div that is positioned over the iframe embed, with text that tells user they need to authenticate, and button to do so
&lt;ul&gt;
	&lt;li&gt;Define callback function for JSONP that hides the &quot;auth required&quot; overlay if the script successfully executes&lt;/li&gt;
	&lt;li&gt;Load the script through a &amp;#x3C;script&gt; tag and pass my callback through the querystring&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Script checks querystring to see how it should respond
&lt;ul&gt;
	&lt;li&gt;If JSONP, it returns JS that calls the callback with the authed users email&lt;/li&gt;
	&lt;li&gt;If embed, returns success HTML to show in actual widget&lt;/li&gt;
	&lt;li&gt;If accessed directly, shows message about being able to close tab, since auth was successful.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is what the result looks like:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Apps-Script-Detecting-User-Authentication-Authorization-Login-for-Iframe-Embeds.png&quot;&gt;&lt;img class=&quot;size-full wp-image-573 aligncenter&quot; src=&quot;/media/Google-Apps-Script-Detecting-User-Authentication-Authorization-Login-for-Iframe-Embeds.png&quot; alt=&quot;Google Apps Script - Detecting User Authentication Authorization Login for Iframe Embeds&quot; width=&quot;1290&quot; height=&quot;694&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can see that on the left side, where I am in an incognito window and have not authorized the script, it detects so, and prompts me to authorize. In the window on the right, I am signed into Google and have authorized the script, so it hides the authorization prompt and displays the success HTML.&lt;/p&gt;
&lt;h4&gt;Minimal code to reproduce:&lt;/h4&gt;
&lt;p&gt;I&apos;ve uploaded the full code used to create the above demo, which includes both the Apps Script side and the embed side, to Github - &lt;a href=&quot;https://github.com/joshuatz/apps-script-authed-embed-demo&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;. If you don&apos;t feel like visiting that, the two most important snippets of code are below:&lt;/p&gt;
&lt;h5&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Embed HTML:&lt;/span&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Script loaded as iframe widget with fallback --&gt;
&amp;#x3C;div class=&quot;appsWidgetWrapper&quot;&gt;
    &amp;#x3C;iframe class=&quot;appsWidget&quot; src=&quot;https://script.google.com/macros/s/SCRIPT_ID/exec?embedIframe&quot;&gt;&amp;#x3C;/iframe&gt;
    &amp;#x3C;div class=&quot;loggedOut&quot;&gt;
        &amp;#x3C;div class=&quot;loggedOutContent&quot;&gt;
            &amp;#x3C;div class=&quot;loggedOutText&quot;&gt;You need to &quot;authorize&quot; this widget.&amp;#x3C;/div&gt;
            &amp;#x3C;button class=&quot;authButton&quot;&gt;Log In / Authorize&amp;#x3C;/button&gt;
        &amp;#x3C;/div&gt;
    &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;p&gt;&amp;#x3C;!— Define JSONP callback and authbutton redirect—&gt;
&amp;#x3C;script&gt;
function authSuccess(email){
console.log(email);
// Hide auth prompt overlay
document.querySelector(‘.loggedOut’).style.display = ‘none’;
}
document.querySelectorAll(‘.authButton’).forEach(function(elem){
elem.addEventListener(‘click’,function(evt){
var currentUrl = document.location.href;
var authPage = ‘&lt;a href=&quot;https://script.google.com/macros/s/SCRIPT_ID/exec?auth=true&amp;#x26;amp;redirect=&quot;&gt;https://script.google.com/macros/s/SCRIPT_ID/exec?auth=true&amp;#x26;amp;redirect=&lt;/a&gt;’ + encodeURIComponent(currentUrl);
window.open(‘authPage’,‘_blank’);
});
});
&amp;#x3C;/script&gt;&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!— Fetch script as JSONP with callback —&gt;
&amp;#x3C;script src=“&lt;a href=&quot;https://script.google.com/macros/s/SCRIPT_ID/exec?jsonpCallback=authSuccess%22&amp;#x26;gt;&amp;#x26;lt;/script&quot;&gt;https://script.google.com/macros/s/SCRIPT_ID/exec?jsonpCallback=authSuccess”&amp;#x26;gt;&amp;#x26;lt;/script&lt;/a&gt;&gt;&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h5&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;Code.gs&lt;/span&gt;&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function doGet(e) {
    var email = Session.getActiveUser().getEmail();
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if (e.queryString &amp;#x26;amp;&amp;#x26;amp; &apos;jsonpCallback&apos; in e.parameter){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // JSONP callback&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Get the string name of the callback function&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    var cbFnName = e.parameter[&apos;jsonpCallback&apos;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Prepare stringified JS that will get evaluated when called from &amp;#x26;lt;script&amp;#x26;gt;&amp;#x26;lt;/script&amp;#x26;gt; tag&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    var scriptText = &quot;window.&quot; + cbFnName + &quot;(&apos;&quot; + email + &quot;&apos;);&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Return proper MIME type for JS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return ContentService.createTextOutput(scriptText).setMimeType(ContentService.MimeType.JAVASCRIPT);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;else if (e.queryString &amp;#x26;amp;&amp;#x26;amp; (&apos;auth&apos; in e.parameter || &apos;redirect&apos; in e.parameter)){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Script was opened in order to auth in new tab&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    var rawHtml = &apos;&amp;#x26;lt;p&amp;#x26;gt;You have successfully authorized the widget. You can now close this tab and refresh the page you were previously on.&amp;#x26;lt;/p&amp;#x26;gt;&apos;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (&apos;redirect&apos; in e.parameter){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        rawHtml += &apos;&amp;#x26;lt;br/&amp;#x26;gt;&amp;#x26;lt;a href=&quot;&apos; + e.parameter[&apos;redirect&apos;] + &apos;&quot;&amp;#x26;gt;Previous Page&amp;#x26;lt;/a&amp;#x26;gt;&apos;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return HtmlService.createHtmlOutput(rawHtml);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Display HTML in iframe&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    var rawHtml = &quot;&amp;#x26;lt;h1&amp;#x26;gt;App Script successfully loaded in iframe!&amp;#x26;lt;/h1&amp;#x26;gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        + &quot;\n&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        + &quot;&amp;#x26;lt;h2&amp;#x26;gt;User&apos;s email used to authorize: &amp;#x26;lt;?= authedEmail ?&amp;#x26;gt;&amp;#x26;lt;/h2&amp;#x26;gt;&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    var template = HtmlService.createTemplate(rawHtml);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    template.authedEmail = email;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return template.evaluate().setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Option B) Inject whatever you want:&lt;/h3&gt;
&lt;p&gt;Since the browser does not know the difference between the response from Google Apps Script and any old Javascript file, there really doesn&apos;t have to be anything special about the JS code that we return; it just needs to be valid JS and match the MIME type. If we want, we can return all kinds of hardcoded values, functions, or whatever we want. Here is quick example that sets a global window variable to a boolean TRUE value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function doGet(e) {
    // Respond back with JS that sets global var
    var windowGlobalKey = &apos;userPassedAppsAuth&apos;
    // Create script content
    var scriptText = &quot;window[&apos;&quot; + windowGlobalKey + &quot;&apos;] = true;&quot;
    return ContentService.createTextOutput(scriptText).setMimeType(ContentService.MimeType.JAVASCRIPT);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you embed your apps script as a &amp;#x3C;script&gt; tag, in any code that runs after it loads, you could query &quot;window.userPassedAppsAuth&quot; and the result should be TRUE if the apps script successfully loaded and was authenticated. You could then take action based on that result, such as redirecting the user to login if the value is FALSE or undefined.&lt;/p&gt;
&lt;h2&gt;Another Solution: contentWindow.length&lt;/h2&gt;
&lt;p&gt;Thanks to &lt;a href=&quot;https://stackoverflow.com/a/63393108/11447682&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this S/O answer&lt;/a&gt;, I learned of another approach that can be used as a solution, which relies on a specific quirk of how Google Apps Scripts returns HTML, and proxied window properties exposed on iframes.&lt;/p&gt;
&lt;p&gt;For security purposes, most cross-origin Iframes block access to their content and properties from the parent window, but certain properties have their values proxied. One of those is &lt;code&gt;window.length&lt;/code&gt;, exposed to the parent window as &lt;code&gt;{iframeElement}.contentWindow.length&lt;/code&gt;. It is a numerical value, which corresponds to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/length&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the number of frames within that window&lt;/a&gt;. Although it is normally an annoyance, the fact that Google wraps returned HTML from our script in its own iframe gives us a (roundabout) way to check for successful authorization: when our script successfully loads, this value should be &lt;code&gt;1&lt;/code&gt;, and if it fails to load, for example when our user has not authorized it yet, it will be &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is the relevant code for this approach:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
// Give iframe some time to load, while re-checking
var retries = 5;
var attempts = 0;
var done = false;
function checkIfAuthed() {
    attempts++;
    console.log(`Checking if authed...`);
    var iframe = document.querySelector(&apos;.appsWidget&apos;);
    if (iframe.contentWindow.length) {
        // User has signed in, preventing x-frame deny issue
        // Hide auth prompt overlay
        document.querySelector(&apos;.loggedOut&apos;).style.display = &apos;none&apos;;
        done = true;
    } else {
        console.log(`iframe.contentWindow.length is falsy, user needs to auth`);
    }
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if (done || attempts &amp;#x26;gt;= retries) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    clearInterval(authChecker);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;}
window.authChecker = setInterval(checkIfAuthed, 200);
document.querySelectorAll(‘.authButton’).forEach(function(elem){
elem.addEventListener(‘click’,function(evt){
var currentUrl = document.location.href;
var authPage = ‘&lt;a href=&quot;https://script.google.com/macros/s/SCRIPT_ID/exec?auth=true&amp;#x26;amp;redirect=&quot;&gt;https://script.google.com/macros/s/SCRIPT_ID/exec?auth=true&amp;#x26;amp;redirect=&lt;/a&gt;’ + encodeURIComponent(currentUrl);
window.open(authPage,‘_blank’);
});
});
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Solutions that would work, but take a lot of effort:&lt;/h2&gt;
&lt;p&gt;Here are some solutions that would work for making sure a user gets redirected to Google login if they are logged out and visit your site with  your widget embedded. However, all of these would take some considerable time, skills, and possibly extra paid services, like paid hosting, to implement.&lt;/p&gt;
&lt;h3&gt;Random IDs + webhook to server&lt;/h3&gt;
&lt;p&gt;This idea work as follows: On the load of your webpage, you create a completely random string (maybe timestamp + random output) and append it as a query string to the iframe source that loads the App Script. In your app script, you retrieve it inside your doGet() function, and immediate use URLFetchApp to send it back to your website host. You would need to  have an endpoint set up that URLFetchApp could use, maybe something like &quot;example.com/authCallback.php&quot;.&lt;/p&gt;
&lt;p&gt;When Apps Script pings your endpoint, you save the random ID to a database table, where it can easily be looked up. You also expose your own internal endpoint, which can be queried with an ID to see if it exists in the table.&lt;/p&gt;
&lt;p&gt;Finally, back on your webpage, you have a short timeout delay, maybe 15 seconds. After the delay ends, you query the ID against your own database, to see if Apps Script has pinged you for that specific user yet. If it has not, you know that the script most likely failed due to the blocked login, and you can at that point redirect the user / display a popup / do whatever you want.&lt;/p&gt;
&lt;h3&gt;Integrate Google Sign-In to your website&lt;/h3&gt;
&lt;p&gt;This feels like a lame answer, because there is nothing unique about it, but I&apos;m guessing that if you were to use &lt;a href=&quot;https://developers.google.com/identity/sign-in/web/sign-in&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Google Sign-In for Web&lt;/a&gt; as a requirement to entering your site, their SDK would have some sort of method that would let you check to see if the current user is authed. Of course, this has all sorts of other issues, including yet another thing that your user has to agree to and be comfortable sharing permission with.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Checking out a Chrome Crash Report on Windows</title><link>https://joshuatz.com/posts/2019/checking-out-a-chrome-crash-report-on-windows/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/checking-out-a-chrome-crash-report-on-windows/</guid><description>How to look at the basics of a Chrome crash report on Windows, using WinDbg to view the minidump file, and how to enable logging on startup.</description><pubDate>Thu, 13 Jun 2019 05:58:54 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;After recently updating Chrome, I ran into certain sites hard-crashing the entire application on certain actions. I&apos;m not talking about the &quot;this tab has crashed&quot; message, I&apos;m talking acts-like-a-kernel-panic, no error shown, every single tab and entire app window exists type crash. Understandably, I got frustrated, but I never like being one of those people that just complains without knowing why, so I wanted to see if I could dig a little deeper. Trying to use Chrome&apos;s dev tools panel wasn&apos;t much help as, well... it crashed along with the browser whenever this happened. So next I looked to using Chrome&apos;s crash reports.&lt;/p&gt;
&lt;h2&gt;Using Chrome&apos;s Crash Reports:&lt;/h2&gt;
&lt;p&gt;A quick Google search pulled up &lt;a href=&quot;https://superuser.com/a/537849&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this SuperUser post&lt;/a&gt;, that says you need to enable the sharing of usage stats and crash reports with Google Corporate in order to use them, which I did not want to do. However, I noticed that even with that setting off, and and &lt;a href=&quot;chrome://crashes/&quot;&gt;chrome://crashes/&lt;/a&gt; showing &quot;Crash reporting is disabled.&quot;, the actual crash dump files were being generated and saved anyways. I found them in &quot;C:\Users\[USERNAME]\AppData\Local\Google\Chrome\User Data\Crashpad\reports&quot;, but your location may differ. They are saved as files with the &quot;DMP&quot; extension, which would indicate &quot;Dump files&quot;, but they are actually &quot;minidump&quot; files, which I confirmed by checking the magic bytes (first 6 bytes read as &quot;MDMP“§&quot;) and then referring to &lt;a href=&quot;https://chromium.googlesource.com/crashpad/crashpad/+/master/doc/overview_design.md&quot;&gt;the readme for Crashdump&lt;/a&gt; and &lt;a href=&quot;https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/getting_started_with_breakpad.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Breakpad&lt;/a&gt;, which are the Chromium tools that actually are responsible for saving and generating the files.&lt;/p&gt;
&lt;p&gt;(EDIT: &lt;a href=&quot;https://www.chromium.org/developers/crash-reports&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;This helpful page from Chromium on Crash Reports&lt;/a&gt; cleared this up more. Breakpad is what used to be used, but is getting replaced by Crashpad, which is now used by default on Windows and Mac.)&lt;/p&gt;
&lt;p&gt;Now how do we read these files? If you are a newbie to this part of software, like I am, you might be a little lost. The &lt;a href=&quot;http://www.chromium.org/developers/decoding-crash-dumps&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;page from Chromium on decoding crash dumps&lt;/a&gt; makes it kind of sound like this is impossible to do on Windows and requires very specialized Unix tools. However, this is not true; minidump is a standardized dump format, actually created by Microsoft for Windows specifically, and there is more than just one tool to analyze them. On Windows, the default tool to use is WinDbg, which you might already have on your computer if you are a developer; it is available as part of the Windows 10 SDK. The full installation guide is &lt;a href=&quot;https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;. WinDbg is also not the only program out there to analyze minidumps - for example, &lt;a href=&quot;https://www.resplendence.com/whocrashed&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;whocrashed&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Without any special setup, simply loading the minidump from Chrome into WinDbg on my machine let me see a lot of helpful details; the most helpful being the call stack. I could see that in every single of my 10+ crashes, the same exact function call was triggering the crash; &lt;a href=&quot;https://cs.chromium.org/chromium/src/chrome/chrome_elf/blacklist/blacklist.cc?type=cs&amp;#x26;g=0&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;blacklist::SuccessfullyBlocked()&lt;/a&gt;. This is a function that triggers when Chrome&apos;s anti-DLL-injection blacklist records that it successfully blocked a &quot;rogue&quot; DLL from injecting code into the browser. I&apos;m going to have to do some more digging, but this is probably related to the real-time web browsing protection offered by my antivirus.&lt;/p&gt;
&lt;p&gt;If you want a more in-depth experience with WinDbg, definitely&lt;a href=&quot;https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/crash-dump-files&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt; read the docs on WinDbg&lt;/a&gt;, and at a minimum, you might want to enable symbol resolution by providing WinDbg with some symbol paths. For example, to pull in symbols for Windows API and Chrome, and I used this command to set the symbol file path:&lt;/p&gt;
&lt;pre&gt;.sympath srv*c:\symbols\chrome*https://chromium-browser-symsrv.commondatastorage.googleapis.com/;.symfix+ c:\symbols\ms&lt;/pre&gt;
&lt;p&gt;After which, you can use&lt;/p&gt;
&lt;pre&gt;.reload&lt;/pre&gt;
&lt;p&gt;and then&lt;/p&gt;
&lt;pre&gt;!analyze -v&lt;/pre&gt;
&lt;p&gt;to run a quick analysis.&lt;/p&gt;
&lt;p&gt;Another option for trying to examine Chrome crashes is through logging.&lt;/p&gt;
&lt;h2&gt;Using Chrome Logs&lt;/h2&gt;
&lt;p&gt;To run Chrome with error logging on, where it logs critical errors to a specific file, use this command flag when you start the application:&lt;/p&gt;
&lt;pre&gt;--enable-logging --v=1&lt;/pre&gt;
&lt;p&gt;In Windows, you can save the flag with the shortcut for the application, or even save it is a separate shortcut, so you can choose at any time to launch the non-logging version of Chrome versus the logging one.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Chrome-Enable-Logging-Startup-Flag-on-Windows.png&quot;&gt;&lt;img class=&quot;size-full wp-image-554 aligncenter&quot; src=&quot;/media/Google-Chrome-Enable-Logging-Startup-Flag-on-Windows.png&quot; alt=&quot;Google Chrome - Enable Logging Startup Flag on Windows&quot; width=&quot;365&quot; height=&quot;512&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once logging is set up, you can view the log file, likely at C:\Users\[USERNAME]\AppData\Local\Google\Chrome\User Data\chrome_debug.log&lt;/p&gt;
&lt;p&gt;You can also use an application called Sawbuck (also made by Google) that can show you logging entries in realtime, and let you filter on levels. See &lt;a href=&quot;https://www.chromium.org/for-testers/enable-logging&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the Chromium page on logging&lt;/a&gt; for details.&lt;/p&gt;
&lt;p&gt;In my scenario, the log did not help or turn up anything useful, as the crash was actually happening so fast, nothing about the crash itself was getting written out to the log before the application exited completely.&lt;/p&gt;
&lt;h2&gt;Update: A further note about DLL injection&lt;/h2&gt;
&lt;p&gt;Another resource to check for which installed DLLs are potentially injecting code into Chrome and could cause a conflict, is this internal Chrome page - &lt;a href=&quot;chrome://conflicts/&quot;&gt;chrome://conflicts/&lt;/a&gt;. Mentioned &lt;a href=&quot;https://www.chromium.org/developers/crash-reports/crash-with-invalid-handle&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Using TypeScript with Google Apps Script and Google Ads Scripting</title><link>https://joshuatz.com/posts/2019/using-typescript-with-google-apps-script-and-google-ads-scripting/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/using-typescript-with-google-apps-script-and-google-ads-scripting/</guid><description>Information on using TypeScript with Google Apps Script and Google Ads, both with and without Clasp. Also discusses how Apps Script treats hoisting.</description><pubDate>Sun, 09 Jun 2019 09:23:10 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I just recently started a project that runs as a Google Apps Script (GAS), and as I also just recently tried TypeScript and loved it, I wanted to see if I could use it and compile it to code that could execute as a regular Google Apps Script. When you think about it, TypeScript is an excellent fit for Google Apps Script, as the Apps Script platform has limited debugging capabilities and uses tons of complex SDK-specific interfaces; TypeScript&apos;s static type checking and support for type declaration files make coding for Apps Script easier and less error-prone.&lt;/p&gt;
&lt;p&gt;However, using TypeScript with GAS or Google Ads Scripting is not as easy as simply calling the TypeScript compiler/transpiler (TSC) to transpile your .ts to .js. True, Google Apps Script is an extension of Javascript and that should in theory mean that transpiled JS should work fine, but it has a few key differences that require building for it be treated differently, primarily how it treats function and variable scope, which you can read about at the &lt;a href=&quot;#apps-scripts-vs-js&quot;&gt;bottom of this post&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Typescript Transpiling Options:&lt;/h2&gt;
&lt;p&gt;I&apos;ve created a simple repo that shows each of these options with the same example code. You can find it here: &lt;a href=&quot;https://github.com/joshuatz/typescript-to-gas-demos&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/joshuatz/typescript-to-gas-demos&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;Option A: Google&apos;s Recommendation - Using clasp&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Clasp, or &lt;strong&gt;C&lt;/strong&gt;ommand &lt;strong&gt;L&lt;/strong&gt;ine &lt;strong&gt;A&lt;/strong&gt;pps &lt;strong&gt;S&lt;/strong&gt;cript &lt;strong&gt;P&lt;/strong&gt;roject, is a command line tool that lets you develop Apps Script code locally, in Javascript or TypeScript, and push via terminal to the Apps Script cloud, in the process magically getting transpiled and formatted for GAS. To begin using Clasp, the best place to start is &lt;a href=&quot;https://developers.google.com/apps-script/guides/clasp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;, or &lt;a href=&quot;https://developers.google.com/apps-script/guides/typescript&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt; if you are specifically interested in using it for TypeScript.&lt;/p&gt;
&lt;p&gt;The trade-off for ease of use is:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Trust: You have to give the Clasp tool scoped permission to touch your Apps Script files, which is handled by requesting and then storing on your harddrive an OAuth token. Of course, you can revoke this at any time, and you can check &lt;a href=&quot;https://github.com/google/clasp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the source code for Clasp itself&lt;/a&gt; to see what it does with it.&lt;/li&gt;
	&lt;li&gt;Increased &quot;black-box&quot; complexity: Although Clasp is open source, the amount of moving parts it adds to an Apps Script project and how it abstracts certain things so you don&apos;t have to worry about them, means that you might understand less about what is happening behind the scenes.
&lt;ul&gt;
	&lt;li&gt;For example, if you are using TypeScript with Clasp, when you use the command clasp push, behind the scenes it actually calls a tool called &quot;&lt;a href=&quot;https://github.com/grant/ts2gas&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ts2gas&lt;/a&gt;&quot;, which is a TypeScript to Google Apps Script transpiler, which in turn, uses the &lt;a href=&quot;https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TypeScript Compiler API&lt;/a&gt; and a final (heavily modified) call to &lt;span class=&quot;pl-en&quot;&gt;transpileModule, which is similar to running TSC. Then Clasp kicks back in and uploads the transpiled JS files to your Google Drive / Apps Script Project.&lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;With your code going through multiple steps like this, can you reliably predict what the transpiled JS will look like after being converted from TypeScript? Are you aware of all the limits of each step of the process?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Lack of compatibility with other Google Script environments
&lt;ul&gt;
	&lt;li&gt;I can find no evidence that Clasp can work with Google Ads Scripts (formerly AdWords scripting)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that neither Clasp nor ts2gas seem to import the Google Apps Script type definitions, so see &lt;a href=&quot;#using_typescript_definitions&quot;&gt;my notes here&lt;/a&gt; on adding them.&lt;/p&gt;
&lt;p&gt;I wanted to understand more about how TypeScript could be transpiled to GAS compatible GS code, which is the impetus behind this post and the further options I explore below.&lt;/p&gt;
&lt;p&gt;If you are developing for a Google Data Studio community connector, you might want to use &lt;a href=&quot;https://developers.google.com/datastudio/connector/local-development&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;dscc-gen&lt;/a&gt;, which is a wrapper around Clasp that provides some convenience methods for connector specific work. I could not get it to work on Windows, which was also part of my research into how all these pieces fit together.&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;Option B: Use ts2gas directly&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;There is nothing preventing you from using &lt;a href=&quot;https://github.com/grant/ts2gas&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ts2gas&lt;/a&gt; directly, instead of through Clasp. However you would need to add a little bit of wrapper code, since unlike TSC which can take files as inputs, ts2gas will only take a string, and also returns a string, not a file. You can find an example of this in the sample repo &lt;a href=&quot;https://github.com/joshuatz/typescript-to-gas-demos/tree/master/ts2gas-direct/input-ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;, and I&apos;ve also pasted the simple build script below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const ts2gas = require(&apos;ts2gas&apos;);
const fse = require(&apos;fs-extra&apos;);
&lt;p&gt;// paths
const inDir = ’./’;
const outDir = ’../output-gs/’;&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Iterate over TS files in this folder
fse.readdir(’./’,(err,fileList)=&gt;{
fileList.forEach((fileName,index)=&gt;{
if (/.ts$/i.test(fileName)){
let tsCode = fse.readFileSync(inDir + fileName,‘utf-8’);
// Send to ts2gas
let gasCode = ts2gas(tsCode);
// Write out transpiled GS
fse.writeFileSync(outDir + fileName.replace(‘.ts’,‘.gs’),gasCode);
}
});
});&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;Option C: Transpile without modules&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The fastest and easiest way to quickly generate a GAS compatible export from TypeScript is to simply turn off modules, telling TSC that everything will be in a shared scope. This is a sample tsconfig.json that meets these requirements:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
    &quot;compilerOptions&quot;: {
        &quot;target&quot;: &quot;es3&quot;,
        &quot;module&quot;: &quot;none&quot;,
        &quot;outDir&quot;: &quot;./out&quot;,
        &quot;strict&quot;: true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These also (should) automatically change a few crucial things in your IDE (works best with Visual Studio Code). Most importantly, because it knows the output shares a scope, it lets you reference functions and global variables across files, without using imports/exports. &lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;Option D: Transpile to single file (simple and advanced)&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The main benefit of transpiling to a single GS file is that, if you chose not to use Clasp due to privacy concerns, having a single file output makes it easier to copy and paste directly into the online editor. The simple version of this example simply uses the outFile option of TSC to force the output to a single file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es3&quot;,
    &quot;module&quot;: &quot;none&quot;,
    &quot;outFile&quot;: &quot;../output-gs/compiled.gs&quot;,
    &quot;strict&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The advanced version uses a custom build script to remove places where the source file uses &quot;import&quot; or &quot;export&quot;, so that you can use imports/exports in your source TS without it messing up the exported GS.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;&lt;a id=&quot;using_typescript_definitions&quot;&gt;&lt;/a&gt;Using TypeScript definitions for Google Apps Script and Google Ads Scripting&lt;/h2&gt;
&lt;p&gt;First, you are going to want to actually install the TypeScript definition packages:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;For Google Apps Script:
&lt;ul&gt;
	&lt;li&gt;Package: &lt;a href=&quot;https://www.npmjs.com/package/@types/google-apps-script&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;@types/google-apps-script&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Created from: &lt;a href=&quot;https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/google-apps-script&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;DefinitelyTyped/types/google-apps-script&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Install: &quot;npm install --save @types/google-apps-script&quot;&lt;/li&gt;
	&lt;li&gt;Add to top of TS: import &apos;google-apps-script&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;For Google Ads
&lt;ul&gt;
	&lt;li&gt;Package: &lt;a href=&quot;https://www.npmjs.com/package/@types/google-adwords-scripts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;@types/google-adwords-scripts&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Created from: &lt;a href=&quot;https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/google-adwords-scripts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;DefinitelyTyped/types/google-adwords-scripts&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;Install: &quot;npm install --save @types/google-adwords-scripts&quot;&lt;/li&gt;
	&lt;li&gt;Add to top of TS: import &apos;google-adwords-scripts&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although, like any node package, you can install types globally with the -g flag, VSCode will not automatically pull from the global folder. The only way I know of to have it recognize a globally installed type module is to use the absolute path - for example, this worked:&lt;/p&gt;
&lt;div&gt;
&lt;pre&gt;import &apos;C:\\laragon\\bin\\nodejs\\node-v10.14.2-win-x64\\node_modules\\@types\\google-apps-script&apos;;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;But of course, that is terrible practice, since it requires other devs to have the same global install path. Even if you are just developing for yourself, you might as well set up a package.json and locally install the type packages.&lt;/div&gt;
&lt;p&gt;You might be wondering how this is handled as far as transpiling goes. Well, Clasp/ts2gas will automatically comment out your &quot;import&quot; lines when it is transpiled to JS, and if you are using a different option, you could manually comment them out by hand before running TSC, or use an automated build script that does so (see my options above for details).&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;apps-scripts-vs-js&quot;&gt;&lt;/a&gt;Apps Script - JS Caveats - Scope and Hoisting&lt;/h2&gt;
&lt;p&gt;First, and please excuse any slightly inaccurate terminology, but Apps Script works a little differently than JS that runs on websites, or NodeJS environments. One of the key differences is how function &amp;#x26; variable hoisting works. Normally, functions are hoisted to the top, along with global variables, meaning that even if you define a function at line 200 of your file, you can call it at line 1, because the interpreter &quot;hoists&quot; the function declaration and parses it first. This lets you do this kind of thing in plain JS:&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;//jsfiddle.net/joshuatz/ewx5p71h/embedded/js,result/&quot; width=&quot;100%&quot; height=&quot;300&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;Apps Script does this too, but with an important distinction as to what &quot;scope&quot; means and order of evaluation. With normal JS, functions are hoisted to the top &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;of their scope&lt;/strong&gt;&lt;/span&gt;, &lt;em&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;when&lt;/strong&gt;&lt;/span&gt;&lt;/em&gt; the code block they are in is evaluated. In practice, this means that although separate script tags can share the same global scope, since they are evaluated separately as the page gets parsed top to bottom, functions declared in a separate script tag can only be called in a different script tag if where they are called from is either a &quot;lower&quot; script tag, or waits to call the function until where it is declared is evaluated by the JS engine. Since examples are much easier to understand than my rambling, here is a very practical example in plain Javascript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Script A --&gt;
&amp;#x3C;script&gt;
    // call alpha before it is defined.
    alpha();
    function alpha(){
        console.log(&apos;running alpha&apos;);
        // call beta before it is defined.
        beta();
    }
&amp;#x3C;/script&gt;
&lt;p&gt;&amp;#x3C;!— Script B —&gt;
&amp;#x3C;script&gt;
function beta(){
console.log(‘running beta’);
}
&amp;#x3C;/script&gt;&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!— Final —&gt;
&amp;#x3C;script&gt;
// Call alpha again
alpha();
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/2019-06-08-18_48_14-Document-http___scratchpad.test_.png&quot;&gt;&lt;img class=&quot;size-full wp-image-534 aligncenter&quot; src=&quot;/media/2019-06-08-18_48_14-Document-http___scratchpad.test_.png&quot; alt=&quot;&quot; width=&quot;581&quot; height=&quot;539&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In the example above, the first time alpha() runs, the script tag that the beta() definition is in has not yet been evaluated, so we get a fatal error. Then that script tag hits, and beta() gets defined, and automatically hoisted to the top of the scope (becoming window.beta()), and finally, the final script tag hits, calls alpha() again, and this time, although alpha is in a different script tag, both alpha() and beta() are now hoisted and can be called and alpha() runs without the fatal error. This is why order matters when adding script tags (either inline or calling external js files), and most web devs are very familiar with accidentally loading dependencies in the wrong order and getting undefined errors.&lt;/p&gt;
&lt;p&gt;How does Google Apps Script differ in function and variable hoisting versus normal JS? Well, it is hard to find documentation on this, but from experimenting and what little public information there is, the conclusion is that AppScript actually considers different script files to not only share the same scope, but also be evaluated at the same time so function &amp;#x26; var declarations are hoisted to the top, &lt;strong&gt;across files and regardless of order&lt;/strong&gt;. Using the same example as above, slightly modified for the Apps Script Environment:&lt;/p&gt;
&lt;h3&gt;File-A.gs&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;test();
function test(){
  // call alpha before it is defined - in this file
  alpha();
  // call final before it is defined - in separate file Final.gs
  final();
}
function alpha(){
  Logger.log(&apos;running alpha&apos;);
  // call beta before it is defined - in separate file File-B.gs
  beta();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;File-B.gs&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function beta(){
  Logger.log(&apos;running beta&apos;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Final.gs&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function final(){
  // Call beta again
  beta();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this was a webpage, and those were JS files getting called via script tags, you would expect all sorts of errors when test() gets called. For example, you would expect to get the same &quot;beta is not defined&quot; error, since beta is in a separate script file and should not yet be hoisted. However, because this is GS, we get no errors, and a perfect output:&lt;/p&gt;
&lt;pre&gt;[19-06-08 19:25:14:102 PDT] running alpha
[19-06-08 19:25:14:103 PDT] running beta
[19-06-08 19:25:14:103 PDT] running beta
&lt;/pre&gt;
&lt;p&gt;So what exactly is going on? My suspicion is that Google Apps Script basically acts like it is joining all your GS files, no matter how many you have, into one single GS file that it then evaluates. Pretty much the only official documentation I have been able to find that backs this up is &lt;a href=&quot;https://developers.google.com/apps-script/guides/import-export#features_and_limitations&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You are not limited to a single server Code.gs file. You can spread server code across multiple files for ease of development. All of the server files are loaded into the same global namespace, so use JavaScript classes when you want to provide safe encapsulation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The other reason why this is a logical conclusion is because it is basically a necessity given the way that the Apps Script online editor works. Unlike the web, where you can reorder &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; tags, you cannot control the order of .GS files within your project and thus, if the rule about sharing scope was not true, you would literally have to guess as to what order your files would be evaluated in and subsequently their functions hoisted.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>LinkedIn Profile to JSON Resume Exporter - Extension and Bookmarklet</title><link>https://joshuatz.com/projects/web-stuff/linkedin-profile-to-json-resume-exporter---extension-and-bookmarklet/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/linkedin-profile-to-json-resume-exporter---extension-and-bookmarklet/</guid><description>A lightweight browser tool for quickly exporting a LinkedIn profile page to a JSON Resume export. Grabs education, work positions, and even skills.</description><pubDate>Thu, 06 Jun 2019 17:19:15 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;/media/LinkedIn-to-JSON-Resume-Bookmarklet-Promo-Image.png&quot; style=&quot;margin: auto; display:block; width: 90%; height: auto; text-align:center;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/LinkedIn-to-JSON-Resume-Bookmarklet-Promo-Image.png&quot; alt=&quot;LinkedIn to JSON Resume Extension Promo Image&quot; style=&quot;width:100%;max-width: 1316px;height:auto;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;installation&quot;&gt;Installation:&lt;/h2&gt;
&lt;p&gt;You can now get this tool as a Chrome Extension, from &lt;a href=&quot;https://chrome.google.com/webstore/detail/json-resume-exporter/caobgmmcpklomkcckaenhjlokpmfbdec&quot;&gt;the official webstore page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Or, if you want to sideload it, you can grab a zip file from &lt;a href=&quot;https://github.com/joshuatz/linkedin-to-jsonresume/releases&quot;&gt;the latest release&lt;/a&gt;.&lt;/p&gt;
&lt;!-- Deprecated: Bookmarklet Installer --&gt;
&lt;!--
&lt;iframe style=&quot;width: 100%; height: 150px;&quot; src=&quot;https://joshuatz.com/static-for-wp-iframes/linkedin-to-json-resume/install-page.html&quot;&gt;&lt;span data-mce-type=&quot;bookmark&quot; style=&quot;display: inline-block; width: 0px; overflow: hidden; line-height: 0;&quot; class=&quot;mce_SELRES_start&quot;&gt;&lt;/span&gt;&lt;/iframe&gt;
--&gt;
&lt;h2 id=&quot;source-code&quot;&gt;Source Code:&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;github.com/joshuatz/linkedin-to-jsonresume&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://github.com/joshuatz/linkedin-to-jsonresume&quot;&gt;https://github.com/joshuatz/linkedin-to-jsonresume&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-is-the-linkedin-to-json-resume-export-tool&quot;&gt;What is the LinkedIn to JSON Resume Export Tool?&lt;/h2&gt;
&lt;p&gt;This is a project that I initially quickly threw together, mostly out of frustration that it didn’t already exist. As I outline in the “readme” file in my &lt;a href=&quot;https://github.com/joshuatz/linkedin-to-jsonresume&quot;&gt;Github repo for this project&lt;/a&gt;, I wanted to export &lt;a href=&quot;https://www.linkedin.com/in/joshuatzucker/&quot;&gt;my LinkedIn profile&lt;/a&gt; to JSON Resume, which is &lt;a href=&quot;https://jsonresume.org/&quot;&gt;an exciting standard&lt;/a&gt; for storing, sharing, parsing, and generating resumes based on a shared underlying data structure, or schema.&lt;/p&gt;
&lt;p&gt;The official LinkedIn APIs are restrictive and require an approval process, and the manual data export option offered by LinkedIn can take up to 72 hours. I was frustrated by this fact, so I built this tool to instantly export a LinkedIn profile page to the JSON Resume standard, which appears in a little popup modal that you can easily copy and paste out of.&lt;/p&gt;
&lt;p&gt;On 8/3/2019, I rewrote the tool from a bookmarklet to a browser extension (Chrome extension) to get around some restrictions placed by LinkedIn that were breaking the functionality of the bookmarklet version.&lt;/p&gt;
&lt;h2 id=&quot;features&quot;&gt;Features&lt;/h2&gt;
&lt;p&gt;Since the initial release as a simple bookmarklet, this project has seen some major improvements and gained additional features.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rewritten as a browser extension, to work with CSP restrictions&lt;/li&gt;
&lt;li&gt;Added support for downloading of JSON file&lt;/li&gt;
&lt;li&gt;Reworked to &lt;em&gt;simultaneously&lt;/em&gt; support multiple different schema versions&lt;/li&gt;
&lt;li&gt;Added support for multilingual profiles, and ability to pick export language&lt;/li&gt;
&lt;li&gt;Added vCard export (for import into address book)&lt;/li&gt;
&lt;li&gt;Constant reworking to support changes in the LinkedIn endpoints, specifications, and restrictions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;demo&quot;&gt;Demo&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://raw.githubusercontent.com/joshuatz/linkedin-to-jsonresume/main/demo-chrome_extension.gif&quot; style=&quot;margin: auto; display:block; width: 95%; height: auto;&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/joshuatz/linkedin-to-jsonresume/main/demo-chrome_extension.gif&quot; alt=&quot;Linkedin to JSON Resume Chrome Extension - Demo&quot; style=&quot;width:100%;max-width: 1038px;height:auto;&quot; loading=&quot;lazy&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-i-used&quot;&gt;What I Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;JSON + JSON Schema&lt;/li&gt;
&lt;li&gt;Webpack&lt;/li&gt;
&lt;li&gt;Chrome extension APIs&lt;/li&gt;
&lt;li&gt;Web APIs&lt;/li&gt;
&lt;li&gt;Fetch&lt;/li&gt;
&lt;li&gt;Vanilla JS + HTML + CSS&lt;/li&gt;
&lt;li&gt;And more!&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Embedding iframes - Static, Third-Party, and Dynamic Options</title><link>https://joshuatz.com/posts/2019/embedding-iframes---static-third-party-and-dynamic-options/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/embedding-iframes---static-third-party-and-dynamic-options/</guid><description>A breakdown of different ways to embed static and dynamic content into iframes, and dynamically generate iframe embeds on both the client-side and server-side.</description><pubDate>Sat, 01 Jun 2019 03:26:35 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I feel like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;iframes&lt;/a&gt; are a very under-appreciated part of the web-development world. Especially as concern grows about the amount of invasive tracking and its negative effect on page performance, iframes continue to be an excellent way to force third-party code into sandboxes, where they can&apos;t interact with the host page (unless explicit data passing has been setup).&lt;/p&gt;
&lt;p&gt;I personally use iframes somewhat often, but infrequent enough that I often forgot things like &quot;can I force this site&apos;s widget into an iframe?&quot;, or &quot;what is the fastest way to throw this into a publicly accessible iframe embed?&quot;. This post is a way for me to organize some notes on the topic and keep a list of resources related to iframe embeds, specifically for developers.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;text-decoration: underline; font-size: 24pt;&quot;&gt;&lt;strong&gt;Table of Contents:&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#basics&quot;&gt;The Basics: Static HTML Upload&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#third_party_gen&quot;&gt;Third Party Generators&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#jsfiddle&quot;&gt;JSFiddle&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#codepen&quot;&gt;CodePen&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#jsbin&quot;&gt;JS Bin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#dynamiciframes_clientside&quot;&gt;Dynamic iframe generation: client-side and inline attributes&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#contentwindow_write&quot;&gt;iframe.contentWindow.document.write&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#data_uri_as_src&quot;&gt;Data URI as src&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#srcdoc&quot;&gt;srcdoc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#generatingiframe_server&quot;&gt;Generating iframe content server-side &amp;#x26; some warnings&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a id=&quot;basics&quot;&gt;&lt;/a&gt;The Basics: Static HTML upload&lt;/h2&gt;
&lt;p&gt;I would be remiss if I didn&apos;t mention the default way to host up an iframe that others can embed; simply throw your code into a static HTML file (or generate with server-side code, more on that later), and shove the file onto your publicly accessible host (shared host, VPS, AWS bucket, etc). Lets pretend you uploaded it to the root of your domain, at &lt;code&gt;example.com/widget-A.html&lt;/code&gt;. Then to embed, you could simply pull it in via the standard iframe element: &lt;code&gt;&amp;#x3C;iframe src=&quot;/widget-A.html&quot;&gt;&amp;#x3C;/iframe&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I use this simple method a few times throughout this site, so that I can pull some custom widgets into a WordPress page and not have to worry about conflicting CSS or messing with HTML escaping in WordPress post content.&lt;/p&gt;
&lt;p&gt;This is always a good fall-back method to wrap anything in an iframe. However, if code relies on access to JS globals or things like window.location.href, you might need to add some extra code to explicitly pass data from the parent window to the embed HTML file. And of course this doesn&apos;t help at all really with dynamic iframe generation, where you want to put user submitted content within iframes.&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;third_party_gen&quot;&gt;&lt;/a&gt;Third-Party Generators:&lt;/h2&gt;
&lt;p&gt;If you are a web developer that likes to share widgets, you are probably already familiar with sites like jsFiddle and CodePen. However, you might not know that many of these sites allow for direct embedding, and some even allow you to natively use iframes as the embed type. However, please note that the intended purpose here is to embed code snippets to share with other developers or show off little demos. Embedding entire applications or exceeding what is considered &quot;normal use&quot; is likely against the TOS and is not a good idea anyway for anything beyond proof-of-concept demos and the such.&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;jsfiddle&quot;&gt;&lt;/a&gt;jsFiddle - An Interesting Lesson in Iframe Mitigation&lt;/h3&gt;
&lt;p&gt;Inside the JSFiddle editor, there is an &quot;Embed&quot; button, which lets you configure an embedded widget that you can copy the code for and paste into your destination of choice. You can easily configure it to generate an embeddable iframe:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/JSFiddle-IFrame-Embed-Option.png&quot;&gt;&lt;img class=&quot;size-full wp-image-512 aligncenter&quot; src=&quot;/media/JSFiddle-IFrame-Embed-Option.png&quot; alt=&quot;JSFiddle IFrame Embed Option&quot; width=&quot;286&quot; height=&quot;462&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The resulting Iframe can be set to just show the &quot;result&quot; of the fiddle, which is desirable if you are trying to show off a demo to users, rather than to other developers. However, JSFiddle injects a top menu bar into all Iframes with a tab selector and &quot;Edit in JSFiddle&quot; text, regardless of how many tabs are set to display:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/JSFiddle-IFrame-Embed-Result-Only-Top-Bar-Included.png&quot;&gt;&lt;img class=&quot;size-full wp-image-514 aligncenter&quot; src=&quot;/media/JSFiddle-IFrame-Embed-Result-Only-Top-Bar-Included.png&quot; alt=&quot;JSFiddle IFrame Embed - Result Only - Top Bar Included&quot; width=&quot;814&quot; height=&quot;554&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You might have noticed that the iframe embed code generated by JSFiddle uses a src of &quot;jsfiddle.net/{{user}}/{{fiddleID}}/embedded/result/&quot;, but the window.location.href in our fiddle shows that what actually loaded was &quot;fiddle.jshell.net/{{user}}/{{fiddleID}}/show/{{theme}}&quot;. Those trying to get rid of the injected menu bar might try setting the iframe source manually to the &quot;fiddle.jshell.net&quot; URL, but would be disappointed to see that the menu bar still gets injected. What gives? How is JSFiddle doing this?&lt;/p&gt;
&lt;p&gt;The answer is interesting, and outlines a good technique for blocking users from abusing an iframe embed system. First, JSFiddle uses a &quot;wrapper&quot; around the final result, which is to say that it double-nests iframes. So the setup looks kind of like this:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Level A: Your Website
&lt;ul&gt;
	&lt;li&gt;Level B: JSFiddle default IFrame Embed (&amp;#x3C;iframe src=&quot;jsfiddle.net/.../.../embedded/result&quot;&gt;&amp;#x3C;/iframe&gt;)
&lt;ul&gt;
	&lt;li&gt;[Automatic] Wrapper(html)
&lt;ul&gt;
	&lt;li&gt;Tabs Bar (html)&lt;/li&gt;
	&lt;li&gt;Level C: Secondary iframe (&amp;#x3C;iframe src=&quot;fiddle.jshell.net/.../.../show/...&quot;&gt;&amp;#x3C;/iframe&gt;)
&lt;ul&gt;
	&lt;li&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;Actual JSFiddle Result&lt;/strong&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Second, and this is the interesting part, it relies on the fact that when resources are loaded, either through AJAX or through resource tags like &amp;#x3C;script src=&quot;&quot;&gt;, the browser automatically includes what website the request is coming from, as &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the &quot;referer&quot; header&lt;/a&gt;. If a request comes from within an Iframe, the referrer matches the host of the Iframe content, not the site that the Iframe is embedded on. JSFiddle is checking this, and if the request is coming from a website outside of its control, it knows that the wrapper/tabs/secondary iframe must not be injected yet, since the wrapper should be loaded inside an iframe that would send a JSFiddle controlled domain as the referrer. Since the tab bar is injected into a wrapper that contains the fiddle result in an iframe, rather than directly into the fiddle result, it also means that your fiddle code can&apos;t modify the wrapper, so you can&apos;t do something like set &quot;display:none&quot; on the tab bar through your fiddle code.&lt;/p&gt;
&lt;p&gt;To summarize:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;HTTP Request to fiddle.jshell.net/.../.../show/
&lt;ul&gt;
	&lt;li&gt;...From a JSFiddle domain:
&lt;ul&gt;
	&lt;li&gt;Response HTML will be your JSFiddle code&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;...From outside a JSFiddle domain:
&lt;ul&gt;
	&lt;li&gt;Response HTML will contain injector code that creates wrapper, tabs, and secondary iframe that requests fiddle.jshell.net/.../.../show/ all over again.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&apos;m not going to go into detail on how to remove the top bar, since that isn&apos;t really something you should be doing, but I will point out that if you set your original Iframe embed src to fiddle.jshell.net/.../.../show instead of the default embed src, then the secondary iframe will actually have the same origin as the wrapper, which means that you can access and modify it with &quot;window.parent.document&quot; from inside your actual Fiddle code.&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;codepen&quot;&gt;&lt;/a&gt;CodePen: Don&apos;t Mess with Us!&lt;/h3&gt;
&lt;p&gt;Like JSFiddle, CodePen also has an IFrame embed option. In fact, they pretty much use exactly the same methods for blocking abuse of the IFrame and removal of the menu and &quot;Edit on CodePen&quot; text:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Nested iframes: &quot;codepen.io/.../embed/...&quot; loads a wrapper, with the menu bar, which in turn nests a secondary iframe with the result at &quot;s.codepen.io/.../fullembedgrid/...&quot;
&lt;ul&gt;
	&lt;li&gt;Because the secondary Iframe has a different domain (subdomain), your CodePen code cannot touch the menu bar in the first, due to cross-origin policy.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;CodePen checks the referer header on the result Iframe src&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is where it is different. Whereas JSFiddle will re-embed the iframe if it detects a mis-matched referrer, CodePen will simply block your request to load the result entirely if it detects the referer not matching a CodePen owned host. If you try to set &quot;s.codepen.io/.../fullembedgrid/...&quot; as the source of an iframe that is not inside your fiddle, you will see this:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/CodePen-IFrame-Result-Embed-Blocking-External-Referrers.png&quot;&gt;&lt;img class=&quot;size-full wp-image-517 aligncenter&quot; src=&quot;/media/CodePen-IFrame-Result-Embed-Blocking-External-Referrers.png&quot; alt=&quot;CodePen IFrame Result Embed - Blocking External Referrers&quot; width=&quot;570&quot; height=&quot;509&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To be clear, this error message is misleading; the browser is sending a referer header, it just isn&apos;t one that CodePen wants to see. What this error message should really say is something like &quot;We see what you are trying to do... cut it out!&quot;.&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;jsbin&quot;&gt;&lt;/a&gt;JS Bin: Ajax Fine, Browser Not&lt;/h3&gt;
&lt;p&gt;JS Bin is a little different than CodePen and JSFiddle in how it treats embeds. It looks like they do allow embeds, but only for Pro users. Also, instead of checking the referer header to block cross-origin result embedding, they are using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;X-Frame-Options&lt;/a&gt;, which is a cleaner and less error-prone way of blocking cross-origin embeds, but only blocks in browsers that support it. JS Bin has set X-Frame-Options on their &quot;output&quot; subdomain to &quot;sameorigin&quot;, which as its name would imply, will block the embed if the origin if the frame does not match the origin of the page.&lt;/p&gt;
&lt;p&gt;I noticed kind of a strange thing with JS Bin, which is that they are not enforcing a referer match and they actually are allowing cross-origin requests. This probably has to do with their live-streaming / hot-reloading features. Anyways, this strange combination of security means that this actually worked for me:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;fetch(&apos;https://output.jsbin.com/{{ID}}&apos;).then(function(response){
    response.text().then(function(html){
        var iframe = document.createElement(&apos;iframe&apos;);
        document.body.appendChild(iframe);
        iframe.contentWindow.document.open();
        iframe.contentWindow.document.write(html);
        iframe.contentWindow.document.close();
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a id=&quot;dynamiciframes_clientside&quot;&gt;&lt;/a&gt;Advanced: Dynamic iframe content injection with client-side Javascript or inline attributes&lt;/h2&gt;
&lt;p&gt;One thing that you might not be aware of is that you can actually use iframes that show content that is not from an actual domain - e.g. dynamically generated content with Javascript or by inline attributes. There are tons of caveats to using iframes in this way though, and I&apos;ll cover them for each of the available methods to dynamically generate iframe content client-side. These are listed in order from most compatible but least friendly, to most friendly but least compatible.&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;contentwindow_write&quot;&gt;&lt;/a&gt;&quot;iframe.contentWindow.document.write&quot; - Maximum cross-compatibility&lt;/h3&gt;
&lt;p&gt;This is pretty much the only solution that works for IE, since things like srcdoc and data-uri do not (or have very limited support), and I found it through &lt;a href=&quot;https://stackoverflow.com/a/10433550/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this StackOverflow answer&lt;/a&gt;. This method works by creating a new iframe element &lt;strong&gt;that has the same origin as the current page&lt;/strong&gt; and then streams raw HTML text into the document of that iframe with document.write(html):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function sameOriginIframeInject(html,target,append,cb){
    cb = typeof(cb)===&apos;function&apos; ? cb : function(){};
    var iframe = document.createElement(&apos;iframe&apos;);
    if (append){
        target.appendChild(iframe);
        iframe.style.width = &apos;100%&apos;;
        iframe.style.height = &apos;100%&apos;;
    }
    else {
        target.replaceWith(iframe);
    }
    iframe.addEventListener(&apos;load&apos;,cb);
    iframe.contentWindow.document.open();
    iframe.contentWindow.document.write(html);
    iframe.contentWindow.document.close();
}
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Example:
sameOriginIframeInject(‘&amp;#x3C;script&gt;alert(“success!”);&amp;#x3C;/script&gt;‘,document.querySelector(‘#iframeWrapper’),true);&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The downside to this method is that it pretty much negates the entire point of iframes in the first place; because the iframe shares origin with the parent, any code you put in the iframe can access your site, and vice-versa. However, standard iframe benefits of separately scoped JS variables and scoped CSS still apply, which is probably why this method still gets some use.&lt;/p&gt;
&lt;p&gt;BONUS Script: Inject a cross-origin URL into a same-origin iframe. This will let you do some crazy stuff, like use iframe.contentWindow.print(), which would normally be unusable with an iframe that displays cross-origin content. Note that this method is very hackish and is likely to break on a large portion of sites.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function injectSameOriginIframeFromRemote(remoteSrcUrl, target, append, OPT_cb, OPT_useProxy) {
    var PROXY_BASE = &apos;https://cors-anywhere.herokuapp.com&apos;;
    useProxy = typeof(OPT_useProxy)===&apos;boolean&apos; ? OPT_useProxy : true;
    cb = typeof (OPT_cb) === &apos;function&apos; ? OPT_cb : function () { };
    var iframe = document.createElement(&apos;iframe&apos;);
    if (append) {
        target.appendChild(iframe);
        iframe.style.width = &apos;100%&apos;;
        iframe.style.height = &apos;100%&apos;;
    }
    else {
        target.replaceWith(iframe);
    }
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4){
            if (xhr.status === 200){
                var rawHtml = xhr.responseText;
                var baseString = &apos;&lt;base href=&quot;&amp;#x27; + remoteSrcUrl + &amp;#x27;&quot;&gt;&apos;;
                rawHtml = rawHtml.replace(/&lt;base[^&gt;]+&gt;/gim,&apos;&apos;);
                rawHtml = rawHtml.replace(/&amp;#x3C;\/head&gt;/gim,baseString + &apos;\n&apos; + &apos;&apos;);
                iframe.contentWindow.document.open();
                iframe.contentWindow.document.write(rawHtml);
                iframe.contentWindow.document.close();
            }
            else {
                cb(false);
            }
        }
    }
    xhr.open(&apos;GET&apos;,PROXY_BASE + &apos;/&apos; + remoteSrcUrl);
    xhr.send();
    iframe.addEventListener(&apos;load&apos;, cb);
}&lt;/base[^&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id=&quot;data_uri_as_src&quot;&gt;&lt;/a&gt;Data URI as Src attribute: Hackish, but works&lt;/h3&gt;
&lt;p&gt;As a method that is in-between the old way (same-origin contentWindow.document.write) and the newest method (srcdoc), using a Data URI as the src of an iframe element is a solution that has decent browser support, but also decent limitations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is a Data URI? - &lt;/strong&gt;A &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Data URI&lt;/a&gt; is, well... data as a URI. Ok, I know, that&apos;s not very helpful, but that really is all it is. Normally &lt;a href=&quot;https://en.wikipedia.org/wiki/Uniform_Resource_Identifier&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;URIs&lt;/a&gt; are strings that identify a resource, and they often they &quot;point&quot; to another location - URLs are a form of URIs that point to web addresses. Or &quot;tel:{{number}}&quot; is a URI that identifies a phone number. Data URIs are unique in that instead of &quot;pointing&quot; to the location of the data, they actually have the raw data inside of the URI. An analogy would be that a book could be theoretically shared by an ISBN URI of &quot;ISBN 0-123-45678-9&quot;, but a Data URI that represents a book could be something like &quot;text/book;charset=US-ASCII,{{ENTIRE_TEXT_OF_BOOK}}&quot;.&lt;/p&gt;
&lt;p&gt;The way that we can use Data URIs with iframes is by setting the src of the iframe element to a data URI that holds raw HTML content, rather than to a URL that points to a web address. Sample:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function dataUriIframeInject(html,target,append,cb){
    cb = typeof(cb)===&apos;function&apos; ? cb : function(){};
    append = typeof(append)===&apos;boolean&apos; ? append : true;
    var iframe = document.createElement(&apos;iframe&apos;);
    // Construct Data URI
    var dataUri = &quot;data:text/html;charset=utf-8,&quot; + escape(html);
    iframe.addEventListener(&apos;load&apos;,cb);
    iframe.setAttribute(&apos;src&apos;,dataUri);
    if (append){
        target.appendChild(iframe);
        iframe.style.width = &apos;100%&apos;;
        iframe.style.height = &apos;100%&apos;;
    }
    else {
        target.replaceWith(iframe);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are several downsides to using Data URIs, and especially with IFrames&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Maximum length
&lt;ul&gt;
	&lt;li&gt;This is not standardized across browser, and sometimes is specified as a maximum character count, and sometimes as a maximum byte size (which would mean max chars depends on encoding type).&lt;/li&gt;
	&lt;li&gt;Good rule of thumb is probably to stay under 2K characters.&lt;/li&gt;
	&lt;li&gt;See &lt;a href=&quot;https://stackoverflow.com/a/41755526/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this StackOverflow answer&lt;/a&gt;, and &lt;a href=&quot;https://dataurl.sveinbjorn.org/#about&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this page&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Limited browser support
&lt;ul&gt;
	&lt;li&gt;Data URIs are supported on IE starting with IE8, but only for assets like images and CSS, not HTML. That makes it unusable with iframes on IE, even up to version 11.&lt;/li&gt;
	&lt;li&gt;See &lt;a href=&quot;https://caniuse.com/#feat=datauri&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;caniuse.com/#feat=datauri&lt;/a&gt; for more details.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Caching 
&lt;ul&gt;
	&lt;li&gt;The page that contains the Data URI can be cached, but the actual Data URI is not cached itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Character escaping
&lt;ul&gt;
	&lt;li&gt;Trying to escape strings is notoriously annoying, and although functions like escape() make this easy for the most part, don&apos;t be surprised if you run into something not working like you were expecting it to.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Limited functionality, and limits are different across browsers
&lt;ul&gt;
	&lt;li&gt;It can be hard to find this kind of info, but there are special security restrictions with content loaded through a data URI. For example, in Chrome, reading and writing cookies is blocked/disabled inside data URIs. Even first-party same-origin cookies.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The main benefit of injecting content into iframes with data URIs over the previous method is that, even though the iframe is essentially loading a virtual location, it maintains a separate origin from the document it is embedded into! This means that cross-origin policies are enforced, and javascript inside of the iframe cannot access your website, and vice-versa. However, be aware that some old versions of browsers might actually treat them as same-origin (or more accurately, inheriting the origin of the parent). For example, it looks like &lt;a href=&quot;https://blog.mozilla.org/security/2017/10/04/treating-data-urls-unique-origins-firefox-57/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Firefox versions below 57 did so&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;srcdoc&quot;&gt;&lt;/a&gt;Srcdoc attribute specifically for iframes: the hip new thing&lt;/h3&gt;
&lt;p&gt;Srcdoc is an attribute that you can use on iframes as an alternative to setting src. You set Srcdoc to a string of (valid) HTML, and it will load it into the iframe:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;iframe srcdoc=&quot;&amp;#x3C;p&gt;Hello World!&amp;#x3C;/p&gt;&quot;&gt;&amp;#x3C;/iframe&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Like the Data URI method, the iframe maintains a separate origin than the page it is embedded into, so cross-origin policies are enforced and javascript cannot break out of the iframe and interact with the parent, and vice-versa.&lt;/p&gt;
&lt;p&gt;Srcdoc is basically the official solution that all of the previous methods tried to hack together unofficially. It is super easy to use, has its own spec, and is gaining in adoption. Furthermore, it seems to have better security controls and documentation on how it sandboxes content. Unfortunately, the main drawback is still browser-compatibility. In fact, srcdoc has zero support on all versions of IE, and only proposed support on Edge. See &lt;a href=&quot;https://caniuse.com/#feat=iframe-srcdoc&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;caniuse.com/#feat=iframe-srcdoc&lt;/a&gt; for a full breakdown of browser support. You also still have to be somewhat careful about escaping strings - for example, the &lt;a href=&quot;https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-srcdoc&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;specs&lt;/a&gt; say that ampersands need to be double escaped.&lt;/p&gt;
&lt;p&gt;If you want to use srcdoc, but support as many browsers as possible, you might want to look at &lt;a href=&quot;https://github.com/jugglinmike/srcdoc-polyfill&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this polyfill&lt;/a&gt;. It lets you use srcdoc normally without worrying about which browsers your users are using, and then goes through and switches to an older method if necessary for the user.&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;generatingiframe_server&quot;&gt;&lt;/a&gt;Generating iframe content server-side:&lt;/h2&gt;
&lt;p&gt;Because of the complexities and more error-prone nature of generating iframes on the client-side with Javascript, or by serving iframes with inline Data URI srcs or srcdocs, a lot of people choose to generate iframe content on the server and use the src attribute of the iframe to point to the generator endpoint with information on what to serve. For example, consider a large commenting system where users can leave comments with embedded code demos - definitely something you want to enforce security on so users can&apos;t break out of the comment area and interact with the DOM or client JS. For this example, you might do something like:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;All comments are stored in a DB with unique IDs&lt;/li&gt;
	&lt;li&gt;Create a generator script that has an endpoint at:
&lt;ul&gt;
	&lt;li&gt;
&lt;pre&gt;&quot;embeds.example.com/comments/&quot;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;In the comment area, embed iframes with corresponding comment ids, like:
&lt;ul&gt;
	&lt;li&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;iframe src=&quot;embeds.example.com/comments/?id=220&quot;&gt;&amp;#x3C;/iframe&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Your script would pick up the ID from the querystring, lookup the comment from the DB, and echo back the raw HTML as a document, which would load into the iframe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course, there are many different ways to do this, and no one way is the &quot;correct way&quot;, but when it comes to security, you definitely want to be careful. If you don&apos;t need users to be able to embed any code, and just want simple comments, you would not use iframes, and instead just escape out any HTML and JS users try to put in comments. Again, this is complex stuff, so you probably just want to find a reputable commenting system rather than rolling your own. Improper escaping of comment forms and &lt;a href=&quot;https://en.wikipedia.org/wiki/Cross-site_scripting&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;XSS (cross-site-scripting) vulnerabilities&lt;/a&gt; have plagued even the &lt;a href=&quot;https://news.slashdot.org/story/10/07/04/1530234/youtube-hit-by-html-injection-vulnerability&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;largest&lt;/a&gt; &lt;a href=&quot;https://www.theguardian.com/technology/blog/2010/sep/21/twitter-hack-explained-xss-javascript&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;companies&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Microsoft Power BI - Playing Around with the Javascript Embed Library and APIs</title><link>https://joshuatz.com/posts/2019/microsoft-power-bi---playing-around-with-the-javascript-embed-library-and-apis/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/microsoft-power-bi---playing-around-with-the-javascript-embed-library-and-apis/</guid><description>Some random notes on using the Power BI Embedded Javascript APIs, such as resetting slicer visuals.</description><pubDate>Fri, 17 May 2019 04:34:04 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;In trying to answer a StackOverflow question, I quickly got sidetracked into playing around with the Microsoft Power BI Javascript Embed API / Library / SDK - something that I had never used before (or Power BI at all for that matter). Here are some &quot;oddities&quot; that I ran into that might help others to report on:&lt;/p&gt;
&lt;h3&gt;How to reset a Slicer state via the JS Embed Library:&lt;/h3&gt;
&lt;p&gt;&quot;Slicers&quot; are a form of Power BI &quot;Visuals&quot; that can contain filters, which is to say they are embedded widgets within a report that let the user filter the data that is shown across the report page. Unlike the global filters, which can be cleared by calling &lt;code&gt;embed.removeFilters()&lt;/code&gt;, there is no built-in method for resetting a slicer, since &lt;code&gt;slicerVisual.removeFilters()&lt;/code&gt; does not necessarily reset the actual slicer state. However, you can easily reset it by calling setSlicerState and setting an &quot;empty&quot; state, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;slicerVisual.setSlicerState({filters:[]})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fun fact - the above is basically how &lt;code&gt;removeFilters()&lt;/code&gt; also works - basically just an alias for &lt;code&gt;setFilters([]);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;How about a practical example - what if you want to reset every slicer filter when a single &quot;reset all filters&quot; button is pressed? You could have the following code execute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function resetAllSlicers() {
  let report = powerbi.embeds[0];
  let pages = await report.getPages();
  for (let x = 0; x &amp;#x3C; pages.length; x++) {
    let visuals = await pages[x].getVisuals();
    for (let x = 0; x &amp;#x3C; visuals.length; x++) {
      if (visuals[x].type === &apos;slicer&apos;) {
        // Clear state, but wait before moving on to next one, since clearing a filter can have a cascade effect
        try {
          let cleared = await visuals[x].setSlicerState({ &apos;filters&apos;: [] });
        }
        catch (e) {
          // setSlicerState or getSlicerState will fail if visual element is not initialized
          // visualConfigIsNotInitialized
        }
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How to use &quot;Hierarchy&quot; filters with reports or slicers&lt;/h3&gt;
&lt;p&gt;There is a strange column type (if you can call it that) within Power BI - a &quot;Hierarchy&quot;. It is similar to column groups, but different in that it is more structured and, like its name would imply, is a hierarchical set of structured data/fields, that can be nested multiple layers deep. Here is what it looks like inside the built-in filter side panel:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/PowerBI-Hierarchy-Rollup-In-Filter-Panel.png&quot;&gt;&lt;img class=&quot;size-full wp-image-499 aligncenter&quot; src=&quot;/media/PowerBI-Hierarchy-Rollup-In-Filter-Panel.png&quot; alt=&quot;PowerBI - Hierarchy Rollup In Filter Panel&quot; width=&quot;441&quot; height=&quot;359&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So, how do we filter on a column/field inside a hierarchy? It doesn&apos;t seem to be well documented, but it is pretty simple, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var report = powerbi.embeds[0];
var filter = {
  &quot;$schema&quot;: &quot;http://powerbi.com/product/schema#basic&quot;,
  &quot;target&quot;: {
    &quot;table&quot;: &quot;Geo&quot;,
    &quot;hierarchy&quot;: &quot;GeoRollup&quot;,
    &quot;hierarchyLevel&quot;: &quot;Zip&quot;
  },
  &quot;operator&quot;: &quot;In&quot;,
  &quot;values&quot;: [
    &quot;73555&quot;
  ]
}
report.setFilters([filter]);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, it seems like you can also just filter on the nested field itself, as if it belonged directly to the table. I&apos;m guessing this is only working for me because there is no naming conflict between a nested field and a regular field:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;var report = powerbi.embeds[0];
var filter = {
  &quot;$schema&quot;: &quot;http://powerbi.com/product/schema#basic&quot;,
  &quot;target&quot;: {
    &quot;table&quot;: &quot;Geo&quot;,
    &quot;column&quot;: &quot;Zip&quot;
  },
  &quot;operator&quot;: &quot;In&quot;,
  &quot;values&quot;: [
    &quot;73555&quot;
  ]
}
report.setFilters([filter]);&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Some helpful resources:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/PowerBI-JavaScript&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Power BI Javascript Client Repo&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/Microsoft/PowerBI-JavaScript/wiki&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Power BI Javascript Wiki&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/PowerBI-JavaScript/blob/master/dist/powerbi-client.d.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TypeScript Declaration File&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/powerbi-models&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Power BI JS/TS Models Repo&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/powerbi-models/blob/master/src/models.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Models file&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://microsoft.github.io/PowerBI-JavaScript/demo/v2-demo/index.html#&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Power BI Embedded Playground (has a bunch of demos, no login required)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Using Fabric.js (Fabric) without Cairo for web use - notes on NPM</title><link>https://joshuatz.com/posts/2019/using-fabricjs-fabric-without-cairo-for-web-use---notes-on-npm/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/using-fabricjs-fabric-without-cairo-for-web-use---notes-on-npm/</guid><description>Some tips on using Fabric.js as a dependency in your project, without requiring that other devs have Cairo installed in order to build.</description><pubDate>Sun, 12 May 2019 07:20:39 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;Fabric.js is &lt;a href=&quot;https://github.com/fabricjs/fabric.js&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;an amazing open-source library&lt;/a&gt; for Canvas manipulation with Javascript - I ended up using it heavily for &lt;a href=&quot;https://joshuatz.com/projects/web-stuff/cloudinary-wysiwyg-visual-editor-for-transformations/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this project&lt;/a&gt;, and have been meaning to post this writeup for a while on some ways to bundle Fabric into your dependencies, without requiring that the system your repo is built on has Cairo installed. These solutions only work if you don&apos;t need any of the node-canvas features, which is true for many web uses of Fabric.&lt;/p&gt;
&lt;p&gt;By default, if you add Fabric.js as a dependency to your package.json and/or run npm install, it will kick off a build process that will fail  if Cairo is not installed globally on your build system. This is because node-canvas (published under NPM as just &quot;&lt;a href=&quot;https://www.npmjs.com/package/canvas&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;canvas&lt;/a&gt;&quot;) is a dependency of Fabric.js, and in the &lt;a href=&quot;https://github.com/Automattic/node-canvas/blob/master/package.json&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;canvas package.json&lt;/a&gt;, the install command will run a build command that requires Cairo. &lt;/p&gt;
&lt;h2&gt;Easy option: Just ignore the build errors and grab JS from /dist&lt;/h2&gt;
&lt;p&gt;Although it probably goes against best practices, the nice builders of Fabric &lt;a href=&quot;https://github.com/fabricjs/fabric.js/tree/master/dist&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;have checked in pre-built versions&lt;/a&gt; of the Fabric.js library into their repository, built without node-canvas support. This means that, as long as you just need the browser version of Fabric, you can simply pull the pre-built file out of /node_modules for use in your app. For example, as a build step, simply copy like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp node_modules/fabric/dist/fabric.js public/lib/js/fabric.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because Canvas is an optional dependency, NPM install will succeed, even though it will spit out all those build errors without Cairo installed.&lt;/p&gt;
&lt;p&gt;As a side note, you could even pull Fabric.js from its Github location rather than NPM, since NPM install lets you specific Git repos as sources for dependencies.&lt;/p&gt;
&lt;h2&gt;Another option - check in your own pre-built version:&lt;/h2&gt;
&lt;p&gt;This is true for almost any scenario, not just for Fabric; you can always build the file the end user will need yourself, in your dev environment, and then check in the fully built file into your source code, and optionally excluding the raw dependencies as part of your package. In general this is a bad idea for multiple reasons; it goes against some principles of version control, makes it harder for other devs to change the build system, and could have legal implications depending on the license of the third party library. However, for a scenario like this one, I think checking in a single pre-built JS file into your source control could outweigh requiring every dev who wants to build your app to install an entire separate build tool like Cairo.&lt;/p&gt;
&lt;h2&gt;Off on a tangent; exploring using CDNJS as a dependency&lt;/h2&gt;
&lt;p&gt;When I initially ran into this issue, one silly thought I had that I wanted to explore to see if it would even be possible, was to pull the prebuilt fabric.min.js file from a URL, ideally from &lt;a href=&quot;https://cdnjs.com/libraries/fabric.js&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CDNJS&lt;/a&gt;. This is not something that should ever be used in a production setting, but it was a fun idea to explore and see if it could easily be scripted with Node (it was). I was able to very quickly script together a file to download the Javascript file directly from CDNJS and place in the spot within my project. I then called that script as my &quot;preinstall&quot; script. I took a little time today to clean that script up and make it into a generic file downloader that can be called via the CLI and optionally can check the downloaded file&apos;s hash, to make sure it matches what is expected (e.i. that the code within the file has not been changed). I have uploaded it as a Github gist &lt;a href=&quot;https://gist.github.com/joshuatz/4c2f321c19e503703037536751d01f29&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Simple Node File Downloader – With SRI (Integrity) Hash Checker</title><link>https://gist.github.com/joshuatz/4c2f321c19e503703037536751d01f29</link><guid isPermaLink="true">https://gist.github.com/joshuatz/4c2f321c19e503703037536751d01f29</guid><description>Reusable Node script that can be passed CLI arguments for a remote file to be downloaded, and optionally integrity checked against a known file hash.</description><pubDate>Sun, 12 May 2019 07:17:07 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_code_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Windows Batch Script to Restart Dropbox Desktop Client</title><link>https://gist.github.com/joshuatz/b8725f9933c9ba5d8b88da773bd07080</link><guid isPermaLink="true">https://gist.github.com/joshuatz/b8725f9933c9ba5d8b88da773bd07080</guid><description>Batch script to kill and restart dropbox client on Windows.</description><pubDate>Sun, 12 May 2019 00:00:56 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_code_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Building a VSCode Extension - Some Tips and Tricks</title><link>https://joshuatz.com/posts/2019/building-a-vscode-extension---some-tips-and-tricks/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/building-a-vscode-extension---some-tips-and-tricks/</guid><description>Some random tips and tricks I&apos;ve been collecting as I work on developing my first VSCode extension.</description><pubDate>Sat, 11 May 2019 01:32:45 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I&apos;m trying out building &lt;a href=&quot;https://code.visualstudio.com/api&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a VSCode extension&lt;/a&gt; for the first time, and having a lot of fun learning the basics of TypeScript and &lt;a href=&quot;https://code.visualstudio.com/api/references/vscode-api&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the VSCode API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&apos;m impressed by the level of documentation for the VSCode APIs, but thought I would share some additional tips and tricks I have been picking up.&lt;/p&gt;
&lt;h3&gt;Watch for Changes in a Specific Document&lt;/h3&gt;
&lt;p&gt;onDidChangeTextDocument and onDidCloseTextDocument are both excellent event listeners to subscribe to for watching for changes in a document. However, they are &quot;attached&quot; to the workspace, not to specific documents, so if you attach a listener to either, you will actually be getting notified when any document is changed or closed, not just the one you are interested. However, even though vscode.TextDocument is a complex type, using an equality operator works just fine, so the best way to listen for changes on a specific doc is to store a reference to the TextDocument of interest, then check if it matches on each event. Here is a quick sample:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;let currOpenEditor = vscode.window.activeTextEditor;
if (currOpenEditor){
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// In this example, we want to start watching the currently open doc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let currActiveDoc = currOpenEditor.document;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let onDidChangeDisposable = vscode.workspace.onDidChangeTextDocument((event: vscode.TextDocumentChangeEvent)=&amp;#x26;gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	if (event.document === currActiveDoc){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;		console.log(&apos;Watched doc changed&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;		console.log(&apos;Non watched doc changed&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let onDidCloseDisposable = vscode.workspace.onDidCloseTextDocument((closedDoc: vscode.TextDocument)=&amp;#x26;gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	if (closedDoc === currActiveDoc){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;		console.log(&apos;Watched doc was closed&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;		console.log(&apos;non watched doc closed&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-typescript&quot;&gt;}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Getting the range of entire document&lt;/h3&gt;
&lt;p&gt;Right now, it looks like there is no single API call that will return the full range that the text content of a document spans. There are a few different ways you can determine the range:&lt;/p&gt;
&lt;p&gt;The popular method - something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// where document is of type vscode.TextDocument
let entireDocRange: vscode.Range = new vscode.Range(0,document.lineAt(0).range.start.character,document.lineCount-1,document.lineAt(document.lineCount-1).range.end.character);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Neat method - Create a range that is purposely larger than the actual range of the document (start at 0,0, then go to end of doc + 1 line), then use &lt;a href=&quot;https://code.visualstudio.com/api/references/vscode-api#69&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;vscode.TextDocument.validateRange()&lt;/a&gt; to shrink the larger range down to the actual text content. Found on &lt;a href=&quot;https://stackoverflow.com/a/50875520/11447682&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;StackOverflow&lt;/a&gt;, demonstrated below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;let entireDocRange = document.validateRange(new vscode.Range(0,0,document.lineCount,0));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Webview - the built-in web renderer&lt;/h3&gt;
&lt;p&gt;If you are looking to display simple HTML content to a VSCode user, you can use &lt;a href=&quot;https://code.visualstudio.com/api/extension-guides/webview&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the built-in webview API&lt;/a&gt; to show a rendered webpage in a new tab - no dealing with deploying a local server, Chrome extensions, etc. You can even communicate with the content inside the webview by using the standard postMessage syntax.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Amp for Email - &lt;amp-list&gt; carousel not showing up - htaccess solution</title><link>https://joshuatz.com/posts/2019/amp-for-email---amp-list-carousel-not-showing-up---htaccess-solution/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/amp-for-email---amp-list-carousel-not-showing-up---htaccess-solution/</guid><description>Getting a JSON source for the AMP-List carousel element working in AMP for Email Playground - resolving CORs header issues through HTACCESS.</description><pubDate>Fri, 03 May 2019 16:21:34 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I recently ran into the issue of an &lt;a href=&quot;https://amp.dev/es/documentation/components/amp-list&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&amp;#x3C;amp-list&gt;&lt;/a&gt; carousel not working inside an AMP email template. More specifically, it was not showing up in the &quot;Gmail AMP for Email Playground&quot; tool - where the slide carousel should have appeared, there was just empty blank space. Opening up the browser dev tools, it was pretty easy to spot that this had to with security issues - the JSON src of the list was fetched just fine (200 status), but the Playground was refusing to parse and show it due to a bunch of missing CORs header.&lt;/p&gt;
&lt;p&gt;A quick search got me what I was was looking for - the AMP for Email specification does indeed require special security related headers for remote JSON sources that are fetched for &amp;#x3C;amp-list&gt; elements:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://developers.google.com/gmail/ampemail/security-requirements#amp_for_email_playground&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Here are the recommended headers for use with the AMP Email Playground&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/ampproject/amphtml/blob/master/spec/amp-cors-requests.md#cors-security-in-amp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Here is a more complete list of security headers that your server should send back in response to AMP resource requests.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I quickly uploaded the JSON source for my list to a special subfolder on my server, and added an HTACCESS file for that directory so I could play around with adding the correct headers. I followed the recommendations given by Google from the first link, and updated the email template with the new source. However, even though I could see that my JSON fetch now returned the right headers, such as &quot;amp-access-control-allow-source-origin&quot;, I was still getting a very specific error: &quot;The amp-access-control-allow-source-origin must be equal to the amp source origin sent in the request.&quot;&lt;/p&gt;
&lt;p&gt;This made even less sense when looking closely at the network request - my response header &lt;em&gt;&lt;strong&gt;exactly &lt;/strong&gt;&lt;/em&gt;matched the _amp_source_origin request parameter!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Amp-For-Email-Amp-Access-Control-Allow-Source-Origin-Error.png&quot;&gt;&lt;img class=&quot;aligncenter wp-image-461 size-medium&quot; src=&quot;/media/Amp-For-Email-Amp-Access-Control-Allow-Source-Origin-Error-293x300.png&quot; alt=&quot;Amp For Email - Amp-Access-Control-Allow-Source-Origin-Error&quot; width=&quot;293&quot; height=&quot;300&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It took me longer than I would like to admit, but after staring at the documentation again, this time at &lt;a href=&quot;https://github.com/ampproject/amphtml/blob/master/spec/amp-cors-requests.md#cors-security-in-amp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the readme for the full list of CORs headers for AMP&lt;/a&gt;, not just for email, I finally realized I was missing one additional header: &quot;access-control-expose-headers&quot;. This is important, because in order for the browser / AMP playground to be able to read back the non-standard AMP-Access-Control-Allow-Source-Origin, it needs to be explicitly exposed through that header. I added it to my HTACCESS file, and lo-and-behold, the carousel showed up and started working. Google should probably update the primary docs for the AMPHTML security page to make this more clear.&lt;/p&gt;
&lt;p&gt;Here is my full working HTACCESS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# CORs
Header add Access-Control-Allow-Origin &quot;*&quot;
Header add Access-Control-Allow-Source-Origin &quot;AMP-Access-Control-Allow-Source-Origin&quot;
# AMP specific
Header add AMP-Access-Control-Allow-Source-Origin &quot;amp@gmail.dev&quot;
# Don&apos;t forget this!
Header add access-control-expose-headers &quot;AMP-Access-Control-Allow-Source-Origin&quot;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>monitorEvents() Demo - Javascript DOM Events Virtual Console</title><link>https://codepen.io/joshuatz/pen/OGeYZP</link><guid isPermaLink="true">https://codepen.io/joshuatz/pen/OGeYZP</guid><description>Quick demo to show a stream of Javascript DOM events emitted when users interact with a page.</description><pubDate>Thu, 02 May 2019 22:58:25 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_code_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Tag Manager - Triggers Breakdown and Javascript Alternatives</title><link>https://joshuatz.com/posts/2019/google-tag-manager---triggers-breakdown-and-javascript-alternatives/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/google-tag-manager---triggers-breakdown-and-javascript-alternatives/</guid><description>A practical guide to GTM triggers, how they work, and Javascript alternatives if you can&apos;t use Google Tag Manager. Meant for both non-developers and developers.</description><pubDate>Thu, 02 May 2019 22:33:27 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;Google Tag Manager (GTM) is a great tool for digital marketing, devops, A/B website testing, analytics, you name it. With just a few clicks, you can easily assign tags to fire based on complex combinations of rules - such as firing a conversion pixel when a user submits a contact form with a certain dropdown field value - without having to write Javascript (JS) by hand, or update the actual source code of the site. However, I also think that GTM gets used a little &lt;em&gt;too&lt;/em&gt; often, without marketers understanding the underlying technology they are using, which can lead to tunnel vision and an inability to come up with solutions when GTM cannot be used for a given problem.&lt;/p&gt;
&lt;p&gt;This post will attempt to explain how GTM works on multiple levels, and serve as a guide on how you might replicate certain GTM functionalities with pure vanilla Javascript.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Table of Contents:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#terminologyWarning&quot;&gt;Warning: Conflicting Terminology&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#triggersHeading&quot;&gt;Triggers:&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#pageViewTriggersHeading&quot;&gt;Page View&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;Page View&lt;/li&gt;
	&lt;li&gt;Dom Ready&lt;/li&gt;
	&lt;li&gt;Window Loaded&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#clickTriggersHeading&quot;&gt;Click&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;All Elements&lt;/li&gt;
	&lt;li&gt;Just Links&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#userEngagementTriggersHeading&quot;&gt;User Engagement&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;Element Visibility&lt;/li&gt;
	&lt;li&gt;Form Submission&lt;/li&gt;
	&lt;li&gt;Scroll Depth&lt;/li&gt;
	&lt;li&gt;YouTube Video&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#otherTriggersHeading&quot;&gt;Other&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;Custom Event&lt;/li&gt;
	&lt;li&gt;History Change&lt;/li&gt;
	&lt;li&gt;JavaScript Error&lt;/li&gt;
	&lt;li&gt;Timer&lt;/li&gt;
	&lt;li&gt;Trigger Group&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#conditionalOperatorsHeading&quot;&gt;GTM Extra: &quot;All ___&quot; vs &quot;Some ___&quot; (conditional operator)&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#extraWaitForTagsHeading&quot;&gt;GTM Extra: &quot;Wait for Tags&quot;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;terminologyWarning&quot;&gt;WARNING: Conflicting terminology&lt;/h2&gt;
&lt;p&gt;First, something that has to be addressed right away; certain words carry a different meaning within the Javascript world that powers GTM, vs the marketing world that uses it. At the top of the list is the word &quot;event&quot;.&lt;/p&gt;
&lt;p&gt;For the duration of this post, I&apos;ll try to be very specific about which type of event I&apos;m talking about. For Browser DOM Javascript Events, I&apos;ll say &quot;JS Events&quot; or just &quot;Events&quot;. For marketing events, I&apos;ll always say which platform it goes with, such as &quot;Google Analytics Events&quot;.&lt;/p&gt;
&lt;h3&gt;Marketing Events vs JS Events:&lt;/h3&gt;
&lt;p&gt;For marketers, an &quot;event&quot; firing usually means a Google Analytics event, Facebook Pixel event, or any other platform recording some sort of user action occurring. Usually these events are not triggered unless specifically set up, and in fact, that is a common use of GTM; use a trigger, such as a button click, to fire a Google Analytics Event.&lt;/p&gt;
&lt;p&gt;However, in the Javascript web development world, &quot;event&quot; means something completely different. In JS, an event, or more accurately a &quot;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Events&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;DOM event&lt;/a&gt;&quot;, is a signal that is emitted by the browser&apos;s JS APIs, which you can then &quot;listen&quot; for. A large amount of them are fired automatically - for example, if you submit a form, the form &quot;submit&quot; event is fired automatically. Browser DOM events are a vast and complicated topic, but the important things to remember for the context of this post is that:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;DOM events happen whether or not you are listening for them&lt;/li&gt;
	&lt;li&gt;You cannot capture events that are outside the scope of where your code is running&lt;/li&gt;
	&lt;li&gt;A lot of GTM functionality is built upon JS events and listening for them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To visualize just how common and automatic JS events are, I&apos;ve setup a &lt;a href=&quot;https://codepen.io/joshuatz/embed/OGeYZP/?height=640&amp;#x26;amp;theme-id=0&amp;#x26;amp;default-tab=result&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;small demo&lt;/a&gt; (opens in new tab). On the right side of the demo, you can see a stream of JS events as they happen in real time. On the left side, you can trigger these events to occur by moving your mouse, clicking on buttons, typing in fields, etc. For the full demo source, visit the &lt;a href=&quot;https://codepen.io/joshuatz/pen/OGeYZP&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;codepen&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;triggersHeading&quot;&gt;Triggers:&lt;/h2&gt;
&lt;h3 id=&quot;pageViewTriggersHeading&quot;&gt;Page View:&lt;/h3&gt;
&lt;p&gt;Help page - &lt;a href=&quot;https://support.google.com/tagmanager/answer/7679319?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;link&lt;/a&gt;.&lt;/p&gt;
&lt;table id=&quot;pageViewTriggerTable&quot; style=&quot;width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;What it Does&lt;/td&gt;
&lt;td&gt;Javascript&lt;/td&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679319?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Page View&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Fires ASAP - basically as soon as GTM starts loading.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Since this fires ASAP, this would be the same as just putting JS code in the same place, or instead of your GTM embed snippet.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679319?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;DOM Ready&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Fires when the basic DOM structure of the page has been parsed. Images, styles, and other things could still be loading.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;window.addEventListener(&apos;DOMContentLoaded&apos;,function(evt){ /* Do Something */ });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, since this won&apos;t fire if the page has *already* hit DOM ready before this code ran, you can wrap it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;(function(){
    function handleEvent(){
        /* Do Something */
    }
    if (document.readyState===&quot;interactive&quot;){
        handleEvent();
    }
    else {
        window.addEventListener(&apos;DOMContentLoaded&apos;,handleEvent);
    }
})();&lt;/code&gt;&lt;/pre&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679319?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Window Loaded&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Fires when the page has been fully loaded (waits for images, scripts, etc). Can take a while!&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;window.addEventListener(&apos;load&apos;,function(evt){ /* Do Something */ });&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;div&gt;
&lt;p&gt;Or, since this won&apos;t fire if the page has *already* hit DOM load before this code ran, you can wrap it like this:&lt;/p&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;(function(){
    function handleEvent(){
        /* Do Something */
    }
    if (document.readyState===&quot;complete&quot;){
        handleEvent();
    }
    else {
        window.addEventListener(&apos;load&apos;,handleEvent);
    }
})();&lt;/code&gt;&lt;/pre&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;clickTriggersHeading&quot;&gt;Click&lt;/h3&gt;
&lt;table id=&quot;clickTriggerTable&quot; style=&quot;width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;What it Does&lt;/td&gt;
&lt;td&gt;Javascript&lt;/td&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679320?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;All Elements&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Listens for any click DOM event.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
document.addEventListener(&quot;click&quot;,function(evt){/* Do Something */});
&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;div&gt;
&lt;p&gt;Or, listen to a specific element&lt;/p&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
var elem = document.querySelector(&quot;#MyElement&quot;);
elem.addEventListener(&quot;click&quot;,function(evt){/* Do Something */});
&lt;/code&gt;&lt;/pre&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679320?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Just Links&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Listens for click events, but &quot;pre&quot; filters to click events that have fired on links.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
var links = document.querySelectorAll(&quot;a&quot;);
for (var x=0; x&amp;#x3C;links.length; x++){
    links[x].addEventListener(&quot;click&quot;,function(evt){/* Do Something */});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;userEngagementTriggersHeading&quot;&gt;User Engagement&lt;/h3&gt;
&lt;table id=&quot;userEngagementTriggerTable&quot; style=&quot;width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;What it Does&lt;/td&gt;
&lt;td&gt;Javascript&lt;/td&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679410?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Element Visibility&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;This is an advanced trigger that really shows off the power of GTM. There are a lot of different options with this trigger, but the basics of what it does is checks a few different things about the element to see if it is visible: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;getComputedStyle&lt;/a&gt; to see if the element has the display property set to hidden, and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;getBoundingClientRect&lt;/a&gt; to see if the boundaries of the element are within the current view of the user.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;This is not trivial to code out by hand, in a way that works across browsers and reliably. One option is to use JQuery and check visiblity whenever the page is scrolled:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
// Note - this fires whenever the element comes into view, regardless if already fired.
(function(){
    function testVisible(){
        if($(&quot;#MyElement&quot;).is(&quot;:visible&quot;)){ /* Do something */ }
    }
    testVisible();
    $(window).on(&quot;scroll&quot;,testVisible);
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, you could follow one of these guides: &lt;a href=&quot;https://www.thepolyglotdeveloper.com/2019/03/track-element-viewability-javascript-google-tag-manager/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt; or &lt;a href=&quot;https://gomakethings.com/how-to-test-if-an-element-is-in-the-viewport-with-vanilla-javascript/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679217?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Form Submission&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Fires whenever a &amp;#x3C;form&gt;&amp;#x3C;/form&gt; is submitted. Listens for the &quot;submit&quot; JS event.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
document.addEventListener(&quot;submit&quot;,function(evt){/* Do Something */});
&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;p&gt;More robust option - listen for specific form, and don&apos;t submit it UNTIL the marketing tag has fired.&lt;/p&gt;
&lt;br&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
var myForm = document.querySelector(&quot;#ContactForm&quot;);
myForm.addEventListener(&quot;submit&quot;,function(evt){
    // Prevent form from being submitted right away
    evt.preventDefault();
    // Do something
    /* sendGoogleAnalyticsEvent(&quot;Form Submitted&quot;); */
    // Use a delay to ensure the above marketing tag has finished
    setTimeout(function(){
        // Submit the form
        myForm.submit();
    },1000);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679218?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Scroll Depth&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;This allows you to configure tags to fire once the user has scrolled a certain amount down the page. Uses &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;scrollTop&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;scrollLeft&lt;/a&gt; to determine offset from origin and whether or not the threshold has been reached.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;p&gt;A simple version that just waits until a threshold has been hit might look something like this:&lt;/p&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
(function(){
    // 50% percent as float
    var threshold = 0.5;
    var fired = false;
    function checkVerticalScroll(){
        if (!fired){
            var totalHeight = parseInt(getComputedStyle(document.body).height.replace(&quot;px&quot;,&quot;&quot;).replace(&quot;%&quot;,&quot;&quot;),10);
            if (document.body.scrollTop &gt; (totalHeight * threshold)){
                /* Do Something */
                fired = true;
            }
        }
    }
    document.addEventListener(&quot;scroll&quot;,checkVerticalScroll);
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679325?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;YouTube Video&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;This trigger checks the page for any embedded YouTube Videos, and if they are there, allows you to fire tags based on a video play event (like start, pause, etc).&lt;/p&gt;
&lt;p&gt;The way this works is a little complicated, since YouTube embeds are usually iFrames. First, it requires that all YouTube embed Iframes contain enablejsapi=1, which is why they have the checkbox &quot;Add JavaScript API support to all YouTube videos&quot;, which automatically adds that string to all embeds. Once an embed has that flag, the video will start sending cross-origin messages (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;via window.postMessage()&lt;/a&gt;) with event data, which GTM is listening for. Finally, GTM checks the event that is passed via postMessage, and parses what type of YouTube event it maps to (play, pause, etc.) and determines if your tag should fire.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;p&gt;Here is some demo code that should work as long as your embeds have the enablejsapi=1 flag turned on.&lt;/p&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
(function(){
    // Change to match your YouTube Iframe embed
    var ytEmbedId = &quot;myvideo&quot;;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if (document.querySelectorAll(&apos;script[src=&quot;https://www.youtube.com/iframe_api&quot;]&apos;).length == 0){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    var tag = document.createElement(&apos;script&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    tag.src = &quot;https://www.youtube.com/iframe_api&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    document.body.appendChild(tag);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;window.onYouTubeIframeAPIReady = function(){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    console.log(&quot;YT Ready&quot;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    var player = new YT.Player(ytEmbedId);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    player.addEventListener(&quot;onStateChange&quot;,function(evt){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        switch (evt.data) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            case YT.PlayerState.UNSTARTED:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                // Video start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                /* Fire video start event */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                break;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            case 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                // Video Ended&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                /* Fire Video End Marketing Tag */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                break;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            case 1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                // Video playing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                break;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            case 2:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                // Video paused&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                /* Fire Video Paused Marketing Tag */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                break;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            default:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                break;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;})();
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;otherTriggersHeading&quot;&gt;Other&lt;/h3&gt;
&lt;table id=&quot;otherTriggersTable&quot; style=&quot;width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;What it Does&lt;/td&gt;
&lt;td&gt;Javascript&lt;/td&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679219?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Custom Event&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;This trigger is very specific to GTM - it can be used to fire a tag when a specific named event is pushed to the GTM DataLayer - which is kind of like a shared stream of events that exists in the page. Often, this kind of trigger is used when multiple vendors are working together to implement something.&lt;/p&gt;
&lt;p&gt;So, for example, a shopping cart vendor can push an event to the DataLayer for when a user adds an item to the cart, and then the ad agency vendor can listen for that event with this custom event trigger and fire a GA conversion event when it is trigger.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;If you are not using GTM in the first place, you wouldn&apos;t really need a JS alternative to this. Just use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events#Creating_custom_events&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;native custom JS events&lt;/a&gt;! For multiple vendors working together, one vendor can use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;dispatchEvent()&lt;/a&gt; to send the event, and another can use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;addEventListener&lt;/a&gt; to listen for it.&lt;/p&gt;
&lt;p&gt;However, if you do not have access to GTM, but another vendor *does*, and they want to share events with you by pushing to the dataLayer, you could still listen for their dataLayer despite not having a GTM login / access. You would basically replicate this trigger with some code like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
(function(){
    // Hold ref to original push fn
    var originalPushFn = window.dataLayer.push;
    // Replace with custom push fn
    window.dataLayer.push = function(){
        // Pass arguments to orginal function - VERY IMPORTANT
        originalPushFn.apply(this,arguments);
        // Our own processing
        for (var x=0; x&amp;#x3C;arguments.length; x++){
            if (typeof(arguments[x])===&quot;object&quot;){
                filterDataLayerPush(arguments[x]);
            }
        }
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;function filterDataLayerPush(pushObj){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Handle&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Example - waiting for event named &quot;addToCart&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (pushObj[&quot;event&quot;] === &quot;addToCart&quot;){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        /* Fire GA Add To Cart Marketing Tag */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;})();
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679322?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;History Change&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Lets you fire a tag when the URL changes in a non-standard way, without the browser actually navigating pages. The first way is through a &quot;URL fragment&quot; change. Most people know this as what follows the &quot;hash mark&quot; - e.g. example.com/subpage#subsectionalpha - where #subsectionalpha could change to #subsectionbeta without the page reloading. This is common on single-page-applications (SPAs). The second thing it listens for is the URL being changed through the HTML5 pushstate API. This is something newer and less common, but becoming more common with the rise of PWA (progress-web-apps).&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;For URL fragment / hash changes, the code is pretty simple. There is built in window JS event you can listen for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
window.addEventListener(&quot;hashchange&quot;,function(evt){
    if (location.hash === &quot;#subsectionalpha&quot;){
        /* Do Something */
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, there is not yet a good built-in JS event to listen for as an indicator of a pushState happening. The easiest current solution is to &lt;a href=&quot;https://stackoverflow.com/a/4585031&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;basically replace the global history.pushState function with your own function&lt;/a&gt; that scans the event and then passes it to the original function. If you look at the source code for GTM, you can see that is exactly what Google wrote their GTM JS code to do!&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679411?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;JavaScript Error&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;This gets triggered whenever an uncaught Javascript error occurs. The main use of this trigger is to capture the error data, and relay it to another platform that consolidates JS errors into some sort of dashboard, such as &lt;a href=&quot;https://sentry.io/welcome/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Sentry.io&lt;/a&gt; or &lt;a href=&quot;https://airbrake.io/languages/javascript-error-reporting&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Airbrake&lt;/a&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Most error collection dashboards probably have SDKs or plug-n-play libraries to make capturing client errors easy, so I doubt you would have to do much hand-coding in terms of catching and relaying JS errors.&lt;/p&gt;
&lt;p&gt;However, if you need to, there is a global window JS event that fires on uncaught JS errors for some browsers, which is aptly named &quot;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;error&lt;/a&gt;&quot;. Or refer to &lt;a href=&quot;https://www.bugsnag.com/blog/js-stacktraces&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this site&lt;/a&gt; on a cross-browser approach.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679323?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Timer&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;This is pretty simple - fires a tag after a set amount of time has passed. Or fires it every __x__ amount of time.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Here is a quick demo of how you could implement this in JS.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
(function(){
    // Configure time in MS and max fires
    var interval = 2000;
    var maxFires = 4;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;var timer;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;var fires = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;timer = setInterval(function(){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (fires &amp;#x26;lt; maxFires){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        /* Do Something */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        fires++;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        clearInterval(timer);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;},interval);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;})();
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/9164222?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Trigger Group&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;A trigger group is just a way in GTM to combine multiple triggers together into one rule. Normally, when you assign triggers to a tag, they function in an &quot;OR&quot; configuration, meaning that the tag will fire if ANY of the triggers get triggered. However, if you assign a trigger group, the triggers in that group function in an &quot;AND&quot; configuration, meaning that ALL of the triggers must have been triggered in order for the tag to fire.&lt;/p&gt;
&lt;p&gt;Example: You want to fire a &quot;high value user&quot; tag if a user scrolls below 50% of the page, AND spends at least 60 seconds on the site. You could enforce this by creating a trigger group that contains both a Timer Trigger as well as a Scroll Depth Trigger, and then assign that trigger group to the tag you want to fire.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;This is specific to GTM, so there is no need for a JS equivalent. JS developers should already be familiar with basic logic, such as AND vs OR - a GTM trigger group is basically the same as doing IF(TRIGGER_A &amp;#x26;&amp;#x26; TRIGGER_B &amp;#x26;&amp;#x26; ...){/* Do Something */}&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;conditionalOperatorsHeading&quot;&gt;GTM Extra: All ____ vs Some ___ (conditional operator)&lt;/h2&gt;
&lt;p&gt;On most triggers, there is an extra option to control firing that enables you to filter to a subset of the trigger. For example, on the &quot;Click&quot; trigger, there is an option that lets you pick between &quot;All Clicks&quot; and &quot;Some Clicks&quot;.&lt;/p&gt;
&lt;p&gt;When you turn on the &quot;Some&quot; filter, you can narrow down by many built in variables and conditions. The list is pretty extensive (&lt;a href=&quot;https://support.google.com/tagmanager/answer/7182738?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;link to built-in variables&lt;/a&gt;), but here are some of the most useful ones and how they map to Javascript:&lt;/p&gt;
&lt;table id=&quot;conditionalOperatorsTable&quot; style=&quot;width: 100%;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;td&gt;Condition&lt;/td&gt;
&lt;td&gt;Can be filtered on:&lt;/td&gt;
&lt;td&gt;What it Does&lt;/td&gt;
&lt;td&gt;Javascript&lt;/td&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://support.google.com/tagmanager/answer/7679109?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;matches CSS selector&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Element (Click,Form)&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/CSS/Introduction_to_CSS/Selectors&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;A CSS selector&lt;/a&gt; is a way to target an element based upon its properties that are mappable to how CSS gets applied to the element. An analogy could be selecting people based upon a description - &quot;Pick all men in the room with blonde hair, glasses, and red shoes&quot;. In the context of GTM, you might use this option to do something like filter link clicks to just click-to-call links, so you can fire a phone call marketing conversion tag. To do so, you could filter by &quot;matches CSS selector&quot;, and then put the value as &apos;a[href^=&quot;tel:&quot;]&apos;&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Most browsers support the &lt;a href=&quot;https://caniuse.com/#feat=matchesselector&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;.matches()&lt;/a&gt; method, so this is easy in JS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
// Attach to all links
document.querySelectorAll(&quot;a&quot;).forEach(function(elem){elem.addEventListener(&quot;click&quot;,filterLinkClicks)});
// ...elsewhere in code
function filterLinkClicks(evt){
    console.log(evt);
    if(evt.target.matches(&apos;a[href^=&quot;tel:&quot;]&apos;)){
        /* Do Something */
        // Fire click-to-call marketing conversion tag
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contains / starts with / equals / etc.&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Pretty much anything...&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Since this can be used as an operator with pretty much any variable, this can mean a lot of different things. For example, when used with &quot;Click Text&quot; or &quot;Form Text&quot;, GTM is basically comparing all the text contained within the HTML element to the value you enter to compare against.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Again, there are tons of different combinations that this filter allows for, but none of them are very complicated to implement in JS. For example, no matter how many elements are nested within another element, you can get all the text that is contained within and check if it contains a string using element.innerText:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
var searchString = &quot;Find Me!&quot;;
&lt;p&gt;// Case sensitive “contains”
if(myElement.innerText.includes(searchString)){
/* Do Something */
}&lt;/p&gt;
&lt;p&gt;// Alternative case-sensitive approach
if (myElement.innerText.indexOf(searchString) !== -1){
/* Do Something */
}&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Case in-sensitive
if (myElement.innerText.toLowerCase().indexOf(searchString.toLowerCase()) !== -1){
/* Do Something */
}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;extraWaitForTagsHeading&quot;&gt;GTM Extra: Wait for Tags:&lt;/h2&gt;
&lt;p&gt;Another &quot;extra&quot; option that appears in multiple spots within GTM is the option to &quot;wait for tags&quot;. This is useful because sometimes a trigger, such as a link click, will actually take the user to a different website, and if that happens before the tag has a chance to fire, then the marketing event never gets processed. By turning on &quot;wait for tags&quot;, GTM will actually STOP the user-initiated event from happening (such as navigating to a different site via a link) UNTIL your tags have fired.&lt;/p&gt;
&lt;p&gt;From a developer perspective, the way this is usually done is through &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;event.preventDefault()&lt;/a&gt; and calling stopPropogation(). Then you do whatver you want (fire tags, log something), then resume the event by either re-emitting it without stopping it again, or calling the native handle (e.g. form.submit())&lt;/p&gt;
&lt;p&gt;The risk, or downside to turning on this feature, is that interrupting the flow of JS events can cause issues with third-party libraries or your own codebase, depending on how various pieces of code are listening for events. &lt;a href=&quot;https://css-tricks.com/dangers-stopping-event-propagation/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Example&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here is a demo of how you might fire off a tag when a user clicks a link to an external site, and stop them navigating to the new site until your tag has fired:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
document.querySelectorAll(&quot;a[href]&quot;).forEach(function(link){
    // Only for external links
    if (link.hostname !== document.location.hostname){
        link.addEventListener(&quot;click&quot;,function(evt){
            // Only preventDefault on links opening in same tab
            if (link.getAttribute(&quot;target&quot;)!== &quot;_blank&quot;){
                // Stop browser from navigating to link
                evt.preventDefault();
                // Continue onwards to link destination after a delay of 0.25 seconds
                setTimeout(function(){
                    document.location.href = link.href;
                },250);
            }
            // If the link opens in the same tab, the code below has 250 ms to execute before it is too late (see above code)
            /* Do Something */
            // Fire Marketing Tag
        });
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;
&lt;style&gt;
#pageViewTriggerTable,#clickTriggerTable,#userEngagementTriggerTable,#otherTriggersTable,#conditionalOperatorsTable {
    table-layout: fixed;
}
/* Three Column Layouts */
#pageViewTriggerTable td,#clickTriggerTable td,#userEngagementTriggerTable td,#otherTriggersTable td {
    max-width: 33%;
}
#pageViewTriggerTable td:nth-child(2),#clickTriggerTable td:nth-child(2),#userEngagementTriggerTable td:nth-child(2),#otherTriggersTable td:nth-child(2) {
    vertical-align: top;
    width: 33%;
}
/* Four Column Layouts */
#conditionalOperatorsTable td {
    max-width: 25%;
}
#conditionalOperatorsTable td:nth-child(2) {
    width: 170px;
}
/* ALL Tables */
.entry-content table {
    border: 1px solid black;
}
.entry-content table td {
    border-left: 1px dashed rgba(0, 0, 0, 0.42);
}
/* All Tables - small first column */
#pageViewTriggerTable td:nth-child(1),#clickTriggerTable td:nth-child(1),#userEngagementTriggerTable td:nth-child(1),#otherTriggersTable td:nth-child(1), #conditionalOperatorsTable td:nth-child(1) {
    width: 170px;
    vertical-align: top;
    font-size: 1.4rem;
}
thead td {
    font-size: 1.4rem;
    text-decoration: underline;
}
&lt;/style&gt;
&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Desktop Cloud Transform (DCT)</title><link>https://joshuatz.com/projects/applications/desktop-cloud-transform-dct/</link><guid isPermaLink="true">https://joshuatz.com/projects/applications/desktop-cloud-transform-dct/</guid><description>Desktop Cloud Transform is an image uploader and transformation manager, which lets you apply complex alterations to your images all in the cloud. Built with QT/C++ and uses Cloudinary&apos;s image transformation services and API.</description><pubDate>Wed, 24 Apr 2019 15:28:27 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I created Desktop Cloud Transform, from scratch to fully functional prototype, in about a week (about 40-50 hours of logged time before feature complete). I used Qt/C++, Cloudinary&apos;s API, SQLite, Javascript, and other technologies. I decided to build it for my own needs, and also as an opportunity to get up to speed on QT and C++.&lt;/p&gt;
&lt;p&gt;Github repo &amp;#x26; readme: &lt;a href=&quot;https://github.com/joshuatz/desktop-cloud-transform&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/joshuatz/desktop-cloud-transform&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;What is Desktop Cloud Transform (DCT)?&lt;/h2&gt;
&lt;p&gt;Elevator pitch: Desktop Cloud Transform (or DCT) is a desktop application that allows you to upload and apply complex transformations to images in the cloud, simply by dragging and dropping. It uses the power of Cloudinary&apos;s transformation engine, which means you can apply almost any combination of alterations to your images just like you could in Photoshop, but since it is all done in the cloud, you don&apos;t need a powerful computer, expensive software, and more importantly,  you don&apos;t need to know &lt;em&gt;how&lt;/em&gt; to use complicated image editing software.&lt;/p&gt;
&lt;p&gt;Here is a sample configuration - taking any local image file, and turning it into a fun Polaroid style image:&lt;/p&gt;
&lt;p&gt;[video width=&quot;1920&quot; height=&quot;1080&quot; mp4=&quot;/media/Desktop-Cloud-Transform-DCT-Polaroid-Demo-John.mp4&quot; loop=&quot;true&quot; autoplay=&quot;true&quot;][/video]&lt;/p&gt;
&lt;p&gt;Here is a more practical business application - showing off UI mockups on actual physical hardware that would normally cumbersome to deploy to and take a picture of, such as a Tesla screen:&lt;/p&gt;
&lt;p&gt;[video width=&quot;1920&quot; height=&quot;1080&quot; mp4=&quot;/media/Desktop-Cloud-Transform-DCT-Tesla-GUI-Demo.mp4&quot;][/video]&lt;/p&gt;
&lt;h2&gt;User Interface&lt;/h2&gt;
&lt;p&gt;I used QML for creating the user interface, which I think turned out nicely. Here is the main UI / home screen:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Desktop-Cloud-Transform-Main-UI.png&quot;&gt;&lt;img class=&quot;size-full wp-image-417 aligncenter&quot; src=&quot;/media/Desktop-Cloud-Transform-Main-UI.png&quot; alt=&quot;Desktop Cloud Transform - Main UI&quot; width=&quot;660&quot; height=&quot;557&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And configuration editor :&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Desktop-Cloud-Transform-Config-Editor-Editing-Existing-Config.png&quot;&gt;&lt;img class=&quot;size-full wp-image-418 aligncenter&quot; src=&quot;/media/Desktop-Cloud-Transform-Config-Editor-Editing-Existing-Config.png&quot; alt=&quot;Desktop Cloud Transform - Config Editor - Editing Existing Config&quot; width=&quot;656&quot; height=&quot;566&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;What is supported? What are DCT &quot;Configurations&quot;?&lt;/h2&gt;
&lt;p&gt;DCT configurations are combinations of DCT settings (such as &quot;overwrite existing file&quot;) and Cloudinary transformation settings (such as &quot;crop to 200 x 200 pixels, blur faces, etc&quot;). Once you save a configuration, you can drag and drop images onto its row and the image you dropped will be uploaded to Cloudinary, have the configuration applied, and then the result downloaded back to your local computer folder.&lt;/p&gt;
&lt;p&gt;Cloudinary does all the heavy lifting for DCT - storing images, transforming, applying effects, etc. So pretty much any transformation that Cloudinary supports can be saved within a DCT &quot;Configuration&quot;. If you are looking to create some complicated presets, I highly recommend taking a look at Cloudinary&apos;s massive &lt;a href=&quot;https://cloudinary.com/documentation/image_transformation_reference&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;Transformation Reference&quot; documentation&lt;/a&gt;. You can also take a look at &lt;a href=&quot;https://joshuatz.com/projects/web-stuff/cloudinary-wysiwyg-visual-editor-for-transformations/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my other Cloudinary project&lt;/a&gt;, which explored some unique ways to combine Cloudinary transformations.&lt;/p&gt;
&lt;h3&gt;Extra Feature: Macro support&lt;/h3&gt;
&lt;p&gt;An extra feature of DCT is the ability to use &quot;macros&quot; within transformation strings. For example, if you put &quot;{UPLOADED}&quot; inside an outgoing transformation string, my program will replace it with the correct string to represent an overlay layer that references the public ID of the image you dragged onto the configuration row. Some other currently supported macros are {filenameNoExt}, to insert the filename of your uploaded image, without the extension (e.g. &quot;My File&quot; instead of &quot;My File.jpg&quot;).&lt;/p&gt;
&lt;p&gt;For an up-to-date list of supported macros, make sure you have the most recent version of the software, and check the documentation &lt;a href=&quot;https://github.com/joshuatz/desktop-cloud-transform/blob/master/README.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Cloudinary SDK for C++:&lt;/h2&gt;
&lt;p&gt;As a technical side note, if you are a fellow developer looking to integrate Cloudinary into your C++ based application, unfortunately I have to tell you that they do not have an existing C++ SDK. However, their API is just as powerful - most of their SDK libraries are basically just translating SDK calls into API calls. In addition, you might find some of the code I have written helpful in getting started; I have some helper functions to do things like generate the special secure API upload signature (see &lt;a href=&quot;https://github.com/joshuatz/desktop-cloud-transform/blob/cb21d13652a70da77b22f00579696af59c943a85/apis/cloudinary.cpp#L17&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;, for example). I&apos;ve used a decent amount of QT specific classes, but it should be moderately easy to convert to std libraries or something using boost.&lt;/p&gt;
&lt;h2&gt;Disclaimers:&lt;/h2&gt;
&lt;p&gt;I built this application as a learning experience to get up to speed on QT/C++ and solve some repetitive image manipulation needs of my own. Not a lot of development time has gone into it so far, so it still has some rough edges and hopefully will see improvements over time.&lt;/p&gt;
&lt;p&gt;In addition, although I have a fondness for Cloudinary and this application is mostly a wrapper around their services, it is not officially endorsed, supported by, or associated with Cloudinary in any manner. Cloudinary is a registered trademark of Cloudinary Ltd.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>QT - Code Snippet for QNetworkRequest Lambda Callback with Connect</title><link>https://joshuatz.com/posts/2019/qt---code-snippet-for-qnetworkrequest-lambda-callback-with-connect/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/qt---code-snippet-for-qnetworkrequest-lambda-callback-with-connect/</guid><description>Using lambda expressions with QT&apos;s newer connect syntax to receive the finished event of a QNetworkRequest / reply without using QObject.</description><pubDate>Fri, 12 Apr 2019 01:00:09 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;This is not best practice, but I wanted to handle the result of a very simple web request (GET a URL) in as few lines of code as possible, primarily just as a test mechanism. Pretty much every example I could find out there either didn&apos;t work, or required connecting true signal and slots, where the class that contains the initiating code would need to be a QObject derived class so that it can function as the slot mechanism for &quot;receiving&quot; the update. What was frustrating to me about this was that I have spent most of my coding time working with Javascript and similar web-oriented languages, which always have tons of options for asynchronous operations and things like callbacks, futures, and async/await.&lt;/p&gt;
&lt;p&gt;Finally, after a little bit of searching and trail and error, I found the solution with Lambda expressions and the &lt;a href=&quot;https://wiki.qt.io/New_Signal_Slot_Syntax&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;new syntax available for QObject::connect&lt;/a&gt; (which does not require that the receiver be a QObject slot):&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void Helpers::checkInternetConnection(){
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;qDebug() &amp;#x26;lt;&amp;#x26;lt; &quot;Checking internet connection...&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;QNetworkAccessManager *netManager = new QNetworkAccessManager();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// Use lambda as receiver with connect to get result of network finished event&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;QObject::connect(netManager,&amp;#x26;amp;QNetworkAccessManager::finished,[=](QNetworkReply *reply) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    qDebug() &amp;#x26;lt;&amp;#x26;lt; &quot;In lambda!&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (reply-&amp;#x26;gt;error()){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        qDebug() &amp;#x26;lt;&amp;#x26;lt; &quot;Fail&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        qDebug() &amp;#x26;lt;&amp;#x26;lt; &quot;Pass!&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// Start the network request&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;QNetworkReply *netReply = netManager-&amp;#x26;gt;get(QNetworkRequest(QString(&quot;https://www.google.com/&quot;)));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-cpp&quot;&gt;}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;You can can even manipulate the value of outside variables through lambdas by including them in the capture argument. Here I&apos;m de-referencing a pointer to a boolean argument and filling in the value based on the result of the network request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// In Helpers class:
void Helpers::checkInternetConnection(bool *res){
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;qDebug() &amp;#x26;lt;&amp;#x26;lt; &quot;Checking internet connection...&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;QNetworkAccessManager *netManager = new QNetworkAccessManager();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// Use lambda as receiver with connect to get result of network finished event&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;QObject::connect(netManager,&amp;#x26;amp;QNetworkAccessManager::finished,[=](QNetworkReply *reply) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    qDebug() &amp;#x26;lt;&amp;#x26;lt; &quot;In lambda!&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (reply-&amp;#x26;gt;error()){&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        qDebug() &amp;#x26;lt;&amp;#x26;lt; &quot;Fail&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        *res = false;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        qDebug() &amp;#x26;lt;&amp;#x26;lt; &quot;Pass!&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        *res = true;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// Start the network request&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;QNetworkReply *netReply = netManager-&amp;#x26;gt;get(QNetworkRequest(QString(&quot;https://www.google.com/&quot;)));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-cpp&quot;&gt;}
// Elsewhere:
bool hasInternet = false;
Helpers::checkInternetConnection(&amp;#x26;hasInternet);
QTimer::singleShot(2000,&lt;a href=&quot;&quot;&gt;&amp;#x26;hasInternet&lt;/a&gt;{
qDebug () &amp;#x3C;&amp;#x3C; “res = ” &amp;#x3C;&amp;#x3C; hasInternet;
});
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Although this is tempting to use for a one-off testing mechanism, once I stopped to think for a moment about what I needed to do, I realized that I was better off not fighting using QObject classes. If I&apos;m trying to track whether or not my application has internet access, I&apos;m much better off creating a App Status QObject class, which would have a &quot;hasInternet&quot; member, which I can then hook together with a signal of &quot;internetConnectionChanged&quot; and have &quot;hasInternet&quot; be the receiver slot for my checkInternetConnection function.&lt;/p&gt;
&lt;p&gt;Frankly, thanks to QT&apos;s macro system, that actually isn&apos;t even all that much boilerplate code, and once I set that up, I can then easily expose my QObject app status class to the QML side, and do something neat like fade out and disable buttons that rely on internet connected features when the application detects a loss of network.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Cross-Platform Interacting with Windows, Titles, Keypresses, and More</title><link>https://joshuatz.com/posts/2019/cross-platform-interacting-with-windows-titles-keypresses-and-more/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/cross-platform-interacting-with-windows-titles-keypresses-and-more/</guid><description>Exploring cross-platform coding library options for window manipulation, active window details, keypress emulation, and more.</description><pubDate>Wed, 03 Apr 2019 13:40:27 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;One of the things that bothers me about development is when you come across a problem, and you think to yourself, &quot;surely someone else has solved this before&quot;, but then you start searching and keep coming up empty. This is the issue I had when starting a recent project that needed to do things like see which Window (application) is open and active in a cross-platform (Windows, Linux, Mac) way.&lt;/p&gt;
&lt;p&gt;In fact, I had such a hard time finding existing cross-platform libraries that I started writing one myself, starting with the Windows side of things, but when the complexity got really  high, I revisited my search, and finally got some results. I wanted to share what I found, since as I said, these resources were not easy to track down.&lt;/p&gt;
&lt;h2&gt;Quick list of options:&lt;/h2&gt;
&lt;p&gt;So far I have only found three libraries that are cross-platform, although I&apos;m still keeping an eye out. See the embedded spreadsheet below for a comparison of features, or &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1Ht_JClrZufJxYb_loA1KvOIDD-r1LHw6sY3-uLzU3fM/edit?usp=sharing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;click here&lt;/a&gt; to open in a new tab:&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://docs.google.com/spreadsheets/d/e/2PACX-1vR-DYr4D3oepZe5g1ZhFWAMu4ladR2FJYrZLrXXfLZrXoQmZJjRTXgKSDjpLBplGL7fa3f9st0y_eHX/pubhtml?widget=true&amp;#x26;headers=false&quot; width=&quot;100%&quot; height=&quot;300px&quot;&gt;&amp;#x3C;span style=&quot;display: inline-block; width: 0px; overflow: hidden; line-height: 0;&quot; data-mce-type=&quot;bookmark&quot; class=&quot;mce_SELRES_start&quot;&gt;﻿&amp;#x3C;/span&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h2&gt;Most Robust: Robot (Robot/robot and Robot/robot-js)&lt;/h2&gt;
&lt;p&gt;I really wish I had found this library sooner. &lt;a href=&quot;https://github.com/Robot/robot&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;The Robot library&lt;/a&gt; is an incredibly thorough and well put-together library that can handle pretty much anything you would need in order to manipulate windows and send keypresses. What makes it even more impressive is that it appears to be written and maintained by just one very dedicated individual; &lt;a href=&quot;https://dave.krutsko.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;David Krutsko&lt;/a&gt;. Kudos to him on building this amazing library.&lt;/p&gt;
&lt;p&gt;Robot can be used as a C++ library, or a NodeJS library. I have just started playing around with it for C++, combined with QT, and I can share a tip related to getting it working. To get it to work for my project, I had to change a few things:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Changed compiler from MinGW to MSVC.
&lt;ul&gt;
	&lt;li&gt;Robot uses the &amp;#x3C;threads&gt; library, which, at least for the version of MinGW shipped with QT, is not available. So trying to compile I got a bunch of errors about mutex and threads. &lt;a href=&quot;https://github.com/meganz/mingw-std-threads&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Someone put together a Thread library for MinGW&lt;/a&gt;, but I couldn&apos;t get it to work, so I just gave up and switched over to MSVC, which basically works out of the box with this library.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Manually linked up wingdi.h / Gdi32.dll Windows GDI library
&lt;ul&gt;
	&lt;li&gt;For Windows, Robot uses some bitmap / drawing functions that rely on the Windows gdi32 library. When trying to compile without any special setup, I got a bunch of linker errors related to the use of this library - things like &quot;Clipboard.obj:-1: error: LNK2019: unresolved external symbol __imp_GetDIBits referenced in function &quot;public: static bool __cdecl Robot::Clipboard::GetImage(class Robot::Image &amp;#x26;)&quot; (?GetImage@Clipboard@Robot@@SA_NAEAVImage@2@@Z)&quot;
&lt;ul&gt;
	&lt;li&gt;If you are using QMake like I am, just add &quot;LIBS += -lgdi32&quot; to either your project file, or in a PRI that gets loaded for this library.&lt;/li&gt;
	&lt;li&gt;Also make sure that QMake can find the Windows Kit folder where the DLL and header files live. You can always add it manually through &quot;INCLUDEPATH +=&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is the PRI file that I created to pull in all the Robot source files into my QT project. Depending on how you have structured your project, you would need to change the ROBOT_LIB_SOURCEPATH variable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;# https://github.com/Robot/robot
&lt;p&gt;ROBOT_LIB_SOURCEPATH = $$PWD/../lib/robot/Source
INCLUDEPATH += $$ROBOT_LIB_SOURCEPATH&lt;/p&gt;
&lt;h1 id=&quot;rare-use-case-will-require-uncommenting-below---1-forces-new-c11-abi&quot;&gt;Rare use case will require uncommenting below - =1 forces new C++11 ABI&lt;/h1&gt;
&lt;p&gt;#DEFINES += “_GLIBCXX_USE_CXX11_ABI=0”&lt;/p&gt;
&lt;h1 id=&quot;robot-needs-some-extra-win-libs---primarily-for-clipboard-and-bitmap-operations&quot;&gt;Robot needs some extra win libs - primarily for clipboard and bitmap operations&lt;/h1&gt;
&lt;p&gt;INCLUDEPATH += “C:/Program Files (x86)/Windows Kits/8.1/Include”
LIBS += -lgdi32&lt;/p&gt;
&lt;p&gt;HEADERS += &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Enum.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Bounds.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Clipboard.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Color.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Global.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Hash.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Image.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Keyboard.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Memory.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Module.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Mouse.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Point.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Process.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Range.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Robot.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Screen.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Size.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Timer.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Types.h &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Window.h&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-makefile&quot;&gt;SOURCES += &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Bounds.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Clipboard.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Color.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Hash.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Image.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Keyboard.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Memory.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Module.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Mouse.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Point.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Process.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Range.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Screen.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Size.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Timer.cc &lt;br&gt;
$$ROBOT_LIB_SOURCEPATH/Window.cc
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Getting Robotjs (octaImage/robotjs) working with C++ and QT&lt;/h2&gt;
&lt;p&gt;Robotjs (not to be confused with Robot-JS by Robot) is a NodeJS desktop manipulation library written by octaImage, aka &lt;a href=&quot;https://jason.stallin.gs/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Jason Stallings&lt;/a&gt;. One of the neat things about this library, besides it being written in C, is that Jason did a really nice job of keeping the C/C++ layer separate from the Javascript / NodeJS layer. This layer of separation allows us to actually use the native C code in its raw form, before it gets compiled by node-gyp into a Node addon.&lt;/p&gt;
&lt;p&gt;I was able to do this surprisingly easily with QT - I just setup a PRI file to import all the raw header and source files, making sure not to pull in the files that wrap functions for node (primarily robotjs.cc). Here is my PRI file, but note that I&apos;m not pulling in 100% of the files, since I only wanted to test part of the library:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;NODE_MODULESPATH = $$PWD/../node_modules
NODE_SOURCEPATH = $$PWD/../node_modules/robotjs/src
INCLUDEPATH += $$NODE_SOURCEPATH
&lt;h1 id=&quot;special---seems-to-be-an-issue-with-a-windows-constant---could-also-be-related-to-_win32_winnt-issue-on-active-windows-lib&quot;&gt;Special - seems to be an issue with a windows constant - could also be related to _WIN32_WINNT issue on active-windows-lib&lt;/h1&gt;
&lt;p&gt;DEFINES += “MOUSEEVENTF_HWHEEL=0x1000”&lt;/p&gt;
&lt;p&gt;HEADERS += &lt;br&gt;
windows.h &lt;br&gt;
$$NODE_SOURCEPATH/os.h &lt;br&gt;
$$NODE_SOURCEPATH/keycode.h &lt;br&gt;
$$NODE_SOURCEPATH/keypress.h &lt;br&gt;
$$NODE_SOURCEPATH/mouse.h &lt;br&gt;
$$NODE_SOURCEPATH/screen.h &lt;br&gt;
$$NODE_SOURCEPATH/types.h &lt;br&gt;
$$NODE_SOURCEPATH/deadbeef_rand.h&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-makefile&quot;&gt;SOURCES += &lt;br&gt;
$$NODE_SOURCEPATH/keycode.c &lt;br&gt;
$$NODE_SOURCEPATH/keypress.c &lt;br&gt;
$$NODE_SOURCEPATH/mouse.c &lt;br&gt;
$$NODE_SOURCEPATH/screen.c &lt;br&gt;
$$NODE_SOURCEPATH/deadbeef_rand.c
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;I also had to set a constant, for MOUSEEVENTF_HWHEEL. I&apos;ve moved on from using this library, so I can&apos;t recall exactly why I had to add it, but I&apos;m guessing the compiler was complaining about not knowing the value for it, which probably has to do with a screwed up path import on my end.&lt;/p&gt;
&lt;h2&gt;Understanding the Inner Workings&lt;/h2&gt;
&lt;p&gt;If you want to roll your own solution, or just understand more about the inner workings of these libraries and how you use system API calls to interact with Windows and inputs, I do actually have some advice for you.&lt;/p&gt;
&lt;p&gt;First, start with the &lt;a href=&quot;https://github.com/sindresorhus/active-win/tree/master/lib&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;source code for the active-win library&lt;/a&gt;. It is well-commented and is basically the textbook example for how these system API calls are meant to be used, for Windows, Mac, and Linux.&lt;/p&gt;
&lt;p&gt;Next, if you are developing for Windows, be aware that most of their desktop APIs use strings in a... ahem... &quot;interesting way&quot;. You will see strings passed around as &quot;LPTSTR&quot;, which is basically a macro that expands to either LPWSTR (long pointer to wide string, which is composed of wchar_t) or LPSTR (composed of char), based on if your project uses Unicode. &lt;a href=&quot;https://stackoverflow.com/questions/321413/lpcstr-lpctstr-and-lptstr/46457146#46457146&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;This stack overflow answer&lt;/a&gt; has a good breakdown of some unique types that the Windows API uses.&lt;/p&gt;
&lt;p&gt;For keypresses, be aware that although we have things like the Unicode standard so that characters and strings are encoded the same across platforms, actual keypresses are usually represented by enums/constants that are unique per platform. For example, Windows represents keypresses through &quot;Virtual-Key Codes&quot;, the constants for which can be found &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/desktop/inputdev/virtual-key-codes&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;. Meanwhile, Unix uses X11 and defines it own set of constants, which can be found &lt;a href=&quot;https://www.cl.cam.ac.uk/~mgk25/ucs/keysymdef.h&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt; among other places. I started compiling a database of these different constants across platforms, which can be found &lt;a href=&quot;https://joshuatz.com/custom-tools/2019/key-codes-reference-spreadsheet-ascii-qt-x11-etc/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, when looking for existing source code to reference and see how this problem has been tackled by others, don&apos;t forget some interesting places to look. Software that is likely to use system APIs to interact with Windowed applications and keypresses is:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Automated testing software&lt;/li&gt;
	&lt;li&gt;&quot;Mocking&quot; software&lt;/li&gt;
	&lt;li&gt;Password database apps
&lt;ul&gt;
	&lt;li&gt;For example, &lt;a href=&quot;https://github.com/dlech/KeePass2.x/blob/6b1160ff4d1b042bf7e59bb8c2e9f0a189a35c4d/KeePass/Native/NativeMethods.New.cs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;you can see how KeePass uses some native Windows API calls&lt;/a&gt; to see which window is open and send passwords to the right one.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Screenshot apps
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/greenshot/greenshot/blob/08e68ad61dcc44483018ce6b0283c92cd069b011/src/Greenshot/Components/HotkeyService.cs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;You can see here how Greenshot uses Dapplo.Windows to register system level keypress hooks&lt;/a&gt; - so they can listen for the Print Screen Key to be pressed, as well as custom combos.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Productivity trackers (ones that automatically record which applications you are spending time in)
&lt;ul&gt;
	&lt;li&gt;Good example: ActivityWatch - &lt;a href=&quot;https://github.com/ActivityWatch/activitywatch&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Python source code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Browsers
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://chromium.googlesource.com/chromium/chromium/+/master/ui/base/keycodes&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Chromium source&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Know of something better?&lt;/h2&gt;
&lt;p&gt;If you feel like I missed something big, feel free to leave a comment below. It is always interesting to see how different solutions are engineered around a shared problem. I&apos;m still looking for a cross-platform solution for key &quot;hooks&quot; - where your code can listen for a keypress regardless of whether or not it is headed to your application, and you can interrupt the press and prevent it from propagating. For example, KeePass, in part, manually implements Windows BlockInput, &lt;a href=&quot;https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-blockinput&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;which is part of the standard User32 lib&lt;/a&gt;. &lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Excluding Employee Traffic from Google Analytics, Pixels, and More</title><link>https://joshuatz.com/posts/2019/excluding-employee-traffic-from-google-analytics-pixels-and-more/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/excluding-employee-traffic-from-google-analytics-pixels-and-more/</guid><description>A guide on implementing internal traffic filtering in analytics, as well as blocking tracking and ads for internal employees using IP addresses and/or cookies.</description><pubDate>Mon, 01 Apr 2019 13:52:01 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;A question that I keep seeing pop up on digital marketing forums, and have had to tackle myself in the past, is how to exclude &quot;internal traffic&quot; from showing up in Google Analytics (e.g. when your own employees are browsing your own website). This post will cover a few different solutions, some of which can work for many different platforms other than just Google Analytics, such as preventing pixel fires coming from employees. There is a lot of misinformation when it comes to marketing technology, so please approach this topic with a critical eye and keep in mind that technology is also subject to change.&lt;/p&gt;
&lt;p&gt;This is a very long and comprehensive guide to this issue, so you might want to jump to the section that interests you using the table of contents below:&lt;/p&gt;
&lt;div id=&quot;tableOfContents&quot;&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;Table of Contents:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#WhyDoesThisMatter&quot;&gt;Why Does This Matter?&lt;/a&gt;&lt;br&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#ImportanceForAnalytics&quot;&gt;Google Analytics and Performance Metrics&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#ImportanceForAdvertising&quot;&gt;Digital Advertising and Spend&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#intro-to-ip-address-exclusions-and-cookie-exclusions&quot;&gt;Intro to IP Address Exclusions and Cookie Based Exclusions&lt;/a&gt;&lt;br&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#ip-vs-cookies&quot;&gt;Comparison of IPs vs Cookies&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#downside-to-geo-exclusions&quot;&gt;Downsides to Geographic Based Exclusions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#how-to-implement-ip-exclusions&quot;&gt;How to Implement IP Address Exclusions&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#ga-ip-excluded-filtered-views&quot;&gt;Using Filtered Views in Google Analytics&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#ip-exclusions-with-dsps&quot;&gt;Across Digital Advertising Platforms&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#advanced-ip-blocking-with-tags&quot;&gt;Advanced Implementation: Any Trackers or Pixels&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#blocking-tag-fires-by-ip-without-gtm&quot;&gt;Without Google Tag Manager (GTM)&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#blocking-tag-fires-by-ip-with-gtm&quot;&gt;With GTM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#how-to-implement-cookie-based-exclusions&quot;&gt;How to Implement Cookie Based Exclusions&lt;/a&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#cookie-based-exclusions-without-gtm&quot;&gt;Without GTM&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#cookie-based-exclusions-with-gtm&quot;&gt;With GTM&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#cookie-based-exclusion-further-note&quot;&gt;Additional Note&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;#final-wrap-up&quot;&gt;Summary, Conclusion, and Final Note&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2&gt;&lt;a id=&quot;WhyDoesThisMatter&quot;&gt;&lt;/a&gt;Quick Reminder: Why does this even matter?&lt;/h2&gt;
&lt;p&gt;If someone else told you to implement this, you might be wondering why it is even worth the time and effort to set up. Especially if your site gets a bunch of traffic already, what difference does it make if a few extra page views show up from internal traffic? Or employees occasionally see your own ads?&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;ImportanceForAnalytics&quot;&gt;&lt;/a&gt;Why internal filtering is important for Google Analytics and other analytics or performance metrics platforms:&lt;/h3&gt;
&lt;p&gt;Although internal traffic might have a small impact on metrics like total page views, it is important to recognize it can greatly affect aggregate metrics like &quot;avg session duration&quot;, &quot;time on page&quot;, &quot;pageviews per session&quot;, and many more.&lt;/p&gt;
&lt;p&gt;Here is a practical example. Let&apos;s say that your company is working on developing a new landing page and wants to evaluate results. So far, 50 actual users have visited the page and each of them has only spent 10 seconds looking at the page before moving on to a different part of the site. There is a team of 2 employees at your company that worked on building the landing page and continue to tweak it, so each of their &quot;time on page&quot; is 30 minutes. If you look at the average time on page for that landing page in GA, you would see 79 seconds as the average time on page, which doesn&apos;t look that bad, despite the majority of users only spending a measly 10 seconds on the page! This can make it very hard to evaluate performance based on site changes, especially since in order to make site changes, more employees have to be interacting with the site.&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;ImportanceForAdvertising&quot;&gt;&lt;/a&gt;Why internal exclusions are important for digital advertising (showing ads to employees):&lt;/h3&gt;
&lt;p&gt;The issue with showing ads to employees is mostly one that is tied to scale; if you are at a small business, this is much less of a concern. However, if you are a large business, displaying ads to employees can actually have a surprising impact on both budget and performance metrics.&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Ad Spend: For display advertising, since impressions can be very inexpensive, it can be easy to forget how spend can add up. For example, lets say your average CPM for a remarketing campaign is $5. That&apos;s just $0.005 per impression. This is an extreme example, but if you have 200 employees at your workplace, your campaign is frequency capped at 60 impressions an hour, and your employees have visited your website but are not being excluded from targeting, assuming an 8 hour work day, you could potentially be wasting $480/day on internal impressions (((200 * 60 * 8)/1000)*5). That could be upwards of 14 grand a month in wasted budget! For PPC advertising, costs can skyrocket if your employees are seeing and clicking your own ads, especially if you are in an industry where clicks are more expensive, such as law firms.&lt;/li&gt;
	&lt;li&gt;Digital marketing metrics: Most people immediately worry about wasted spend when considering impressions wasted on employees, but the impact on metrics is important to consider as well. Employees are not likely to click on your own ads (and for good reason; one would hope they already are familiar with the place they work every day!) which will drive down your CTR for display campaigns. For paid search campaigns, such as AdWords, the negative effect on CTR is worse, since CTR and expected CTR is a component of quality score and ad rank, both of which can affect your final CPC and ad positions!&lt;/li&gt;
	&lt;li&gt;If your own employees are trying to find your company website (to log into a internal portal, read documentation, etc.), every time they search your company name on Google, if you have a brand campaign without employee exclusions, one of two things is likely to happen:&lt;br&gt;
&lt;ul&gt;
	&lt;li&gt;A) They see your ad, click on it, and you have now just paid for your own employee to visit your own site. Or...&lt;/li&gt;
	&lt;li&gt;B) They see your ad, they know it costs you money if they click on it, so they ignore it to scroll further down the page and click on the regular search result for your site. But, now Google has seen that user ignore your ad, which means your CTR will decrease and potentially your expected CTR metrics as well!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a id=&quot;intro-to-ip-address-exclusions-and-cookie-exclusions&quot;&gt;&lt;/a&gt;Introduction to IP Address Exclusion and  Cookie Based Exclusion&lt;/h2&gt;
&lt;p&gt;The two main overall methods to block or exclude internal traffic is IP based filtering and cookie based exclusions. Here is a brief generic description of both:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;IP address exclusion: Your &quot;public&quot; IP address is a special address, kind of like your postal address, that is provided by your internet service provider (e.g. Comcast, Frontier, etc.) and is unique to your network (the internet connection that is shared across devices at your work), but not unique per device on the same network. It is automatically sent by your computer as part of every web requests, such as loading a web page. Google Analytics does not collect IP addresses (at least not in a way you can view as a user), but allows you to exclude traffic based on it.&lt;/li&gt;
	&lt;li&gt;Cookie based exclusion: A cookie is a very small digital file that can contain arbitrary text, and websites can &quot;set&quot; cookies on your computer just by you visiting them. The power of cookies is that they flow in both directions, from the server to your computer, and then back again from your computer to the server. This is how websites know that you are a logged in user, and why if you clear your cookies, you suddenly need to log back into all your password protected websites. Using the power of cookies, it is possible to block internal traffic for a variety of purposes and across networks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a id=&quot;ip-vs-cookies&quot;&gt;&lt;/a&gt;Comparison of IP Address vs Cookie Based Exclusion&lt;/h2&gt;
&lt;p&gt;First, here is a quick comparison of the two main methods that can be used to differentiate employee traffic from non-employee traffic - IP based exclusion vs cookie based exclusion:&lt;/p&gt;
&lt;table id=&quot;comparisonTable&quot; class=&quot;aligncenter&quot; style=&quot;border-style: solid; border-color: #000000;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #fcfbcc;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;IP Based Exclusion&lt;/strong&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;Cookie Based Exclusion&lt;/strong&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No action required from employees to deploy&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Employees have to either visit a special page or your business needs to already have some sort of internal site in use&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Can be used to block:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;GA recording traffic&lt;/li&gt;
	&lt;li&gt;Showing ads to your own employees*&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Can&apos;t be used to block:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Most pixel fires**&lt;/li&gt;
	&lt;li&gt;FB pixel**&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;* = Depends on ad platform, more on this later.&lt;/p&gt;
&lt;p&gt;**  = Not without special setup, see section on advanced IP exclusion.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Can be used to block:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;GA recording traffic&lt;/li&gt;
	&lt;li&gt;Most pixel fires&lt;/li&gt;
	&lt;li&gt;FB pixel&lt;/li&gt;
	&lt;li&gt;showing ads to your own employees *&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;* = Depends on ad platform, more on this later.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Works across devices (as long as they are using the same network). If 200 employees are all sharing the same network, they can be blocked by excluding a single IP address.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Each device needs to have its own cookie set. Per device, each browser also needs its own cookie set (e.g. Chrome vs Firefox)&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Each network (E.g. building A vs building B) would need an IP address filter set up for it. IP addresses can change, so lists would need to be kept up to date.&lt;/p&gt;
&lt;p&gt;Can&apos;t be used to block traffic if employees take laptops home, to conferences, travel, use VPNs or proxies, etc.&lt;/p&gt;
&lt;p&gt;Can&apos;t be (easily) used with dynamic IPs&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Cookies persist, no matter how many times a device switches networks, even if it is a mobile device.&lt;/p&gt;
&lt;p&gt;Employees can manually clear cookies, which would require them to be re-registered with the blocking cookie.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;One time setup is easy, if you just want to exclude traffic from Google Analytics or block paying to show ads to your own employees.&lt;/p&gt;
&lt;p&gt;However, requires more upkeep if IP addresses change, employees use VPNs or Proxies, etc.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;One time setup is more complicated, but should stay working for a long time. Furthermore, a more advanced setup will allow you to block internal traffic for more than just GA.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;You cannot (at least not easily) temporarily or permanently turn off exclusions for a single employee that uses a shared IP address you are blocking.&lt;/p&gt;
&lt;p&gt;Example: You have 200 employees you want to avoid showing your own ads to, but the CEO of the company likes seeing his own ads. This kind of setup is not really possible with IP based exclusions.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;You can very easily disable exclusions for certain employees, or in a rush, simply use an incognito browser.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;Testing exclusions is hard, or impossible&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Testing exclusions is easy&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No changes necessary to website&lt;br&gt;
&lt;p&gt; &lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Changes have to be either made directly to website, or through tag manager like Google Tag Manager (GTM)&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;
&lt;style&gt;
#comparisonTable td, #comparisonTable th {
	border: 1px dotted black;
}
&lt;/style&gt;
&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;downside-to-geo-exclusions&quot;&gt;&lt;/a&gt;Why not use geographic based exclusion?&lt;/h2&gt;
&lt;p&gt;If you live in the digital marketing world, you might be familiar with location targeting and location exclusions available in most digital advertising platforms (AdWords, Facebook, DSPs / RTB systems). As such, you might also be wondering why I don&apos;t simply advise that everyone exclude the location of their office building from targeting and Google Analytics monitoring. The short answer is that, depending on the scenario, I might actually advise that you use both, or multiple combinations of exclusions and targeting (see summary at bottom of this post). The longer answer is a little complicated.&lt;/p&gt;
&lt;p&gt;You see, the term &quot;location&quot; has become a very loose term in the digital marketing space, and depending on the technology stack you are using (Google Analytics, Facebook, etc.) it can mean very different things. On many platforms, location is actually determined by... IP address! In fact, Google Analytics currently uses IP-to-geography technology for its location metrics, &lt;a href=&quot;https://support.google.com/analytics/answer/6160484&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;as mentioned on their official help page&lt;/a&gt;. The problem with this, as they mention, is that mapping IP addresses to physical locations is &quot;approximate&quot;, and from personal experience, not very reliable. IP-to-Geo works by looking up an IP address to a table (think Excel or Google Sheets) of known physical locations associate with that IP. These tables are usually bought and sold (examples: &lt;a href=&quot;https://www.maxmind.com/en/geoip2-databases&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;MaxMind&lt;/a&gt;, &lt;a href=&quot;https://www.home.neustar/security-intelligence/ip-geopoint&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Neustar&lt;/a&gt;, etc.), and are usually created by other companies brokering data that they acquired through other companies that... well you get the picture. The end result is that IP-to-geo technology is notoriously variable in nature, and Google Analytics might report a location for a user that is &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;10-100 miles off&lt;/strong&gt;&lt;/span&gt; from the location reported by AdWords / Google Ads for the same exact user!&lt;/p&gt;
&lt;p&gt;If location is being determined by IP address, this also means that VPNs will introduce very inaccurate reports, as the location of the VPN provider will be recorded, not the user.&lt;/p&gt;
&lt;p&gt;The good news is that although Google Analytics uses IP-to-geo, most ad platforms, including Google Ads, rely on newer and more reliable technology (**&lt;em&gt;when it is available)&lt;/em&gt; to determine user location, like GPS, logged-in user PII, and even SSID (router names) scanning. So excluding your office building from targeting in your Google Ads campaign is actually not a bad idea. The only caveat here is the same as IP blocking; you can&apos;t easily turn it off for a single employee, so if your boss really wants to see their own ads, location exclusion is not going to work well for you, and you are much better off using cookies.&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;how-to-implement-ip-exclusions&quot;&gt;&lt;/a&gt;How to Implement IP Address Exclusions&lt;/h2&gt;
&lt;p&gt;As mentioned above, IP addresses can be used to exclude internal traffic from showing up in Google Analytics, as well as some other purposes. Here are instructions for each goal:&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;ga-ip-excluded-filtered-views&quot;&gt;&lt;/a&gt;How to exclude employee visits, traffic, and events from showing up in Google Analytics by excluding IP addresses:&lt;/h3&gt;
&lt;p&gt;To exclude traffic in GA based on IP address, the steps are pretty simple. You can follow my steps below, or &lt;a href=&quot;https://support.google.com/analytics/answer/1034840?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;visit the official guide from Google&lt;/a&gt;.&lt;/p&gt;
&lt;ol&gt;
	&lt;li&gt;First, gather the IP addresses that you will need to block. As a reminder, if you are trying to implement this for a large company, there might be more than one IP address being used at a single geographic location; each building might have its own IP address, or even each floor of a building. It all depends on how your IT department has set up networking. Make sure you are recording the &quot;public&quot; IP address, which is the one that is shared across employees. To find it, you can simply Google &quot;what is my ip&quot; on the network you want to get the IP of, or visit a site like &lt;a href=&quot;https://www.whatismyip.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this one&lt;/a&gt;.&lt;/li&gt;
	&lt;li&gt;Once you have the IP addresses you want to exclude traffic from, you can go ahead and setup a filter in the admin area of Google Analytics to exclude traffic from them. However, you need to pick which view to apply the filter to. I highly recommend having at least one &quot;view&quot; in your account that is unfiltered, so you might need to create a brand new view if you only have 1 view in your account at the moment.&lt;/li&gt;
	&lt;li&gt;Navigate to: Google Analytics -&gt; Admin -&gt; Property -&gt; View (the view you want to add the IP filter on) -&gt; Filters -&gt; + Add Filter.&lt;/li&gt;
	&lt;li&gt;Give your filter a name, leave filter type as &quot;predefined&quot;, select &quot;exclude&quot; as filter type, and then under &quot;Select source or destination&quot; select &quot;traffic from the IP addresses&quot;, and for &quot;select expression&quot; use &quot;that are equal to&quot;. Then paste in an IP address into the field below, and hit save!&lt;/li&gt;
	&lt;li&gt;If you have multiple IP addresses that you want to block traffic from, you will have to create a filter for each one using the standard method above. However, you can use Regular Expressions (aka Regex) to create a single filter that can block dozens of IP addresses at once. See &lt;a href=&quot;https://www.creare.co.uk/blog/google-products/filter-multiple-ip-addresses-in-google-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this page&lt;/a&gt; for details, and/or use &lt;a href=&quot;https://joshuatz.com/custom-tools/2019/ip-address-regular-expressions-generator-for-google-analytics/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my custom built tool&lt;/a&gt; to quickly generate a pattern for you!&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;a id=&quot;ip-exclusions-with-dsps&quot;&gt;&lt;/a&gt;How to use IP exclusions for digital advertising (AdWords, Display Ads, etc.)&lt;/h3&gt;
&lt;p&gt;There are so many advertising platforms out there, so my main recommendation here is to simply search the help guides/ documentation for the platform you are using, or reach out to an account rep and ask if IP exclusions are possible for your account. To help expedite this, here is a table of guides on IP excluding per platform, or a note if it is not possible:&lt;/p&gt;
&lt;table style=&quot;border-style: solid; border-color: #000000;&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f6fab1;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;AdWords / Google Ads&lt;/strong&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;Link&lt;/strong&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DoubleClick Bid Manager / Display &amp;#x26; Video 360&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;&lt;a href=&quot;https://support.google.com/displayvideo/answer/3090357&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Carrier and ISP Targeting&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[IP exclusion might or might not be possible natively]&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Facebook Ads&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Not possible natively&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bing Paid Search&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;&lt;a href=&quot;https://advertise.bingads.microsoft.com/en-us/resources/training/campaign-exclusions&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link&lt;/a&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AdRoll&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;&lt;a href=&quot;https://help.adroll.com/hc/en-us/articles/212676207-Exclude-By-IP-Address&quot;&gt;Link&lt;/a&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;&lt;a id=&quot;advanced-ip-blocking-with-tags&quot;&gt;&lt;/a&gt;Advanced: Blocking remarketing tags/pixels from firing based on IP address:&lt;/h3&gt;
&lt;p&gt;If the ad platform you are using does not support IP based exclusions, and/or you want to be able to block remarketing/retargeting tags from even firing in the first place based on IP, that is actually possible.&lt;/p&gt;
&lt;h4&gt;&lt;a id=&quot;blocking-tag-fires-by-ip-without-gtm&quot;&gt;&lt;/a&gt;Without Google Tag Manager:&lt;/h4&gt;
&lt;p&gt;First, here is a developer oriented solution that does not require Google Tag Manager, or any special JS libraries like jQuery. You can conditionally load marketing tags either server-side (PHP, NodeJS, etc.) or client-side (Javascript), with server-side being the recommended method.&lt;/p&gt;
&lt;p&gt;Here is client side first, as a vanilla JS solution (&lt;a href=&quot;https://gist.github.com/joshuatz/1b40e8b81f780707c2298ad2d4f8ee0b&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;un-minified source&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;!function(){window.conditionallyLoadTags=function(t,n,o){n=&quot;function&quot;==typeof n?n:function(){},o=&quot;function&quot;==typeof o?o:function(){};var e,i,a=!1,c=!1;if(&quot;object&quot;==typeof t&amp;#x26;&amp;#x26;null!==t&amp;#x26;&amp;#x26;(&quot;object&quot;==typeof t.cookie&amp;#x26;&amp;#x26;(e=t.cookie.name,((i=document.cookie.match(&quot;(^|;) ?&quot;+e+&quot;=([^;]*)(;|$)&quot;))?i[2]:null)==t.cookie.value.toString()&amp;#x26;&amp;#x26;(a=!0)),!a&amp;#x26;&amp;#x26;&quot;object&quot;==typeof t.ipAddresses)){c=!0;var f=t.ipAddresses;!function(url,method,callback,OPT_failCallback){method=&quot;string&quot;==typeof method&amp;#x26;&amp;#x26;&quot;&quot;!==method?method:&quot;GET&quot;,callback=&quot;function&quot;==typeof callback?callback:function(){},failCallback=&quot;function&quot;==typeof OPT_failCallback?OPT_failCallback:function(){console.warn(&quot;httpFetchVanilla failed&quot;)};var t=new XMLHttpRequest;t.onreadystatechange=function(){4==t.readyState&amp;#x26;&amp;#x26;(200==t.status?callback(t.responseText):failCallback())},t.open(method,url,!0),t.send()}(&quot;https://api.ipify.org/?format=json&quot;,&quot;GET&quot;,function(t){try{var e=!1,i=JSON.parse(t).ip;Array.isArray(f)?-1!==f.indexOf(i)&amp;#x26;&amp;#x26;(e=!0):f.test(i)&amp;#x26;&amp;#x26;(e=!0),!0===e?o():n()}catch(t){n()}},function(){n()})}c||(!0===a?o():n())}}();
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;conditionallyLoadTags({
‘ipAddresses’ : [‘216.3.128.12’,‘19.117.63.126’]
},function(){
console.log(‘You are not an employee. Firing tags.’);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag(‘js’, new Date());
gtag(‘config’,‘UA-12345678-01’);
},function(){
console.log(‘EMPLOYEE RECOGNIZED!!! Not firing tags.’);
});&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;For a server side solution, it can vary greatly based on what technology you are using, so here is a generic solution that would work across most PHP based sites:&lt;/p&gt;
&lt;p&gt;&lt;script src=&quot;https://gist.github.com/joshuatz/885a976c3db22baa412804b8f311dc75.js&quot;&gt;&lt;/script&gt;&lt;/p&gt;
&lt;h4&gt;&lt;a id=&quot;blocking-tag-fires-by-ip-with-gtm&quot;&gt;&lt;/a&gt;Using Google Tag Manager&lt;/h4&gt;
&lt;p&gt;Using IP addresses with Google Tag Manager (GTM) introduces some additional complexity, but allows for more reusable code, since once it is set up, you can easily assign an IP address variable as a blocking trigger for multiple marketing and collection tags.&lt;/p&gt;
&lt;p&gt;I&apos;ll defer to &lt;a href=&quot;https://www.simoahava.com/analytics/block-internal-traffic-gtm/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Simo Ahava&apos;s guide on implementing this&lt;/a&gt;, as it does an adequate job and he is kind of the king of GTM stuff.&lt;/p&gt;
&lt;p&gt;Once this is setup, you can set up all kinds of conditional tags that fire or don&apos;t fire for internal employees. You can block FB pixels from firing, remarketing tags, conversion pixels, and more.&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;how-to-implement-cookie-based-exclusions&quot;&gt;&lt;/a&gt;How to implement cookie based exclusions:&lt;/h2&gt;
&lt;p&gt;This is where I see a lot of guides fall short, despite cookies being a great way to differentiate employees vs non-employees, especially if you have internal webpages that only employees visit (e.g. employeeportal.example.com, example.com/internal/wiki-page, etc.).&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;cookie-based-exclusions-without-gtm&quot;&gt;&lt;/a&gt;Without GTM:&lt;/h3&gt;
&lt;p&gt;The summarized version of how this works, is that whenever an employee visits a page that only an employee could see (Wordpress Admin section, internal wiki page, task management page) we use Javascript to set a cookie that basically says &quot;Hey, server! I&apos;m an employee!&quot;. Then in our website server code, we check for the existence and value of that cookie - if we see it, we don&apos;t return tracking code that we return for all other users.&lt;/p&gt;
&lt;p&gt;I actually use this exact method on this website; I don&apos;t like logging my own hits to GA, so whenever I am logged into Wordpress and browsing my own site, I make sure a cookie is set marking myself as an internal user, which doesn&apos;t expire for a full year. You can see this &lt;a href=&quot;https://github.com/joshuatz/joshuatz-wp-theme/blob/fe65a84e0c10d6147ccd0928ca69bf732d75b1dd/inc/everypage-execute.php#L33-L72&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;actual code on the Github repo for this site&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you are looking for some generic code, you can drop this Javascript on any internal page where you want to set the &quot;employee marking cookie&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function setCookie(name, value, days){
	var d = new Date;
	d.setTime(d.getTime() + 24*60*60*1000*days);
	document.cookie = name + &quot;=&quot; + value + &quot;;path=/;expires=&quot; + d.toGMTString();
}
setCookie(&apos;isInternalEmployee&apos;,&apos;true&apos;,365);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then for conditionally loading tags based on that cookie, you can either implement code server side like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#x3C;?php if($_COOKIE[&apos;isInternalEmployee&apos;]!==&apos;true&apos;): ?&gt;
    &amp;#x3C;!-- Put all the tags you want to conditionally load here. GA example below: --&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x26;lt;!-- Global Site Tag (gtag.js) - Google Analytics --&amp;#x26;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x26;lt;script async src=&quot;https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID&quot;&amp;#x26;gt;&amp;#x26;lt;/script&amp;#x26;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x26;lt;script&amp;#x26;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;window.dataLayer = window.dataLayer || [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;function gtag(){dataLayer.push(arguments);}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;gtag(&apos;js&apos;, new Date());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;gtag(&apos;config&apos;, &apos;GA_MEASUREMENT_ID&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x26;lt;/script&amp;#x26;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#x3C;?php endif; ?&gt;&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Or client side, with Javascript, like this (&lt;a href=&quot;https://gist.github.com/joshuatz/1b40e8b81f780707c2298ad2d4f8ee0b&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;un-minified source&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;!function(){window.conditionallyLoadTags=function(t,n,o){n=&quot;function&quot;==typeof n?n:function(){},o=&quot;function&quot;==typeof o?o:function(){};var e,i,a=!1,c=!1;if(&quot;object&quot;==typeof t&amp;#x26;&amp;#x26;null!==t&amp;#x26;&amp;#x26;(&quot;object&quot;==typeof t.cookie&amp;#x26;&amp;#x26;(e=t.cookie.name,((i=document.cookie.match(&quot;(^|;) ?&quot;+e+&quot;=([^;]*)(;|$)&quot;))?i[2]:null)==t.cookie.value.toString()&amp;#x26;&amp;#x26;(a=!0)),!a&amp;#x26;&amp;#x26;&quot;object&quot;==typeof t.ipAddresses)){c=!0;var f=t.ipAddresses;!function(url,method,callback,OPT_failCallback){method=&quot;string&quot;==typeof method&amp;#x26;&amp;#x26;&quot;&quot;!==method?method:&quot;GET&quot;,callback=&quot;function&quot;==typeof callback?callback:function(){},failCallback=&quot;function&quot;==typeof OPT_failCallback?OPT_failCallback:function(){console.warn(&quot;httpFetchVanilla failed&quot;)};var t=new XMLHttpRequest;t.onreadystatechange=function(){4==t.readyState&amp;#x26;&amp;#x26;(200==t.status?callback(t.responseText):failCallback())},t.open(method,url,!0),t.send()}(&quot;https://api.ipify.org/?format=json&quot;,&quot;GET&quot;,function(t){try{var e=!1,i=JSON.parse(t).ip;Array.isArray(f)?-1!==f.indexOf(i)&amp;#x26;&amp;#x26;(e=!0):f.test(i)&amp;#x26;&amp;#x26;(e=!0),!0===e?o():n()}catch(t){n()}},function(){n()})}c||(!0===a?o():n())}}();
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;conditionallyLoadTags({
‘cookie’ : {
‘name’ : ‘isInternalEmployee’,
‘value’ : ‘true’
}
},function(){
console.log(‘You are not an employee. Firing tags.’);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag(‘js’, new Date());
gtag(‘config’,‘UA-12345678-01’);
},function(){
console.log(‘EMPLOYEE RECOGNIZED!!! Not firing tags.’);
});&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;dasf&lt;/p&gt;
&lt;h3&gt;&lt;a id=&quot;cookie-based-exclusions-with-gtm&quot;&gt;&lt;/a&gt;Using Cookies with GTM to conditionally fire tags:&lt;/h3&gt;
&lt;p&gt;The nice thing about GTM, is you can set up all of the above without needing to actually touch server-side or front-end code - you can built it all entirely within GTM. Here is how I would approach that.&lt;/p&gt;
&lt;h4&gt;Add the tag that drops the employee cookie:&lt;/h4&gt;
&lt;p&gt;First, we are going to built a tag that &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;em&gt;&lt;strong&gt;sets&lt;/strong&gt;&lt;/em&gt;&lt;/span&gt; the employee cookie. That way we can easily assign that tag and cookie to hundreds of internal employee pages, as long as they have GTM installed. We can reuse the code above, but wrap it in a script tag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;#x3C;script&gt;
function setCookie(name, value, days){
	var d = new Date;
	d.setTime(d.getTime() + 24*60*60*1000*days);
	document.cookie = name + &quot;=&quot; + value + &quot;;path=/;expires=&quot; + d.toGMTString();
}
setCookie(&apos;isInternalEmployee&apos;,&apos;true&apos;,365);
&amp;#x3C;/script&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and put it in a script tag, like this - go to &quot;Tags -&gt; New -&gt; Choose Tag Type -&gt; Custom HTML&quot;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/GTM-Set-Employee-Cookie-isInternalEmployee-Trigger-on-All-Internal-Pages.png&quot;&gt;&lt;img class=&quot;size-full wp-image-368 aligncenter&quot; src=&quot;/media/GTM-Set-Employee-Cookie-isInternalEmployee-Trigger-on-All-Internal-Pages.png&quot; alt=&quot;GTM - Set Employee Cookie - isInternalEmployee - Trigger on All Internal Pages&quot; width=&quot;1044&quot; height=&quot;758&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now, you need to tell GTM which pages your employees are visiting and on which it should drop this cookie. If you have already set up a trigger for internal pages, you can assign it, otherwise you will need to create one from scratch. You can use simple URL pattern rules to target pages like example.com/internal/wiki or dev.example.com, but if you have a more advanced internal system or CRM, more setup might be necessary. You assign the pages as a &quot;Page View&quot; trigger to the cookie-setting tag, which you can see assigned above as &quot;Internal Employee Pages&quot;.&lt;/p&gt;
&lt;h4&gt;Configure GTM to check for the value of that cookie and store as a variable&lt;/h4&gt;
&lt;p&gt;Now, we need a way to access this cookie inside of GTM and check if it is there or not. We can use a GTM &quot;variable&quot; to do this. Go to &quot;Variables -&gt; User-Defined Variables -&gt; New -&gt; Variable Configuration -&gt; 1st Party Cookie&quot;. Now use the same name of the cookie that we are setting above as the cookie to retrieve - if you haven&apos;t changed anything, it should be &quot;isInternalEmployee&quot;, like this:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/GTM-Read-Employee-Cookie-Into-isInternalEmployee-Cookie.png&quot;&gt;&lt;img class=&quot;size-full wp-image-369 aligncenter&quot; src=&quot;/media/GTM-Read-Employee-Cookie-Into-isInternalEmployee-Cookie.png&quot; alt=&quot;GTM - Read Employee Cookie Into isInternalEmployee Cookie&quot; width=&quot;1037&quot; height=&quot;315&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now, lets set up a trigger based on the value of this cookie, so we can either exclude or target employees with any given tag. Go to &quot;Triggers -&gt; New -&gt; Choose trigger type -&gt; Page View&quot;. Change &quot;This trigger fires on&quot; from &quot;All Page Views&quot; to &quot;Some Page Views&quot;, and then select the cookie we created as the condition to test against, and then &quot;equals&quot; or &quot;contains&quot; the string we set.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Tag-Manager-Employee-Pageview-Trigger-To-Be-Used-As-Exception-Trigger.png&quot;&gt;&lt;img class=&quot;size-full wp-image-370 aligncenter&quot; src=&quot;/media/Google-Tag-Manager-Employee-Pageview-Trigger-To-Be-Used-As-Exception-Trigger.png&quot; alt=&quot;Google Tag Manager - Employee Pageview Trigger - To Be Used As Exception Trigger&quot; width=&quot;1037&quot; height=&quot;448&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;OK, now we can use this trigger as a &quot;blocking&quot; trigger on any tag! Google Analytics, Google Ads Remarketing, Facebook, Custom HTML, you name it! On any tag, once you have defined what triggers it, in the same &quot;Triggering&quot; section, just hit the &quot;Add Exception&quot; button, then select the &quot;Employee View&quot; trigger that we created to have as an exception - this will prevent the tag from firing if someone has the employee cookie!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/GTM-Exclude-Internal-Traffic-from-Hitting-Google-Analytics.png&quot;&gt;&lt;img class=&quot;wp-image-371 size-full aligncenter&quot; src=&quot;/media/GTM-Exclude-Internal-Traffic-from-Hitting-Google-Analytics.png&quot; alt=&quot;GTM - Exclude Internal Traffic from Hitting Google Analytics&quot; width=&quot;1039&quot; height=&quot;658&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;&lt;a id=&quot;cookie-based-exclusion-further-note&quot;&gt;&lt;/a&gt;Further note: Instead of excluding employees, you can also &lt;em&gt;target &lt;/em&gt;them&lt;/h4&gt;
&lt;p&gt;There is probably not much use for this, but still something cool to think about: you can use the strategies above to target employees for campaigns instead of excluding them. What sort of campaign would need that? Well, for a large company, for example McDonald&apos;s, maybe they want to promote an employee benefit, like paid managerial classes or an internal job opening. Using a cookie based approach, you could create a remarketing campaign targeting just your own employees for this goal. Or maybe you want to deploy special tracking just for employees; if you have a public-facing wiki, you might care more about how your employees use it than the general public, so you can know which topics need more training materials.&lt;/p&gt;
&lt;h2&gt;What about Local Storage (localStorage)?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Local storage&lt;/a&gt; (aka localStorage aka Web Storage) is a browser feature similar to that of cookies. It lets websites save and retrieve small amounts of text data to your browser, which is persisted even if you close and open your browser back up. Like cookies, localStorage is &quot;scoped&quot; to the originating site, so that example.com can&apos;t access the localStorage set by joshuatz.com.&lt;/p&gt;
&lt;p&gt;You could easily swap out most of my cookie code for code that would store the internal user flag in localStorage. See this page for an example.&lt;/p&gt;
&lt;p&gt;The benefit to localStorage over cookies is that localStorage is kept even if a user clears their cookies. &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;em&gt;However&lt;/em&gt;&lt;/span&gt;, this is only true if a user individually clears cookies; using Chrome or Firefox&apos;s &quot;clear browsing data&quot; to erase everything will also clear out localStorage (this is something that a lot of people miss when recommending localStorage over cookies for internal traffic blocking).&lt;/p&gt;
&lt;p&gt;The immediate downsides to localStorage over cookies are simply that it is slightly more complicated and less supported (cookies have been around for longer and act pretty much exactly the same on any given browser). However, there is also a less obvious downside: certain browsers (e.g. Safari) will not persist localStorage values in an incognito browser window, even in the same tab. This seems unlikely to have a significant impact, but my point is really that there is not a great reason to pick localStorage over cookies, at least for the time being.&lt;/p&gt;
&lt;p&gt;A further disclaimer here is that there continues to be pushback against the use of cookies and web tracking technology, both from consumers as well as developers. As such, browsers continue to change how they let sites interact with browser technologies, a recent example being Safari choosing to block third party cookies by default. It could be that in a year from now, my advice here will be less valid as first party cookies come under scrutiny. Or maybe localStorage read/writes get limited because of some security exploit, so cookies remain the winner. Who knows what the future holds.&lt;/p&gt;
&lt;h2&gt;&lt;a id=&quot;final-wrap-up&quot;&gt;&lt;/a&gt;Summary, Conclusion, and Notes:&lt;/h2&gt;
&lt;p&gt;As you might have already guessed, my preference is for cookie based exclusions over IP address or geography based exclusions. As someone who understands the &quot;behind the scenes&quot; technology, it seems clear to me that where cookies are reliable, technically sound, and configurable, things like IP address and geographic targeting can be &quot;black boxes&quot; of data that have passed through hundreds of brokers before reaching you, and you are not always getting what is promised. &lt;em&gt;&lt;strong&gt;However,&lt;/strong&gt;&lt;/em&gt;there really is no harm in combining many of these strategies together as you see fit. For example, maybe for a branded PPC campaign that has an incredibly high CPC, you use all three exclusions to prevent any employees from clicking on your own ads, but then on a general low-cost remarketing display campaign, you just set up a simple cookie exclusion in GTM, that way some employees can still see the remarketing ads if they want to.&lt;/p&gt;
&lt;p&gt;If you disagree with anything here, feel free to chime in. I am happy to make adjustments to the article if I find your reasoning sound and based in facts. In general, these kinds of topics could use a lot more discussion in the digital marketing world :)&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>IP Address Regular Expressions Generator for Google Analytics</title><link>https://joshuatz.com/mini-tools/2019/ip-address-regular-expressions-generator-for-google-analytics/</link><guid isPermaLink="true">https://joshuatz.com/mini-tools/2019/ip-address-regular-expressions-generator-for-google-analytics/</guid><description>Tool for generating a regular expression (Regex) custom filter to match against multiple IP addresses at the same time, such as for Google Analytics.</description><pubDate>Sun, 31 Mar 2019 11:51:54 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I threw together this tool (took about 15 minutes to make) for a related post about filtering traffic by IP address in Google Analytics views. Excluding multiple IP addresses at the same time with a single filter in GA is only possible by &lt;a href=&quot;https://support.google.com/analytics/answer/1034840?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;using a custom IP address filter pattern&lt;/a&gt;, which uses regular expression pattern matching. This tool is for those that want to quickly generate a regex pattern to use based on a list of IP addresses.&lt;/p&gt;
&lt;p&gt;Simply paste your list of addresses into the left box, and than copy the generated pattern from the right box and paste right into your new Google Analytics filter!&lt;/p&gt;
&lt;p&gt;Click the &quot;Demo&quot; button to see an example. You can validate results using &lt;a href=&quot;https://regexr.com/4bb84&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this tool&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;&lt;iframe style=&quot;width: 100%;&quot; title=&quot;IP Address Regex Filter Generator&quot; src=&quot;https://codepen.io/joshuatz/full/MRWboq&quot; height=&quot;460&quot;&gt;
  See the Pen &amp;#x3C;a href=&quot;https://codepen.io/joshuatz/pen/MRWboq/&quot;&gt;IP Address Regex Filter Generator&amp;#x3C;/a&gt; by Joshua T
  (&amp;#x3C;a href=&quot;https://codepen.io/joshuatz&quot;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&quot;https://codepen.io&quot;&gt;CodePen&amp;#x3C;/a&gt;.&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;The code behind this tool can be found &lt;a href=&quot;https://codepen.io/joshuatz/pen/MRWboq&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>NodeJS - Interacting on CMD Line / Shell - WITHOUT Creating a JS File</title><link>https://joshuatz.com/posts/2019/nodejs---interacting-on-cmd-line-shell---without-creating-a-js-file/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/nodejs---interacting-on-cmd-line-shell---without-creating-a-js-file/</guid><description>Some simple ways to execute NodeJS code through the command line interface, as well as pipe output in and out.</description><pubDate>Tue, 19 Mar 2019 00:51:36 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I was having trouble finding this info in one spot, so I made this post as &quot;refresher&quot; to myself on some easy ways to interact with NodeJS from the command line, and without needing to have an actual JS file as the input. The main reason why I wanted to figure out how to do this, was so I could check my &lt;a href=&quot;https://nodejs.org/en/download/releases/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;NodeJS ABI version&lt;/a&gt; with a single command from my CLI&lt;/p&gt;
&lt;p&gt;As usual, one of the best sources of info is the official docs. &lt;a href=&quot;https://nodejs.org/api/cli.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Here is the documentation page for the Node command line interface (CLI)&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The built in &quot;REPL&quot;&lt;/h2&gt;
&lt;p&gt;REPL, which is an acronym for &quot;Read-Eval-Print-Loop&quot;, is a feature baked into the Node.js standard installation. From your command line, simply type &quot;node&quot; and press enter without anything after it. You will enter the REPL, in which you can type out JavaScript and instantly execute it with results. By default, Node will even detect when you are trying to write a multi-line statement (for example, a &lt;code&gt;for&lt;/code&gt; loop), and will let you write it out before executing.&lt;/p&gt;
&lt;p&gt;Alternative, you can also type &lt;code&gt;.editor&lt;/code&gt; and press enter, after entering the REPL, to enter the &quot;editor mode&quot;. This will let you write out a longer script in the console, including line breaks, and only executing it once you are ready.&lt;/p&gt;
&lt;p&gt;For more details and a better guide, check out Flavio&apos;s guide here - &lt;a href=&quot;https://flaviocopes.com/node-repl/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://flaviocopes.com/node-repl/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Using &quot;print&quot; (node -p)&lt;/h2&gt;
&lt;p&gt;The -p (or -print) argument basically evals the string following it and prints the result. For example, to get the ABI #, we can use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -p &quot;process.versions.modules&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using &quot;eval&quot; (node -e)&lt;/h2&gt;
&lt;p&gt;The &quot;eval&quot; option takes a string input and &quot;evals&quot; it, which is to execute it as though it were raw code being passed in. Since we are running in the command line interface and node will exit immediately after eval&apos;ing, if we want to see the result of the eval option, you either need to use &quot;node -p&quot; instead, or in your code, make sure output is going to be passed to the console. For example, this results in the same result as my node -p example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -e &quot;console.log(process.versions.modules)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Don&apos;t forget about piping!&lt;/h2&gt;
&lt;p&gt;Just like with almost anything on the CLI, you can pipe data to node, by using the pipe operator (&quot;|&quot;). For example, to print &quot;hello world&quot; to the console:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Hello World&quot; | node -p&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;... Or, pipe the output of Node to something else! For example, pipe it to grep to search for a pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -p &quot;process.versions&quot; | grep &quot;openssl:\s*&apos;(?:[^\&apos;])+&quot; -P&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or send version info to a text file:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/NodeJS-Saving-Version-Info-to-a-Text-File-With-Cmd.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-331&quot; src=&quot;/media/NodeJS-Saving-Version-Info-to-a-Text-File-With-Cmd.png&quot; alt=&quot;NodeJS - Saving Version Info to a Text File With Cmd&quot; width=&quot;690&quot; height=&quot;212&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;I got what I wanted:&lt;/h2&gt;
&lt;p&gt;Here is a single line command that will spit out the node version and ABI # to command prompt, with some line breaks around it so it is easy to read (this is the same command shown in the thumbnail for this post):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -p &quot;&apos;\n\n\n\n   Node Version = &apos; + process.versions.node +&apos;  ||  &apos; + &apos;Node Version ABI # = &apos; + process.versions.modules&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;/media/NodeJS-Getting-Version-and-ABI-Version-from-Command-Line.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-330&quot; src=&quot;/media/NodeJS-Getting-Version-and-ABI-Version-from-Command-Line.png&quot; alt=&quot;NodeJS - Getting Version and ABI Version from Command Line&quot; width=&quot;1397&quot; height=&quot;295&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Additional Info:&lt;/h2&gt;
&lt;h3&gt;Getting installation paths:&lt;/h3&gt;
&lt;p&gt;Get the install path of NPM by using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm config get prefix&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or by using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;which npm&lt;/code&gt;&lt;/pre&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Getting QT Creator Working for Android Development - First Steps</title><link>https://joshuatz.com/posts/2019/getting-qt-creator-working-for-android-development---first-steps/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/getting-qt-creator-working-for-android-development---first-steps/</guid><description>A post on trying to get QT Creator to be able to, at the very least, build and run an example project for Android and see it on an emulator.</description><pubDate>Tue, 12 Mar 2019 00:44:48 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;This post was written as I worked through trying to get QT Creator to be able to, at the very least, build and run an example project for Android and see it on an emulator.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;Errors encountered:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;&quot;Android build SDK not defined&quot;
&lt;ul&gt;
	&lt;li&gt;Under &quot;Projects &gt; Active Project &gt; Build&quot; you need to specify which Android Build APK to target / build for&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li style=&quot;list-style-type: none !important;&quot;&gt;
&lt;ul&gt;
	&lt;li style=&quot;list-style-type: none !important;&quot;&gt;
&lt;ul&gt;
	&lt;li&gt;If this dropdown is empty, it probably means that your SDK configuration is not set up properly. See below, and if you are using Android Studio, you might need to try manually downloading and installing the Android SDK separately on your hard drive&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;For &quot;Select Android Device&quot; when trying to run app, AVD emulator shows under &quot;Incompatible Devices&quot;, with &quot;API Level of device is: -1&quot;
&lt;ul&gt;
	&lt;li&gt;I think that, at least in my situation, this was due to a XML mismatch between what avdmanager generates and what QTCreator is expecting. It looks like for older versions of the Android API, the AVD XML config file does not have the &quot;AvdId&quot; property, whereas newer ones do (or maybe it was because I created my new AVD from the command line avdmanager instead of Android Studio). Either way, it looks like QT needs &quot;AvdId&quot; to be in the XML file for it to parse it correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li style=&quot;list-style-type: none !important;&quot;&gt;
&lt;ul&gt;
	&lt;li style=&quot;list-style-type: none !important;&quot;&gt;
&lt;ul&gt;
	&lt;li&gt;You can find your generated AVD config XML file under C:\Users\[USERNAME]\.android\avd\[AVD_ID]&lt;/li&gt;
	&lt;li&gt;I simply added the missing AvdId property to my XML file that matched the ID and the folder name&lt;/li&gt;
	&lt;li&gt;Afterward, the AVD instance started showing under compatible devices, although in the Devices panel it still shows with &quot;AVD Target: API -1&quot; which is not correct.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;Under &quot;Build Settings &gt; Build Steps &gt; Build Android APK&quot;, only SDK version 25 (&quot;android-25&quot;) appears selectable in the &quot;Android Build SDK Dropdown&quot;
&lt;ul&gt;
	&lt;li&gt;After installing a new SDK, either through the QT GUI or the sdkmanager, I had to reset my SDK path field to get QT Creator to re-scan the directory, then completely close and open QT creator back up again&lt;/li&gt;
	&lt;li&gt;Also, make sure you have installed both the emulator image, as well as the &quot;SDK Platform&quot; under the android version you want&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;When trying to run on an emulator generated by Android Studio, you get this &quot;AVD Start Error&quot; : &quot;emulator: ERROR: unknown skin name &apos;nexus_5x&apos;
&lt;ul&gt;
	&lt;li&gt;Similar to another issue, this seems to be something to do with how QT parses android config files vs how Android Studio does. You need to find the &quot;config.ini&quot; file that goes with your emulator instance. For example, if you are trying to get the default Android Studio x86 emulator running, it might be at something like &quot;C:\Users\Joshua\.android\avd\Nexus_5X_API_28_x86.avd\config.ini&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li style=&quot;list-style-type: none !important;&quot;&gt;
&lt;ul&gt;
	&lt;li style=&quot;list-style-type: none !important;&quot;&gt;
&lt;ul&gt;
	&lt;li&gt;Open it, and look for the line with the keypair of &quot;skin.path&quot;. It probably looks like &quot;skin.path=skins\nexus_5x&quot;. You will want to change this to point to the actual full path of the skin, like &quot;skin.path=C:\Users\Joshua\AppData\Local\Android\Sdk\skins\nexus_5x&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;When trying to run on my actual Android phone, not the emulator, my phone shows up under &quot;incompatible devices&quot;, with a message about &quot;ABI is incompatible&quot;
&lt;ul&gt;
	&lt;li&gt;In my case, I had selected the kit to target Android x86, but my phone (a OnePlus 2) has an ARM 64 processor. I just had to switch my kit before running and then it worked!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;
&lt;p&gt;When trying to use any QtQuick submodules (e.g. qtquick.controls) I would get an error in my QML like &quot;QML module not found (QtQuick.Controls)&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;This is probably an indicator that my kits are set up incorrectly, as QT *should* be able to automatically resolve QML dependencies for Android vs Desktop, but for whatever reason, in my case, it is not. I had to add conditional sections to my project/qmake file that modifies the QML_IMPORT_PATH variable to pull in the right QML sources based on Android version (based on tip &lt;a href=&quot;https://www.qtcentre.org/threads/59089-Android-ARM-vs-Android-x86-in-qmake-pro-file&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;). See below:&lt;/li&gt;
	&lt;li&gt;
&lt;pre&gt;&lt;code&gt;# Android specific build settings
android {
    log(&quot;ANDROID_TARGET_ARCH=&quot;)
    log($${ANDROID_TARGET_ARCH})
    equals(ANDROID_TARGET_ARCH, armeabi-v7a) {
        #[doAndroidArmStuff]
        QML_IMPORT_PATH += C:/Qt/5.12.1/android_armv7/qml
        QML_IMPORT_PATH += C:/Qt/5.12.1/android_armv7/qml/QtQuick
    }
    equals(ANDROID_TARGET_ARCH, arm64-v8a) {
        #[doAndroidArmStuff]
        QML_IMPORT_PATH += C:/Qt/5.12.1/android_arm64_v8a/qml
        QML_IMPORT_PATH += C:/Qt/5.12.1/android_arm64_v8a/qml/QtQuick
    }
    equals(ANDROID_TARGET_ARCH, armeabi) {
        #[doAndroidArmeabiStuff]
    }
    equals(ANDROID_TARGET_ARCH, x86)  {
        #[doAndroidx86Stuff]
        QML_IMPORT_PATH += C:/Qt/5.12.1/android_x86/qml
        QML_IMPORT_PATH += C:/Qt/5.12.1/android_x86/qml/QtQuick
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Pre-requirements&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;Download and install Android Studio, since that will auto-install a bunch of helpful tools and emulators&lt;/li&gt;
	&lt;li&gt;Download and Install the Java JDK
&lt;ul&gt;
	&lt;li&gt;Right now, version 8 works, whereas &gt;8 breaks&lt;/li&gt;
	&lt;li&gt;Make sure to set windows user var JAVA_HOME to this path&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Create a root level folder for easy access - C:/Android
&lt;ul&gt;
	&lt;li&gt;Manually download the Android NDK and install there, like &quot;C:\Android\ndk\android-ndk-r19b&quot;&lt;/li&gt;
	&lt;li&gt;If you want to manually install Android SDK here, create C:\Android\sdk
&lt;ul&gt;
	&lt;li&gt;Now, download and extract the command line SDK tools download from Google (the one that contains the sdkmanager)&lt;/li&gt;
	&lt;li&gt;Navigate to sdkmanager (should be under c:\Android\sdk\tools\bin after you extracted) and in cmd prompt, run &quot;sdkmanager &quot;platforms;android-25&quot; or whatever version of the SDK you want to install&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;Now, go to Qt Creator &gt; Tools &gt; Options &gt; Devices, and under &quot;Android&quot;, plug in the necessary paths (Java JDK, Android NDK &amp;#x26; SDK)&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/QT-Creator-Devices-Java-and-Android-Settings.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-315&quot; src=&quot;/media/QT-Creator-Devices-Java-and-Android-Settings.png&quot; alt=&quot;QT Creator - Devices - Java and Android Settings&quot; width=&quot;563&quot; height=&quot;432&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;QT should automatically create kits for you and detect installed Android Platforms / Emulators&lt;/li&gt;
	&lt;li&gt;If you are using a manual SDK path outside of Android Studio, QT will also prompt you to auto-download and install extra tools it needs
&lt;ul&gt;
	&lt;li&gt;However, it won&apos;t install emulator stuff automatically…&lt;/li&gt;
	&lt;li&gt;It won&apos;t auto download the emulator process, so make sure you check the install box for &quot;Android Emulator&quot;&lt;/li&gt;
	&lt;li&gt;it won&apos;t auto download an emulator image, so you need to go to the &quot;SDK Manager&quot; tab and install at least one system image to use as an emulator (with AVD Manager). You usually want &quot;Google APIs Intel x86 Atom System Image&quot;
&lt;ul&gt;
	&lt;li&gt;For example, here I have download an Android 7.1 image, and then created an AVD for it&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;/media/QT-Creator-SDK-Manager-for-Android.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-317&quot; src=&quot;/media/QT-Creator-SDK-Manager-for-Android.png&quot; alt=&quot;QT Creator - SDK Manager for Android&quot; width=&quot;554&quot; height=&quot;211&quot;&gt;&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;/media/QT-Creator-GUI-for-Creating-a-new-AVD-targeting-android-25.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-316&quot; src=&quot;/media/QT-Creator-GUI-for-Creating-a-new-AVD-targeting-android-25.png&quot; alt=&quot;QT Creator - GUI for Creating a new AVD targeting android-25&quot; width=&quot;441&quot; height=&quot;222&quot;&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li style=&quot;list-style-type: none !important;&quot;&gt;
&lt;ul&gt;
	&lt;li&gt;If the QT avd manager is not working (it wasn&apos;t for me) you can manually provision with the cmd tool for it. This command is functionally equivalent to the result of the dialog above
&lt;ul&gt;
	&lt;li&gt;avdmanager create avd -n &quot;Android_a25_s7.1_x86&quot; -k &quot;system-images;android-25;google_apis;x86&quot; -c 400M&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Nested Opacity Inheritance in QML and CSS - How does this work again?</title><link>https://joshuatz.com/posts/2019/nested-opacity-inheritance-in-qml-and-css---how-does-this-work-again/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/nested-opacity-inheritance-in-qml-and-css---how-does-this-work-again/</guid><description>An ELI5 type post on how opacity inheritance works with nested components or elements in QML or CSS. Covers the easiest solutions for both QT&apos;s QML and CSS.</description><pubDate>Fri, 22 Feb 2019 21:29:28 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;The Problem:&lt;/h2&gt;
&lt;p&gt;I&apos;m brand new to QML, and so when I started on my first project and couldn&apos;t get nested opacity inheritance to work like I was expecting it to, I immediately blamed QML and thought &quot;this would be so much easier with CSS&quot;. However, after reflecting for a second and firing up a Codepen to test, I realized I was wrong, and I had incorrectly remembered how CSS handles the opacity property in nested elements (hint, it is the same as QML in this case).&lt;/p&gt;
&lt;p&gt;The problem is pretty basic; in both CSS and QML, if you nest a component (CSS people, think &quot;element&quot;), the child element ends up somewhat inheriting the opacity of the parent, to the effect that the child can not have an opacity that is &lt;em&gt;more than&lt;/em&gt; the parents opacity. In practice, this means that by only manipulating the opacity property, a nested component can be &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;less&lt;/strong&gt; &lt;/span&gt;opaque than the parent, but &lt;strong&gt;not&lt;/strong&gt; &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;more&lt;/strong&gt;&lt;/span&gt; opaque. Here is a super simple example:&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;Works as expected: Nested element opacity = 0.5, parent element opacity = 1.0&lt;/p&gt;
&lt;p style=&quot;padding-left: 80px;&quot;&gt;&lt;a href=&quot;/media/CSS-Child-Opacity-Less-Than-Parent-Blending-Example.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-296&quot; src=&quot;/media/CSS-Child-Opacity-Less-Than-Parent-Blending-Example.png&quot; alt=&quot;CSS - Child Opacity Less Than Parent - Blending Example&quot; width=&quot;186&quot; height=&quot;387&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;Doesn&apos;t work as expected: Nested element opacity = 1.0, parent element opacity = 0.5&lt;/p&gt;
&lt;p style=&quot;padding-left: 80px;&quot;&gt;&lt;a href=&quot;/media/CSS-Child-Opacity-Greater-Than-Parent-Parent-Overriding-Child.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-297&quot; src=&quot;/media/CSS-Child-Opacity-Greater-Than-Parent-Parent-Overriding-Child.png&quot; alt=&quot;CSS - Child Opacity Greater Than Parent - Parent Overriding Child&quot; width=&quot;188&quot; height=&quot;396&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can see in the example above where the nested component has an opacity of 1, you might think that the child &lt;em&gt;should&lt;/em&gt; appear with completely solid background, while the parent has a transparent red background. However, due to the way that both CSS and QML handle blending, relative values, rendering nodes, and other technicalities beyond the scope of this post, the practical result is that, in actuality, the child ends up with an opacity capped at that of the parent, so both appear as 0.5.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://codepen.io/joshuatz/pen/JxVVVQ&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Here is a fun little demo I set up to show how this works with CSS&lt;/a&gt;. Use the slider to change the opacity, and the button to swap which elements are opaque.&lt;/p&gt;
&lt;p&gt;Another way to think of it that might make more sense is by referring to opacity as a &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;em&gt;&lt;strong&gt;relative&lt;/strong&gt; &lt;/em&gt;&lt;/span&gt;percentage - in this case, if the parent is set to 50% opacity, and the child is set to 100%, 100% of the parent (50%) is still just 50%, so both end up with the same value.&lt;/p&gt;
&lt;p&gt;You could also think of a piece of paper with two squares printed on it, one is half as &quot;bright&quot; as the other. If you submerge the whole paper in a solution that fades it to be half as visible, now both squares are half as visible as they were before (50%) and one square is still half as &quot;bright&quot; relative to the other, which means it really is 25% visible (0.5 * 0.5).&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The Solution:&lt;/h2&gt;
&lt;p&gt;One of the easiest solutions, in both CSS and QT / QML, is to just avoid nesting when you can. Here is a comparison showing how you can have a component appear within another with a higher opacity when it is really a sibling, vs when it is a child:&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&lt;a href=&quot;/media/CSS-and-QML-Nested-Opacity-vs-Sibling-Opacity-Simple-Example.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-298&quot; src=&quot;/media/CSS-and-QML-Nested-Opacity-vs-Sibling-Opacity-Simple-Example.png&quot; alt=&quot;CSS and QML - Nested Opacity vs Sibling Opacity - Simple Example&quot; width=&quot;424&quot; height=&quot;413&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But what if, understandably, you really want to leave you components in a nested structure, but still get nested opacity to work? Well, there is a few options, but the easiest, and my favorite, is one that works for both CSS and QML - simply use RGBA (RGB-&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;ALPHA&lt;/strong&gt;&lt;/span&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;)&lt;/span&gt; instead of opacity! This is actually the solution that I use throughout this website, and I use it so often that I forgot I do it, and is the reason why when I started using opacity instead of RGBA in QT I had to remind myself of &lt;em&gt;&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;why&lt;/strong&gt;&lt;/span&gt;&lt;/em&gt; I&apos;m always using it.&lt;/p&gt;
&lt;p&gt;Here is how you might use RGBA to display the solid green rectangle inside the transparent red one, like above, with CSS (&lt;a href=&quot;https://codepen.io/joshuatz/pen/QYXeQy&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;codepen link&lt;/a&gt;):&lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&lt;a href=&quot;/media/CSS-Using-RGBA-for-Nested-Opacity-in-Elements-Quick-Example.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-300&quot; src=&quot;/media/CSS-Using-RGBA-for-Nested-Opacity-in-Elements-Quick-Example.png&quot; alt=&quot;CSS - Using RGBA for Nested Opacity in Elements - Quick Example&quot; width=&quot;579&quot; height=&quot;473&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Or with QML (full code repo &lt;a href=&quot;https://github.com/joshuatz/QML-Inherited-Opacity-Demo&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p style=&quot;padding-left: 40px;&quot;&gt;&lt;a href=&quot;/media/QML-Using-RGBA-for-Nested-Opacity-in-Components-Quick-Example.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-301&quot; src=&quot;/media/QML-Using-RGBA-for-Nested-Opacity-in-Components-Quick-Example.png&quot; alt=&quot;QML - Using RGBA for Nested Opacity in Components - Quick Example&quot; width=&quot;923&quot; height=&quot;699&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;A cool thing in QML - Reusing colors with RGBA&lt;/h3&gt;
&lt;p&gt;One thing that is very neat about QML that you can&apos;t really do in CSS (at least not in pure CSS, I&apos;m not talking about Sass or JSX) is referencing values from other components. A cool application of this in combination with RGBA is telling QML to use the same color as another component, but fade it a bit. For example, this is a great way to achieve a nice glow effect around an element without having to keep hardcoding in colors. This also allows you to keep the same opacity in a nested component while allowing for a dynamic color (e.g. if the component color is bound to a C++ value).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/QML-Using-Reference-Component-Color-for-RGBA-Opacity-Effect-Example.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-303&quot; src=&quot;/media/QML-Using-Reference-Component-Color-for-RGBA-Opacity-Effect-Example.png&quot; alt=&quot;QML - Using Reference Component Color for RGBA Opacity Effect - Example&quot; width=&quot;909&quot; height=&quot;479&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Further options in QML:&lt;/h3&gt;
&lt;p&gt;QML has some additional options for altering how inherited opacity works, such as the &quot;&lt;a href=&quot;https://doc.qt.io/qt-5/qml-qtquick-item.html#layer.enabled-prop&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;layer.enabled&lt;/a&gt;&quot; property, or &lt;a href=&quot;https://doc.qt.io/archives/qt-5.8/qml-qtquick-shadereffect.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;shader effects&lt;/a&gt;. However, at least in my view, these are all much more complicated and with potential side effects that far eclipse the simple implementation of RGBA.&lt;/p&gt;
&lt;p&gt;Another quick solution similar to RGBA though, is use a &lt;a href=&quot;https://doc.qt.io/archives/qt-4.8/qml-color.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;quad color code&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>QT - FontAwesome, Import, and Using Files Outside Source Directory</title><link>https://joshuatz.com/posts/2019/qt---fontawesome-import-and-using-files-outside-source-directory/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/qt---fontawesome-import-and-using-files-outside-source-directory/</guid><description>My experience as a brand new QT beginner while trying to use Font Awesome with QML / QT, dealing with relative import paths, and importing outside the source directory.</description><pubDate>Fri, 15 Feb 2019 02:39:01 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;Disclaimer: This was written while working on my very first QT and C++ project. Take with a very large grain of salt.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Oh boy. Whenever you think something is going to take 5 seconds and be super simple, that is when it takes all day and makes you want to smash your keyboard into bits. Enter the world of trying to use external resources in QT, relative paths, QRC files, and .PRO files. Not fun.&lt;/p&gt;
&lt;p&gt;I wanted to use FontAwesome (an icon set) with QML, but I didn&apos;t want to add the FontAwesome files into my actual QT workspace, for several reasons. One of which is that this is something I would prefer to use a package manager, like NPM, for.&lt;/p&gt;
&lt;p&gt;So, in this case, I set up a simple package.json a level up from my QT project directory, npm installed FontAwesome, and then attempted to use the files in QML. My file structure looked something like this:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;package.json&lt;/li&gt;
	&lt;li&gt;node_modules&lt;/li&gt;
	&lt;li&gt;lib&lt;/li&gt;
	&lt;li&gt;Actual_QT_Workspace
&lt;ul&gt;
	&lt;li&gt;my-project.pro&lt;/li&gt;
	&lt;li&gt;main.cpp&lt;/li&gt;
	&lt;li&gt;qml.qrc&lt;/li&gt;
	&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, I quickly ran into an issue...&lt;/p&gt;
&lt;h2&gt;Issue 1: How to include files outside the Source directory in resources, so you can use &quot;import&quot; with them?&lt;/h2&gt;
&lt;p&gt;The very first thing I tried (man can you imagine if this simply worked) is using a typical relative path syntax in my QML file pointing to the path of the file in node_modules, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import &quot;../node_modules/@fortawesome/fontawesome-free/js/all.js&quot; as FontAwesome&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I thought it might work at first, since I am new to QT and didn&apos;t know any better, and also that the editor autocompleted the path, showing that it recognized the file as existing. However, trying to build resulted in this kind of error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qrc:/main.qml:5 Script qrc:/node_modules/@fortawesome/fontawesome-free/js/all.js unavailable
qrc:/node_modules/@fortawesome/fontawesome-free/js/all.js:-1 No such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is because, and I&apos;m grossly over-simplifying here, but QT uses QRC (QT Resource Collection) files to bundle resources together that get passed to the compiler. In order for my application to use a file off the disk, the easiest way to access it is by specifying the file in a QRC that is included as part of the build process.&lt;/p&gt;
&lt;h3&gt;Using a relative path in a .qrc file&lt;/h3&gt;
&lt;p&gt;So the next thing I tried was adding this local file to the default QRC file, qml.qrc. I tried using the built in QRC editor panel in QT Creator (right click *.qrc, hit &quot;Open in Editor&quot;, then use the &quot;Add &gt; Add Files&quot;), but when trying to add the file, I got this error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The file C:\Users... is not in a subdirectory of the resource file. You now have the option to copy this file to a valid location.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No! I don&apos;t want to copy the file - that is the whole point of this - I want it left in node_modules. This actually seems like a bug, because you can easily get around this by right clicking on the *.qrc and instead of using the editor, hit &quot;Add existing files...&quot;, then select the file you want to add. This actually worked! However, it was a little silly, because all it does is use the relative path in the &amp;#x3C;file&gt;&amp;#x3C;/file&gt; element in the QRC doc - I could have easily have tried that by hand instead of using the menu. The resulting QRC ended up looking something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;RCC&gt;
    &amp;#x3C;qresource prefix=&quot;/&quot;&gt;
        &amp;#x3C;file&gt;main.qml&amp;#x3C;/file&gt;
        &amp;#x3C;file&gt;../node_modules/@fortawesome/fontawesome-free/js/all.js&amp;#x3C;/file&gt;
    &amp;#x3C;/qresource&gt;
&amp;#x3C;/RCC&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another option is to create a brand new QRC file, leave it in the parent directory / outside directory, and include the QRC in your .PRO file as part of RESOURCES. Then in your new QRC file, you don&apos;t always have to keep traversing up a directory (or multiple, depending on your structure). For example:&lt;/p&gt;
&lt;p&gt;.pro file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ...
RESOURCES += qml.qrc
RESOURCES += ../external-assets.qrc
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then in external-assets.qrc :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;RCC&gt;
    &amp;#x3C;qresource prefix=&quot;/&quot;&gt;
        &amp;#x3C;file&gt;node_modules/@fortawesome/fontawesome-free/js/all.js&amp;#x3C;/file&gt;
    &amp;#x3C;/qresource&gt;
&amp;#x3C;/RCC&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Issue 2: You can&apos;t use FontAwesome like that Joshua!&lt;/h2&gt;
&lt;p&gt;Bah! I should have known that would be too easy to work! Although QT ships with a JS engine, and you can definitely use JS with it, you have to remember that there are many many caveats to that. In short, you can&apos;t just import FontAwesome&apos;s all.js into your QML and have it load everything; for one, all.js loads a bunch of CSS files, of which QML does not natively parse / apply. In addition, you&apos;ll get errors about setTimeout, and other stuff like that where all.js is trying to use normal window JS globals, but of course QML!==window DOM.&lt;/p&gt;
&lt;p&gt;So... how do we get around this? Well, unfortunately it is a little complicated. Lucky for me, I found &lt;a href=&quot;https://martin.rpdev.net/2018/03/30/using-fontawesome-5-in-qml.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;this great in-depth guide&lt;/a&gt; by Martin Höher that lays out how to use FontAwesome files with QML, step by step. Although Font-Awesome recently bumped up versions from 4.x to 5.x, his Python script for generating the Icons.qml Singleton still works perfectly! However, currently Font-Awesome is excluding icons.json from their NPM package, which is what his script uses as the input. You can still find the icons.json if you manually go to their website and download the zip, which is what I am doing in the meantime to avoid the headache of having to parse their icons without a .json index.&lt;/p&gt;
&lt;p&gt;I&apos;m still keeping the font-awesome files outside my QT directory, although there is not much reason to at this point while I currently can&apos;t really use the NPM version of FA. I just extract the four files I need from the downloaded zip and place them in one flat directory under /lib/.&lt;/p&gt;
&lt;p&gt;Here is close to what my files look like now:&lt;/p&gt;
&lt;p&gt;qmldir:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;singleton CustomFonts 1.0 CustomFonts.qml
singleton Icons 1.0 Icons.qml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CustomFonts.qml:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-qml&quot;&gt;pragma Singleton
import QtQuick 2.0
&lt;p&gt;Item {
id : customFonts
readonly property FontLoader fontAwesomeRegular: FontLoader {
source: ”../lib/fontawesome-free-5.7.2-web/fa-regular-400.ttf”
}
readonly property FontLoader fontAwesomeSolid: FontLoader {
source: ”../lib/fontawesome-free-5.7.2-web/fa-solid-900.ttf”
}
readonly property FontLoader fontAwesomeBrands: FontLoader {
source: ”../lib/fontawesome-free-5.7.2-web/fa-brands-400.ttf”
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;readonly property var names : {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;fontAwesomeRegular&quot; : customFonts.fontAwesomeRegular.name,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;fontAwesomeSolid&quot; : customFonts.fontAwesomeSolid.name,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &quot;fontAwesomeBrands&quot; : customFonts.fontAwesomeBrands.name&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-qml&quot;&gt;}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;main.qml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-qml&quot;&gt;import QtQuick 2.7
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.0
import QtQuick.Controls.Material 2.3
// Singleton import
import &quot;.&quot;
&lt;p&gt;ApplicationWindow {
id : root
visible: true&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// Chrome icon&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Text {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    color: &quot;black&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    font.pixelSize: 40&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    text: Icons.faChrome&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    font.family: CustomFonts.names.fontAwesomeRegular&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-qml&quot;&gt;}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;qml.qrc&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;RCC&gt;
    &amp;#x3C;qresource prefix=&quot;/&quot;&gt;
        &amp;#x3C;!-- ... --&gt;
        &amp;#x3C;file&gt;../lib/fontawesome-free-5.7.2-web/fa-regular-400.ttf&amp;#x3C;/file&gt;
        &amp;#x3C;file&gt;../lib/fontawesome-free-5.7.2-web/fa-solid-900.ttf&amp;#x3C;/file&gt;
        &amp;#x3C;file&gt;../lib/fontawesome-free-5.7.2-web/fa-brands-400.ttf&amp;#x3C;/file&gt;
        &amp;#x3C;file&gt;../lib/fontawesome-free-5.7.2-web/icons.json&amp;#x3C;/file&gt;
        &amp;#x3C;!-- ... --&gt;
    &amp;#x3C;/qresource&gt;
&amp;#x3C;/RCC&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;To-Do:&lt;/h2&gt;
&lt;p&gt;There are lots of ways I could optimize how this is loaded, especially if I want to be considerate of how others might have different build systems. If I find time, I might revisit this post, especially as I learn more about QT and its internals.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>wow.js - force trigger after display visibility change</title><link>https://joshuatz.com/posts/2019/wowjs---force-trigger-after-display-visibility-change/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/wowjs---force-trigger-after-display-visibility-change/</guid><description>Some code examples on how to force wow.js (aka WOW) to trigger on content that suddenly becomes visible, and/or force it to reanimate on ANY element.</description><pubDate>Thu, 14 Feb 2019 07:17:14 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;&lt;a href=&quot;https://www.delac.io/wow/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Wow.js&lt;/a&gt; is a very light-weight Javascript library for automatically showing simple animations on page scroll.&lt;/p&gt;
&lt;p&gt;Most of the time it works perfectly without issue, but today I ran into a curious issue where &lt;span style=&quot;text-decoration: underline;&quot;&gt;I couldn&apos;t get it to trigger when an element that should have animated was initially hidden and then became visible or &quot;revealed&quot;&lt;/span&gt; (via it&apos;s parent element having display:none or something similar and then becoming visible). In fact, where the element should have shown up and animated, there was just a white empty space.&lt;/p&gt;
&lt;p&gt;There are a few threads related to this problem, such as&lt;a href=&quot;https://github.com/matthieua/WOW/issues/175&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; this open issue&lt;/a&gt;, but I couldn&apos;t quite get any of the solutions listed to work for me, and was having trouble finding docs on the internals of wow.js.&lt;/p&gt;
&lt;p&gt;However, after just a little digging, I was able to find some easy ways around this.  For pretty much all of these solutions, make sure that you have a window level variable pointing to your WOW instance, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// wow.js init
window.wow = new WOW({
    // settings here
});
wow.init();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Easiest solution:&lt;/h2&gt;
&lt;p&gt;The easiest solution is to simply call &quot;wow.show(element)&quot;, where element is the element that needs to be animated.
&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;wow.show(element);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is an example where .card-reveal is the parent element that is changing visibility, and I’m forcing WOW to animate when someone clicks the button that changes that visibility:&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Get all expandable cards
$(&apos;div.card &gt; .card-reveal&apos;).parent().each(function(index){
    var card = this;
    // Attach listener to all activator triggers
    $(card).find(&apos;.activator&apos;).on(&apos;click&apos;,function(){
        $(card).find(&apos;.card-reveal .wow&apos;).each(function(){
            wow.show(this);
        });
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Force re-animation regardless of scroll:&lt;/h2&gt;
&lt;p&gt;Finally, if you need to &quot;reanimate&quot; an element, regardless of scroll position, you will want to use this code, since by default WOW will not trigger an animation more times than once, and not more iterations than specified by &quot;data-wow-iteration&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function forceWowReanimation(element){
	element.classList.remove(&apos;animated&apos;);
	element.style.removeProperty(&apos;animation-iteration-count&apos;);
	element.style.removeProperty(&apos;animation-delay&apos;);
	element.style.removeProperty(&apos;animation-iteration-count&apos;);
	element.style.removeProperty(&apos;animation-name&apos;);
	wow.applyStyle(element,true);
	setTimeout(function(){
		wow.show(element);
	},10);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The setTimeout in the above code is normally something I would try to avoid, but in this case it is necessary because &lt;a href=&quot;https://github.com/graingert/WOW/blob/34712a3deadd7ba24633aa8cf5a97675a1c4b56f/src/WOW.js#L251&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;wow.show sets visibility to &quot;visible&quot; regardless of arguments&lt;/a&gt;, so if I don&apos;t give a delay between applyStyle (which sets visible to &quot;hidden&quot;) and .show(), the animation will not trigger.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Key Codes Reference Spreadsheet - ASCII, QT, X11, Etc.</title><link>https://joshuatz.com/mini-tools/2019/key-codes-reference-spreadsheet---ascii-qt-x11-etc/</link><guid isPermaLink="true">https://joshuatz.com/mini-tools/2019/key-codes-reference-spreadsheet---ascii-qt-x11-etc/</guid><description>Work in progress - compiled tables of keycodes and related information for ASCII, QT, X11, and more - in one Google Spreadsheet, as well as some SQL stuff.</description><pubDate>Wed, 13 Feb 2019 12:24:48 GMT</pubDate><content:encoded>&lt;p&gt;Links:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://gist.github.com/joshuatz/c24a7d20197e9ac8ef1cf8939b35de96&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;My SQL code to create a table and fill with all ASCII codes&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/d/1OkLbDJw3gj67ZAMqq7N1MQDZHOWmS3wRXJCvPzHTadg/edit#gid=983452674&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Google Sheets file with tables for QT:Key,X11,Ascii,etc.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;I&apos;ve just started working on a project that involves keypresses and character codes, and just skimming the surface of what it entails, I can tell this is going to be a headache to implement. From a cursory glance, it seems like a lot of this information is spread out all over the place, from OS to OS, to framework to framework.&lt;/p&gt;
&lt;p&gt;I&apos;m trying to use this post as a way to compile some of the more crucial information, mostly for myself, but I also thought others might find it useful as well.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Update 4/3/2019 - Added Mac Keycodes to master Google Sheet. Mac/OSX &lt;a href=&quot;https://developer.apple.com/documentation/coregraphics/cgevent/1456564-init&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;uses CGKeyCode&lt;/a&gt; provided through the macOS SDK.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Cloudinary WYSIWYG Visual Editor for Transformations</title><link>https://joshuatz.com/projects/web-stuff/cloudinary-wysiwyg-visual-editor-for-transformations/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/cloudinary-wysiwyg-visual-editor-for-transformations/</guid><description>My first ReactJS project: a visual editor (&quot;what-you-see-is-what-you-get&quot; or &quot;WYSIWYG&quot;) to arrange, preview, and generate dynamic Cloudinary transformation combinations and URL templates.</description><pubDate>Sat, 09 Feb 2019 15:39:59 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;&lt;a href=&quot;/media/Cloudinary-WYSIWYG-Quick-Editor-Demo.gif&quot;&gt;&lt;img class=&quot;size-full wp-image-259 aligncenter&quot; src=&quot;/media/Cloudinary-WYSIWYG-Quick-Editor-Demo.gif&quot; alt=&quot;Cloudinary WYSIWYG Quick Editor Demo&quot; width=&quot;500&quot; height=&quot;384&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;If you are looking to jump directly to the tool, &lt;a href=&quot;https://joshuatz.com/static-for-wp-iframes/cloudinary-wysiwyg/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;click here&lt;/a&gt; to open it (in a new tab)&lt;/span&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-size: 18pt;&quot;&gt;Source Code: &lt;a href=&quot;https://github.com/joshuatz/cloudinary-wysiwyg&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://github.com/joshuatz/cloudinary-wysiwyg&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6 Minute Intro Video&lt;/h2&gt;
&lt;p&gt;This video covers most of the content on this page, so feel free to give it a view instead of, or in addition to the content below.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;iframe src=&quot;https://www.youtube-nocookie.com/embed/Cf1KgYF8pJU?rel=0&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&amp;#x3C;span style=&quot;display: inline-block; width: 0px; overflow: hidden; line-height: 0;&quot; data-mce-type=&quot;bookmark&quot; class=&quot;mce_SELRES_start&quot;&gt;﻿&amp;#x3C;/span&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Spreadsheet used in Demo Video with samples and formulae:&lt;/h2&gt;
&lt;p&gt;Link: &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1PiGPE003KcrACWn9GHf0vF-8jHFT71zWkNTdT5orDfI/edit?usp=sharing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Here&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;What is Cloudinary?&lt;/h2&gt;
&lt;p&gt;Normally, when you combine images, text, and other elements into a new compiled image, perhaps with Photoshop, in order to share that resulting image with other people, you would need to save it and then upload it somewhere. Cloudinary changes the game; upload a &quot;base&quot; image, and then you can apply as many transformations, effects, or overlays to that image simply by manipulating a shareable URL. This is easier to show than it is to explain, so here is an image that shows 4 different URLs, all generated from one original image (the top left image):&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/2019-02-06-19_31_03-Cloudinary-Simple-URL-Demo.png&quot;&gt;&lt;img class=&quot;size-full wp-image-258 aligncenter&quot; src=&quot;/media/2019-02-06-19_31_03-Cloudinary-Simple-URL-Demo.png&quot; alt=&quot;&quot; width=&quot;679&quot; height=&quot;839&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;What is the Cloudinary-WYSIWYG Editor Tool?&lt;/h2&gt;
&lt;p&gt;I found the concept of Cloudinary fascinating, and I realized that their transformations (crop, rotate, skew, etc) could be combined in ways that pushed the limits of the platforms, but a major limiting factor is how confusing they can be to manually join together and keep track of. To illustrate that, here is a URL that was generated by this editor tool, but would have been practically impossible to parse out by hand:&lt;/p&gt;
&lt;pre class=&quot;wrap&quot;&gt;&lt;code&gt;https://res.cloudinary.com/ACCOUNT_NAME/image/upload/c_fill,h_500,o_40,w_500/h_224,l_pixel,w_224/co_rgb:070707,e_colorize,fl_layer_apply,g_north_west,x_28,y_75/h_395,l_pixel,w_454/co_rgb:2744a6,e_colorize,fl_layer_apply,g_north_west,x_23,y_35/h_210,l_pixel,w_210/co_rgb:ffffff,e_colorize,fl_layer_apply,g_north_west,x_34,y_82/c_scale,h_188,l_profilepicture,w_215/fl_layer_apply,g_north_west,x_34,y_107/h_209,l_pixel,w_208/co_rgb:ffffff,e_colorize,fl_layer_apply,g_north_west,x_254,y_82/l_text:Roboto_37_normal:Stats:/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_316,y_93/l_text:Roboto_19_normal:Flowers%20Pollinated%20:%2020/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_266,y_141/l_text:Roboto_19_normal:People%20Chased%20:%2025/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_266,y_167/l_text:Roboto_55_normal:Buzzy%20McBuzzer/co_rgb:ffffff,e_colorize,fl_layer_apply,g_north_west,x_52,y_314/l_text:Roboto_31_normal:Rank%20:%2010/co_rgb:ffffff,e_colorize,fl_layer_apply,g_north_west,x_170,y_380/h_119,l_pixel,w_119/co_rgb:c8c61f,e_colorize,fl_layer_apply,g_north_west,r_59,x_295,y_194/l_text:Roboto_42_normal:%3C10%3E/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_308,y_237/sample&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This editor let me build that URL by dragging and dropping shapes, images, and text, all without ever needing to manually edit a URL or understand how editing steps convert to Cloudinary transformation parameters. However, if you are a developer that is looking to learn more about Cloudinary, this is also a great learning tool because it lets you instantly see how a visual change (resize, move, color, etc.) is reflected by a change in the Cloudinary transformation &quot;chain&quot;.&lt;/p&gt;
&lt;h2&gt;But what is it for??? / Practical Applications:&lt;/h2&gt;
&lt;p&gt;Beyond serving as a learning tool for Cloudinary novices, calling it an editor might lead to some misgivings. This tool is really not meant to be a replacement for Photoshop, or even Paint.net for that matter. If you just need a one-off image, this editor is going to leave you disappointed. What this tool was meant for is when you need to generate 1,000+ unique images, all based on a set of dynamic inputs. This is usually needed when you are developing an application or something that wants to be able to &quot;share&quot; rich media updates to Facebook, Instagram, Twitter, Slack, etc. For example, let say you are making a social gaming application, and you want users to be able to share an achievement with their friends (something like &quot;Reached Level 10!&quot;). You could simply have them share a block of text, but that is not very attractive, and if you are looking to grow your fanbase, you might want them to be able to share a flashy image that might attract the attention of their friends. Again, you could just create a single image that everyone shares when they reach level 10, but again, not that impressive. What if you could have them share an image that features the achievement text, their profile picture, and other dynamic details? You can with Cloudinary, and you can use this tool to build the template! In fact, that super long URL in the above section was created with my tool, and generates this image based on user profile data:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Cloudinary-Dynamic-Gaming-Badge-Overlay-Generated-Example.jpg&quot;&gt;&lt;img class=&quot;size-full wp-image-202 aligncenter&quot; src=&quot;/media/Cloudinary-Dynamic-Gaming-Badge-Overlay-Generated-Example.jpg&quot; alt=&quot;Cloudinary - Dynamic Gaming Badge Overlay Generated Example&quot; width=&quot;500&quot; height=&quot;500&quot;&gt;&lt;/a&gt;Another example is generating images where the base template, e.g. the background of the image, does not change, but the overlaying text changes frequently and is unique per user. The example I provide in my demo video is for a theoretical SlackBot that messages Jira users with a daily task summary:&lt;/p&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;&lt;a href=&quot;/media/Cloudinary-Jira-Daily-Task-Summary-Generated-Image-Sample.jpg&quot;&gt;&lt;img class=&quot;aligncenter wp-image-261 size-full&quot; src=&quot;/media/Cloudinary-Jira-Daily-Task-Summary-Generated-Image-Sample.jpg&quot; alt=&quot;Cloudinary - Jira Daily Task Summary Generated Image Sample&quot; width=&quot;720&quot; height=&quot;560&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this Jira example, I create the background template outside of my editor tool (but could have actually used it if I wanted to) and then used it as the &quot;base layer&quot; in my editor, while positioning text on top. Then, using JS/NodeJS/PHP/Google Sheets/Whatever I want to use, I can dynamically generate thousands of these status updates, completely unique per user with their stats.&lt;/p&gt;
&lt;p&gt;Some other practical applications:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;E-commerce sites
&lt;ul&gt;
	&lt;li&gt;Overlay product details in text over product image&lt;/li&gt;
	&lt;li&gt;Embed templated images into your product feed for use with Google Shopping, Facebook Product Catalogs, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Cloudinary learning tool
&lt;ul&gt;
	&lt;li&gt;Mess around with different combinations to see how different objects get converted into chained transformations, and subsequently into a URL string&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Dynamic image generation for Google Sheets or any other spreadsheet program
&lt;ul&gt;
	&lt;li&gt;You can use this tool to create the template, then use spreadsheet functions like CONCAT() to create the URL dynamically given values in other columns&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Dynamic image generation for sites that can&apos;t use server-side image manipulation - this way you can use Javascript, plus the export from this tool, to generate an &amp;#x3C;img&gt; tag src completely 100% client side.&lt;/li&gt;
	&lt;li&gt;Meme generators / image macro sites&lt;/li&gt;
	&lt;li&gt;Creating image embeds for Slack Apps / Slack bots / FB Messenger Bots&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Behind the Scenes / Tech Used:&lt;/h2&gt;
&lt;p&gt;The &lt;em&gt;true&lt;/em&gt; originating force behind the creation of this tool was rather simple; I wanted to learn the basics of ReactJS and build something cool as my first project using it, then thought of how, a bit earlier, I had been wishing that something like this tool existed. Although I feel like the end product is pretty neat, I still have mixed feeling about React even after getting my feet wet. My next project will probably use VueJS, which although similar, has some key differences that I think my suit my development habits.&lt;/p&gt;
&lt;p&gt;A library / framework that I used extensively on this tool, and can talk about much more positively than React, is &lt;a href=&quot;http://fabricjs.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Fabric.js&lt;/a&gt;. Don&apos;t let their somewhat cheesy homepage fool you; this is an amazing library for manipulating Canvas elements with Javascript. Like, seriously amazing. Every time I thought I was going to have to write my own code to handle something I assumed they wouldn&apos;t cover - bam - they would have support for it out of the box. For example, if you want to get the coordinates of an imaginary &quot;bounding box&quot; that contains a canvas object of interest, you can do so with just one line of code.&lt;/p&gt;
&lt;p&gt;Obviously, kudos to Cloudinary as well, for providing a very impressive service, comprehensive documentation, and quickly responding to my GitHub issue :)&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Brainstorming - Checking for Remote Power Failure Over the Internet</title><link>https://joshuatz.com/posts/2019/brainstorming---checking-for-remote-power-failure-over-the-internet/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/brainstorming---checking-for-remote-power-failure-over-the-internet/</guid><description>Some ideas and instructions on how one might go about setting up a system for remotely checking if their house has lost power or if it has been restored.</description><pubDate>Fri, 08 Feb 2019 16:39:00 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I live in Washington state, which, due to the amount of trees and their proximity to power lines, often means that I might lose power in my apartment, while just blocks away, everyone else is powered up. I also often visit my parents, and this has led to multiple instances where I am torn between spending the night at their house, where they still have power, or starting the drive back without knowing whether or not I will have hot water and my favorite electronics ready for use.&lt;/p&gt;
&lt;p&gt;At these times, I have often wished for an easy way to check remotely, over the internet, whether or not my home has power. Building and setting up a home server is something on my to-do list (and has been on it for a while) and it would easily solve this issue, but with a potential snowstorm on the way, and a visit to my parents planned, I wanted to set something up in a jiffy. I was also thinking - given the plethora of internet-connected devices the average consumer, and I myself own, surely there must be some way to accomplish this already?&lt;/p&gt;
&lt;p&gt;To be clear - the easiest solution is to simply check with the power utility company. However, since during the last outage their reporting system went down with the power (LOL) I was annoyed enough to look into my own method for checking.&lt;/p&gt;
&lt;p&gt;Here are some of the ideas I explored. My quick solution for today is at the bottom (or click &lt;a href=&quot;#solution&quot;&gt;here&lt;/a&gt; to jump to it).&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Idea: Leave a spare Laptop / PC on and running TeamViewer (remote control software for screen sharing). This would allow me to log in remotely and even check out the webcam remotely. If it fails to connect, I know my power has gone out.
&lt;ul&gt;
	&lt;li&gt;Failure: Most computers, including the ones I own, &lt;span style=&quot;text-decoration: underline;&quot;&gt;do not automatically start up after power has been lost and then restored&lt;/span&gt;. Some BIOS configurations will allow for enabling a setting which turns this feature on, but it is not often found on laptops or tablets, which are the only spare devices I own and would be comfortable leaving TeamViewer installed as a server on.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Idea: Use an old Android phone as a remote security camera. If I can&apos;t connect, I know the power has gone off. Or I could install some simple server software on it.
&lt;ul&gt;
	&lt;li&gt;There are a bunch of free applications that cover these needs
&lt;ul&gt;
	&lt;li&gt;Remote security camera:
&lt;ul&gt;
	&lt;li&gt;Alfred Home Security Camera: &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.ivuu&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Play Store Link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Simple home server
&lt;ul&gt;
	&lt;li&gt;LAMP like stack: &quot;HTTP Server Powered by Apache&quot; - &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.esminis.server.apache&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Play Store Link&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;ISSUE: Same as the Laptop / PC issue. Many Android phones do not automatically boot themselves back up after losing and regaining power. HOWEVER, this is still one of the best solutions, for multiple reasons:
&lt;ul&gt;
	&lt;li&gt;If the screen is off, even old phones should hold a charge for a decent amount of time before depleting the battery. Hopefully your power comes back on before then.&lt;/li&gt;
	&lt;li&gt;There is a way to force your phone to auto boot back up after power has been restored. However, it requires root - instructions &lt;a href=&quot;https://www.instructables.com/id/AutoBoot-Android-When-Charger-Connected/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At this point, I started thinking &quot;what devices do most people own, that are always plugged in, and already feature auto-restart / auto-boot on power failure and subsequent restoration?&quot;...&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Idea: Roku / Amazon Fire Stick / Smart TV
&lt;ul&gt;
	&lt;li&gt;This led me down a very fascinating rabbit hole of technology, which I could read about for days&lt;br&gt;
&lt;ul&gt;
	&lt;li&gt;Both Roku and Amazon FireTV/Fire Stick have network endpoints which you can make requests to and get data back. Both support something called &lt;a href=&quot;http://www.dial-multiscreen.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DIAL&lt;/a&gt;, which is sort a protocol for 1st-screen (TV) discovery and control by 2nd-screen (smartphone, tablet) devices.&lt;br&gt;
&lt;ul&gt;
	&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul&gt;
	&lt;li&gt;Roku is much more developer friendly, and has &lt;a href=&quot;https://sdkdocs.roku.com/display/sdkdoc/External+Control+API#ExternalControlAPI-DIAL(DiscoveryandLaunch)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;published docs on using DIAL&lt;/a&gt;. In addition, they have &lt;a href=&quot;https://sdkdocs.roku.com/display/sdkdoc/External+Control+API&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;their own API they have developed&lt;/a&gt;, which is surprisingly in-depth and powerful. A little scary actually, in terms of what can be controlled without authentication...
&lt;ul&gt;
	&lt;li&gt;For example, as long as I&apos;m on the same network, I can actually &quot;press&quot; keys on the Roku remote just by issuing network requests. For example, making a POST request to this URL presses the &quot;Home&quot; key on my Roku!&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;http://[Roku_Local_IP]:8060/keypress/Home&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
	&lt;li&gt;Amazon is much more secretive and harder to find information on. They also support DIAL, but it is less clear on what functionality is supported. However, by poking around, it looks like they support the general DIAL syntax. For example, if Netflix is installed, a GET request to this URL will return XML information about the application.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;http://[FireStick_Local_IP:8009/apps/Netflix&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
	&lt;li&gt;Idea (and &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;SOLUTION&lt;/strong&gt;&lt;/span&gt;, at least for today): ROUTERS!!!
&lt;ul&gt;
	&lt;li&gt;If you are already using DD-WRT (open source firmware for routers, great stuff), you have tons of options at your fingertips, especially since it &lt;a href=&quot;https://wiki.dd-wrt.com/wiki/index.php/Index:Scripting&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;supports scripting&lt;/a&gt;.&lt;/li&gt;
	&lt;li&gt;In general, this is a risky option, since the last thing you want to do with a router is purposefully allow outside access pointing in.
&lt;ul&gt;
	&lt;li&gt;This is basically the whole point of firewalls, which are built into most routers.
&lt;ul&gt;
	&lt;li&gt;If you try to make a GET/POST/etc HTTP request to your Public IP address, that is very unlikely to get routed anywhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;&lt;span id=&quot;solution&quot; style=&quot;font-size: 18pt;&quot;&gt;Works for me - Pinging the router!&lt;/span&gt; -&lt;/strong&gt; For my quick, no fuss setup, I saw that my ISP&apos;s router had &quot;Drop Incoming ICMP Echo Requested to Device WAN address&quot; actually turned to &lt;em&gt;&lt;strong&gt;off&lt;/strong&gt;&lt;/em&gt;&lt;em&gt;  &lt;/em&gt;by default - 2 seconds later, I had fired up my terminal and a simple &quot;ping [MY_PUBLIC_IP]&quot; later...  low and behold, the router responded and no packets dropped!
&lt;ul&gt;
	&lt;li&gt;I should have checked this first, but had assumed that this would be locked down. To be fair, many routers might have this locked down, so you might have to explore the other options above if this is not working for you.&lt;/li&gt;
	&lt;li&gt;Since I have an Android phone, I can use Android&apos;s terminal to ping from my parents house and see if my router is up and running.
&lt;ul&gt;
	&lt;li&gt;There are also dedicated apps for checking on ping status - such as &lt;a href=&quot;https://play.google.com/store/apps/details?id=bg.softel.pingmonitor&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;this one&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Please note that an ICMP ECHO packet is different from HTTP - see wiki &lt;a href=&quot;https://en.wikipedia.org/wiki/Ping_(networking_utility)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;here&lt;/a&gt; - but the reason I mention this is because you can&apos;t use typical website uptime monitoring tools to check if your router is up or down. If you want to use an automated alert site, look for one that specifically supports &quot;ping&quot;, such as &lt;a href=&quot;https://uptimerobot.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;UptimeRobot&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Strange Tip - Quickly Generating Typewriter Effect Videos (MP4, GIF, etc.)</title><link>https://joshuatz.com/posts/2019/strange-tip---quickly-generating-typewriter-effect-videos-mp4-gif-etc/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/strange-tip---quickly-generating-typewriter-effect-videos-mp4-gif-etc/</guid><description>As a quick way to create typewriter effect videos, you can throw text in PowerPoint, apply an animation, and screen capture the presentation to GIF, MP4, etc.</description><pubDate>Thu, 07 Feb 2019 12:50:57 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;File this under &quot;shouldn&apos;t work, but does&quot;.&lt;/p&gt;
&lt;p&gt;I was in need of a tool today to quickly create a &quot;typewriter effect&quot; video - you know, a video where text appears letter by letter, as if someone was typing it out while being recorded. I rarely ever make videos, so I don&apos;t have the best software (I&apos;m using Magix Movie Edit) and it does not have a typewriter effect animation built in.&lt;/p&gt;
&lt;p&gt;There are solutions out there online for generating GIFs, but they the quality seemed off and there was not much configuration available in terms of font and sizing. Commercial solutions are in the hundreds of dollars, which is way out of budget for a one-off project like the one I was working on.&lt;/p&gt;
&lt;p&gt;After a little searching, I stumbled across an interesting response to a &lt;a href=&quot;https://www.quora.com/How-do-I-make-a-video-word-typing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;related question&lt;/a&gt; on Quora that suggested &lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;using Microsoft PowerPoint&lt;/strong&gt;&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Hmm. Could that work?... IT DOES! This feels like such a &quot;dirty&quot; solution, but this was seriously so easy - I just created a slide in Microsoft Powerpoint with dimensions of 1920x1080 to match my video, added the text, set the animation effect to &quot;Appear&quot; and &quot;Animate Text&quot; to &quot;By letter&quot;, and then ran the animation while running screen capture software. Here is a sample output (not bad, right?!!):&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://www.youtube-nocookie.com/embed/WC2ZLG7zWkQ?rel=0&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;If you are looking for a step by step process for PowerPoint, here is a video off YouTube that covers how to add it:&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://www.youtube-nocookie.com/embed/Jx5BffbBo0w&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Sheets - Quick Start with Base64 - With and Without Scripting</title><link>https://joshuatz.com/posts/2019/google-sheets---quick-start-with-base64---with-and-without-scripting/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/google-sheets---quick-start-with-base64---with-and-without-scripting/</guid><description>I went overboard in trying to use Base64 encoding in Google Sheets and ended up coming up with a &quot;pure formula&quot; for generating Base64 from a text string, which does not require the use of custom scripting. This post covers that formula, how I came up with it, and scripting alternatives.</description><pubDate>Fri, 01 Feb 2019 06:53:18 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h1 style=&quot;font-size: 3rem;&quot;&gt;Sample Spreadsheet showing both solutions: &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1qLs1GliJnXwQKMaliPPXNRVKHPIze5sM_Z-spkFVatA/edit?usp=sharing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Link&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I&apos;m used to Google Sheets having built-in formulae for practically everything, so I was surprised when I needed to Base64 encode a string today and could not find a function to do so. The &lt;strong&gt;easiest&lt;/strong&gt; solution is to use a &quot;&lt;a href=&quot;https://developers.google.com/apps-script/guides/sheets/functions&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Custom Function&lt;/a&gt;&quot;, which allows you write your own named function in JavaScript, and then call that function from anywhere in your Google Sheets file, just as if it was a built-in function. That is &lt;a href=&quot;#solutionA&quot;&gt;Solution A&lt;/a&gt;, below. Skip to &lt;a href=&quot;#solutionB&quot;&gt;Solution B&lt;/a&gt; if you want to see how I created a custom formula that only uses native Google Sheets functions!&lt;/p&gt;
&lt;!-- Solution A --&gt;
&lt;h2 id=&quot;solutionA&quot;&gt;Solution A - Using a Google Sheets Script &quot;Custom Function&quot;:&lt;/h2&gt;
&lt;div style=&quot;margin-left: 30px;&quot;&gt;
&lt;p&gt;This is super easy. Just add the following code to the scripts file attached to your Google Sheet, by going to &quot;Menu &gt; Tools &gt; Script Editor&quot;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Sheets-Getting-to-the-Script-Editor-to-add-Custom-Functions.png&quot;&gt;&lt;img class=&quot;alignnone wp-image-216 size-full&quot; src=&quot;/media/Google-Sheets-Getting-to-the-Script-Editor-to-add-Custom-Functions.png&quot; alt=&quot;&quot; width=&quot;661&quot; height=&quot;218&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
/**
 * Base64 Encode Input
 * @param {any | Array&amp;#x3C;any[]&gt;} input - Input cell, or range of cells
 * @param {boolean} [OPT_webSafe=true] - If should use websafe variant of base64
 * @param {boolean} [OPT_plainText=false] - If should treat input as plaintext instead of UTF-8
 */
function base64Encode(input, OPT_webSafe, OPT_plainText) {
  if (!input) return input;
  const charSet = OPT_plainText ? Utilities.Charset.US_ASCII : Utilities.Charset.UTF_8;
  const useWebSafe = OPT_webSafe !== false;
  const encoder = useWebSafe ? Utilities.base64EncodeWebSafe : Utilities.base64Encode;
  if (Array.isArray(input)) {
    return input.map(t =&gt; base64Encode(t, OPT_webSafe, OPT_plainText));
  }
&lt;p&gt;return encoder(input, charSet);
}&lt;/p&gt;
&lt;p&gt;/**&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Base64 Decode Input&lt;/li&gt;
&lt;li&gt;@param {any | Array&amp;#x3C;any[]&gt;} input - Input cell, or range of cells&lt;/li&gt;
&lt;li&gt;@param {boolean} [OPT_webSafe=true] - If should use websafe variant of base64&lt;/li&gt;
&lt;li&gt;@param {boolean} [OPT_plainText=false] - If should treat input as plaintext instead of UTF-8
*/
function base64Decode(input, OPT_webSafe, OPT_plainText) {
if (!input) return input;
const charSet = OPT_plainText ? Utilities.Charset.US_ASCII : Utilities.Charset.UTF_8;
const useWebSafe = OPT_webSafe !== false;
const decoder = useWebSafe ? Utilities.base64DecodeWebSafe : Utilities.base64Decode;
if (Array.isArray(input)) {
return input.map(t =&gt; base64Decode(t, OPT_webSafe, OPT_plainText));
}&lt;/li&gt;
&lt;/ul&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;return Utilities.newBlob(decoder(input, charSet)).getDataAsString();
}
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Once you have pasted and saved that code into the script editor, switch back to your spreadsheet, and reload the page. You should now be able to use the custom function very easily. 99% of the time, you should be able to use it with default settings, like so:&lt;/p&gt;
&lt;a href=&quot;/media/Google-Sheets-Base64-Encoding-with-UTF-Emoji.png&quot;&gt;&lt;img src=&quot;/media/Google-Sheets-Base64-Encoding-with-UTF-Emoji.png&quot; alt=&quot;Google Sheets Base64 Encoding with Emoji UTF Input&quot; style=&quot;width:100%; height:auto; max-width: 500px; margin: auto; display: block;&quot;&gt;&lt;/a&gt;
&lt;p&gt;You can even use the formula with an array of cells as the input,and it will automatically fill down!&lt;/p&gt;
&lt;a href=&quot;/media/Google-Sheets-Base64-Encoding-with-Array-of-Cells.png&quot;&gt;&lt;img src=&quot;/media/Google-Sheets-Base64-Encoding-with-Array-of-Cells.png&quot; alt=&quot;Google Sheets - Base64 Encoding with Array of Cells&quot; style=&quot;width:100%; height:auto; max-width: 1200px; margin: auto; display: block;&quot;&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;!-- Solution A - broken down --&gt;
&lt;h3 id=&quot;solutionA-2&quot;&gt;Breaking down the custom function:&lt;/h3&gt;
&lt;div style=&quot;margin-left: 30px;&quot;&gt;
&lt;p&gt;This is basically the same code as above, but broken down into separate functions, so you can see how it works.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
function base64Encode(input) {
  return Utilities.base64Encode(input,Utilities.Charset.UTF_8);
}
function base64EncodeWebSafe(input) {
  return Utilities.base64EncodeWebSafe(input,Utilities.Charset.UTF_8);
}
function base64Decode(input) {
  return Utilities.newBlob(Utilities.base64Decode(input,Utilities.Charset.UTF_8)).getDataAsString();
}
function base64DecodeWebSafe(input) {
  return Utilities.newBlob(Utilities.base64DecodeWebSafe(input,Utilities.Charset.UTF_8)).getDataAsString();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main difference between WebSafe (aka &quot;base64url&quot;) and non-WebSafe is that &quot;/&quot; is replaced with &quot;_&quot; for WebSafe, since slashes are reserved in URLs for path separators. For more details see &lt;a href=&quot;https://tools.ietf.org/html/rfc4648&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RFC-4648&lt;/a&gt;.&lt;/p&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;&lt;a href=&quot;/media/Google-Sheets-Using-Script-Custom-Functions-to-Base64-Encode.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-217&quot; src=&quot;/media/Google-Sheets-Using-Script-Custom-Functions-to-Base64-Encode.png&quot; alt=&quot;&quot; width=&quot;654&quot; height=&quot;139&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Sheets-Using-Script-Custom-Functions-to-Base64-Decode.png&quot;&gt;&lt;img class=&quot;wp-image-582 aligncenter&quot; src=&quot;/media/Google-Sheets-Using-Script-Custom-Functions-to-Base64-Decode.png&quot; alt=&quot;Google Sheets - Using Script Custom Functions to Base64 Decode&quot; width=&quot;720&quot; height=&quot;180&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Notice that the &quot;could not decode string&quot; error in the screenshot above is because you should not use base64DecodeWebSafe to decode something that was originally encoded with the WebSafe variant of Base64. In this case, it doesn&apos;t like the &quot;_&quot; character, which is specific to the WebSafe variant. In general, you are probably safer just using the WebSafe version most of the time, if you want to avoid these kinds of errors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EDIT&lt;/strong&gt;: Thank you to Maxime for pointing out an error in my original code; I did not account for the fact that Utilities.base64Decode &amp;#x26; Utilities.base64DecodeWebSafe both return byte-arrays, not strings. For some reason I only tested my encode functions and did not notice this. This post has been updated, and for further reference, see Maxime&apos;s comments at the bottom, and &lt;a href=&quot;https://developers.google.com/apps-script/reference/utilities/utilities#base64DecodeWebSafe(String)&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this Apps Script doc page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EDIT: &lt;/strong&gt;I added the UTF-8 argument to all the functions, in case anyone is using this with non ASCII text (for example, a foreign language that uses accented or non Latin characters). If you are only using ASCII, you could leave off this argument, and Sheets will default. However, as ASCII is part of UTF-8, it doesn&apos;t hurt to leave it.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;solutionB&quot;&gt;Solution B: &quot;Pure Formula&quot; function - No Google Apps Scripting required!&lt;/h2&gt;
&lt;div style=&quot;margin-left: 30px;&quot;&gt;
&lt;p&gt;Custom functions through Google Sheets App Scripting is fast and simple. However, this is not always the most &quot;performant&quot; solution, and I felt like taking on a challenge today, so I actually came up with a formula that uses &lt;strong&gt;only&lt;/strong&gt; native Google Sheets functions to Base64 encode a string - no scripting! This is also helpful if you are not allowed to use Scripts within your Google Sheets file due to restrictions placed by your employer, or you are just looking for the quickest copy and paste solution.&lt;/p&gt;
&lt;p&gt;Here is the full (insanely long and complex) Google Sheets formula that converts text to Base64, nothing else required. Just replace &quot;A2&quot; with the cell you want to convert the text from:&lt;/p&gt;
&lt;pre class=&quot;googlesheets&quot; data-linewrap=&quot;true&quot;&gt;&lt;code&gt;
=CONCATENATE(JOIN(&quot;&quot;,ARRAYFORMULA(SWITCH(BIN2DEC(TEXT(SPLIT(REGEXREPLACE(CONCATENATE(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))),REPT(&quot;0&quot;,(FLOOR((LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))))+(6-1))/6)*6)-LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8)))))),&quot;(.{6})&quot;,&quot;$1/&quot;),&quot;/&quot;),&quot;000000&quot;)),0,&quot;A&quot;,1,&quot;B&quot;,2,&quot;C&quot;,3,&quot;D&quot;,4,&quot;E&quot;,5,&quot;F&quot;,6,&quot;G&quot;,7,&quot;H&quot;,8,&quot;I&quot;,9,&quot;J&quot;,10,&quot;K&quot;,11,&quot;L&quot;,12,&quot;M&quot;,13,&quot;N&quot;,14,&quot;O&quot;,15,&quot;P&quot;,16,&quot;Q&quot;,17,&quot;R&quot;,18,&quot;S&quot;,19,&quot;T&quot;,20,&quot;U&quot;,21,&quot;V&quot;,22,&quot;W&quot;,23,&quot;X&quot;,24,&quot;Y&quot;,25,&quot;Z&quot;,26,&quot;a&quot;,27,&quot;b&quot;,28,&quot;c&quot;,29,&quot;d&quot;,30,&quot;e&quot;,31,&quot;f&quot;,32,&quot;g&quot;,33,&quot;h&quot;,34,&quot;i&quot;,35,&quot;j&quot;,36,&quot;k&quot;,37,&quot;l&quot;,38,&quot;m&quot;,39,&quot;n&quot;,40,&quot;o&quot;,41,&quot;p&quot;,42,&quot;q&quot;,43,&quot;r&quot;,44,&quot;s&quot;,45,&quot;t&quot;,46,&quot;u&quot;,47,&quot;v&quot;,48,&quot;w&quot;,49,&quot;x&quot;,50,&quot;y&quot;,51,&quot;z&quot;,52,&quot;0&quot;,53,&quot;1&quot;,54,&quot;2&quot;,55,&quot;3&quot;,56,&quot;4&quot;,57,&quot;5&quot;,58,&quot;6&quot;,59,&quot;7&quot;,60,&quot;8&quot;,61,&quot;9&quot;,62,&quot;-&quot;,63,&quot;_&quot;,64,&quot;=&quot;))),REPT(&quot;=&quot;,(FLOOR((LEN(JOIN(&quot;&quot;,ARRAYFORMULA(SWITCH(BIN2DEC(TEXT(SPLIT(REGEXREPLACE(CONCATENATE(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))),REPT(&quot;0&quot;,(FLOOR((LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))))+(6-1))/6)*6)-LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8)))))),&quot;(.{6})&quot;,&quot;$1/&quot;),&quot;/&quot;),&quot;000000&quot;)),0,&quot;A&quot;,1,&quot;B&quot;,2,&quot;C&quot;,3,&quot;D&quot;,4,&quot;E&quot;,5,&quot;F&quot;,6,&quot;G&quot;,7,&quot;H&quot;,8,&quot;I&quot;,9,&quot;J&quot;,10,&quot;K&quot;,11,&quot;L&quot;,12,&quot;M&quot;,13,&quot;N&quot;,14,&quot;O&quot;,15,&quot;P&quot;,16,&quot;Q&quot;,17,&quot;R&quot;,18,&quot;S&quot;,19,&quot;T&quot;,20,&quot;U&quot;,21,&quot;V&quot;,22,&quot;W&quot;,23,&quot;X&quot;,24,&quot;Y&quot;,25,&quot;Z&quot;,26,&quot;a&quot;,27,&quot;b&quot;,28,&quot;c&quot;,29,&quot;d&quot;,30,&quot;e&quot;,31,&quot;f&quot;,32,&quot;g&quot;,33,&quot;h&quot;,34,&quot;i&quot;,35,&quot;j&quot;,36,&quot;k&quot;,37,&quot;l&quot;,38,&quot;m&quot;,39,&quot;n&quot;,40,&quot;o&quot;,41,&quot;p&quot;,42,&quot;q&quot;,43,&quot;r&quot;,44,&quot;s&quot;,45,&quot;t&quot;,46,&quot;u&quot;,47,&quot;v&quot;,48,&quot;w&quot;,49,&quot;x&quot;,50,&quot;y&quot;,51,&quot;z&quot;,52,&quot;0&quot;,53,&quot;1&quot;,54,&quot;2&quot;,55,&quot;3&quot;,56,&quot;4&quot;,57,&quot;5&quot;,58,&quot;6&quot;,59,&quot;7&quot;,60,&quot;8&quot;,61,&quot;9&quot;,62,&quot;-&quot;,63,&quot;_&quot;,64,&quot;=&quot;))))+(4-1))/4)*4)-LEN(JOIN(&quot;&quot;,ARRAYFORMULA(SWITCH(BIN2DEC(TEXT(SPLIT(REGEXREPLACE(CONCATENATE(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))),REPT(&quot;0&quot;,(FLOOR((LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))))+(6-1))/6)*6)-LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8)))))),&quot;(.{6})&quot;,&quot;$1/&quot;),&quot;/&quot;),&quot;000000&quot;)),0,&quot;A&quot;,1,&quot;B&quot;,2,&quot;C&quot;,3,&quot;D&quot;,4,&quot;E&quot;,5,&quot;F&quot;,6,&quot;G&quot;,7,&quot;H&quot;,8,&quot;I&quot;,9,&quot;J&quot;,10,&quot;K&quot;,11,&quot;L&quot;,12,&quot;M&quot;,13,&quot;N&quot;,14,&quot;O&quot;,15,&quot;P&quot;,16,&quot;Q&quot;,17,&quot;R&quot;,18,&quot;S&quot;,19,&quot;T&quot;,20,&quot;U&quot;,21,&quot;V&quot;,22,&quot;W&quot;,23,&quot;X&quot;,24,&quot;Y&quot;,25,&quot;Z&quot;,26,&quot;a&quot;,27,&quot;b&quot;,28,&quot;c&quot;,29,&quot;d&quot;,30,&quot;e&quot;,31,&quot;f&quot;,32,&quot;g&quot;,33,&quot;h&quot;,34,&quot;i&quot;,35,&quot;j&quot;,36,&quot;k&quot;,37,&quot;l&quot;,38,&quot;m&quot;,39,&quot;n&quot;,40,&quot;o&quot;,41,&quot;p&quot;,42,&quot;q&quot;,43,&quot;r&quot;,44,&quot;s&quot;,45,&quot;t&quot;,46,&quot;u&quot;,47,&quot;v&quot;,48,&quot;w&quot;,49,&quot;x&quot;,50,&quot;y&quot;,51,&quot;z&quot;,52,&quot;0&quot;,53,&quot;1&quot;,54,&quot;2&quot;,55,&quot;3&quot;,56,&quot;4&quot;,57,&quot;5&quot;,58,&quot;6&quot;,59,&quot;7&quot;,60,&quot;8&quot;,61,&quot;9&quot;,62,&quot;-&quot;,63,&quot;_&quot;,64,&quot;=&quot;))))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is a slightly shorter version of that formula, but requires having a Base64 lookup sheet as part of your Google Sheet file. See sample sheet &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1qLs1GliJnXwQKMaliPPXNRVKHPIze5sM_Z-spkFVatA/edit#gid=172453716&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;googlesheets&quot; data-linewrap=&quot;true&quot;&gt;&lt;code&gt;
=CONCATENATE(JOIN(&quot;&quot;,ARRAYFORMULA(VLOOKUP(BIN2DEC(TEXT(SPLIT(REGEXREPLACE(CONCATENATE(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))),REPT(&quot;0&quot;,(FLOOR((LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))))+(6-1))/6)*6)-LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8)))))),&quot;(.{6})&quot;,&quot;$1/&quot;),&quot;/&quot;),&quot;000000&quot;)),Base64Lookup!$A$1:$B$65,2,FALSE))),REPT(&quot;=&quot;,(FLOOR((LEN(JOIN(&quot;&quot;,ARRAYFORMULA(VLOOKUP(BIN2DEC(TEXT(SPLIT(REGEXREPLACE(CONCATENATE(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))),REPT(&quot;0&quot;,(FLOOR((LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))))+(6-1))/6)*6)-LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8)))))),&quot;(.{6})&quot;,&quot;$1/&quot;),&quot;/&quot;),&quot;000000&quot;)),Base64Lookup!$A$1:$B$65,2,FALSE))))+(4-1))/4)*4)-LEN(JOIN(&quot;&quot;,ARRAYFORMULA(VLOOKUP(BIN2DEC(TEXT(SPLIT(REGEXREPLACE(CONCATENATE(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))),REPT(&quot;0&quot;,(FLOOR((LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8))))+(6-1))/6)*6)-LEN(JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))),2,8)))))),&quot;(.{6})&quot;,&quot;$1/&quot;),&quot;/&quot;),&quot;000000&quot;)),Base64Lookup!$A$1:$B$65,2,FALSE))))))
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 id=&quot;solutionBAdditionalBackground&quot;&gt;Additional background on Solution B and how I came up with the formula:&lt;/h3&gt;
&lt;p&gt;I started off trying to understand how strings are converted to Base64 representations, period. I found &lt;a href=&quot;https://stackabuse.com/encoding-and-decoding-base64-strings-in-node-js/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this page&lt;/a&gt;, which although written about NodeJS, actually does a good job of covering the basics of how ASCII is converted to Base64. I then went, step by step, through the process of manually converting a string inside Google Sheets, first into Unicode, then into binary, then into 6 bit chunks, then into decimal, and finally, into base64 by using a decimal-to-base64 lookup table. Some of these steps had built in Google Sheets functions to accomplish them, such as getting the Unicode value of a character (that is simply &quot;UNICODE(A2)&quot;), but some other steps required crafting my own complicated formula. Some examples:&lt;/p&gt;
&lt;div style=&quot;margin-left: 30px;&quot;&gt;
&lt;h4&gt;Converting ASCII (Text) to Binary (base2)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;
=JOIN(&quot;&quot;,ARRAYFORMULA(BASE(UNICODE(SPLIT(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(.{1})&quot;,&quot;~$1&quot;),&quot;~&quot;)),2,8)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s break this down step by step.&lt;/p&gt;
&lt;div style=&quot;margin-left: 30px;&quot;&gt;
&lt;pre&gt;&lt;code&gt;SPLIT(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(.{1})&quot;,&quot;~$1&quot;),&quot;~&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This splits apart a string into individual characters, by inserting &quot;~&quot; before each character, then splitting by &quot;~&quot;. This returns an array, which is why ARRAYFORMULA is required.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BASE(UNICODE(...),2,8)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This converts each character into its unicode value, then converts it into base2, which is binary, and pads it to 8 bits in length (a byte)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=JOIN(&quot;&quot;,ARRAYFORMULA(...))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, this joins all the binary bytes into one cell. If you want to, you could use JOIN(&quot; &quot;,...) to visualize the binary value as byte-separated.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;margin-left: 30px;&quot;&gt;
&lt;h4&gt;RIGHT-Padding a string to a desired length (adding trailing characters)&lt;/h4&gt;
&lt;p&gt;There is tons of information out there on keeping leading characters in Google Sheets, or left-padding, but almost nothing on right-padding, or adding trailing characters to get to a desired length. Furthermore, I wanted something even more specific; to right-pad a string until the entire string was evenly divisible by a specific integer. This is important because Base64 uses padding on both the input and the output - it breaks input into chunks of 6 bits before converting, and output needs to be divisible by 4. I came up with two separate formula that both produce the same result, but one uses an IF statement, which can get messy in Google Sheets if you have a lot of nested IFs already. Replace A2 with the cell you want to pad, replace &quot;MIN_DIVISIBLE&quot; with the integer you want A2 to become divisible by, and replace the equals in &quot;=&quot; with the character you want to pad to the right with.&lt;/p&gt;
&lt;div style=&quot;margin-left: 30px;&quot;&gt;
&lt;h5&gt;Without IF() statement:&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;=CONCATENATE(A2,REPT(&quot;=&quot;,(FLOOR((LEN(A2)+(MIN_DIVISIBLE-1))/MIN_DIVISIBLE)*MIN_DIVISIBLE)-LEN(A2)))&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;With IF() statement:&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;=IF(MOD(LEN(A2),MIN_DIVISIBLE),CONCATENATE(A2,REPT(&quot;=&quot;,MIN_DIVISIBLE-MOD(LEN(A2),MIN_DIVISIBLE))),A2)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Here is an example showing padding binary to 6 bits, with trailing zeros.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=CONCATENATE(A2,REPT(&quot;0&quot;,(FLOOR((LEN(A2)+(6-1))/6)*6)-LEN(A2)))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which results in &quot;11&quot; becoming &quot;110000&quot;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Google-Sheets-Right-Padding-String-to-Be-Evenly-Divisible-by-6.png&quot;&gt;&lt;img class=&quot;alignnone size-full wp-image-219&quot; src=&quot;/media/Google-Sheets-Right-Padding-String-to-Be-Evenly-Divisible-by-6.png&quot; alt=&quot;Google Sheets - Right Padding String to Be Evenly Divisible by 6&quot; width=&quot;933&quot; height=&quot;94&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Non-ASCII characters - beware binary padding!&lt;/h3&gt;
&lt;p&gt;It was brought to my attention that my original &quot;pure&quot; formula was not handling foreign characters correctly. This wasn&apos;t too surprising, but after I started digging into my own work, I was wondering why it shouldn&apos;t work as-is; after all, the Google Sheets Unicode() function should be able to handle all non-ASCII chars, and the lookup tables should stay the same (standard ASCII) regardless of input charset. After breaking down my own function into pieces, I found the culprit - the BASE() function.&lt;/p&gt;
&lt;p&gt;I was using BASE() to turn each Unicode (UTF-8 presumably) code point into binary, then pad to 8 digits long. Can you guess the problem? I&apos;ll give you a hint. The standard+extended ASCII table ranges from decimal 0 to 255. If you want to use the full UTF-8 range, and map other characters, you need to go above. For example, this character - &quot;ă&quot;, or &quot;latin small letter a with breve&quot; is mapped to Unicode 259. Found the issue yet? Once you go past 255, converting from decimal (base10) to binary (base2) gives you an output that &lt;strong&gt;exceeds&lt;/strong&gt; 8 bits (a byte).  259 in binary is &quot;100000011&quot;, which is 9 bits long. I was telling BASE() to pad all outputs to 8 bits, but when the output was already longer, instead of padding to a multiple of it, it just ignored it and left it at 9 bits long. This was breaking the rest of my formula, which relied on byte chunking (groups of 8 bits)!&lt;/p&gt;
&lt;p&gt;Solution: There could be a solution to this, although it would be rather complicated since multiple spots in the formula rely on chunking and an expected 8 bit starting chunk.&lt;/p&gt;
&lt;h3 id=&quot;moreResources&quot;&gt;More resources:&lt;/h3&gt;
&lt;div style=&quot;margin-left: 30px;&quot;&gt;
&lt;p&gt;I also frequently used base64 online converters, to ensure my formula work was correct, such as &lt;a href=&quot;https://www.base64encode.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this instant online converter&lt;/a&gt; and &lt;a href=&quot;https://cryptii.com/pipes/binary-to-base64&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this one&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;How about you? Do you have any tips or feedback related to Base64 conversion inside Google Sheets? I didn&apos;t cover Excel much, since I rarely ever use it, and it would take a while to come up with an Excel equivalent of my custom formula, since Excel doesn&apos;t have functions such as REGEXREPLACE(). However, if you are interested in an Excel solution, it is probably possible to achieve using VBA - I might start with &lt;a href=&quot;https://stackoverflow.com/questions/41572920/encoding-special-chracters-to-base64-in-excel-vba&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;something like this&lt;/a&gt; as a starting point.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;updates&quot;&gt;Updates:&lt;/h3&gt;
&lt;p&gt;1/27/2021: “Pure formula” fixes&lt;/p&gt;
&lt;details&gt;
	&lt;summary&gt;1/27/2021: Summary&lt;/summary&gt;
&lt;p&gt;Fixed issue with the “pure formula” approach. It wasn’t working with input strings that contained line breaks (e.g. &lt;code&gt;\n&lt;/code&gt;), as it would trim off the first character following the line break character. This was traced back to the RegEx pattern that splits the input into individual characters - I had forgotten the flag to let the dot pattern (&lt;code&gt;.&lt;/code&gt;) match all characters &lt;em&gt;&lt;strong&gt;including&lt;/strong&gt;&lt;/em&gt; line breaks!. I also took this opportunity to fix two other bugs - the handling of &lt;code&gt;&apos;&lt;/code&gt; (single quotes) and &lt;code&gt;~&lt;/code&gt; (used in my formula as a delimiter)&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
	&lt;summary&gt;1/27/2021: Detailed Notes&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;The fix to match &lt;code&gt;.&lt;/code&gt; not matching newlines was simply to add the appropriate dotall flag for RE2 (the RegEx flavor that Google Sheets uses). In effect:
&lt;ul&gt;
&lt;li&gt;Replaced: &lt;code&gt;REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(.{1})&quot;,&quot;$1~&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;With: &lt;code&gt;REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1~&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In fixing the above, I noticed another issue: input strings with &lt;code&gt;&apos;&lt;/code&gt; (single quotations) were not working, &lt;strong&gt;period&lt;/strong&gt; (throwing fatal error &lt;code&gt;Function UNICODE parameter 1 value should be non-empty.&lt;/code&gt;)
&lt;ul&gt;
&lt;li&gt;The fix for this was to start &lt;em&gt;double escaping&lt;/em&gt; single quotes&lt;/li&gt;
&lt;li&gt;Replace: &lt;code&gt;REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1~&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;With: &lt;code&gt;REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1~&quot;),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;NOTE: Order of operations is important here! Prefixing each character with the delimiter needs to come first, or else the double escaped string (&lt;code&gt;&apos;&apos;&lt;/code&gt;) will be treated prematurely as two characters, instead of an escaped single char.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Another issue, which had actually been bothering me from the start, is that the pure formula cannot handle input strings with &lt;code&gt;~&lt;/code&gt;, since internally that is used within the formula as a delimiter.
&lt;ul&gt;
&lt;li&gt;I finally found a solution; I was able to find a non-printable ASCII character code to use a delimiter, which should (probably) never show up in input strings!&lt;/li&gt;
&lt;li&gt;Replace: &lt;code&gt;SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1~&quot;),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),&quot;~&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;With: &lt;code&gt;SPLIT(REGEXREPLACE(REGEXREPLACE(A2&amp;#x26;&quot;&quot;,&quot;(?s)(.{1})&quot;,&quot;$1&quot;&amp;#x26;CHAR(127)),&quot;&apos;&quot;,&quot;&apos;&apos;&quot;),CHAR(127))&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Cloudinary - Adding Solid Shape Overlays (Squares, Circles, Etc.)</title><link>https://joshuatz.com/posts/2019/cloudinary---adding-solid-shape-overlays-squares-circles-etc/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/cloudinary---adding-solid-shape-overlays-squares-circles-etc/</guid><description>How to overlay solid shapes onto an image with Cloudinary, with demos and explanations. Useful for creating dynamic overlays, watermarks, and more!</description><pubDate>Wed, 23 Jan 2019 07:08:44 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;Disclaimer: For anyone wondering, this post was &lt;strong&gt;not &lt;/strong&gt;sponsored by Cloudinary or is in any way officially affiliated, endorsed, or supported by them.  I am just a big fan of them, and enjoy seeing how I can use all their features in unusual combinations.&lt;/p&gt;
&lt;h2&gt;Background Info:&lt;/h2&gt;
&lt;p&gt;This task, overlaying solid shapes on Cloudinary, is simple (somewhat), yet might be missed by many as even being possible. It uses a concept that I am fascinated by, that what is displayed on our computer screens is not a single image, but the result of many transformations and calculations. For example, it is impossible (in practical methods) to display a true curved line (or anything other than a perfectly horizontal or vertical line) on a pixel based display; pixels are squares, so the best you can do is push more and more pixels together until, when viewed from a distance, squares that are placed in a non-linear (e.g. polynomial) arrangement start to appear curved. This is why curves in really old video games, that are low resolution, look blocky and jagged.&lt;/p&gt;
&lt;p&gt;For those interested, the process of transforming non-pixel-based shapes, such as a vector representation of a circle, into a pixel-based form (which is what you are looking at right now), is called &quot;&lt;a href=&quot;https://en.wikipedia.org/wiki/Rasterisation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;rasterisation&lt;/a&gt;&quot;.&lt;/p&gt;
&lt;p&gt;[caption id=“attachment_185” align=“aligncenter” width=“300”]&lt;a href=&quot;/media/Rasterization-Turning-Non-Pixel-Based-Sources-or-Vectors-into-Pixels.png&quot;&gt;&lt;img class=&quot;smallCaption wp-image-185 size-medium&quot; src=&quot;/media/Rasterization-Turning-Non-Pixel-Based-Sources-or-Vectors-into-Pixels-300x300.png&quot; alt=&quot;Rasterization - Turning Non-Pixel Based Sources or Vectors into Pixels&quot; width=&quot;300&quot; height=&quot;300&quot;&gt;&lt;/a&gt; Wojciech Muła [GFDL (&lt;a href=&quot;http://www.gnu.org/copyleft/fdl.html&quot;&gt;http://www.gnu.org/copyleft/fdl.html&lt;/a&gt;), CC-BY-SA-3.0 (&lt;a href=&quot;http://creativecommons.org/licenses/by-sa/3.0/&quot;&gt;http://creativecommons.org/licenses/by-sa/3.0/&lt;/a&gt;) or CC BY-SA 2.5 (&lt;a href=&quot;https://creativecommons.org/licenses/by-sa/2.5&quot;&gt;https://creativecommons.org/licenses/by-sa/2.5&lt;/a&gt;)], via Wikimedia Commons[/caption]&lt;/p&gt;
&lt;p&gt;Anyway, I digress. The reason why I brought up transformations is because they are part of the core of Cloudinary, and what allows me to overlay custom shapes on top of images. The fact that Cloudinary lets users &quot;chain&quot; transformations together in an unlimited fashion, means that almost any shape can be dynamically rendered with a Cloudinary URL, given the right combination of pixel transformations.&lt;/p&gt;
&lt;h3&gt;Note:&lt;/h3&gt;
&lt;p&gt;Throughout this article, ACCOUNT_NAME stands in for the name of your Cloudinary cloud account, which should be a short string of characters and numbers.&lt;/p&gt;
&lt;h2&gt;Adding Squares:&lt;/h2&gt;
&lt;p&gt;Here is a practical example: A simple blue square added to the default Cloudinary sample image. Here is the sample image without any transformations, and the URL used to fetch it.&lt;/p&gt;
&lt;pre class=&quot;wrap&quot;&gt;&lt;code&gt;https://res.cloudinary.com/ACCOUNT_NAME/image/upload/sample&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;/media/cloudinary-sample-image.jpg&quot;&gt;&lt;br&gt;
&lt;img class=&quot;aligncenter wp-image-190 size-medium&quot; src=&quot;/media/cloudinary-sample-image-300x200.jpg&quot; alt=&quot;&quot; width=&quot;300&quot; height=&quot;200&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;And here is the same image, with a blue square rendered over it:&lt;/p&gt;
&lt;pre class=&quot;wrap&quot;&gt;&lt;code&gt;https://res.cloudinary.com/ACCOUNT_NAME/image/upload/l_pixel,w_200,h_200/co_rgb:4a90e2,e_colorize,fl_layer_apply,g_north_west,x_270,y_100/sample&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;/media/Cloudinary-Drawing-Blue-Square-Over-Sample-Image.jpg&quot;&gt;&lt;img class=&quot;aligncenter wp-image-192 size-medium&quot; src=&quot;/media/Cloudinary-Drawing-Blue-Square-Over-Sample-Image-300x200.jpg&quot; alt=&quot;Cloudinary - Drawing Blue Square Over Sample Image&quot; width=&quot;300&quot; height=&quot;200&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The trick here is that &quot;l_pixel&quot; maps to an image I&apos;ve uploaded to my account, with the public ID of &quot;pixel&quot;, and is a 1x1 solid colored pixel saved as an image file. The transformations applied above blow it up to 200 x 200 pixels, position it at x=270 and y=100, and &quot;colorize&quot; it with an RGB / Hex string corresponding to blue. You might think this is very basic, but the fact that you can chain these together means you can do crazy things like this:&lt;/p&gt;
&lt;pre class=&quot;wrap&quot;&gt;&lt;code&gt;https://res.cloudinary.com/ACCOUNT_NAME/image/upload/c_fill,w_500,h_500,o_40/h_302,l_pixel,w_17,x_146,y_48/co_rgb:4a90e2,e_colorize,fl_layer_apply,g_north_west,x_146,y_48/h_21,l_pixel,w_100,x_63,y_332/co_rgb:4a90e2,e_colorize,fl_layer_apply,g_north_west,x_63,y_332/h_97,l_pixel,w_20,x_63,y_254/co_rgb:4a90e2,e_colorize,fl_layer_apply,g_north_west,x_63,y_254/h_20,l_pixel,w_153,x_70,y_47/co_rgb:4a90e2,e_colorize,fl_layer_apply,g_north_west,x_70,y_47/h_100,l_pixel,w_100,x_166,y_70/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_166,y_70/h_15,l_pixel,w_85,x_246,y_50/co_rgb:6283aa,e_colorize,fl_layer_apply,g_north_west,x_246,y_50/h_83,l_pixel,w_15,x_246,y_51/co_rgb:6283aa,e_colorize,fl_layer_apply,g_north_west,x_246,y_51/h_20,l_pixel,w_89,x_246,y_117/co_rgb:6283aa,e_colorize,fl_layer_apply,g_north_west,x_246,y_117/h_76,l_pixel,w_17,x_321,y_117/co_rgb:6283aa,e_colorize,fl_layer_apply,g_north_west,x_321,y_117/h_22,l_pixel,w_100,x_240,y_173/co_rgb:6283aa,e_colorize,fl_layer_apply,g_north_west,x_240,y_173/h_138,l_pixel,w_19,x_327,y_173/co_rgb:27a456,e_colorize,fl_layer_apply,g_north_west,x_327,y_173/h_21,l_pixel,w_79,x_331,y_235/co_rgb:27a456,e_colorize,fl_layer_apply,g_north_west,x_331,y_235/h_100,l_pixel,w_23,x_386,y_238/co_rgb:27a456,e_colorize,fl_layer_apply,g_north_west,x_386,y_238/sample&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;/media/Cloudinary-Multiple-Solid-Colored-Squares-Overlay-on-Image-Josh.jpg&quot;&gt;&lt;img class=&quot;wp-image-196 size-medium aligncenter&quot; src=&quot;/media/Cloudinary-Multiple-Solid-Colored-Squares-Overlay-on-Image-Josh-300x300.jpg&quot; alt=&quot;Cloudinary - Multiple Solid Colored Squares Overlay on Image - Josh&quot; width=&quot;300&quot; height=&quot;300&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Adding Circles:&lt;/h2&gt;
&lt;p&gt;Again, this is a bit of a &quot;cheat&quot;; there is not built-in way to overlay a solid circle with Cloudinary. However... it is possible to create a circle by first creating a square, and then rounding the corners with a radius. Check it out!&lt;/p&gt;
&lt;pre class=&quot;wrap&quot;&gt;&lt;code&gt;https://res.cloudinary.com/ACCOUNT_NAME/image/upload/c_fill,h_500,w_500/g_north_west,l_pixel,r_150,w_300,h_300/b_rgb:4a90e2,co_rgb:4a90e2,e_colorize,fl_layer_apply,g_north_west,x_90,y_90/sample&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;/media/Cloudinary-Drawing-Blue-Circle-Over-Sample-Image.jpg&quot;&gt;&lt;img class=&quot;size-medium wp-image-198 aligncenter&quot; src=&quot;/media/Cloudinary-Drawing-Blue-Circle-Over-Sample-Image-300x300.jpg&quot; alt=&quot;Cloudinary - Drawing Blue Circle Over Sample Image&quot; width=&quot;300&quot; height=&quot;300&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Let&apos;s Kick It Up a Notch:&lt;/h2&gt;
&lt;p&gt;OK, so far this might not seem all that impressive. What is the point? Well, think about how this could be applied to situations where you need to dynamically generate an image overlay based on a set of criteria. For example, if you make an online game and need to generate an achievement badge that can be shared to Facebook as an image, but the colors are based on the user&apos;s profile settings. You can generate that image with Cloudinary, just by passing transformations in the image path, which means the image URL can even be generated completely client-side. Check out this doozy of a URL:&lt;/p&gt;
&lt;pre class=&quot;wrap&quot;&gt;&lt;code&gt;https://res.cloudinary.com/ACCOUNT_NAME/image/upload/c_fill,h_500,o_40,w_500/h_224,l_pixel,w_224/co_rgb:070707,e_colorize,fl_layer_apply,g_north_west,x_28,y_75/h_395,l_pixel,w_454/co_rgb:2744a6,e_colorize,fl_layer_apply,g_north_west,x_23,y_35/h_210,l_pixel,w_210/co_rgb:ffffff,e_colorize,fl_layer_apply,g_north_west,x_34,y_82/c_scale,h_188,l_profilepicture,w_215/fl_layer_apply,g_north_west,x_34,y_107/h_209,l_pixel,w_208/co_rgb:ffffff,e_colorize,fl_layer_apply,g_north_west,x_254,y_82/l_text:Roboto_37_normal:Stats:/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_316,y_93/l_text:Roboto_19_normal:Flowers%20Pollinated%20:%2020/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_266,y_141/l_text:Roboto_19_normal:People%20Chased%20:%2025/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_266,y_167/l_text:Roboto_55_normal:Buzzy%20McBuzzer/co_rgb:ffffff,e_colorize,fl_layer_apply,g_north_west,x_52,y_314/l_text:Roboto_31_normal:Rank%20:%2010/co_rgb:ffffff,e_colorize,fl_layer_apply,g_north_west,x_170,y_380/h_119,l_pixel,w_119/co_rgb:c8c61f,e_colorize,fl_layer_apply,g_north_west,r_59,x_295,y_194/l_text:Roboto_42_normal:%3C10%3E/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_308,y_237/sample&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;/media/Cloudinary-Dynamic-Gaming-Badge-Overlay-Generated-Example.jpg&quot;&gt;&lt;img class=&quot;size-medium wp-image-202 aligncenter&quot; src=&quot;/media/Cloudinary-Dynamic-Gaming-Badge-Overlay-Generated-Example-300x300.jpg&quot; alt=&quot;Cloudinary - Dynamic Gaming Badge Overlay Generated Example&quot; width=&quot;300&quot; height=&quot;300&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Crazy, right? However, once you come up with a set of transformations you want to use together, you could either save it as a saved transformation, or dynamically generate the URL with just a little bit of code, for example, like this:&lt;/p&gt;
&lt;pre class=&quot;wrap&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;
function getGamerBadge(gamerProfile){
	return &apos;https://res.cloudinary.com/ACCOUNT_NAME/image/upload/c_crop,h_500,o_40,w_500/h_224,l_pixel,w_224/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_28,y_75/h_395,l_pixel,w_454/co_rgb:&apos; + gamerProfile.colors.dark + &apos;,e_colorize,fl_layer_apply,g_north_west,x_23,y_35/h_210,l_pixel,w_210/co_rgb:&apos; + gamerProfile.colors.light + &apos;,e_colorize,fl_layer_apply,g_north_west,x_34,y_82/c_scale,h_188,l_&apos; + gamerProfile.profileImageId + &apos;,w_215/fl_layer_apply,g_north_west,x_34,y_107/h_209,l_pixel,w_208/co_rgb:&apos; + gamerProfile.colors.light + &apos;,e_colorize,fl_layer_apply,g_north_west,x_254,y_82/l_text:Roboto_37_normal:Stats:/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_316,y_93/l_text:Roboto_19_normal:Flowers%20Pollinated%20:%20&apos; + gamerProfile.stats.pollinated + &apos;/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_266,y_141/l_text:Roboto_19_normal:People%20Chased%20:%20&apos; + gamerProfile.stats.chased + &apos;/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_266,y_167/l_text:Roboto_55_normal:&apos; + encodeURIComponent(gamerProfile.name) + &apos;/co_rgb:&apos; + gamerProfile.colors.light + &apos;,e_colorize,fl_layer_apply,g_north_west,x_52,y_314/l_text:Roboto_31_normal:Rank%20:%20&apos; + gamerProfile.rank + &apos;/co_rgb:&apos; + gamerProfile.colors.light + &apos;,e_colorize,fl_layer_apply,g_north_west,x_170,y_380/h_119,l_pixel,w_119/co_rgb:&apos; + gamerProfile.colors.badge +  &apos;,e_colorize,fl_layer_apply,g_north_west,r_59,x_295,y_194/l_text:Roboto_42_normal:%3C&apos; + gamerProfile.rank + &apos;%3E/co_rgb:000000,e_colorize,fl_layer_apply,g_north_west,x_308,y_237/sample&apos;;
}
var buzzyGamerProfile = {
	name : &apos;Buzzy McBuzzer&apos;,
	rank : 10,
	stats : {
		pollinated : 20,
		chased : 25
	},
	profileImageId : &apos;profilepicture&apos;,
	colors : {
		dark : &apos;2744A6&apos;,
		light : &apos;ffffff&apos;,
		badge : &apos;c8c61f&apos;
	}
}
var buzzyGamerBadgeImageSrc = getGamerBadge(buzzyGamerProfile);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;More information:&lt;/h2&gt;
&lt;p&gt;I am currently working on a tool related to all of these features, but in the mean time, here are some additional resources to check out if you are interested in the power-user features of Cloudinary transformations:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;https://cloudinary.com/documentation/image_transformation_reference&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cloudinary Transformation Reference / Cheat Sheet&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://cloudinary.com/cookbook/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Transformation &quot;Cookbook&quot;&lt;/a&gt; - shows examples of different effects and transformation chains, with URL and code examples.&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;https://github.com/cloudinary/cloudinary_js&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cloudinary JavaScript library source code&lt;/a&gt; - useful for looking at class definitions of different Cloudinary layers and how they work together. As mentioned &lt;a href=&quot;https://joshuatz.com/posts/2018/lesson-cloudinary-transformations-remote-fetch-as-overlay/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;in another post&lt;/a&gt;, this is where I tracked down a bug I was running into related to remote fetch overlay transformations.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>No Fuss Prism Toolbar (J-Prism-Toolbar)</title><link>https://github.com/joshuatz/j-prism-toolbar</link><guid isPermaLink="true">https://github.com/joshuatz/j-prism-toolbar</guid><description>Quick JS plugin for PrismJS (code embed syntax highlighter) that I put together, and use throughout this site . Typical &quot;windows&quot; style toolbar, with minimize/expand, fullscreen, and copy-to-clipboard buttons.</description><pubDate>Sat, 12 Jan 2019 19:04:11 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_code_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>CSS to JS Converter / Escaper</title><link>https://joshuatz.com/mini-tools/2019/css-to-js-converter-escaper/</link><guid isPermaLink="true">https://joshuatz.com/mini-tools/2019/css-to-js-converter-escaper/</guid><description>A tool for pasting in CSS and converting it to an escaped Javascript string. Options for keeping line breaks and tabs, or removing all (e.i. minifying).</description><pubDate>Wed, 09 Jan 2019 18:23:22 GMT</pubDate><content:encoded>&lt;p&gt;In general, you probably want to avoid putting CSS directly into your Javascript. However, there are times when it is unavoidable, or simply not worth the time. I made this tool for myself, for those times when I simply need to paste in a block of CSS into JS code as a string literal, while preserving (or removing) formatting.&lt;/p&gt;
&lt;p&gt;Just paste your CSS into the left panel, and then copy the generated JS from the right side.&lt;/p&gt;
&lt;iframe height=&quot;549&quot; scrolling=&quot;no&quot; title=&quot;CSS to JS Converter Tool&quot; src=&quot;//codepen.io/joshuatz/embed/yGqWyy/?height=549&amp;#x26;theme-id=0&amp;#x26;default-tab=result&quot; frameborder=&quot;no&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot; style=&quot;width: 100%;&quot;&gt;See the Pen &amp;#x3C;a href=&apos;https://codepen.io/joshuatz/pen/yGqWyy/&apos;&gt;CSS to JS Converter Tool&amp;#x3C;/a&gt; by Joshua T (&amp;#x3C;a href=&apos;https://codepen.io/joshuatz&apos;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&apos;https://codepen.io&apos;&gt;CodePen&amp;#x3C;/a&gt;.
&lt;/iframe&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Frustrating Day with Postman - Problems Log - 1/4/2019</title><link>https://joshuatz.com/posts/2019/frustrating-day-with-postman---problems-log---142019/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/frustrating-day-with-postman---problems-log---142019/</guid><description>A random list of issues encountered with Postman in 2019</description><pubDate>Fri, 04 Jan 2019 16:05:25 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;Postman is my go-to tool for testing a new API, replaying HTTP requests, and general mucking about with network requests. They call their tool a &quot;API Development Environment&quot;, and if you deal at all with APIs and/or endpoints like webhooks, you should &lt;a href=&quot;https://www.getpostman.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;check them out&lt;/a&gt;. In general I am a fan, and have been for some time now, but today I have been running into a seemingly endless supply of roadblocks trying to get something simple working. I thought I would jot down what I ran into, some of which is my own fault, and some is partly due to how Postman is designed.&lt;/p&gt;
&lt;p&gt;In no particular order:&lt;/p&gt;
&lt;h2&gt;Postman refuses to load on Windows / crashes / GUI screen flashes white and black / glitches out&lt;/h2&gt;
&lt;h3&gt;The problem:&lt;/h3&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;This problem actually started a while ago for me (at least how I remember it), but seems to have gotten progressively worse. At first Postman would just take an incredibly long time to launch, and would flash a bunch and hang for a while until it finally loaded. However, today it started its normal glitching out when trying to launch, but then seemed to never load and basically crashed.&lt;/p&gt;
&lt;h3&gt;The Fix:&lt;/h3&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;I assumed this would be complicated to fix, but actually it is very simple. This is &lt;a href=&quot;https://github.com/postmanlabs/postman-app-support/issues/4594&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;a known issue&lt;/a&gt; with how Postman uses certain GPUs, and you just need to &lt;a href=&quot;https://github.com/postmanlabs/postman-app-support/issues/4594#issuecomment-391601621&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;set a global system environmental variable&lt;/a&gt; that tells Postman to avoid using the GPU. See screenshot below (click for larger version):&lt;/p&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;&lt;a href=&quot;/media/Postman-How-to-disable-GPU-through-System-Environmental-Variable-flag-POSTMAN_DISABLE_GPU.png&quot;&gt;&lt;img class=&quot;alignnone size-large wp-image-162&quot; src=&quot;/media/Postman-How-to-disable-GPU-through-System-Environmental-Variable-flag-POSTMAN_DISABLE_GPU-1024x448.png&quot; alt=&quot;Postman - How to disable GPU through System Environmental Variable flag POSTMAN_DISABLE_GPU&quot; width=&quot;1024&quot; height=&quot;448&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;Alternatively, if you are feeling lazy and want an even faster route, launch a Windows Command Line window with Administrator rights and run this command:&lt;/p&gt;
&lt;pre style=&quot;padding-left: 60px;&quot;&gt;setx -m POSTMAN_DISABLE_GPU &quot;true&quot;&lt;/pre&gt;
&lt;h3&gt;A POST request was working with the body set as &quot;raw&quot;, but not with the body set to &quot;x-www-form-urlencoded&quot; or anything else&lt;/h3&gt;
&lt;h3&gt;The problem:&lt;/h3&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;Pretty much exactly what the heading says. I was trying to get a simple POST request to work, which I had imported from Chrome. It would work if I used &quot;raw&quot; as the body content, but whenever I tried to send the exact same body content but as form-data or x-www-form-urlencoded instead, I would get unexpected results.&lt;/p&gt;
&lt;h3&gt;More details:&lt;/h3&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;This was actually my bad (well mostly). I had created my request in Postman by using Chrome&apos;s &quot;copy as cURL (bash)&quot; and then using the import feature in Postman. However, by default this only copies the body as raw, and Postman doesn&apos;t seem to be able to auto-parse the raw into form-data, so I had manually copied and pasted from the &quot;Form Data&quot; panel in Chrome network requests to the &quot;Bulk Edit&quot; field editor in Postman -&gt; x-www-form-urlencoded. What I missed is that Chrome includes a space between key and value pairs in the dev panel, which got pasted into Postman. Since Postman doesn&apos;t use trim() in parsing the Bulk Edit input, the value got passes as a literal space before each value. So what should have been sent as something like:&lt;/p&gt;
&lt;pre style=&quot;padding-left: 60px;&quot;&gt;myProp=myVal&lt;/pre&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;... was getting sent as:&lt;/p&gt;
&lt;pre style=&quot;padding-left: 60px;&quot;&gt;myProp= myVal&lt;/pre&gt;
&lt;h3&gt;The fix:&lt;/h3&gt;
&lt;p style=&quot;padding-left: 30px;&quot;&gt;The easy fix here was simply to find and replace &quot;: &quot; with &quot;:&quot; before pasting from Chrome into Bulk Edit. However, this could maybe be fixed by using Postman Pre-Request Scripts.&lt;/p&gt;
&lt;h2&gt;Postman console not matching actual requests&lt;/h2&gt;
&lt;h3&gt;The problem:&lt;/h3&gt;
&lt;p&gt;This was a strange one. I was trying to figure out why a request wasn&apos;t working, and I wanted to see the actual request once it left Postman, so I opened up the Postman Console, which conveniently logs requests and lets you inspect them after you hit &quot;Send&quot;.&lt;/p&gt;
&lt;p&gt;I was dumbstruck when I noticed that, according to the console, Postman was sending key-value pairs in my request body that I had very specifically DISABLED / unchecked in the Postman GUI. What the heck? I tried a few different things (making sure I didn&apos;t have duplicate key/value pairs, trying different variations), but I kept seeing unchecked and disabled value pairs showing up in the Postman console.&lt;/p&gt;
&lt;p&gt;I finally got frustrated and wanted to verify that the console itself was making sense. So I changed the endpoint I was working on to a RequestBin endpoint (a service that gives you a URL you can make requests to and it logs them online for you to inspect). I then made the same request and compared the Postman console with the actual request that came through to RequestBin. They didn&apos;t match - &lt;strong&gt;Postman&apos;s Console did &lt;span style=&quot;text-decoration: underline;&quot;&gt;not&lt;/span&gt; match the ACTUAL request sent and received&lt;/strong&gt;!!! RequestBin showed what I wanted, that Postman indeed was &lt;strong&gt;not &lt;/strong&gt;sending values I had unchecked, but that still didn&apos;t explain why the console was incorrectly showing those being sent. ARGH!&lt;/p&gt;
&lt;h3&gt;The fix:&lt;/h3&gt;
&lt;p&gt;As much as my gut reaction is to blame myself (PEBKAC, blah blah), this appears to either be an actual legitimate bug, or poorly documented functionality that I would argue goes against user expectation. As of right now, I see multiple bug reports (&lt;a href=&quot;https://github.com/postmanlabs/postman-app-support/issues/5207&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;here&lt;/a&gt;, &lt;a href=&quot;https://github.com/postmanlabs/postman-app-support/issues/5648&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;here&lt;/a&gt;, and &lt;a href=&quot;https://github.com/postmanlabs/postman-app-support/issues/4692&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;here&lt;/a&gt;) on Github that are about this exact issue, which reassures me that I&apos;m not crazy (at least in &lt;em&gt;this&lt;/em&gt; instance).&lt;/p&gt;
&lt;p&gt;The quick and dirty fix, if you simply want to see EXACTLY what Postman &lt;em&gt;actually&lt;/em&gt; sent as a request, is to add a &quot;Pre-Request Script&quot; and/or &quot;Test Script&quot; that includes the following:&lt;/p&gt;
&lt;pre style=&quot;padding-left: 30px;&quot;&gt;console.log(request);&lt;/pre&gt;
&lt;p&gt;Whereas the default request logging that Postman does seems to suffer from this bug, the &quot;request&quot; variable actually does represent the real request that gets sent, and does not show things like unchecked variables. I would recommend putting the console.log(request) in the &quot;Test&quot; script panel, since that one runs after the request executes, so it should be the most accurate.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Revisiting Chrome Extension Development - Build System Tips</title><link>https://joshuatz.com/posts/2019/revisiting-chrome-extension-development---build-system-tips/</link><guid isPermaLink="true">https://joshuatz.com/posts/2019/revisiting-chrome-extension-development---build-system-tips/</guid><description>Revisiting Chrome extension development after a while and outlining some things that have helped me with development and distribution.</description><pubDate>Thu, 03 Jan 2019 07:28:48 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;Yesterday, I realized something not very good. There was a chrome extension I developed years back (&lt;a href=&quot;/projects/web-stuff/timeclockwizard-chrome-extension/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;this one&lt;/a&gt;) that depended on a service outside my control (TimeClockWizard). When that service changed a bunch of its code a year or so ago, it broke all functionality in my extension. When I found out, I posted a notice on my site that the extension was &quot;deprecated&quot;, but forgot to pull the actual extension from the Chrome Webstore. Or maybe I had just assumed that people would stop trying to use and download it when it had stopped working. However, when I was browsing my Google Analytics account yesterday, I saw some pings from the extension and realized that people were still actively trying to install and/or use it. It made me feel bad, so I thought I would give fixing the extension a crack. It actually went really well, and I&apos;m probably going to publish a fix to the webstore soon that should fix everything in the extension for everyone.&lt;/p&gt;
&lt;p&gt;However, what I want to talk about in this post is how certain decisions I made earlier, in terms of tooling, made coming back to work on the extension, even years later, a lot easier. I also made some further improvements as I worked on the repairs yesterday and today. Here is some of what was the most helpful to have setup in my dev pipeline:&lt;/p&gt;
&lt;h2&gt;Auto ZIP Archive Creator for Webstore Upload / Releases&lt;/h2&gt;
&lt;p&gt;If you want to upload your Chrome extension to the Chrome Webstore, you need to zip it up in order to upload it. Plus, ZIP archives are always decent to have if you want to use them as a way to distribute &quot;releases&quot;. Since the Chrome Webstore requires you to provide a version number in your manifest that gets bumped up on new releases, I setup a script that bundles up all my required assets and archives them into a ZIP that has a filename corresponding to current version # in my manifest.json file. It will also overwrite the zip file every time it runs, so you can keep running it until you have released a new version, than bump your version number up, so the next time it runs it will create a new archive file.&lt;/p&gt;
&lt;p&gt;I have this run after the code has been minified, and just the files I want to upload have been moved to &quot;/dist&quot; (see section on package.json further below). This is super simple - basically just a slight modification of the demo code from the npm &quot;&lt;a href=&quot;https://www.npmjs.com/package/archiver&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;archiver&lt;/a&gt;&quot; package:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
/**
 * @file Archive_Creator.js
 */
var fs = require(&quot;fs&quot;);
var archiver = require(&quot;archiver&quot;);
&lt;p&gt;// Get version info
var manifestJSON = require(”../manifest.json”);
var versionString = manifestJSON.version.toString();&lt;/p&gt;
&lt;p&gt;var output = fs.createWriteStream(”./builds/build_“+versionString+“.zip”);
var archive = archiver(“zip”, {
zlib : {level : 6} // compression level
});&lt;/p&gt;
&lt;p&gt;// listen for all archive data to be written
output.on(“close”, function() {
console.log(archive.pointer() + ” total bytes”);
console.log(“archiver has been finalized and the output file descriptor has closed.”);
});&lt;/p&gt;
&lt;p&gt;// good practice to catch this error explicitly
archive.on(“error”, function(err) {
throw err;
});&lt;/p&gt;
&lt;p&gt;// pipe archive data to the file
archive.pipe(output);&lt;/p&gt;
&lt;p&gt;// append files from a directory
archive.directory(”./dist/”,&quot;&quot;);&lt;/p&gt;
&lt;/code&gt;&lt;p&gt;&lt;code class=&quot;language-javascript&quot;&gt;// finalize the archive (ie we are done appending files but streams have to finish yet)
archive.finalize();
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Prepping files for zipping / minifying / copying&lt;/h2&gt;
&lt;p&gt;It is a good idea when creating a zip to upload to the Chrome webstore to only upload A) what is absolutely necessary to run your extension and B) minified versions of your files. This is as a courtesy to users to keep file sizes down, and in case Google ever starts getting restrictive about the maximum size of extension ZIPs.&lt;/p&gt;
&lt;p&gt;I have my system setup like this: minify files (&quot;Minifier.js&quot;)-&gt; copy minified files and assets files (PNG, etc.) to /dist (&quot;Prepare_Dist.js&quot;) -&gt; take entire /dist folder and archive it, sending archive to /builds/build_[version].zip (&quot;Archive_Creator.js&quot;). You can see this pipeline reflected in my package.json script section below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
&quot;scripts&quot;: {
	&quot;prepare-dist&quot;: &quot;node ./npm/Prepare_Dist.js&quot;,
	&quot;minify&quot;: &quot;node ./npm/Minifier.js&quot;,
	&quot;zip&quot;: &quot;node ./npm/Archive_Creator.js&quot;,
	&quot;stage&quot;: &quot;npm run minify &amp;#x26;&amp;#x26; npm run prepare-dist&quot;,
	&quot;build&quot;: &quot;npm run minify &amp;#x26;&amp;#x26; npm run prepare-dist &amp;#x26;&amp;#x26; npm run zip&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Good tip on debugging popup window&lt;/h2&gt;
&lt;p&gt;This seems like a no-brainer kind of thing, but while working on repairs, I realized that I needed to be able to debug some of my popup&apos;s JavaScript, but the code I needed to debug was running before I could even open Chrome Dev tools after opening the popup. I could use SetTimeout as a quick hack, but there is a much better way - simply right click on the button for your Chrome Extension in the toolbar and click &quot;Inspect Popup&quot;! Thank you Google, and thank you &lt;a href=&quot;https://stackoverflow.com/questions/5039875/debug-popup-html-of-a-chrome-extension&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;StackOverflow&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;localStorage is pretty cool for persisting data&lt;/h2&gt;
&lt;p&gt;If you need to persist user data across sessions, the built in browser &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;localStorage API&lt;/a&gt; is pretty nifty and convenient. It is incredibly simple to use (values are stored as key-pairs) and access is automatically handled by the browser and scoped to the origin of the initiating code (i.e. my extension should not be able to access the localStorage set by another extension or website).&lt;/p&gt;
&lt;h2&gt;Advice I wish I had followed from the beginning - multiple files are OK&lt;/h2&gt;
&lt;p&gt;Since so much of a Chrome extension is centered around distribution and bundling of assets, I was tempted from the beginning to keep my source code to a minimal number of files. For example, if you want to give certain script files different privileges (such as running in the background) you have to specify them by filename in your manifest.json, which could be messy if you have dozens of files. This led to some annoyances where it would have been a lot cleaner and easier to look through my own code if I had split certain functional components into their own files.&lt;/p&gt;
&lt;p&gt;What is especially silly about this is that I could have just as easily added a step to my build process that concatenates multiple files into one - so the final zip archive file would have stayed around the same size, and my manifest.json could also have stayed (relatively) the same.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Lesson Learned - Cloudinary Transformations - Remote Fetch as Overlay</title><link>https://joshuatz.com/posts/2018/lesson-learned---cloudinary-transformations---remote-fetch-as-overlay/</link><guid isPermaLink="true">https://joshuatz.com/posts/2018/lesson-learned---cloudinary-transformations---remote-fetch-as-overlay/</guid><description>Getting the Cloudinary JQuery SDK to allow an overlay transformation to overlay a remote fetched image - a painful lesson in reference materials.</description><pubDate>Fri, 28 Dec 2018 13:19:35 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;I&apos;m working on a random project at the moment - I won&apos;t go into details at the moment - but the basic building blocks are ReactJS + Cloudinary + JQuery + some other stuff. Everything was going smoothly until I got to implementing a feature that is integral to the project - &quot;chaining&quot; the output of a Cloudinary remote fetch as a transformation overlay on top of another image. In particular, I wanted to use the core Javascript library functions to do this, as opposed to their React SDK (I&apos;m not even sure if it would be possible with just the React SDK) If you have come to this post through a Google search, chances are that you are looking for this kind of info, and already know what all of this means, but if you didn&apos;t, here is the quick and dirty of what I was trying to do (if you don&apos;t care about this, click &lt;a href=&quot;#solution&quot;&gt;here&lt;/a&gt; to skip to the solution).&lt;/p&gt;
&lt;h2&gt;What I was trying to do:&lt;/h2&gt;
&lt;p&gt;Cloudinary is a cloud-based SaaS product that can serve as both a CDN and image manipulation platform. You most likely have used their services before without knowing it; thousands of sites use their platform as a backbone for fast image serving and manipulation. At the moment, my project involves the image processing part of Cloudinary. One feature that they offer is remote fetch - you give Cloudinary an already hosted image URL, and they mirror the image to your account and provide a CDN link. I wanted to go a step further and overlay a remote URL onto a Cloudinary image, but I ran into a bunch of issues.&lt;/p&gt;
&lt;h2 id=&quot;solution&quot;&gt;Solution:&lt;/h2&gt;
&lt;p&gt;The crux of the solution is that you need to pass an object to the &quot;overlay&quot; method on a transformation object - like this:&lt;/p&gt;
&lt;pre class=&quot;prettyprint&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;let cloudinaryImageTag = cloudinaryInstance.imageTag(YOUR_IMAGE_BASE);
let tr = cloudinary.Transformation.new();
tr.overlay({
  resourceType : &apos;fetch&apos;,
  url : remoteSrc
});
cloudinaryImageTag.transformation(tr);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Failed Approaches / Lesson Learned:&lt;/h2&gt;
&lt;p&gt;The reason why I didn&apos;t arrive at this solution faster is two-fold. One, Cloudinary, although powerful, has very fragmented documentation covering dozens of libraries and the distinction between them is not always clear. Two, and this was a learning lesson for me, I should have gone to check the actual source code of the libraries a lot sooner. I&apos;ve always been a RTFM kind of guy, but sometimes even the manual doesn&apos;t line up with what *actually* is going on. In this case, the documentation suggested a few conflicting things:&lt;/p&gt;
&lt;ol&gt;
	&lt;li&gt;It might not even be possible
&lt;ul&gt;
	&lt;li&gt;Pretty much &lt;a href=&quot;https://cloudinary.com/cookbook/tag/overlay&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;every sample code snippet&lt;/a&gt; and &lt;a href=&quot;https://cloudinary.com/documentation/image_transformations#adding_image_overlays&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;documentation page&lt;/a&gt; referencing the overlay transformation parameter shows the way to do it is to pass in the publicId of the image you want to overlay like this:&lt;/li&gt;
	&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let tr = cloudinary.Transformation.new();
tr.overlay(new cloudinary.Layer().publicId(&apos;my_cloudinary_publicId&apos;));&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
	&lt;li&gt;Of course, remote URLs don&apos;t have a public ID - that is the whole point! I need Cloudinary to fetch the remote asset and clone it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;This started to lead me to believe that what I wanted to do wasn&apos;t even possible, and I would need to implement some logic to upload the remote image first and retrieve a publicId before trying to overlay...
&lt;ul&gt;
	&lt;li&gt;FALSE: After a bit of digging, I finally stumbled across &lt;a href=&quot;https://cloudinary.com/product_updates/overlay_and_underlay_a_fetched_image&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;this page&lt;/a&gt;, which suggested that maybe Cloudinary didn&apos;t support this until recently (2017), but at least this was a sign I was still going in the right direction and they definitely do support it now&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;That based on the above, I should base64 encode my remote URL and pass it as part of the publicId. 
&lt;ul&gt;
	&lt;li&gt;The syntax for overlay is l_{publicId}, and since their example uses l_fetch:{base64_OF_IMAGE_URL}, I I tried this with some variation of code like this:&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;let tr = cloudinary.Transformation.new();
let publicId = &apos;fetch:&apos; + base64.encode(remoteSrc);
tr.overlay(new cloudinary.Layer().publicId(publicId));&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;padding-left: 90px;&quot;&gt;This did not work...&lt;/p&gt;
&lt;ol&gt;
	&lt;li&gt;Some of the (auto-generated?) documentation pages started pointing me towards FetchLayer, which extends the Layer class. This seemed like it made sense - FetchLayer is specifically a Layer that is supposed to be referencing a &quot;fetched&quot; asset, so it doesn&apos;t have a PublicId
&lt;ul&gt;
	&lt;li&gt;This is where I started actually looking at the source code for the core Cloudinary JS library. If you look at &lt;a href=&quot;https://github.com/cloudinary/cloudinary_js/blob/a50f25d3bb308c98549308667836fe165f9ff383/src/layer/fetchlayer.coffee&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;the source&lt;/a&gt;, you can see that I was actually somewhat on the right path with my attempt #2, but me base64 encoding the URL was redundant since FetchLayer already does this.&lt;/li&gt;
	&lt;li&gt;This is actually where I got really frustrated. Based on the source and docs, ANY of the following *&lt;em&gt;&lt;strong&gt;should&lt;/strong&gt;&lt;/em&gt;&lt;em&gt;* &lt;/em&gt;have worked, but didn&apos;t:
&lt;ul&gt;
	&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;tr.overlay(new cloudinary.FetchLayer().url(remoteSrc));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
	&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;tr.overlay(new cloudinary.FetchLayer(remoteSrc));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
	&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;tr.overlay(new cloudinary.FetchLayer({
    url : remoteSrc,
    resource_type : &apos;fetch&apos;
}));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;All of the above threw the same error:&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre style=&quot;padding-left: 60px;&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;Uncaught TypeError: layerOptions.substr is not a function&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
	&lt;li&gt;FINALLY - I found two things, I think at the same time. Both brought me to the working solution
&lt;ol&gt;
	&lt;li&gt;There is an actual bug with FetchLayer - methods that rely on turning options into a string (.serialize(), .toHtml()) will all fail with the error outlined in attempt #3, because of this line of code, &lt;a href=&quot;https://github.com/cloudinary/cloudinary_js/blob/a50f25d3bb308c98549308667836fe165f9ff383/src/parameters.coffee#L231&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;here&lt;/a&gt;, and pasted below:
&lt;ul&gt;
	&lt;li&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;} else if (/^fetch:.+/.test(layerOptions)) {
    result = new FetchLayer(layerOptions.substr(6)).toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
	&lt;li&gt;There is &lt;a href=&quot;https://github.com/cloudinary/cloudinary_js/pull/141&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;a pull request that specifically addresses and fixes this bug&lt;/a&gt;, which is partly how I found out about it in the first place, but it hasn&apos;t been merged in yet&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;If you pass a plain object into the transformation object constructor, the first if statement that checks for a plain object evaluates to true, and bypasses the faulty chunk of code entirely! This lets all the regular methods, such as .serialize(), work! The solution is up at the top of the page, or click &lt;a href=&quot;#solution&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Amazon Add to Wishlist Bookmarklet</title><link>https://joshuatz.com/projects/web-stuff/amazon-add-to-wishlist-bookmarklet/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/amazon-add-to-wishlist-bookmarklet/</guid><description>The bookmarklet that I designed lets you add products to your Amazon wishlist from any site, including AliExpress and other competitors. No tracking!</description><pubDate>Tue, 25 Dec 2018 10:43:44 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;7/12/2019 Update - Project Status&lt;/h2&gt;
&lt;p&gt;Unfortunately, this project should now be considered &quot;dead&quot; - Amazon has removed and/or locked down all the services and endpoints that this tool relies on to work, and I could not find suitable replacements, even after a thorough search. Details on this can be found on &lt;a href=&quot;https://github.com/joshuatz/amazon-add-to-wishlist-bookmarklet/issues/1&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this Github issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Github page: &lt;a href=&quot;https://github.com/joshuatz/amazon-add-to-wishlist-bookmarklet&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Intro:&lt;/h2&gt;
&lt;p&gt;The short explanation for what this is: a &quot;bookmarklet&quot; - which is a bookmark that executes code when clicked - that lets you add a product from almost any website to your Amazon wishlist (aka an Amazon Universal Wishlist). Here is a demo showing me adding an item to my wishlist from Target:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/joshuatz/amazon-add-to-wishlist-bookmarklet/raw/master/images/Adding%20from%20the%20Same%20Tab%20(AJAX)(Compressed).gif&quot;&gt;&lt;img class=&quot;responsive-img&quot; src=&quot;https://github.com/joshuatz/amazon-add-to-wishlist-bookmarklet/raw/master/images/Adding%20from%20the%20Same%20Tab%20(AJAX)(Compressed).gif&quot; alt=&quot;Adding an item to my Amazon wishlist from Target.com&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Amazon used to offer a bookmarklet that functioned very similarly to what I built, usually referred to as the &quot;Amazon Universal Wishlist Button&quot; or the &quot;Amazon Add to Wishlist Bookmarklet&quot;. However, they retired it in 2018 and have been only allowing users to add non-Amazon hosted products to their wishlist if you use &lt;a href=&quot;https://www.amazon.com/gp/BIT/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;their new Chrome Extension&lt;/a&gt;. I got annoyed by this decision, because the bookmarklet was perfect for what was needed and was very unobtrusive, whereas the new Chrome Extension runs in the background, is way overkill for just adding things to your wishlist, and has the potential to collect data on your web browsing activities as well as alter pages you look at.&lt;/p&gt;
&lt;p&gt;I created this bookmarklet mostly because I was curious if it was even possible to make something that would bypass the need for the Chrome Extension, and furthermore, I wanted to use the bookmarklet myself (and am now doing so).&lt;/p&gt;
&lt;h2&gt;Installation:&lt;/h2&gt;
&lt;p&gt;If you want the hard way, you can clone my Github repo, and create a local installer page. If you want the easy way, just use the installer page embedded below:&lt;/p&gt;
&lt;!--
&lt;p&gt;&lt;iframe src=&quot;https://joshuatz.com/static-for-wp-iframes/amazon-add-to-wishlist/install-page.html&quot; width=&quot;100%&quot; height=&quot;400px&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
--&gt;
&lt;p&gt;** Installation embed removed due to project status **&lt;/p&gt;
&lt;h2&gt;How do I use this?&lt;/h2&gt;
&lt;p&gt;To use this tool, simply click the bookmark you dragged into your toolbar whenever you are on a product page that you want to add to your wishlist - the popup should appear almost instantly after you click it. You do not have to share any logins or information with me, however, you have to not be logged out of Amazon in the browser you are trying to use the bookmarklet in. Amazon keeps you logged in for a very long time, so unless you logged yourself out on purpose, or are trying to use the bookmarklet in an incognito window, this should not be an issue.&lt;/p&gt;
&lt;h2&gt;Something is not working!&lt;/h2&gt;
&lt;p&gt;I created this tool on a whim, mostly because I wanted to see if it was possible to make in the first place, and then also be able to use it for myself. As such, I&apos;m likely not going to devote much time to improving it, or fixing it if Amazon changes something that breaks its functionality. However, feel free to contact me anyways if you have a concern - use the contact link at the top of this page.&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Flic + Automagic + Toggl API = Easy Time Tracker Button</title><link>https://joshuatz.com/posts/2018/flic-automagic-toggl-api-easy-time-tracker-button/</link><guid isPermaLink="true">https://joshuatz.com/posts/2018/flic-automagic-toggl-api-easy-time-tracker-button/</guid><description>How I setup an Android Automagic flow that will start, stop, or resume a Toggl timer when a Flic bluetooth button is pressed. Full Automagic export, plus demo.</description><pubDate>Wed, 19 Dec 2018 09:01:05 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;Background:&lt;/h2&gt;
&lt;p&gt;At my last job, I was given a &lt;a href=&quot;https://amzn.to/2R2nAim&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Flic button&lt;/a&gt; to use as I saw fit - at first, after looking up what they cost (around $30) I was skeptical if I could get that kind of value of what was basically just a remote button for my smartphone. However, after playing around with it for a bit, I quickly realized that the real value in the Flic button was not the built-in integrations, but the fact that they allow extending its functionality by the use of Android &quot;Intents&quot;, which are a way for different Android Apps and services to communicate across the system. For those technically inclined, being able to use Android Intents as an action when the Flic Button is pressed means that you can basically tie the Flic button to any other Android App, service, or HTTP request, through the use of an automation app like &lt;a href=&quot;https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Tasker&lt;/a&gt; or &lt;a href=&quot;https://play.google.com/store/apps/details?id=ch.gridvision.ppam.androidautomagic&amp;#x26;hl=en_US&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Automagic&lt;/a&gt;. For me, this opened a world of opportunities.&lt;/p&gt;
&lt;h2&gt;Flic + Automagic + Toggl&lt;/h2&gt;
&lt;p&gt;I was so impressed with Flic and their service, that I ended up purchasing a Flic button for my home. This post is about my favorite use of it - as a start/stop/resume button for the time tracker I use, &lt;a href=&quot;https://toggl.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Toggl&lt;/a&gt;. I did this by creating a very custom flow in an Android automation app called Automagic. Through the flow I set up, if I press the Toggl button my Automagic flow and code will execute, which ends up doing a few different things depending on the state of my Toggl time tracker:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;If...
&lt;ul&gt;
	&lt;li&gt;Case A: I have an existing time entry already started / in progress
&lt;ul&gt;
	&lt;li&gt;The Flic button acts as a &quot;stop timer&quot; button and stops the current timer and time entry&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Case B: My Toggl timer is not running, but my flow is set to &quot;resume&quot; mode and there is a recent time entry
&lt;ul&gt;
	&lt;li&gt;The Flic button &quot;resumes&quot; my last time entry. Toggl doesn&apos;t really have a true &quot;resume&quot; function - this basically starts a duplicate entry, which Toggl is smart enough to combine with the previous in reporting&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Case C: My Toggl timer is not running, but my flow is set to &quot;new&quot; mode and/or there is no recent time entry
&lt;ul&gt;
	&lt;li&gt;The Flic button &quot;starts&quot; a brand new timer going, with the description set to generic &quot;Working Time&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;For the creation of new time entries or resumes, my flow passes additional attributes, such as a tag of &quot;API&quot;, and &quot;created_with&quot; equal to &quot;Flic + Automagic + API&quot; - this way I can easily identify later on which timer entries were created with the button and which were created manually&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Edit 2/26/2019: I&apos;ve updated my flow and the code in my Github gist to also correctly use the same project ID that was used in the last time entry if resuming an entry. Toggl is not great at grouping entries together unless you explicitly tell it they had the same project ID.&lt;/p&gt;
&lt;h2&gt;Demonstration Video:&lt;/h2&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;iframe src=&quot;https://www.youtube-nocookie.com/embed/6r-G9KvgkbU?rel=0&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h2&gt;Setting up Flic to broadcast a &quot;global intent&quot;&lt;/h2&gt;
&lt;p&gt;In order for Automagic to be able to know about the button click from Flic, we need to need to somehow pass an even from Flic to the Automagic app. The Android OS actually has something baked into it for this type of cross-app communication - an &lt;a href=&quot;https://developer.android.com/guide/components/intents-filters.html&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;intent&quot;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Basically, you can set the Flic app to emit a global message that the Automagic app can listen in on, by having it emit an intent with the &quot;Send Intent&quot; action. You can set this up in Flic by doing the following:&lt;/p&gt;
&lt;ol&gt;
	&lt;li&gt;Open the flic app and tap the button you want to add the intent to&lt;/li&gt;
	&lt;li&gt;Remove any existing action attached to the trigger you want to use (&quot;Click&quot;, &quot;Double Click&quot;, etc.).&lt;/li&gt;
	&lt;li&gt;Tap the trigger name to start selecting an action&lt;/li&gt;
	&lt;li&gt;Now, in the actions menu, find &quot;Tools&quot; in the bottom of the menu, click it, and then find &quot;Send Intent&quot; as the action&lt;/li&gt;
	&lt;li&gt;Now you can create a completely custom intent that Flic will emit when it is triggered. It is up to you what values you use; just make sure they match what you are listening for / filtering on in the Automagic app or wherever you are listening for the intent&lt;/li&gt;
	&lt;li&gt;Make sure to hit &quot;Save Action&quot;!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I created a video that walks through these steps, creating a custom intent with an action string of &quot;globalflicintent-home&quot;, which corresponds with my Automagic code. You can see it below:&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;iframe src=&quot;https://www.youtube-nocookie.com/embed/mHl2WjZsMZo&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;h2&gt;The Automagic Flow&lt;/h2&gt;
&lt;p&gt;If you want to use my flow, you will have to make one minor modification. I have redacted my Toggl authentication token (for obvious reasons), so you will need to generate your own token and paste it as the string that will become the global variable called &quot;global_togglAuth&quot; in &quot;set togglAuth to Home&quot;. Note that you are not pasting your API token, you should be pasting the Base64 encoded string of either &quot;[YOUR_API_TOKEN]:api_token&quot; or &quot;[USERNAME]:[PASSWORD]&quot;. See &lt;a href=&quot;https://github.com/toggl/toggl_api_docs/blob/master/chapters/authentication.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this page&lt;/a&gt; for details. Optionally:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Change the string on the first step to match whatever the Android Intent is that you are sending from Flic.&lt;/li&gt;
	&lt;li&gt;Change step 3, toggl_action=&apos;resume&apos; to toggl_action=&apos;new&apos; to have the flow create a brand new time entry if the timer is stopped, instead of resuming the most recent previous time entry&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have uploaded the XML export of my Automagic flow and posted it as a Github gist &lt;a href=&quot;https://gist.github.com/joshuatz/c9a7c260df00d2eb406f7105c0c8a750&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here is the visual representation of the flow (click the image for the full size zoom-able version):&lt;/p&gt;
&lt;div style=&quot;width: 100%; text-align: center;&quot;&gt;&lt;a href=&quot;/media/Flic_Toggl_Automagic_Automation_Flow.png&quot;&gt;&lt;img class=&quot;size-medium wp-image-131 aligncenter&quot; src=&quot;/media/Flic_Toggl_Automagic_Automation_Flow-169x300.png&quot; alt=&quot;Flic Toggl Automagic Automation Flow&quot; width=&quot;169&quot; height=&quot;300&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;h2&gt;Reminders / &quot;Gotchas&quot; about Automagic Scripting&lt;/h2&gt;
&lt;p&gt;I really like Automagic as an automation app, particularly because it supports custom scripting. However, although the scripting language looks and functions a lot like JavaScript, it is not, and there are some mistakes that JS programmers are like to make that tripped me up a little and are worth mentioning:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;Checking if a variable is set / defined / not null - with getValue();
&lt;ul&gt;
	&lt;li&gt;Let say I want to check if a variable is defined in order to determine how to continue the flow. The easy way to this is with getValue() which is described the docs as follows:
&lt;ul&gt;
	&lt;li&gt;
&lt;pre&gt;&lt;code&gt;Object getValue(String name, Object default)
&lt;/code&gt;Returns the value of the variable named &lt;code&gt;name&lt;/code&gt; or returns &lt;code&gt;default&lt;/code&gt; when the variable is undefined or null.&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;It is easy to glance over this and miss something important... I&apos;ll show you the mistake I made:
&lt;ul&gt;
	&lt;li&gt;
&lt;pre&gt;myVariable = &apos;Hello World&apos;;
isDefined = getValue(myVariable,false);
if (isDefined!=false){
   // Do something
}&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;In the above code, isDefined actually evaluates to false and the IF statement will never evaluate to true! The key here is that getValue expects a string as the first parameter, which should be the name of the variable, not the variable itself. Here is the correct code:
&lt;ul&gt;
	&lt;li&gt;
&lt;pre&gt;myVariable = &apos;Hello World&apos;;
isDefined = getValue(&lt;span style=&quot;text-decoration: underline;&quot;&gt;&lt;strong&gt;&apos;myVariable&apos;&lt;/strong&gt;&lt;/span&gt;,false);
if (isDefined!=false){
   // Do something
}&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;You can&apos;t initialize an object variable the normal way
&lt;ul&gt;
	&lt;li&gt;If I want to create a new variable that is equal to an object, my instinct is to do something like this:
&lt;ul&gt;
	&lt;li&gt;
&lt;pre&gt;myObj = {&quot;Lorem&quot; : &quot;Ipsum&quot;};&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;This is wrong! This will not work with Automagic. Here is how to construct this in Automagic
&lt;ul&gt;
	&lt;li&gt;
&lt;pre&gt;myObj = fromJSON(&apos;{}&apos;);
myObj[&apos;Lorem&apos;] = &apos;Ipsum&apos;;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;Using the above syntax, you can even create nested objects, and then later, use toJSON() to convert to a JSON string to use in a POST HTTP request.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;If you want to check the length of an array, use length(arrVar), not arrVar.length&lt;/li&gt;
	&lt;li&gt;Use bracket notation to retrieve object values, not dot notation - e.g. myObj[&apos;myKey&apos;] not myObj.myKey.&lt;/li&gt;
	&lt;li&gt;To use variables in non-script actions (e.g. an HTTP request) wrap the variable name in {} - this is actually easy to remember since this is how many templating engines and languages handle combining strings with variables.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Reminder to self: How to fix super slow typing when using USB OTG plugged into smartphone&lt;/h2&gt;
&lt;p&gt;I was editing this Automagic script on my smartphone, and plugged in a USB keyboard (via OTG) to make editing easier. However, every single keypress took forever and lagged terribly. The problem was instantly fixed when I changed the active Android Keyboard from &quot;Gboard&quot; (Google&apos;s distributed keyboard app) to the default &quot;Android Keyboard (AOSP)&quot;. My guess is that Google&apos;s fancy keyboard app was trying to spellcheck the entire script field and was not detecting a plugged in USB keyboard, whereas the stock keyboard maybe has lower level access and knew to butt out of the &quot;conversation&quot;. Regardless, it was an easy fix!&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Materialize CSS Styled Business Card</title><link>https://codepen.io/joshuatz/pen/wRKpNR</link><guid isPermaLink="true">https://codepen.io/joshuatz/pen/wRKpNR</guid><description>A quick MaterializeCSS styled business card I threw together for this portfolio site.</description><pubDate>Wed, 12 Dec 2018 23:33:06 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_code_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Word Permutation Builder</title><link>https://codepen.io/joshuatz/embed/MmxwWm?height=265&amp;theme-id=0&amp;default-tab=js,result</link><guid isPermaLink="true">https://codepen.io/joshuatz/embed/MmxwWm?height=265&amp;theme-id=0&amp;default-tab=js,result</guid><description>&lt;!-- 302 REDIRECT TO externally_hosted_code_url --&gt;</description><pubDate>Thu, 22 Nov 2018 09:08:28 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_code_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Weekend Project - OkCupid Messaging Improvements - UI Mockup and Demo</title><link>https://joshuatz.com/projects/web-stuff/weekend-project---okcupid-messaging-improvements---ui-mockup-and-demo/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/weekend-project---okcupid-messaging-improvements---ui-mockup-and-demo/</guid><description>A quick UI mockup, demo, and writeup I created for a feature a popular dating site could implement to improve the messaging experience and cut down on unwanted messages. Demo created using injected Javascript.</description><pubDate>Mon, 27 Nov 2017 06:00:00 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;style&gt;ul li{margin-bottom:12px;font-size:large}div#main_post_wrapper p{padding-left:20px;padding-right:20px}div#examples,div#features{margin-left:15px}h2{text-align:left}#table_of_contents{background-color:#F3F6F9;width:40%;margin-left:auto;margin-right:auto;padding:10px;border-radius:10px;border-color:#3A6DB3;border-width:4px;border-style:solid;color:#3A6DB3;margin-bottom:10px;margin-top:10px}.video_wrapper{width:100%;text-align:center}&lt;/style&gt;
&lt;h1&gt;Weekend Project - OkCupid Messaging Improvements - UI Mockup and Demo&lt;/h1&gt;
&lt;h4 style=&quot;text-align:center;&quot;&gt;Published: 11/26/2017&lt;/h4&gt;
&lt;div class=&quot;hero_image_wrapper&quot;&gt;
	&lt;img src=&quot;/media/OkCupid/OkCupid_Messaging_Challenge_CombinationImage_DS.png&quot; style=&quot;max-width:840px;min-width:720px;&quot;&gt;&lt;/div&gt;
&lt;div id=&quot;table_of_contents&quot;&gt;
&lt;h2&gt;Table of Contents:&lt;/h2&gt;
&lt;ul&gt;
 	&lt;li&gt;&lt;a href=&quot;#background&quot;&gt;Background&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#basics&quot;&gt;Basics&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#solution_a&quot;&gt;Solution A: Timer&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#solution_b&quot;&gt;Solution B: Custom Question&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#recap&quot;&gt;Recap and Notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;ul class=&quot;collapsible&quot; id=&quot;background&quot;&gt;
 	&lt;li class=&quot;active&quot;&gt;
&lt;div class=&quot;collapsible-header&quot;&gt;&lt;i class=&quot;material-icons&quot;&gt;info&lt;/i&gt;Background (Click to Hide/Show)&lt;/div&gt;
&lt;div class=&quot;collapsible-body&quot;&gt;
&lt;p&gt;A common complaint for female users of almost any dating site is the constant barrage of messages from men that are unsolicited, inappropriate, and show that the senders didn’t even skim their profile information. I have heard this from others, and it also seems especially prevalent where I live, where the ratio of (single) females to males seems skewed so that there are more single males. This issue also works both ways, and I’m sure is not limited to the binary representation of gender either.&lt;/p&gt;
&lt;p&gt;Obviously, it would be best if society could fix the root cause, and everyone could agree to stop being rude and stop sending messages that are just the word “Hi” or “Ur 2 Htt, Msg Me Rt now”, without even reading someone’s profile, but I don’t have high hopes for that. However, I think that dating sites could do a lot to address this, and I’m a little surprised that OkCupid, which often is ahead of the trend, has yet to do something like this (at least that I’m aware of).&lt;/p&gt;
&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div id=&quot;basics&quot;&gt;
&lt;h2&gt;Basics:&lt;/h2&gt;
I came up with two simple solutions that they (OkCupid or another dating site) could implement in their User Interface (UI) to help reduce the quantity of low quality messages that certain users like to send out frequently and without discrimination. These would serve as &quot;challenges&quot; to make it more difficult for users to send generic messages without reading someones profile.
&lt;p&gt;Both of these solutions prevent a user from sending their first message to another user without having spent some time looking at the profile before initiating. Once the user completes one of the challenges, or if a message has been exchanged between the two users prior, these features/challenges should disable themselves.&lt;/p&gt;
&lt;p&gt;Demo for each solution was created using Javascript, and a fake profile page with user information replaced with dummy text.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;solution_a&quot;&gt;
&lt;h2&gt;Solution A: Timer&lt;/h2&gt;
This is the easiest solution to implement, and to me, is a no-brainer. Requiring users to spend a minimum amount of time (in my example, 30 seconds) looking at a profile before sending a message seems like a completely reasonable thing to do, and I would be flabbergasted if anyone could come up with a solid argument *against* it. If you can&apos;t spend at least 30 seconds reading what someone thoughtfully wrote in their profile, why should they spend 1+ minutes reading, thinking, and responding to your message?
&lt;h3 style=&quot;text-align:center;width:100%;&quot;&gt;Demo Video - Click Below to Play&lt;/h3&gt;
&lt;div class=&quot;video_wrapper&quot;&gt;
		&lt;video controls muted=&quot;true&quot; preload=&quot;metadata&quot; class=&quot;video_native&quot; style=&quot;width:100%; height:auto; max-width:1000px; max-height:720px;min-width:770px; margin-left:auto; margin-right:auto;&quot; onclick=&quot;this.paused ? this.play() : this.pause();&quot; loop=&quot;true&quot; poster=&quot;/media/OkCupid/OkCupid_Video_Poster.png&quot;&gt;&lt;source src=&quot;/media/OkCupid/OkCupid%20UI%20Feature%20-%20Timer%20Countdown%20(Edit).mp4&quot; type=&quot;video/mp4&quot;&gt;Sorry, your browser does not seem to support the HTML5 Video Element&lt;/video&gt;&lt;/div&gt;
Note that the developers console on the right is shown for demonstration purposes.
&lt;p&gt;In this example, the timer stops counting if something is hiding the profile from view. This stops users from simply leaving the error message about the timer up until the time passes, while looking at content elsewhere. This feature could be fleshed out further, and more requirements could be added, such as scrolling to the very bottom of the profile, expanding sections, etc.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;solution_b&quot;&gt;
&lt;h2&gt;Solution B: Custom Question&lt;/h2&gt;
This solution is more complicated to implement than the first (simple timer). In this feature, a user (the receiver) can set a custom challenge and expected response. The user (sender) trying to send their first message to the receiver will be prompted to answer the custom question, and if their answer matches the one saved by the receiver, they will be allowed to send their message.
&lt;h3 style=&quot;text-align:center;width:100%;&quot;&gt;Demo Video - Click Below to Play&lt;/h3&gt;
&lt;div class=&quot;video_wrapper&quot;&gt;
		&lt;video controls muted=&quot;true&quot; preload=&quot;metadata&quot; class=&quot;video_native&quot; style=&quot;width:100%; height:auto; max-width:1000px; max-height:720px;min-width:770px; margin-left:auto; margin-right:auto;&quot; onclick=&quot;this.paused ? this.play() : this.pause();&quot; loop=&quot;true&quot; poster=&quot;/media/OkCupid/OkCupid_Video_Poster.png&quot;&gt;&lt;source src=&quot;/media/OkCupid/OkCupid%20UI%20Feature%20-%20Custom%20Profile%20Question%20(Edit).mp4&quot; type=&quot;video/mp4&quot;&gt;Sorry, your browser does not seem to support the HTML5 Video Element&lt;/video&gt;&lt;/div&gt;
Note that the developers console on the right is shown for demonstration purposes.
&lt;p&gt;These questions could theoretically be automatically generated by the system - for example, if the user has indicated in their profile settings that they own dogs (a simple checkbox in OkC’s UI) the system could generate a corresponding question, “Does user [username here] have dogs?, Yes or No”.&lt;/p&gt;
&lt;p&gt;The complication with this feature is that it would require creating and exposing a new internal API endpoint to validate answers, since having the answer matching logic purely in client side code would make this easily defeat-able to any tech-savy users, and someone could easily release a chrome extension or bookmarklet so that any user could circumnavigate the custom questions.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;recap&quot;&gt;
&lt;h2&gt;Recap and Further Thoughts:&lt;/h2&gt;
Although this is a working bare-bones demo/mockup, there is a lot of wiggle room on how this could be implemented. For example, instead of enabling this feature across all users, OkCupid could only trigger it for &quot;suspicious&quot; users. For example, if a user is sending a large volume of messages per day in comparison to the average, they could start seeing the 30 second timer while others do not. This would be similar to how ISPs might throttle the connection speed for users abusing bandwidth, or how Google ReCaptcha v2 can be set up to only show when suspicious user activity is detected.
&lt;p&gt;In addition, controls over the functionality of these features could be extended to the receiving users themselves - or adapted automatically. For example, the system could automatically turn on the feature for users that &lt;em&gt;receive&lt;/em&gt; a higher amount of messages per day than others, but all users could get a toggle switch to turn it on if they want to.&lt;/p&gt;
&lt;/div&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Marketing Dashboards</title><link>https://joshuatz.com/projects/marketing/marketing-dashboards/</link><guid isPermaLink="true">https://joshuatz.com/projects/marketing/marketing-dashboards/</guid><description>Click the image above to see a spreadsheet I compiled comparing various marketing reporting dashboards, BI tools, and more.</description><pubDate>Sun, 19 Nov 2017 06:18:13 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;It seems like every day there is a new dashboard / BI (Business Intelligence) / reporting / widget platform arriving, and it can be daunting to keep track of and compare them. Recently, I started organizing a spreadsheet to keep track of the major reporting dashboard offerings, and this is the result of that work.&lt;/p&gt;
&lt;p&gt;I’ve embedded the entire Google Sheets comparison doc below, but the formatting is a bit funky with embeds, so if you want a better view, just visit the actual doc &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1dxHD8Wp3gfKDh61WbpKI5N6rhb6igp4N9XM96sM3IxM/edit?usp=sharing&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&quot;embed_wrapper&quot; style=&quot;&quot;&gt;
		&lt;iframe src=&quot;https://docs.google.com/spreadsheets/d/e/2PACX-1vRDnorxht0DLqwncGsiKhk1n33SvGSVbFLfIcw-lG3lX8kujSXTriCt3GHXUGlBUG1IgnrvaSg9GFAS/pubhtml?widget=true&amp;#x26;headers=false&quot; width=&quot;100%&quot; height=&quot;600px&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;div class=&quot;in_depth_explanation&quot;&gt;
&lt;p&gt;With the shift from traditional media marketing (newspapers, print, etc) to digital marketing (especially data-driving digital marketing), more and more data has been flowing out of campaigns, but not necessarily into the proper funnels. Data about performance, data about spend, and even data about operational procedures that were required to launch the campaign all get shot out of various APIs, manual CSV exports, and aggregators. Finding a platform that can ingest this data to produce pleasant looking client facing automated reports, and/or insights for internal operations can be a daunting task. Hopefully this page can help.&lt;/p&gt;
&lt;p&gt;By no means is it 100% comprehensive or representative of everything that is out there, but it covers most of the big ones (Google Data Studio, Power BI, Tableau), as well as some of the small(er) guys like Cyfe, Looker, and ReportGarden.&lt;/p&gt;
&lt;p&gt;If anything, hopefully this can help marketers as a starting point for quickly identifying which dashboard platforms are outside the scope of their needs, and which ones they should invest more time in researching and scheduling demos with.&lt;/p&gt;
&lt;p&gt;If you are upset that your favorite reporting dashboard / BI tool is not on this list, feel free to drop me a line, and I’ll take a look and add it if I think it meets the basic criteria. You can even use &lt;a target=&quot;_blank&quot; href=&quot;https://docs.google.com/forms/d/e/1FAIpQLSdfRRB7RiPQylBs4GbjG4IR8DpwoqHEokdweQ2-b0xLNLSqNQ/viewform?usp=sf_link&quot;&gt;this easy to fill out form&lt;/a&gt; to submit a suggestion.&lt;/p&gt;
&lt;/div&gt;
&lt;script&gt;&lt;br /&gt;
	// Make embedded iframe resizable with JQuery UI&lt;br /&gt;
	$(&quot;.embed_wrapper&quot;).resizable({ alsoResize : &apos;.embed_wrapper &gt; iframe&apos;});&lt;br /&gt;
	&lt;/script&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Chrome Extension - for TimeClockWizard</title><link>https://joshuatz.com/projects/web-stuff/google-chrome-extension---for-timeclockwizard/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/google-chrome-extension---for-timeclockwizard/</guid><description>I developed a chrome extension for a popular online time clock so that you could clock in, clock out, and check hours all from an easy to use popup that can be interacted with no matter what page you are on.</description><pubDate>Sat, 06 May 2017 05:00:00 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;1/4/2019 - UPDATE: Extension Brought Back from the Dead!&lt;/h2&gt;
&lt;p&gt;I stopped using TimeClockWizard personally a while ago, so when they changed their website sometime in 2017 or 2018 and broke this extension, I don&apos;t think I even noticed (or at least not for a while). However, I had some spare time recently, so I decided to rebuild this extension to get it working again. It actually only took about a day, and now is functional again. If TCW changes their system again, and the extension once again breaks, feel free to let me know by commenting on this page. However,  I can&apos;t guarantee that I will continue supporting this extension indefinitely.&lt;/p&gt;
&lt;div class=&quot;hero_image_wrapper&quot;&gt;&lt;img style=&quot;max-width: 615px;&quot; src=&quot;/media/web-stuff/TCW_Chrome_Extension/Promotional_920x680.png&quot;&gt;&lt;/div&gt;
&lt;div style=&quot;font-size: large; width: 100%; text-align: center;&quot;&gt;You can get the extension &lt;a href=&quot;https://chrome.google.com/webstore/detail/timeclockwizard-quick-act/nfaekpiccelmomnjdngmkokjnkgemdho&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;here&lt;/a&gt;!&lt;/div&gt;
&lt;div id=&quot;table_of_contents&quot;&gt; &lt;/div&gt;
&lt;p&gt;For work, I&apos;ve been using a site called &lt;a href=&quot;http://timeclockwizard.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;&quot;Time Clock Wizard&quot;&lt;/a&gt;. From a managerial side of things, it is pretty good, since the cost is low and it fulfils all the basic bookkeeping requirements. However, from the user perspective, the site leaves a lot to be desired; it constantly logs everyone out, it displays accumulated hours per pay period instead of the normal Monday-Friday (which means we constantly have to pull custom reports to see how close we are to hitting overtime), and it is slow to navigate.&lt;/p&gt;
&lt;p&gt;Since I&apos;ve been toying around with Chrome Extensions lately, I figured it would be fun to try and make one so that I could more easily use the TimeClockWizard service - so that is exactly what I did - I coded up an entire chrome extension over a single weekend.&lt;/p&gt;
&lt;div id=&quot;features&quot; class=&quot;features&quot;&gt;
&lt;h2&gt;Features:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;Only have to sign in once!&lt;/li&gt;
	&lt;li&gt;Clock In and Out all from the extension popup page&lt;/li&gt;
	&lt;li&gt;See your last punch time at a glance - answer &quot;What time did I clock out for lunch?&quot;&lt;/li&gt;
	&lt;li&gt;Check your weekly hours - avoid accidentally going over&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;examples&quot; class=&quot;examples_wrapper&quot;&gt;
&lt;h2 style=&quot;text-align: left;&quot;&gt;See it in action!&lt;/h2&gt;
&lt;div class=&quot;example&quot;&gt;
&lt;h4&gt;Clock In:&lt;/h4&gt;
&lt;div class=&quot;example_image_wrapper&quot;&gt;&lt;img src=&quot;/media/web-stuff/TCW_Chrome_Extension/Clocking%20In.gif&quot; alt=&quot;TimeClockWizard Chrome Extension - Clocking In&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;example&quot;&gt;
&lt;h4&gt;Clock Out:&lt;/h4&gt;
&lt;div class=&quot;example_image_wrapper&quot;&gt;&lt;img src=&quot;/media/web-stuff/TCW_Chrome_Extension/Clocking%20Out.gif&quot; alt=&quot;TimeClockWizard Chrome Extension - Clocking Out&quot;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;example&quot;&gt;
&lt;h4&gt;Resyncing Full Time Report:&lt;/h4&gt;
&lt;div class=&quot;example_image_wrapper&quot;&gt;&lt;img src=&quot;/media/web-stuff/TCW_Chrome_Extension/Resyncing%20Time%20Report.gif&quot; alt=&quot;TimeClockWizard Chrome Extension - Syncing Time&quot;&gt;&lt;/div&gt;
&lt;p&gt;Sometimes, if you are clocking in both through the website and through this extension, the estimated total weekly hours can start to go adrift. You can use the button provided to force a full resync of total weekly hours.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;technical_stuff&quot;&gt;
&lt;h2&gt;Technical Stuff!&lt;/h2&gt;
&lt;p&gt;After a weekend project I like to sum up cool things I used / learned / experienced - either for the first time, or in some way that felt transformative. Here are those things:&lt;/p&gt;
&lt;ul style=&quot;margin-left: 32px;&quot;&gt;
	&lt;li&gt;Chrome Extensions (duh!)&lt;/li&gt;
	&lt;li&gt;Local Storage (aka localStorage)&lt;/li&gt;
	&lt;li&gt;Date formatting
&lt;ul&gt;
	&lt;li&gt;Unlike other aspects of code that I start to like the more I use them, the more I work with date formatting (converting, offsetting, etc.) the more I HATE it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
	&lt;li&gt;XMLHttpRequest(s) - simple GET and POSTS can be so powerful :)&lt;/li&gt;
	&lt;li&gt;Async stuff (callbacks, promises)&lt;/li&gt;
	&lt;li&gt;Regex (is there ever a project that &lt;strong&gt;&lt;i&gt;doesn&apos;t&lt;/i&gt;&lt;/strong&gt; use rexeg? Don&apos;t answer, that was rhetorical).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are interested enough to be reading this section, you might already know, this but TimeClockWizard does not have a published API. Thus, the most challenging part of this project was not the UI or related code; it was trying to figure out how TCW handles network requests - spoiler: it is not as straightforward as one might think.&lt;/p&gt;
&lt;p&gt;Source code &lt;i&gt;might&lt;/i&gt; be published at some point...&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;
&lt;style&gt;ul li{margin-bottom:12px;font-size:large}div.example_image_wrapper{width:90%;margin-left:auto;margin-right:auto;text-align:center}div.example_image_wrapper&gt;img{max-width:100%}div.example&gt;h4{text-align:center;font-size:x-large;margin:10px 0}div.example{background-color:rgba(133,169,179,.46);border-radius:20px;margin-bottom:20px;border-style:dashed;width:80%;margin-left:auto;margin-right:auto}div#main_post_wrapper p{padding-left:20px;padding-right:20px}div#examples,div#features{margin-left:15px}h2{text-align:left}&lt;/style&gt;
&lt;/p&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Analytics Tester Bookmarklet</title><link>https://joshuatz.com/projects/web-stuff/google-analytics-tester-bookmarklet/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/google-analytics-tester-bookmarklet/</guid><description>A nifty little bookmarklet to test any webpage for Google Analytics, verify that event tracking is working properly, and return a list of all linked Google Analytics IDs!</description><pubDate>Mon, 07 Mar 2016 06:42:48 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2&gt;Grab the Google Analytics Bookmarklet!&lt;/h2&gt;
&lt;p style=&quot;font-size:x-large&quot;&gt;To use the bookmarklet, simply drag the following link to your toolbar, then click on the bookmark whenever you want to test a page! - Drag this link: &lt;a href=&quot;javascript:void%20function(){var%20t=document.getElementsByTagName(%22head%22)[0],n=document.createElement(%22script%22);n.type=%22text/javascript%22,n.text=&amp;#x27;var%20has_old_GA=%22null%22,has_new_GA=%22null%22,has_DC_A=%22null%22,old_GA_trackers=[],new_GA_trackers=[],reportContents=%22%22,event_type_for_listener;window.setTimeout(%22attach_all_events()%22,1E3);function%20attach_all_events(){analyticschecker()}\r\nfunction%20attach_single_GA_event(a,b,c,d,f){event_type_for_listener=event_type_for_listener||%22click%22;event_type_for_listener=event_type_for_listener.toString();void%200==a.length%26%26(a=[a]);if(1==has_old_GA)for(var%20e=0;e%3Ca.length;e++){var%20g=a[e];null!==g%26%26void%200!==g%26%26(console.log(%22Attaching%20Old%20GA%20Event%20to%20Found%20Node%22),g.addEventListener(event_type_for_listener,function(){multi_account_event_handler(b,c,d,f)}))}if(1==has_new_GA)for(e=0;e%3Ca.length;e++)g=a[e],null!==g%26%26void%200!==g%26%26(console.log(%22Attaching%20New%20GA%20Event%20to%20Found%20Node%22),\r\ng.addEventListener(event_type_for_listener,function(){multi_account_event_handler(b,c,d,f)}))}\r\nfunction%20multi_account_event_handler(a,b,c,d){if(1==has_old_GA)for(var%20f=0;f%3Cold_GA_trackers.length;f++){var%20e=old_GA_trackers[f]._getAccount();_gaq.push([%22_setAccount%22,e]);void%200===c%26%26void%200===d%26%26_gaq.push([%22_trackEvent%22,a.toString(),b.toString()]);void%200!==c%26%26void%200===d%26%26_gaq.push([%22_trackEvent%22,a.toString(),b.toString(),c.toString()]);void%200===c%26%26void%200!==d%26%26_gaq.push([%22_trackEvent%22,a.toString(),b.toString(),d.toString()]);void%200!==c%26%26void%200!==d%26%26_gaq.push([%22_trackEvent%22,a.toString(),b.toString(),\r\nc.toString(),d.toString()])}if(1==has_new_GA)for(f=0;f%3Cnew_GA_trackers.length;f++)e=new_GA_trackers[f].get(%22name%22),void%200===c%26%26void%200===d%26%26ga(e+%22.send%22,%22event%22,a.toString(),b.toString()),void%200!==c%26%26void%200===d%26%26ga(e+%22.send%22,%22event%22,a.toString(),b.toString(),c.toString()),void%200===c%26%26void%200!==d%26%26ga(e+%22.send%22,%22event%22,a.toString(),b.toString(),d.toString()),void%200!==c%26%26void%200!==d%26%26ga(e+%22.send%22,%22event%22,a.toString(),b.toString(),c.toString(),d.toString())}\r\nfunction%20analyticschecker(){var%20a=document.getElementsByTagName(%22script%22),b=len=0;b;for(len=a.length;b%3Clen;b+=1)/.google-analytics\\.com\\/ga\\.js/.test(a[b].src)%26%26(console.log(%22found%20old%20GA%22),has_old_GA=!0,old_GA_trackers=_gat._getTrackers()),/.google-analytics\\.com\\/analytics\\.js/.test(a[b].src)%26%26(console.log(%22found%20new%20GA%22),has_new_GA=!0,new_GA_trackers=ga.getAll());generateReport()}\r\nfunction%20generateReport(){reportContents=%22%22;var%20a=new%20Date,b=new_GA_trackers.length+old_GA_trackers.length;reportContents+=%22Report%20Generated%20on:%20%22+a.toLocaleDateString()+%22%20at%20%22+a.toLocaleTimeString()+%22\\n%22;reportContents+=%22Report%20for:%20%22+window.location.hostname+window.location.pathname+%22\\n\\n%22;reportContents+=%22Total%20number%20of%20Google%20Analytics%20Trackers:%20%22+b+%22\\n%22;reportContents+=%22Old%20Trackers:%20%22+old_GA_trackers.length+%22\\n%22;if(0%3Cold_GA_trackers.length)for(x=0;x%3Cold_GA_trackers.length;x++)reportContents+=\r\n%22\\tOld%20Tracker%20%23%22+(x+1)+%22%20-%20AccountID%20=%20%22+old_GA_trackers[x]._getAccount(),reportContents+=%22\\n%22;reportContents+=%22New%20Trackers:%20%22+new_GA_trackers.length+%22\\n%22;if(0%3Cnew_GA_trackers.length)for(x=0;x%3Cnew_GA_trackers.length;x++)reportContents+=%22\\tNew%20Tracker%20%23%22+(x+1)+%22%20-%20AccountID%20=%20%22+new_GA_trackers[x].get(%22trackingId%22),reportContents+=%22\\n%22;console.log(reportContents);return%20reportContents}\r\nvar%20UI_HTML=\&amp;#x27;%3Cstyle%3E\\r\\ndiv%23UI_Wrapper{\\r\\n\\twidth:100%25;\\r\\n\\theight:100%25;\\r\\n\\tposition:absolute;\\t\\r\\n\\ttop:0px;\\r\\n}\\r\\ndiv.hidden%20{\\r\\n\\tvisibility:hidden;\\r\\n\\topacity:0;\\r\\n\\t/*display:none;%20*/\\r\\n}\\r\\ndiv.visible%20{\\r\\n\\tvisibility:visible;\\r\\n\\topacity:1;\\r\\n}\\r\\ndiv%23Toggle_Visibility_Icon%20{\\r\\n\\ttop:15px;\\r\\n\\tpadding-top:10px;\\r\\n\\tpadding-left:7px;\\r\\n\\tborder-radius:8px;\\r\\n\\tbackground-color:rgba(255,255,255,0.75);\\r\\n\\twidth:40px;\\r\\n\\theight:40px;\\r\\n\\tz-index:999999999999999999999;\\r\\n\\tposition:fixed;\\r\\n}\\r\\ndiv%23Toggle_Visibility_Icon%20div{\\r\\n\\tbackground:%20url(\\\&amp;#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAdCAYAAAC9pNwMAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAKTSURBVEhLzdZJyE1hHMfxa57nMYWypEQWSiyURJENWVlY2FpIsrcSUSIKC1OUMUOGkEhmMpMyLBRFSsmQ8P3e+z5v//d43vvea7j86tP7nnOfc57zPOcZTul/yFDswMjyUYPSDw/xHffgQzQk3bETVqzbGIKUjhiPhdiAM7iFO7iMg1iBmeiNurMVqXJvPBimG04h/VbNS6zDaNSVWPlNDITxdZxHrKSaT1iNXqg5dmG6wTX0h7EbL+AJdsFut5ufI1Ya3cUEtJmN+IJ48RXYYtMZXSr/NscxsgAvEK9L3mM6Ws0W5C7UJfRFTLumvynDcAO56z9iMn7KMuQuiC4ijtolsMvHlI8qGY43yF3/Cj5cc8bhK3KFfVIHWTp2gKUBMwi+Bs9P80RTliOVLzqK5pxGrpCc0z2wN5w7h54wvnMH3AdYzoyADxzvE81BaX44kbMdpgNiy88iVeQ8v4q55aPKu3+AeJ/IV1Z6HE7krEXKAcTf7CkrNQMQ37WDMZaN7J3SDHxrOpGzDSmbUPz9JLoixhbfR7FsUm6x2YdcAblppExCrsxxxLk9FrlyySyUMwo2P1dIU5CyFLkeOgYHmnGDsVXFMrKRLeLOkysod6D2SJmIlXDBOYFU7jA6wfRBsfJnSGt/i6xCLBg5ut0ec3FjSb1wCKmcK10aZO/getFq1iBWGNkCF4rYemPXPkUqtx9OP+Ma78NMLR+1kcX4jFhp5IDbjc04gtcovncXnOID1hS3smorWi324JczG66vbuy5mxe9Rdwe3UR+K04599z1cN76geA67iZhdzsw58H37ch1CU2V+/XasLiEXkeq3FmR1va/HrfNuLEsQsPil+ojOKXq+vD7E/FjMS2n/zKl0g+AuGLBdLnyOwAAAABJRU5ErkJggg==\\\&amp;#x27;);\\r\\n\\twidth:30px;\\r\\n\\theight:29px;\\r\\n\\tz-index:9999999999999;\\r\\n}\\r\\n\\r\\ndiv%23Floating_UI_Box{\\r\\n\\ttransition-duration:1s;\\r\\n\\ttransition-property:all;\\r\\n\\tborder:thick;\\r\\n\\tborder-color:black;\\r\\n\\tborder-style:double;\\r\\n\\twidth:60%25;\\r\\n\\tmax-width:360px;\\r\\n\\theight:30%25;\\r\\n\\tmin-height:130px;\\r\\n\\tmargin-left:5%25;\\r\\n\\ttop:%2060%25;\\r\\n\\tz-index:9999999999999;\\r\\n\\tbackground-color:rgba(255,255,255,0.86);\\r\\n\\tposition:fixed;\\r\\n}\\r\\ndiv%23UI_Output_Text{\\r\\n\\twidth:100%25;\\r\\n\\theight:70%25;\\r\\n\\toverflow:auto;\\r\\n}\\r\\ndiv%23UI_Output_Text%20textarea%20{\\r\\n\\tbackground-color:rgba(255,255,255,0.0);\\r\\n\\twidth:100%25;\\r\\n\\theight:100%25;\\r\\n\\t-webkit-box-sizing:%20border-box;\\r\\n\\t-moz-box-sizing:%20border-box;\\r\\n\\tbox-sizing:%20border-box;\\r\\n}\\r\\ndiv%23UI_Buttons{\\r\\n\\twidth:100%25;\\r\\n\\theight:30%25;\\r\\n}\\r\\ndiv%23UI_Close_Button{\\r\\n\\twidth:10%25;\\r\\n\\tmargin-left:90%25;\\r\\n\\tfloat:right;\\r\\n\\tposition:absolute;\\r\\n}\\r\\ndiv%23UI_Close_Button%20img{\\r\\n\\twidth:100%25;\\r\\n\\theight:auto;\\r\\n}\\r\\ndiv%23UI_Buttons%20div%20{\\r\\n\\twidth:100%25;\\r\\n\\theight:50%25;\\r\\n}\\r\\ndiv%23UI_Buttons%20input%20{\\r\\n\\twidth:100%25;\\r\\n\\theight:100%25;\\r\\n}\\r\\n%3C/style%3E\\r\\n%3Cdiv%20id=%22Toggle_Visibility_Icon%22%20class=%22hidden%22%20onClick=%22openUIBox();%22%3E%3Cdiv%3E%3C/div%3E%3C/div%3E\\r\\n%3Cdiv%20id=%22UI_Wrapper%22%3E\\r\\n\\r\\n\\r\\n%3Cdiv%20id=%22Floating_UI_Box%22%20class=%22visible%22%3E\\r\\n%3Cdiv%20id=%22UI_Close_Button%22%3E%3Cimg%20src=%22data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAABm1BMVEUAAABEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREBEREA3NzM1NTIYGBcAAAAfHx4gIB4eHh4fHx0fHx0eHhsdHRsaGhoZGRkeHhwdHRscHBkfHx8bGxsdHRkkJCQdHRsdHRscHBwcHBobGxodHRsdHRsdHRsfHx0bGxsdHRsdHRscHBsbGxsAAAAcHBwdHRscHBtAQD0gIB0jIyMfHx1AQD0kJCMdHRozMzMdHRodHRogIBwaGhofHx0dHRsiIiIfHx0mJiImJiIeHhoeHhkjIyMdHRsdHRkjIyIdHRslJSEfHx0lJSEkJCAdHRkmJiIcHBwlJSEkJCMeHhokJCEgIB0yMi8uLiwgIB4gIB0gIB0eHh4bGxsVFRUbGxscHBsTExMcHBsaGhkfHx4dHRsdHRkeHhsgIB0fHxofHx0qKiMcHBogIB4dHRkeHh49PTodHRsgIB0qKiQfHx0fHx0dHRsdHRsWpnPQAAAAhXRSTlMAAQIDAwEEBQEFBgEHCAEICQIKCwILDA4MAg8OAhMRBgQ2oeD9/N6aMAqf/pgIxL4Hk/0skdT1/Pz8uvTyvagDP/Q7ErNJ9RRLrwWuuMI59vJE80ZFOjtL8zxM80b0SEc9TT5JSEdYwxocycXEDcIMw8QNxQ/I+9KblDH9NpmWv70VlJU1DC6LGwAABYlJREFUeNrtm+lz00YYhxWfsWzHjuVTkhNxmBYoPbiUVGmJGzDQQgO0pU6Da2NoSumR0gNKL0pbx/qzK9mSIjm72vWuVqYz7CfmnZ19foOV1bPvaDnuRRtzkUhkbgY1e0RjsVh0BjVrxBPJZDIRD71mj/kUz/Op+dBr9khnstlsJh16zR4LuXw+n1sIvWaPxYIgCIXF0Gv2KJbK5XKpGHrNyVaqVCrVmqdWq7KvOb9NoVwRJdlTkyVRZF3jrP0onRPKlRnw49HI+G8zkxfK1fD584nYKEA8lc0LpVro/HQqOQ6Q4LP5QjF0/kKGT8bMZyCa5LO5xcn59frSsuIey0t1mtoB/mIuyyfNd9JcLMlnvHvTocNHjg72hrp7DPcGA5pa49grr3r2hEI+yyfMf0ViyZRnbz5+gpIFq5187dT+nlAS8tlUfBwg4Xk3HX+dEd8Yb7xp/8bVspDPjLmRqPfdfIIdX9ffsp+xSlnIpT17kfP7s+Trp89Yz3ilXAC/E+XDLPm6ftb6m6yUwO9EWTrClK+fs/aEahHCF48y5evnrT2pBuPX2fJ1FfhOdO2XdbZ8fQXBF5fY8oercH5tlG2ZLX+vAeUXq6NsClv+oAHjG044yqaw5Q8aEL7jhApb/mAV5YQKfI23NVzW2jvQeSsoJ1Sg6757Yb2JyX9v4yJsnopyQgXO5+RLLTy+JF6+CJmnopxQ8eFL4pUWHl8UN66C56koJ1T8+KJ4qYnHl+T3PwDOU1FOqPjyJXldw+NznCuBa56KckLFn89xrgS+fFcC9zwV5YQKgu9KgOA7CTzzVJQTKii+kwDJtxJ456koJ1SQfCsBBn+UYGKeinLCZTR/lACLbyS4OjFPRTnh0sS61y6AznWgPRHE5+SN6955KsoJ6xPrrn0IPGse3BPBfOnKpneeOrUT3rgJPOtO7omYfBInvPUR8Kzt3RNx+UROeOtj4FnfvSdi88mc8MYnwF7DfgJ8PqETrt0G9jrsBBD+p5vBOeF+AsCeOA2f2Anbt4G9HjPBVHxyJxwnAOyJ0/EpnNBMANoTp+NTOKHe3sLvyX0G45M7oTG279DziZ1wVOt8Ts0ndUKr1unS8gmd0Klt9yj5ZE7oWqPdQ/Hv+vKJnNCzhtaj4pM44cQadgIyPoETHlhD61Pwp3dCwBpmAlL+9E4Ieia1dWI+gRMOcf1XEu91wukTwvii2N0Oo08I50tyr82+T+jH5zhXAkZ9Qn++KwGjPiGK7yRg1CdE840EGqs+IR5/lGDIok+IyzcS+PQTKZwQm8/JXXg/kcIJ8fnGntgK3gkh/LtgT7zfDNoJYfzNzh3g/0lfC9YJoXzDE7eAv0lfC9IJffiGJ24Bn4m+FpwT+vJ1VwLPM9nXgnJCCH/fv9s96Nk5CCdE8u0EOP1EAifE4I8TYPUTp3dCLL6ZAK+fOL0TfoF5/truAvfEHWon/PIB5vmvcw/A/+ohvRN+/QCz/9HqovlETvjNt5j9l2YPySdzwu928fo/rpMrjE/ohK4EfvyJszOIT+qE3+9i8e2TK5xP7IRWAnD/F3h2BvPJnfDRLhbfOTtD+BRO+OgHLL6ZwIdP44Q//oTF1/XmfTifqk/4884m3ncKrZ2HjPqEAdQo+4T0Ndo+IXWNuk9IW6PvE1LWAugT0tWC6BNS1QLpE9LUHqOcUGyw/Z5PQzmheIzt93xPUHzpF7bf8z1FOaH860mW/N9+Rzkhx/3B8nu6P0WUE3LcqWfs+H89F1FOaCb4+zSj70n/eY55x+TM2XPn1ZXVRqOxuqK6B3ntsfbk6b8v75j8v+6YMOf73TEJg+9zxyQUvs8dk1D4fndMwrhjZ9Sgd0xCu3dYeDHvXc763ums793O+t7xrO9dz/re+Yzu3f8HMXRKSTdQuL4AAAAASUVORK5CYII=%22%20onClick=%22closeUIBox();%22%3E%3C/div%3E\\r\\n%3Cdiv%20id=%22UI_Output_Text%22%3E\\r\\n\\t\\x3c!--%3Ctextarea%20readonly%20id=%22UI_Output_Text_TextArea%22%3E%3C/textarea%3E--\\x3e\\r\\n%3C/div%3E\\r\\n%3Cdiv%20id=%22UI_Buttons%22%3E\\r\\n\\t%3Cdiv%20id=%22Send%20Test%20Event%22%3E%3Cinput%20type=%22button%22%20name=%22Send_Test_Event%22%20value=%22Send%20Test%20Event%22%20onClick=%22buttonPressed_SendTestEvent();%22%3E%3C/div%3E\\r\\n%20%20%20%20%3Cdiv%20id=%22Re-Test%22%3E%3Cinput%20type=%22button%22%20name=%22ReTest%22%20value=%22Re-Test%20Page%22%20onClick=%22buttonPressed_ReTest();%22%3E%3C/div%3E\\r\\n%3C/div%3E\\r\\n%3C/div%3E\\r\\n%3C/div%3E\&amp;#x27;,bodyID=\r\ndocument.getElementsByTagName(%22body%22)[0],newdiv=document.createElement(%22div%22);newdiv.innerHTML=UI_HTML;bodyID.appendChild(newdiv);window.setTimeout(function(){buttonPressed_ReTest()},200);function%20closeUIBox(){document.getElementById(%22Floating_UI_Box%22).className=%22hidden%22;document.getElementById(%22UI_Wrapper%22).style.display=%22none%22;document.getElementById(%22Toggle_Visibility_Icon%22).className=%22visible%22}\r\nfunction%20openUIBox(){document.getElementById(%22UI_Wrapper%22).style.display=%22block%22;document.getElementById(%22Floating_UI_Box%22).className=%22visible%22;document.getElementById(%22Toggle_Visibility_Icon%22).className=%22hidden%22}function%20buttonPressed_SendTestEvent(){multi_account_event_handler(%22www.joshuatz.com%20-%20GA%20Event%20Testing%20Bookmarklet%22,%22Event%20Test%22,%22www.joshuatz.com%22,%22100%22)}\r\nfunction%20buttonPressed_ReTest(){analyticschecker();var%20a=reportContents.replace(/\\t/g,%22%26nbsp;%26nbsp;%26nbsp;%26nbsp;%26nbsp;%26nbsp;%22).replace(/\\n/g,%22%3Cbr/%3E%22);document.getElementById(%22UI_Output_Text%22).innerHTML=a};&amp;#x27;,t.appendChild(n)}();&quot;&gt;Google Analytics Tool&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p style=&quot;color:rgba(255,94,97,1.00)&quot;&gt;Warning: This tool is in beta! Released 3/6/2016. Page will be revised if updates are made.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;What does this tool do?&lt;/h2&gt;
I created this bookmarklet primarily for my own uses, but seeing as there is nothing out there like it, I felt it was worth sharing. Here is what this Google Analytics checker and event testing bookmarklet does:
&lt;ul&gt;
 	&lt;li&gt;&lt;strong&gt;Google Analytics Code Checker&lt;/strong&gt;: Checks for ALL Google Analytics tracking codes that are on a page (regardless of what version of GA they are using - e.g. newer analytics.js vs older ga.js)&lt;/li&gt;
 	&lt;li&gt;&lt;strong&gt;Google Analytics Accound ID Report Generator&lt;/strong&gt;: Spits out a report of all the google analytics account IDs that are found on the page, and what type of analytics library they are using.&lt;/li&gt;
 	&lt;li&gt;&lt;strong&gt;Google Analytics Event Tester&lt;/strong&gt;: Verify that your Google Analytics is receiving data by clicking a button and triggering an event to be recorded. My bookmarklet will send the event to all the analytics accounts that are properly tracking the page.&lt;/li&gt;
 	&lt;li&gt;&lt;strong&gt;Retest the page after DOM has changed&lt;/strong&gt;: You can temporarily close the floating bookmarklet overlay panel by pressing on the x, manipulate the page and trigger changes, then reopen by clicking on the eye icon in the upper left, and continue to re-test Google Analytics or trigger more events!&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;h2&gt;Video Demo of Tool in Use&lt;/h2&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube-nocookie.com/embed/EhC26vltOBg?rel=0&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;div style=&quot;text-align:center;&quot;&gt;
&lt;h2&gt;GIF Showing Hiding and Re-Opening of Floating Report Window&lt;/h2&gt;
&lt;img src=&quot;/media/GA-Bookmarklet-Example.gif&quot;&gt;
&lt;/div&gt;
This tool does NOT inject any external script sources into the page. Although doing so would allow me to update the bookmarklet without requiring it be removed and re-added to users&apos; browsers, it also would mean that the bookmarklet wouldn&apos;t work on sites that use the newer &lt;a href=&quot;http://www.html5rocks.com/en/tutorials/security/content-security-policy/&quot; target=&quot;_blank&quot;&gt;Content Security Policy (CSP)&lt;/a&gt;, which I wanted to avoid as CSP gains momentum. The downside means that you might want to check this page every once in a while if you care about if the tool gets updated.
&lt;p&gt;This bookmarklet is composed of a few major components:&lt;/p&gt;
&lt;ul&gt;
 	&lt;li&gt;Code that detects GA scripts and what type they are - this is trivial to do, and you can do so simply by regex testing against all scripts and their sources on the page (see &lt;a href=&quot;http://www.simoahava.com/analytics/check-if-google-analytics-is-in-page-template/&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;)&lt;/li&gt;
 	&lt;li&gt;Code that handles creating events for both versions of the Google Analytics JS library and sending them to ALL attached accounts - this took me a while to make, and ended up being a project on its own. I&apos;ll post about it soon and the reusable JS you can use to easily trigger Google Analytics events on button clicks, hovers, and other event listeners.&lt;/li&gt;
 	&lt;li&gt;An injected UI widget - this is the floating div that appears on the page when you use the bookmarklet. I am severely graphically untalented, so CSS continues to be a struggle for me. I think I did alright with this though.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Wijmo Combobox with an AJAX Data Source (Wikipedia API)</title><link>https://joshuatz.com/projects/web-stuff/wijmo-combobox-with-an-ajax-data-source-wikipedia-api/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/wijmo-combobox-with-an-ajax-data-source-wikipedia-api/</guid><description>A demo I made showing how the Wijmo combobox can be combined with a dynamic data source via AJAX, such as the Wikipedia API. Writeup includes demo, build steps, and discussion of cross-domain issues.</description><pubDate>Sun, 28 Feb 2016 05:44:33 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;style&gt;div.Code_Example{width:100%;max-width:1000px;}div.Code_Example div {height:100%;}&lt;/style&gt;
&lt;div id=&quot;Skip_Option&quot; style=&quot;text-align:center; width:90%; margin-left:5%; font-size:large;&quot;&gt;Want to skip right to the working demo? Click &lt;a href=&quot;#Working_Demo&quot; target=&quot;_self&quot;&gt;here&lt;/a&gt;.&lt;/div&gt;
&lt;div id=&quot;Table_of_Contents&quot;&gt;
&lt;h2&gt;Table of Contents:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;#Introduction&quot; target=&quot;_self&quot;&gt;Introduction&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#Building_the_Demo&quot; target=&quot;_self&quot;&gt;Building a Wijmo Combobox with a Dynamic Data Source&lt;/a&gt;&lt;/li&gt;
    &lt;ul&gt;
    	&lt;li&gt;&lt;a href=&quot;#Working_Demo&quot; target=&quot;_self&quot;&gt;Working Example / Demo&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;#Building_Step1&quot; target=&quot;_self&quot;&gt;Step 1: Load Resources, Set Options&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;#Building_Step2&quot; target=&quot;_self&quot;&gt;Step 2: Write HTML&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;#Building_Step3&quot; target=&quot;_self&quot;&gt;Step 3: Write the Javascript!&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
    &lt;li&gt;&lt;a href=&quot;#ComboBox_Issues&quot; target=&quot;_self&quot;&gt;Combobox Issues&lt;/a&gt;&lt;/li&gt;
    &lt;ul&gt;
    	&lt;li&gt;&lt;a href=&quot;#HTML_Input&quot; target=&quot;_self&quot;&gt;HTML Input Types&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;#Cross_Domain&quot; target=&quot;_self&quot;&gt;Cross-Domain Issues&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
    &lt;li&gt;&lt;a href=&quot;#Further_Reading&quot; target=&quot;_self&quot;&gt;Further Reading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id=&quot;Introduction&quot;&gt;Intro:&lt;/h2&gt;
&lt;p&gt;For a recent project, I needed to implement a ComoBox that a user could type into, and they would be offered a dropdown list of matching selections based on a third-party API (think of how Google Auto-suggest acts). This is difficult for a number of reasons, &lt;a href=&quot;#ComboBox_Issues&quot;&gt;which I list further down&lt;/a&gt;. However, an easy workaround/hack I found was to use JSONP requests (courtesy of &lt;a href=&quot;http://stackoverflow.com/a/22780569&quot; target=&quot;_blank&quot;&gt;this answer on Stack Overflow&lt;/a&gt;), combined with the &lt;a href=&quot;http://wijmo.com/products/wijmo-3/&quot; target=&quot;_blank&quot;&gt;Wijmo 3 UI Widget Framework&lt;/a&gt;. The Wijmo ComboBox widget can be a little confusing to implement though, especially with a dynamic data source, which is the main reason why I came up with a demo and wrote this post (as a weekend project).&lt;/p&gt;
&lt;h2 id=&quot;Building_the_Demo&quot;&gt;Building a Wijmo ComboBox with an AJAX Data Source&lt;/h2&gt;
&lt;h3 id=&quot;Working_Demo&quot;&gt;Working Example:&lt;/h3&gt;
&lt;p&gt;Since this is probably what most people are interested in seeing first, here is a working example. I am using the &lt;a href=&quot;https://en.wikipedia.org/w/api.php&quot; target=&quot;_blank&quot;&gt;(unofficial) Wikipedia API&lt;/a&gt; as my source, so as you type in the ComboBox, you will see entries from Wikipedia matching your query. Pretty cool, right?&lt;/p&gt;
&lt;iframe height=&quot;268&quot; scrolling=&quot;no&quot; src=&quot;//codepen.io/joshuatz/embed/MyWOXG/?height=268&amp;#x26;theme-id=0&amp;#x26;default-tab=result&quot; frameborder=&quot;no&quot; allowtransparency=&quot;true&quot; allowfullscreen=&quot;true&quot; style=&quot;width: 100%;&quot;&gt;See the Pen &amp;#x3C;a href=&apos;http://codepen.io/joshuatz/pen/MyWOXG/&apos;&gt;Wijmo ComboBox with Cross-Domain AJAX Data&amp;#x3C;/a&gt; by Joshua T (&amp;#x3C;a href=&apos;http://codepen.io/joshuatz&apos;&gt;@joshuatz&amp;#x3C;/a&gt;) on &amp;#x3C;a href=&apos;http://codepen.io&apos;&gt;CodePen&amp;#x3C;/a&gt;.
&lt;/iframe&gt;
&lt;h3 id=&quot;Building_Step1&quot;&gt;Step 1 - Load Wijmo resources, widgets, and set initial options&lt;/h3&gt;
&lt;p&gt;The first step is to load all the necessary Wijmo resources (JS, CSS, etc). It is best to do this in the head of your document. Here is how I did it:&lt;/p&gt;
&lt;div class=&quot;Code_Example&quot;&gt;
&lt;div style=&quot;background: #ffffff; overflow:auto;width:auto;border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;height:450px&quot;&gt;&lt;pre style=&quot;margin: 0; line-height: 125%&quot;&gt;&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;head&gt;&lt;/span&gt;
&lt;span style=&quot;color: #888888&quot;&gt;&amp;#x3C;!--         Wijmo Dependencies       --&gt;&lt;/span&gt;
&lt;span style=&quot;color: #888888&quot;&gt;&amp;#x3C;!--Theme--&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;link&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;href=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;&quot;http://cdn.wijmo.com/themes/aristo/jquery-wijmo.css&quot;&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;rel=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;&quot;stylesheet&quot;&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;type=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;&quot;text/css&quot;&lt;/span&gt; &lt;span style=&quot;color: #007700&quot;&gt;/&gt;&lt;/span&gt;
&lt;p&gt;&lt;span style=&quot;color: #888888&quot;&gt;&amp;#x3C;!—Wijmo Widgets CSS—&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;link&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;href=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“&lt;a href=&quot;http://cdn.wijmo.com/jquery.wijmo-pro.all.3.20153.83.min.css&quot;&gt;http://cdn.wijmo.com/jquery.wijmo-pro.all.3.20153.83.min.css&lt;/a&gt;”&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;rel=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“stylesheet”&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;type=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“text/css”&lt;/span&gt; &lt;span style=&quot;color: #007700&quot;&gt;/&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #888888&quot;&gt;&amp;#x3C;!—RequireJs—&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;script &lt;/span&gt;&lt;span style=&quot;color: #0000CC&quot;&gt;type=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“text/javascript”&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;src=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“&lt;a href=&quot;http://cdn.wijmo.com/external/require.js&quot;&gt;http://cdn.wijmo.com/external/require.js&lt;/a&gt;”&lt;/span&gt;&lt;span style=&quot;color: #007700&quot;&gt;&gt;&amp;#x3C;/script&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #888888&quot;&gt;&amp;#x3C;!—Config Jquery for Wijmo—&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;script &lt;/span&gt;&lt;span style=&quot;color: #0000CC&quot;&gt;type=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“text/javascript”&lt;/span&gt;&lt;span style=&quot;color: #007700&quot;&gt;&gt;&lt;/span&gt;
requirejs.config({
baseUrl&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;“&lt;a href=&quot;http://cdn.wijmo.com/amd-js/3.20153.83&quot;&gt;http://cdn.wijmo.com/amd-js/3.20153.83&lt;/a&gt;”&lt;/span&gt;,
paths&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; {
&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“jquery”&lt;/span&gt;&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;“jquery-1.11.1.min”&lt;/span&gt;,
&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“jquery-ui”&lt;/span&gt;&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;“jquery-ui-1.11.0.custom.min”&lt;/span&gt;,
&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“jquery.ui”&lt;/span&gt;&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;“jquery-ui”&lt;/span&gt;,
&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“jquery.mousewheel”&lt;/span&gt;&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;“jquery.mousewheel.min”&lt;/span&gt;,
&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“globalize”&lt;/span&gt;&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;“globalize.min”&lt;/span&gt;
}
});
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;/script&gt;&lt;/span&gt;
&lt;span style=&quot;color: #888888&quot;&gt;&amp;#x3C;!—         End Wijmo Dependencies       —&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #888888&quot;&gt;&amp;#x3C;!—		 Setup Wijmo ComboBox		  —&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;script&gt;&lt;/span&gt;
require([&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“wijmo.wijcombobox”&lt;/span&gt;], &lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt; () {
$(&lt;span style=&quot;color: #007020&quot;&gt;document&lt;/span&gt;).ready(&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt; () {
$(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“#Wikipedia_ComboBox”&lt;/span&gt;).wijcombobox({
data&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; [{
label&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;‘Please start typing to get suggestions.’&lt;/span&gt;,
value&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;‘null’&lt;/span&gt;
}]
});
$(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“#Wikipedia_ComboBox”&lt;/span&gt;).wijcombobox({
select &lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt;(){afterSelection()}
});
$(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“#Wikipedia_ComboBox”&lt;/span&gt;).wijcombobox(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“option”&lt;/span&gt;,&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“autoComplete”&lt;/span&gt;,&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;false&lt;/span&gt;); &lt;span style=&quot;color: #888888&quot;&gt;//turn off auto-complete&lt;/span&gt;
$(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“#Wikipedia_ComboBox”&lt;/span&gt;).wijcombobox(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“option”&lt;/span&gt;,&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“forceSelectionText”&lt;/span&gt;,&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;false&lt;/span&gt;);
});
});
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;/script&gt;&lt;/span&gt;
&lt;span style=&quot;color: #888888&quot;&gt;&amp;#x3C;!—			End WijmoComboBox Setup				—&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;/head&gt;&lt;/span&gt;
&lt;/p&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;You don&apos;t really need to worry about the above HTML, just swap &quot;Wikipedia_ComboBox&quot; for the ID of your own ComboBox. The only thing to note is that the&lt;/p&gt; &lt;pre style=&quot;margin-left:30px;&quot;&gt;$(&quot;#Wikipedia_ComboBox&quot;).wijcombobox({select : function(){afterSelection()}});&lt;/pre&gt; &lt;p&gt;code is a cool trick - Wijmo is letting me define a custom function I want to have called after a user makes a selection within the ComboBox, which in this case, is my afterSelection function (see more later).&lt;/p&gt;
&lt;h3 id=&quot;Building_Step2&quot;&gt;Step 2: Write out your HTML&lt;/h3&gt;
&lt;p&gt;Obviously, this step is going to be very different for each person and their needs. In my case, I&apos;m just creating a demo, so my HTML is very simple - a single input that will get turned into a combobox, and an extra div where the selected Wikipedia link will be displayed.&lt;/p&gt;
&lt;div class=&quot;Code_Example&quot;&gt;
&lt;!-- HTML generated using hilite.me --&gt;&lt;div style=&quot;background: #ffffff; overflow:auto;width:auto;border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;&quot;&gt;&lt;pre style=&quot;margin: 0; line-height: 125%&quot;&gt;&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;body&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;input&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;id=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;&quot;Wikipedia_ComboBox&quot;&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;placeholder=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;&quot;Type to Search and Use Dropdown&quot;&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;class=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;&quot;wijmo-wijcombobox wijcombobox&quot;&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;style=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;&quot;width:500px; height:50px;&quot;&lt;/span&gt;&lt;span style=&quot;color: #007700&quot;&gt;/&gt;&lt;/span&gt;
&lt;p&gt;&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;div&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;id=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“Entry_Selection”&lt;/span&gt;&lt;span style=&quot;color: #007700&quot;&gt;&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;div&gt;&lt;/span&gt;Here is the link of your selected Wikipedia entry: &lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;/div&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;div&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;id=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“Wikipedia_Link_Area”&lt;/span&gt;&lt;span style=&quot;color: #007700&quot;&gt;&gt;&amp;#x3C;a&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;href=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“null”&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;target=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“_blank”&lt;/span&gt; &lt;span style=&quot;color: #0000CC&quot;&gt;id=&lt;/span&gt;&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“Wikipedia_Link”&lt;/span&gt;&lt;span style=&quot;color: #007700&quot;&gt;&gt;&lt;/span&gt;Entry Link&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;/a&gt;&amp;#x3C;/div&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;/div&gt;&lt;/span&gt;
&lt;span style=&quot;color: #007700&quot;&gt;&amp;#x3C;/body&gt;&lt;/span&gt;
&lt;/p&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;Building_Step3&quot;&gt;Step 3: Write out the Javascript!&lt;/h3&gt;
&lt;p&gt;Here is the basic flow of my Javascript:&lt;/p&gt;&lt;p&gt;
&lt;/p&gt;&lt;ol&gt;
	&lt;li&gt;User enters a character into the combobox, which triggers a new query&lt;/li&gt;
  &lt;li&gt;I use the contents of the ComboBox as the input for my Wikipedia search (the search parameter). I use the JSONP-hack function to make the request with my query and send the result to my callback function&lt;/li&gt;
    &lt;li&gt;After a response is received from the Wikipedia API, my custom callback function is triggered. It parses the return data (which is JSON formatted) into an array, which is then passed to the Wijmo combobox &lt;a href=&quot;http://wijmo.com/docs/wijmo/Wijmo~wijmo.combobox.wijcombobox.options~data.html&quot; target=&quot;_blank&quot;&gt;via the data option&lt;/a&gt;. The standard format for the Wijmo data option is [{label: label, value: value}].&lt;/li&gt;
  &lt;li&gt;The user selects a Wikipedia entry from the dropdown auto-suggest list&lt;/li&gt;
    &lt;li&gt;Wijmo remembers I gave it a function to call if a selection is made, so it triggers that function. My function kicks in and updates the Wikipedia Link section with a link to the selected Wikipedia entry.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And, here is all the Javascript:&lt;/p&gt;
&lt;div class=&quot;Code_Example&quot;&gt;
&lt;!-- HTML generated using hilite.me --&gt;&lt;div style=&quot;background: #ffffff; overflow:auto;width:auto;border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;&quot;&gt;&lt;pre style=&quot;margin: 0; line-height: 125%&quot;&gt;&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;var&lt;/span&gt; dynamicData &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; [];
&lt;p&gt;&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt; jsonp(url, callback) {
&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;var&lt;/span&gt; callbackName &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;‘jsonp_callback_’&lt;/span&gt; &lt;span style=&quot;color: #333333&quot;&gt;+&lt;/span&gt; &lt;span style=&quot;color: #007020&quot;&gt;Math&lt;/span&gt;.round(&lt;span style=&quot;color: #0000DD; font-weight: bold&quot;&gt;100000&lt;/span&gt; &lt;span style=&quot;color: #333333&quot;&gt;*&lt;/span&gt; &lt;span style=&quot;color: #007020&quot;&gt;Math&lt;/span&gt;.random());
&lt;span style=&quot;color: #007020&quot;&gt;window&lt;/span&gt;[callbackName] &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt;(data) {
&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;delete&lt;/span&gt; &lt;span style=&quot;color: #007020&quot;&gt;window&lt;/span&gt;[callbackName];
&lt;span style=&quot;color: #007020&quot;&gt;document&lt;/span&gt;.body.removeChild(script);
callback(data);
};&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;span style=&quot;color: #008800; font-weight: bold&quot;&gt;var&amp;#x3C;/span&gt; script &amp;#x3C;span style=&quot;color: #333333&quot;&gt;=&amp;#x3C;/span&gt; &amp;#x3C;span style=&quot;color: #007020&quot;&gt;document&amp;#x3C;/span&gt;.createElement(&amp;#x3C;span style=&quot;background-color: #fff0f0&quot;&gt;&amp;#x26;#39;script&amp;#x26;#39;&amp;#x3C;/span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;script.src &amp;#x3C;span style=&quot;color: #333333&quot;&gt;=&amp;#x3C;/span&gt; url &amp;#x3C;span style=&quot;color: #333333&quot;&gt;+&amp;#x3C;/span&gt; (url.indexOf(&amp;#x3C;span style=&quot;background-color: #fff0f0&quot;&gt;&amp;#x26;#39;?&amp;#x26;#39;&amp;#x3C;/span&gt;) &amp;#x3C;span style=&quot;color: #333333&quot;&gt;&amp;#x26;gt;=&amp;#x3C;/span&gt; &amp;#x3C;span style=&quot;color: #0000DD; font-weight: bold&quot;&gt;0&amp;#x3C;/span&gt; &amp;#x3C;span style=&quot;color: #333333&quot;&gt;?&amp;#x3C;/span&gt; &amp;#x3C;span style=&quot;background-color: #fff0f0&quot;&gt;&amp;#x26;#39;&amp;#x26;amp;&amp;#x26;#39;&amp;#x3C;/span&gt; &amp;#x3C;span style=&quot;color: #333333&quot;&gt;:&amp;#x3C;/span&gt; &amp;#x3C;span style=&quot;background-color: #fff0f0&quot;&gt;&amp;#x26;#39;?&amp;#x26;#39;&amp;#x3C;/span&gt;) &amp;#x3C;span style=&quot;color: #333333&quot;&gt;+&amp;#x3C;/span&gt; &amp;#x3C;span style=&quot;background-color: #fff0f0&quot;&gt;&amp;#x26;#39;callback=&amp;#x26;#39;&amp;#x3C;/span&gt; &amp;#x3C;span style=&quot;color: #333333&quot;&gt;+&amp;#x3C;/span&gt; callbackName;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;span style=&quot;color: #007020&quot;&gt;document&amp;#x3C;/span&gt;.body.appendChild(script);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt; handleAjaxResponse(input){
&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;var&lt;/span&gt; temp &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #007020&quot;&gt;document&lt;/span&gt;.getElementById(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“Wikipedia_ComboBox”&lt;/span&gt;).value;
dynamicData &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; []; &lt;span style=&quot;color: #888888&quot;&gt;// reset our data source before filling it with resopnse&lt;/span&gt;
&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;for&lt;/span&gt; (x &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #0000DD; font-weight: bold&quot;&gt;0&lt;/span&gt;; x &lt;span style=&quot;color: #333333&quot;&gt;&amp;#x3C;&lt;/span&gt; input[&lt;span style=&quot;color: #0000DD; font-weight: bold&quot;&gt;1&lt;/span&gt;].length; x&lt;span style=&quot;color: #333333&quot;&gt;++&lt;/span&gt;){
dynamicData.push({&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“label”&lt;/span&gt;&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; input[&lt;span style=&quot;color: #0000DD; font-weight: bold&quot;&gt;1&lt;/span&gt;][x].toString(), &lt;span style=&quot;background-color: #fff0f0&quot;&gt;“value”&lt;/span&gt;&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; input[&lt;span style=&quot;color: #0000DD; font-weight: bold&quot;&gt;3&lt;/span&gt;][x].toString()});
}
$(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“#Wikipedia_ComboBox”&lt;/span&gt;).wijcombobox({
data&lt;span style=&quot;color: #333333&quot;&gt;:&lt;/span&gt; dynamicData
});
&lt;span style=&quot;color: #007020&quot;&gt;document&lt;/span&gt;.getElementById(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“Wikipedia_ComboBox”&lt;/span&gt;).value &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; temp; &lt;span style=&quot;color: #888888&quot;&gt;// Necessary for a stupid reason; manipulating the data for the combobox seems to reset the input field, which we don’t want&lt;/span&gt;
}&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt; afterSelection(){
setTimeout(&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt;(){ &lt;span style=&quot;color: #888888&quot;&gt;// setTimeout is necessary to avoid strange results due to wijmo/ajax delay &lt;/span&gt;
console.log(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“An entry has been selected”&lt;/span&gt;);
&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;var&lt;/span&gt; Wiki_Link &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #007020&quot;&gt;document&lt;/span&gt;.getElementById(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“Wikipedia_Link”&lt;/span&gt;);
Wiki_Link.href &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; $(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“#Wikipedia_ComboBox”&lt;/span&gt;).wijcombobox(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“option”&lt;/span&gt;,&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“selectedValue”&lt;/span&gt;);
Wiki_Link.innerHTML &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt;  &lt;span style=&quot;color: #007020&quot;&gt;document&lt;/span&gt;.getElementById(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“Wikipedia_ComboBox”&lt;/span&gt;).value;
},&lt;span style=&quot;color: #0000DD; font-weight: bold&quot;&gt;500&lt;/span&gt;);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #007020&quot;&gt;document&lt;/span&gt;.getElementById(&lt;span style=&quot;background-color: #fff0f0&quot;&gt;“Wikipedia_ComboBox”&lt;/span&gt;).onkeyup &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt; (){
&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;var&lt;/span&gt; query &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;this&lt;/span&gt;.value;
&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;var&lt;/span&gt; URL &lt;span style=&quot;color: #333333&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;background-color: #fff0f0&quot;&gt;“&lt;a href=&quot;https://en.wikipedia.org/w/api.php?format=json&amp;#x26;amp;action=opensearch&amp;#x26;amp;limit=10&amp;#x26;amp;search=&quot;&gt;https://en.wikipedia.org/w/api.php?format=json&amp;#x26;amp;action=opensearch&amp;#x26;amp;limit=10&amp;#x26;amp;search=&lt;/a&gt;“&lt;/span&gt;&lt;span style=&quot;color: #333333&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color: #007020&quot;&gt;encodeURI&lt;/span&gt;(query);
jsonp(URL,&lt;span style=&quot;color: #008800; font-weight: bold&quot;&gt;function&lt;/span&gt;(data){handleAjaxResponse(data);});
}
&lt;/p&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ComboBox_Issues&quot;&gt;
&lt;h2&gt;ComboBox Issues&lt;/h2&gt;
&lt;h3 id=&quot;HTML_Input&quot;&gt;ComboBox is not a native HTML input! &lt;/h3&gt;
&lt;p&gt;A Combobox, as its name might imply, is really a mashup of separate entities; the &lt;a href=&quot;http://www.w3schools.com/tags/tag_input.asp&quot; target=&quot;_blank&quot;&gt;standard HTML input tag&lt;/a&gt; with a &quot;text&quot; type, and the &lt;a href=&quot;http://www.w3schools.com/tags/tag_select.asp&quot; target=&quot;_blank&quot;&gt;HTML select tag&lt;/a&gt; (drop-down selector). Thus, the only real way that a combobox can be created is through Javascript. Luckily, there are several JS UI frameworks that have pre-built comboboxes to use. Here are some nice ones:&lt;/p&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;http://www.telerik.com/kendo-ui&quot; target=&quot;_blank&quot;&gt;Kendo UI&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://www.shieldui.com/&quot; target=&quot;_blank&quot;&gt;Shield UI&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;http://webix.com/&quot; target=&quot;_blank&quot;&gt;Webix&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;http://wijmo.com&quot; target=&quot;_blank&quot;&gt;Wijmo 3 &amp;#x26; 5&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;Cross_Domain&quot;&gt;HTML has a lot of Cross-Domain Restrictions!&lt;/h3&gt;
&lt;p&gt;Restrictions on the ability of Javascript and HTML to make network requests and access cross-domain content is very good for the sake of the user; this prevents embedded banner ads from executing malicious attacks and other various attack vectors. Unfortunately, it also makes integrating APIs and third-party tools into a first-party page very tricky.&lt;/p&gt;
&lt;p&gt;For example, many sites will let you embed a generated &amp;#x3C;form&gt; into your page, but only through an iframe. The embedded iframe keeps the source of the original site, which means it has a different domain than your own site that you are embedding it into it. Now, due to the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy&quot; target=&quot;_blank&quot;&gt;same-origin policy&lt;/a&gt;, you can&apos;t interact with it via Javascript. Bummer.&lt;/p&gt;
&lt;p&gt;OK, so forms are out as a way to interact with third party content. How about actual HTTP requests? The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest&quot; target=&quot;_blank&quot;&gt;XMLHttpRequest API&lt;/a&gt; is a super easy way to send and receive request via vanilla Javascript and even allows us to do advanced things like specify the request method and whether we are making an asynchronous request or not. Oh wait... the XMLHttpRequest method is also subject to the browser&apos;s cross-domain policy! So, you can make requests to your own domain, but requests to other domains are out, unless they have &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS&quot; target=&quot;_blank&quot;&gt;CORS enabled&lt;/a&gt; for your specific domain (which 99% of the time means you are SOL).&lt;/p&gt;
&lt;p&gt;Argh... OK, so how about a compromise? What if we use server-side PHP with cURL to make and receive the request, while client side Javascript talks to that PHP page? Success! That will work! Of course, we are basically making our own API on our own domain, which is fine if we are building a first-party system, but for accessing a third-party API/datasource means double the work with double the latency. Another issue is that now instead of request originating from the user&apos;s IP, 100% of request are originating from our server, which without IP rotation, means all from the same IP. Thus, very easy to get either banned or rate-limited access to a third-party API. Not to mention increased use of our bandwidth and server processing resources. However, this is an OK tool &lt;a href=&quot;https://mondaybynoon.com/avoiding-iframes-via-php-and-curl/&quot; target=&quot;_blank&quot;&gt;for quick and dirty iframe-busting&lt;/a&gt;. Otherwise, not a great solution for integrating third-party APIs. Bummer.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;Further_Reading&quot;&gt;
&lt;h2&gt;Comboboxes and AJAX - More Resources:&lt;/h2&gt;
&lt;ul&gt;
	&lt;li&gt;&lt;a href=&quot;http://json-p.org/&quot; target=&quot;_blank&quot;&gt;Summary of JSON-P and cross-domain AJAX issues&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;http://www.ajax-cross-origin.com/how.html#js-cross-domain-methods&quot; target=&quot;_blank&quot;&gt;Summary of cross-domain AJAX request workaround methods&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://www.getpostman.com/&quot; target=&quot;_blank&quot;&gt;Postman&lt;/a&gt; - a very useful tool for playing around with HTTP requests, especially if you are trying to understand third-party APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Google Scripts - ExaVault Direct Link Generator</title><link>https://joshuatz.com/projects/web-stuff/google-scripts---exavault-direct-link-generator/</link><guid isPermaLink="true">https://joshuatz.com/projects/web-stuff/google-scripts---exavault-direct-link-generator/</guid><description>A code snippet I developed to have Google Scripts generate a direct link to a file in ExaVault via the ExaVault API, so it can be used with the Google Scripts URLFetchApp service.</description><pubDate>Mon, 11 Jan 2016 06:00:00 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;ExaVault is essentially a service that provides file hosting with a simple directory interface, but has the distinct feature of offering FTP access and advanced permissions (which is why its user base is mainly businesses / enterprise). To make part of my job easier at work, I wanted to integrate our ExaVault access into a Google Scripts project I was working on. I wanted to have my Google Script retrieve a CSV from our ExaVault folder and parse and process it into a specific format. However, I quickly ran into an issue.&lt;/p&gt;
&lt;p&gt;ExaVault, by default, &lt;a href=&quot;https://www.exavault.com/news/item/21/linking-directly-to-files-in-your-ftp-account/&quot; target=&quot;_blank&quot;&gt;provides links to files through the FTP protocol&lt;/a&gt;. The links look like “ftp://links:&lt;a href=&quot;mailto:pass@test.exavault.com&quot;&gt;pass@test.exavault.com&lt;/a&gt;/transfer.pdf”. Unfortunately, the &lt;a href=&quot;https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app&quot; target=&quot;_blank&quot;&gt;UrlFetchApp&lt;/a&gt;, which is a service Google Scripts uses to handle web requests, only works with HTTP/HTTPS requests/responses, and not the FTP protocol. So, I needed to figure out how get a direct link to my CSV in ExaVault, instead of a FTP link. The solution? &lt;a href=&quot;https://www.exavault.com/developer/api-docs/#!/v1/getDownloadFileUrl&quot; target=&quot;_blank&quot;&gt;Use the ExaVault API to generate a temporary direct link&lt;/a&gt;. It ended up being much easier than it sounds - ExaVault’s API is extremely easy to use.&lt;/p&gt;
&lt;p&gt;Here is my reusable code (below) - the return of my function is the direct download link to your file, which you can then pass to the URLFetchApp. Feel free to modify/reuse/do whatever you want with it (attribution not required, but is always appreciated). Username, password, and filepath are all mandatory arguments for the function, whereas opt_downloadname is optional. Make sure “api_key” is declared and set to your api key value (you will need to do a one-time generation of an API key here: &lt;a href=&quot;https://clients.exavault.com/clientarea.php?action=products&quot; target=&quot;_blank&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://clients.exavault.com/clientarea.php?action=products&quot;&gt;https://clients.exavault.com/clientarea.php?action=products&lt;/a&gt;).&lt;/p&gt;
&lt;!-- HTML generated using hilite.me --&gt;
&lt;div style=&quot;background: #272822; overflow:auto;width:auto;border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;&quot;&gt;
&lt;pre style=&quot;margin: 0; line-height: 125%&quot;&gt;&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;api_key&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #e6db74&quot;&gt;&quot;PUT_YOUR_API_KEY_HERE&quot;&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;;&lt;/span&gt; &lt;span style=&quot;color: #FF9800&quot;&gt;// DO NOT SHARE WITH ANYONE, PRIVATE API KEY FOR EXAVAULT&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;access_token&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;;&lt;/span&gt;
&lt;p&gt;&lt;span style=&quot;color: #66d9ef&quot;&gt;function&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;Exavault_URL_Fetcher&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;username&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;password&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;filepath&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;opt_downloadname&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;)&lt;/span&gt; &lt;span style=&quot;color: #f8f8f2&quot;&gt;{&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;done&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #66d9ef&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;;&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;downloadname&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;opt_downloadname&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;||&lt;/span&gt; &lt;span style=&quot;color: #e6db74&quot;&gt;“downloadfile”&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;);&lt;/span&gt; &lt;span style=&quot;color: #FF9800&quot;&gt;// use the optional download file name if provided, otherwise default to “downloadfile”&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;while&lt;/span&gt; &lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;done&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;==&lt;/span&gt; &lt;span style=&quot;color: #66d9ef&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;){&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;if&lt;/span&gt; &lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;access_token&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;==&lt;/span&gt; &lt;span style=&quot;color: #66d9ef&quot;&gt;undefined&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;){&lt;/span&gt;
&lt;span style=&quot;color: #a6e22e&quot;&gt;Logger&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #e6db74&quot;&gt;“Retreiving access token using user+pass”&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;);&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;payload&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #f8f8f2&quot;&gt;{&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“username”&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;username&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“password”&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;password&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“api_key”&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;api_key&lt;/span&gt;
&lt;span style=&quot;color: #f8f8f2&quot;&gt;};&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;options&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #f8f8f2&quot;&gt;{&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“method”&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #e6db74&quot;&gt;“post”&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“payload”&lt;/span&gt;&lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;payload&lt;/span&gt;
&lt;span style=&quot;color: #f8f8f2&quot;&gt;};&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;response&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;UrlFetchApp&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;fetch&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #e6db74&quot;&gt;“&lt;a href=&quot;https://api.exavault.com:443/v1/authenticateUser&quot;&gt;https://api.exavault.com:443/v1/authenticateUser&lt;/a&gt;”&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;options&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;)&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;parsed&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;);&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;token&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;parsed&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;results&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;accessToken&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;;&lt;/span&gt;
&lt;span style=&quot;color: #a6e22e&quot;&gt;access_token&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;token&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;;&lt;/span&gt;
&lt;span style=&quot;color: #f8f8f2&quot;&gt;}&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;else&lt;/span&gt; &lt;span style=&quot;color: #f8f8f2&quot;&gt;{&lt;/span&gt;
&lt;span style=&quot;color: #a6e22e&quot;&gt;Logger&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;log&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #e6db74&quot;&gt;“Retreiving Download URL using token+key”&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;);&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;payload&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #f8f8f2&quot;&gt;{&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“access_token”&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;access_token&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“filePaths”&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;filepath&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“downloadName”&lt;/span&gt;&lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;opt_downloadname&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“api_key”&lt;/span&gt;&lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;api_key&lt;/span&gt;
&lt;span style=&quot;color: #f8f8f2&quot;&gt;};&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;options&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #f8f8f2&quot;&gt;{&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“method”&lt;/span&gt;&lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #e6db74&quot;&gt;“post”&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt;
&lt;span style=&quot;color: #e6db74&quot;&gt;“payload”&lt;/span&gt;&lt;span style=&quot;color: #f92672&quot;&gt;:&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;payload&lt;/span&gt;
&lt;span style=&quot;color: #f8f8f2&quot;&gt;};&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;response&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;UrlFetchApp&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;fetch&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #e6db74&quot;&gt;“&lt;a href=&quot;https://api.exavault.com:443/v1/getDownloadFileUrl&quot;&gt;https://api.exavault.com:443/v1/getDownloadFileUrl&lt;/a&gt;”&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;,&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;options&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;)&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;parsed&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;);&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;var&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;downloadURL&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;parsed&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;results&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #a6e22e&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;;&lt;/span&gt;
&lt;span style=&quot;color: #66d9ef&quot;&gt;return&lt;/span&gt; &lt;span style=&quot;color: #a6e22e&quot;&gt;downloadURL&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;;&lt;/span&gt;
&lt;span style=&quot;color: #a6e22e&quot;&gt;done&lt;/span&gt; &lt;span style=&quot;color: #f92672&quot;&gt;=&lt;/span&gt; &lt;span style=&quot;color: #66d9ef&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color: #f8f8f2&quot;&gt;;&lt;/span&gt;
&lt;span style=&quot;color: #f8f8f2&quot;&gt;}&lt;/span&gt;
&lt;span style=&quot;color: #f8f8f2&quot;&gt;}&lt;/span&gt;
&lt;span style=&quot;color: #f8f8f2&quot;&gt;}&lt;/span&gt;
&lt;/p&gt;&lt;/pre&gt;&lt;p&gt;&lt;/p&gt;
&lt;/div&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Photography Gallery</title><link>https://joshuatz.com/projects/other/photography-gallery/</link><guid isPermaLink="true">https://joshuatz.com/projects/other/photography-gallery/</guid><description>Some photography samples of mine.</description><pubDate>Wed, 29 Apr 2015 11:21:46 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;style&gt;#gallery,#maincontent table,h1,h2{text-align:center}#gallery{margin-left:auto;margin-right:auto;display:block}#maincontent #bannerimage img{text-align:center;display:block;box-shadow:4px 2px 40px rgba(0,0,0,.61);-moz-box-shadow:4px 2px 40px rgba(0,0,0,.61);-webkit-box-shadow:4px 2px 40px rgba(0,0,0,.61);margin:20px auto}#maincontent #gallery img{margin:.5%;width:80%;max-width:240px;box-shadow:4px 2px 40px rgba(0,0,0,.61);-moz-box-shadow:4px 2px 40px rgba(0,0,0,.61);-webkit-box-shadow:4px 2px 40px rgba(0,0,0,.61)}.spacer{background-color:#FFF;height:20%;min-height:20px;width:100%}h3{text-height:1}&lt;/style&gt;
&lt;h2 style=&quot;font-size:24px; display:block;&quot;&gt;Here are some samples of my basic photography skills&lt;/h2&gt;
&lt;div id=&quot;maincontent&quot;&gt;
&lt;div id=&quot;bannerimage&quot;&gt;
		&lt;img src=&quot;/media/Photography_Samples/Moon_WM2.jpg&quot; width=&quot;80%&quot; height=&quot;auto&quot; style=&quot;max-width:630px&quot; class=&quot;wow fadeIn&quot;&gt;&lt;/div&gt;
&lt;h3&gt;Note:&lt;/h3&gt;
If you want to see a larger version of any the pictures below, click the thumbnail for an enlarged view.
&lt;h3&gt;Gallery:&lt;/h3&gt;
&lt;div id=&quot;gallery&quot;&gt;
		&lt;a href=&quot;/media/Photography_Samples/Yaquina_Bay_Bridge.jpg&quot; data-fancybox=&quot;gallery&quot;&gt;&lt;img src=&quot;/media/Photography_Samples/Yaquina_Bay_Bridge_TN.jpg&quot; height=&quot;auto&quot; class=&quot;wow bounce&quot; data-wow-delay=&quot;1s&quot;&gt;&lt;/a&gt;
&lt;a href=&quot;/media/Photography_Samples/Flicker_Woodpecker.JPG&quot; data-fancybox=&quot;gallery&quot;&gt;&lt;img src=&quot;/media/Photography_Samples/Flicker_Woodpecker_TN.JPG&quot; height=&quot;auto&quot; class=&quot;wow bounce&quot; data-wow-delay=&quot;1.5s&quot;&gt;&lt;/a&gt;
&lt;a href=&quot;/media/Photography_Samples/KenmoreAir.JPG&quot; data-fancybox=&quot;gallery&quot;&gt;&lt;img src=&quot;/media/Photography_Samples/KenmoreAir_TN.JPG&quot; height=&quot;auto&quot; class=&quot;wow bounce&quot; data-wow-delay=&quot;2s&quot;&gt;&lt;/a&gt;
&lt;a href=&quot;/media/Photography_Samples/ParmesanCrustedTilapia.JPG&quot; data-fancybox=&quot;gallery&quot;&gt;&lt;img src=&quot;/media/Photography_Samples/ParmesanCrustedTilapia_TN.JPG&quot; height=&quot;auto&quot; class=&quot;wow bounce&quot; data-wow-delay=&quot;2.5s&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Stukent Online Marketing Simulation (Competition)</title><link>https://joshuatz.com/projects/marketing/stukent-online-marketing-simulation-competition/</link><guid isPermaLink="true">https://joshuatz.com/projects/marketing/stukent-online-marketing-simulation-competition/</guid><description>Click the image above to see how I successfully managed a student team in an electronic marketing class and led us to rank #1 overall in an online marketing simulation competition. I also managed to increase ROI to more than double its original amount, decrease cost-per-action to less than half, and boost our conversion ratio from 7.6% to 13.54%.</description><pubDate>Sun, 26 Apr 2015 05:00:00 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;h2 style=&quot;font-size:24px; display:block;&quot;&gt;Here is an overview of my work on a marketing simulation (called &quot;Stukent&quot;)&lt;/h2&gt;
&lt;div id=&quot;maincontent&quot;&gt;
&lt;img src=&quot;/media/Stukent_Promo_TS.png&quot; style=&quot;max-width:630px; width:80%; height:auto; margin-left:auto; margin-right:auto; display:block;&quot;&gt;
&lt;h3&gt;Overview:&lt;/h3&gt;
In December of 2014, I had the opportunity to use the world&apos;s first online marketing simulation, named &quot;&lt;a href=&quot;http://www.stukent.com/&quot; title=&quot;Link to Stukent Website&quot; target=&quot;_blank&quot;&gt;Stukent&lt;/a&gt;&quot;, in a class I was taking on electronic marketing. In a class of 43 students, we competed against each other in small groups over the course of the quarter, with a total of 6 rounds. The goal was to generate as many sales as possible of various tablets, and with the highest return on investment. I was selected team leader and ended up making 95% of the decisions over the course of the simulation, which meant setting up landing pages, creating email campaigns, managing SEM (both organic and paid), selecting keywords and adjusting bid amounts, and creating ad copy. I kept track of all metrics and decisions in Excel, and by the end of the course, was running 23 ad campaigns, 19 email campaigns (with multiple A/B tests), and 21 landing pages (with an average score of 91/100).
&lt;h3&gt;Results:&lt;/h3&gt;
&lt;img src=&quot;/media/Stukent_ValueVsCost.png&quot; style=&quot;max-width:974px; width:95%; height:auto; margin-left:auto; margin-right:auto; display:block;&quot;&gt;
&lt;p&gt;In one sentence: We placed in the two top for every single round, and &lt;strong&gt;were the #1 team&lt;/strong&gt; at the end of the simulation, &lt;strong&gt;placing first in ROAS/ROI, traffic, market share&lt;/strong&gt;… essentially every single metric.&lt;/p&gt;
&lt;p&gt;Here is a breakdown of the improvement in our metrics by round:&lt;/p&gt;
&lt;table width=&quot;98%&quot; border=&quot;1&quot; cellspacing=&quot;1px&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th width=&quot;11%&quot; scope=&quot;col&quot;&gt;Round&lt;/th&gt;
&lt;th width=&quot;15%&quot; scope=&quot;col&quot;&gt;Impressions&lt;/th&gt;
&lt;th width=&quot;9%&quot; scope=&quot;col&quot;&gt;Clicks&lt;/th&gt;
&lt;th width=&quot;13%&quot; scope=&quot;col&quot;&gt;CTR&lt;/th&gt;
&lt;th width=&quot;8%&quot; scope=&quot;col&quot;&gt;CPC&lt;/th&gt;
&lt;th width=&quot;15%&quot; scope=&quot;col&quot;&gt;Conversion Ratio&lt;/th&gt;
&lt;th width=&quot;13%&quot; scope=&quot;col&quot;&gt;ROI&lt;/th&gt;
&lt;th width=&quot;16%&quot; scope=&quot;col&quot;&gt;CPA&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round 1&lt;/td&gt;
&lt;td&gt;67,384&lt;/td&gt;
&lt;td&gt;1,685&lt;/td&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;td&gt;$0.81&lt;/td&gt;
&lt;td&gt;7.6%&lt;/td&gt;
&lt;td&gt;2.99&lt;/td&gt;
&lt;td&gt;$10.71&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round 2&lt;/td&gt;
&lt;td&gt;75,049&lt;/td&gt;
&lt;td&gt;1,812&lt;/td&gt;
&lt;td&gt;2.41%&lt;/td&gt;
&lt;td&gt;$0.80&lt;/td&gt;
&lt;td&gt;7.51%&lt;/td&gt;
&lt;td&gt;2.99&lt;/td&gt;
&lt;td&gt;$10.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round 3&lt;/td&gt;
&lt;td&gt;65,567&lt;/td&gt;
&lt;td&gt;1,548&lt;/td&gt;
&lt;td&gt;2.36%&lt;/td&gt;
&lt;td&gt;$0.65&lt;/td&gt;
&lt;td&gt;9.56%&lt;/td&gt;
&lt;td&gt;4.46&lt;/td&gt;
&lt;td&gt;$6.83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round 4&lt;/td&gt;
&lt;td&gt;107,525&lt;/td&gt;
&lt;td&gt;2,598&lt;/td&gt;
&lt;td&gt;2.42%&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;10.66%&lt;/td&gt;
&lt;td&gt;6.43&lt;/td&gt;
&lt;td&gt;$4.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round 5&lt;/td&gt;
&lt;td&gt;180,670&lt;/td&gt;
&lt;td&gt;4,521&lt;/td&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;td&gt;$0.55&lt;/td&gt;
&lt;td&gt;12.12%&lt;/td&gt;
&lt;td&gt;6.51&lt;/td&gt;
&lt;td&gt;$4.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round 6&lt;/td&gt;
&lt;td&gt;247,848&lt;/td&gt;
&lt;td&gt;6,647&lt;/td&gt;
&lt;td&gt;2.68%&lt;/td&gt;
&lt;td&gt;$0.56&lt;/td&gt;
&lt;td&gt;13.54%&lt;/td&gt;
&lt;td&gt;7.42&lt;/td&gt;
&lt;td&gt;$4.14&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
To summarize the improvements, we more than doubled our return on investment by the end, generating $7.42 per every dollar invested. Our cost per action decreased to less than 50% of its original value, as a result of our CPC falling even as click-through-rate, impressions, and conversion ratio all rose.
&lt;p&gt;Even if you average our scores across the entire simulation, we still came out as the top ranking team:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Stukent_TopTeams.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/media/Stukent_TopTeams.png&quot; style=&quot;max-width:800px; width:95%; height:auto; margin-left:auto; margin-right:auto; display:block;&quot; class=&quot;wow fadeInUp&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>School Traffic Light: Timer and Noise Detector</title><link>https://joshuatz.com/projects/electronics/school-traffic-light-timer-and-noise-detector/</link><guid isPermaLink="true">https://joshuatz.com/projects/electronics/school-traffic-light-timer-and-noise-detector/</guid><description>Another project involving both electronics and programming: a traffic light for a classroom that can indicate to students how much time is left to work on an assignment, or what the noise level of the classroom is at.</description><pubDate>Sun, 12 Apr 2015 00:00:48 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;style&gt;#copyright,#postbody{margin-left:auto;margin-right:auto}#titleofproject,#videosection,.image,.image img{text-align:center}#postbody li,h2{margin-left:10%}#postbody{width:95%}#titleofproject{font-size:24px;font-weight:700}#copyright{font-family:&quot;Courier New&quot;,Courier,monospace;border-style:solid;border-width:1px;width:75%;display:block;margin-top:20px}.image img{height:auto;width:90%;max-width:500px}h2{text-decoration:underline}#postbody li{padding-top:20px}&lt;/style&gt;
&lt;div id=&quot;videosection&quot;&gt;
&lt;p&gt;I made a video to demonstrate the build process and features of this project - it is the fastest way to get an overview of what this is all about&lt;/p&gt;
&lt;iframe style=&quot;max-width: 560px;&quot; src=&quot;https://www.youtube-nocookie.com/embed/A8evMmvCnds?rel=0&quot; width=&quot;80%&quot; height=&quot;315&quot; frameborder=&quot;0&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;div id=&quot;copyright&quot;&gt;All content on this page is protected under copyright, except for the images, which I will allow anyone to use as long as you follow Creative Commons 3.0 Attribution License (basically, just link to this webpage).&lt;/div&gt;
&lt;div id=&quot;writeup&quot;&gt;
&lt;h2&gt;Inspiration:&lt;/h2&gt;
This project came about thanks from a suggestion from my mother, who is an elementary school teacher. I was looking for a fun new electronics project to work on, and she mentioned that she would like a way to visually let students know how much time they have left on an assignment, or to adjust their voices to match the desired volume of the room. She said she had tried existing products (such as the Time Tracker), but they were too small and complicated. She had heard of the Yacker Tracker, but had not heard great things about it, and it was rather expensive. I came up with the idea to combine the two requirements (noise and time tracking) into one device, which I could build using a microcontroller (Arduino platform) and simple electronics (transistors, buzzers, capacitors, etc).
&lt;h2&gt;Creation&lt;/h2&gt;
Rather than completely fabricate the body of a traffic light from scratch, I decided to start with a premade body. I looked around online, and my best option seemed to be a randomly blinking traffic light, which are sold as novelty items for dorm rooms / bachelor pads / etc. I found one on eBay that fit my size and price requirements, and bought it (see below).
&lt;div class=&quot;image&quot;&gt;&lt;a href=&quot;/media/School_Traffic_Light/UnBlurred.jpg&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img style=&quot;max-width: 500px;&quot; src=&quot;/media/School_Traffic_Light/UnBlurred_(Small).jpg&quot; alt=&quot;Flashing Traffic Light Box&quot; width=&quot;100%&quot;&gt;&lt;/a&gt;&lt;/div&gt;
I took the traffic light apart, and found that the bulbs were powered directly from AC, but were too dim to be visible from across the room. I ended up completely removing all the internals in the enclosure and fabricating my own bulbs from SMD LEDs (See below picture of a custom LED bulb I created).
&lt;div class=&quot;image&quot;&gt;&lt;a href=&quot;/media/School_Traffic_Light/DSC_0259_(Custom).JPG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;/media/School_Traffic_Light/DSC_0259_(Small).JPG&quot; alt=&quot;Custom SMD LED Panel for Traffic Light&quot; width=&quot;302&quot;&gt;&lt;/a&gt;&lt;/div&gt;
They are very bright, safe to use, and consume very little power. As shown in the image below, all three SMD LED panels operating at the same time only consumes a total of slightly over 200mA, low enough that a standard laptop can power the device.
&lt;div class=&quot;image&quot;&gt;&lt;a href=&quot;/media/School_Traffic_Light/DSC_0267.JPG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;/media/School_Traffic_Light/DSC_0267_(Small).JPG&quot; alt=&quot;Traffic Light SMD Led Operation&quot; width=&quot;302&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;image&quot;&gt;&lt;a href=&quot;/media/School_Traffic_Light/DSC_0278_(Custom).JPG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;/media/School_Traffic_Light/DSC_0278_(Small).JPG&quot; alt=&quot;Custom School Traffic Light&quot; width=&quot;302&quot;&gt;&lt;/a&gt;&lt;/div&gt;
A lot went into this project, but here are a few major components.
&lt;ul&gt;
 	&lt;li&gt;A small circuit (based on &lt;a href=&quot;http://www.inexglobal.com/downloads/ZX-sound_e.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;this design&lt;/a&gt;) to take the output from a standard microphone and amplify it so the Arduino can measure the sound level in the room.&lt;/li&gt;
 	&lt;li&gt;A small circuit (based on &lt;a href=&quot;http://www.instructables.com/id/Arduino-3-wire-Matrix-Keypad&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;this design&lt;/a&gt;) to take the output from a matrix keypad and send it to a single analog input pin on the Arduino.&lt;/li&gt;
 	&lt;li&gt;The actual code for the microcontroller to control everything and process user input - around 1,200 lines of C in total.&lt;/li&gt;
 	&lt;li&gt;Most of my time was spent trying to make the device as user-friendly as possible. This meant creating prompts that show up on the LCD screen to instruct the user how to input choices, logic implemented in code that prevents users from making choices that don&apos;t make sense, and controls that are easy to use and clearly indicated.&lt;/li&gt;
&lt;/ul&gt;
I&apos;m sure I left out some other aspects of what went into the build process, but it would take me forever to write up documentation on every step of the project.
&lt;h2&gt;Finished Product&lt;/h2&gt;
Here are some pictures of the final result of all my hard work!
&lt;div class=&quot;image&quot;&gt;&lt;a href=&quot;/media/School_Traffic_Light/DSC_0955.JPG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;/media/School_Traffic_Light/DSC_0955_(Small).JPG&quot; alt=&quot;Completed School Traffic Light&quot; width=&quot;302&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;image&quot;&gt;&lt;a href=&quot;/media/School_Traffic_Light/DSC_0958.JPG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;/media/School_Traffic_Light/DSC_0958_(Small).JPG&quot; alt=&quot;User Interface Side of School Traffic Light&quot; width=&quot;302&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;image&quot;&gt;&lt;a href=&quot;/media/School_Traffic_Light/DSC_0953_(Custom).JPG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;/media/School_Traffic_Light/DSC_0953_(Small).JPG&quot; alt=&quot;User Control Panel for Custom School Traffic Light&quot; width=&quot;302&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;image&quot;&gt;&lt;a href=&quot;/media/School_Traffic_Light/DSC_0965_(Custom).JPG&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;/media/School_Traffic_Light/DSC_0965_(Small).JPG&quot; alt=&quot;User Prompt on LCD&quot; width=&quot;302&quot;&gt;&lt;/a&gt;&lt;/div&gt;
I also created an instruction sheet for my mom to have as a quick reference guide to using the traffic light:
&lt;div class=&quot;image&quot;&gt;&lt;a href=&quot;/media/School_Traffic_Light/Traffic_Light_Instructions.png&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;/media/School_Traffic_Light/Traffic_Light_Instructions_(Small).png&quot; alt=&quot;School Traffic Light Instructions&quot; width=&quot;304&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;Things I Could Have Done Differently:&lt;/h3&gt;
&lt;ul&gt;
 	&lt;li&gt;Used pre-made SMD bulbs (such as those sold for auto use) instead of taking the time to solder my own on perfboard.&lt;/li&gt;
 	&lt;li&gt;Purchased a pre-made traffic light that uses LED bulbs, instead of the one that I bought (that I had to strip and ended up being built with cheap materials).&lt;/li&gt;
&lt;/ul&gt;
I actually think this turned out great, considering the cost of the individual parts and the effort that went into it. Hopefully it will survive the classroom...
&lt;h3 style=&quot;text-align: center; text-decoration: underline;&quot;&gt;Thanks for checking out my project page!&lt;/h3&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Mini Bookstore Project</title><link>https://joshuatz.com/projects/other/mini-bookstore-project/</link><guid isPermaLink="true">https://joshuatz.com/projects/other/mini-bookstore-project/</guid><description>For a gift, I created a model bookstore in Sketchup (a 3D design program), figured out how to export to 2D files, and then printed a design that I assembled in real life.</description><pubDate>Mon, 23 Jun 2014 03:59:06 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;p&gt;Just thought I would share a small project I just finished - a mini model
bookstore for my mom (Robin) as a gift. I started with a design in Sketchup (a
3D-modeling program), then flattened all the individual components into 2d
objects, printed them out, cut and glued them onto cardboard, cut the shapes
out, and glued everything together. It turned out much better than I expected,
and I was even able to add some electronics to the project (a flickering
fireplace and overhead lights) and with an auto-shutoff powered by a 555-timer
circuit.&lt;/p&gt;
&lt;div id=&quot;3d build process&quot; style=&quot;text-align:center&quot;&gt;Here is an animated GIF showing the 3D-modeling process in Sketchup:
&lt;br&gt;
&lt;img src=&quot;/media/Mini_Bookstore_Project/3D-Build-Animation.gif&quot; alt=&quot;&quot; width=&quot;640px&quot; height=&quot;480px&quot;&gt;
&lt;/div&gt;
&lt;div id=&quot;build process&quot; style=&quot;text-align:center&quot;&gt;
Here is an animated GIF showing the actual build process:
&lt;br&gt;
&lt;img src=&quot;/media/Mini_Bookstore_Project/Build-Animation.gif&quot; alt=&quot;&quot; width=&quot;640px&quot; height=&quot;480px&quot;&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div id=&quot;finished product&quot; style=&quot;text-align:center&quot;&gt;
Here are some pictures showing what the finished product looks like:
&lt;br&gt;
&lt;img src=&quot;/media/Mini_Bookstore_Project/DSC_0625.jpg&quot; alt=&quot;&quot;&gt;
&lt;br&gt;
The awesome flickering Fireplace:
&lt;br&gt;
&lt;img src=&quot;/media/Mini_Bookstore_Project/Flickering-Fireplace.gif&quot; alt=&quot;&quot; width=&quot;640px&quot; height=&quot;480px&quot;&gt;
&lt;br&gt;
&lt;img src=&quot;/media/Mini_Bookstore_Project/DSC_0613.jpg&quot; alt=&quot;&quot;&gt;
&lt;br&gt;
&lt;img src=&quot;/media/Mini_Bookstore_Project/DSC_0614.jpg&quot; alt=&quot;&quot;&gt;
&lt;br&gt;
&lt;img src=&quot;/media/Mini_Bookstore_Project/DSC_0617.jpg&quot; alt=&quot;&quot;&gt;
&lt;br&gt;
&lt;img src=&quot;/media/Mini_Bookstore_Project/Fireplace4.jpg&quot; alt=&quot;&quot;&gt;
&lt;/div&gt;
&lt;div id=&quot;behind the scenes&quot;&gt;
For those that want to know more about how this was created, here are the tools that you need:
	&lt;span&gt;&lt;ol&gt;
		&lt;li&gt;&lt;a href=&quot;http://www.sketchup.com/&quot; target=&quot;_blank&quot;&gt;Sketchup 2014&lt;/a&gt; (formerly owned by Google, no longer)&lt;/li&gt;
		&lt;li&gt;The &quot;Flattery&quot; plugin for sketchup. This will allow you to &quot;unfold&quot; models so they become 2D.
		&lt;a href=&quot;http://www.pumpkinpirate.info/flattery/&quot;&gt;Official site&lt;/a&gt; is no longer good, use
		&lt;a href=&quot;http://sketchucation.com/pluginstore?pln=Flattery&quot; target=&quot;_blank&quot;&gt;this page&lt;/a&gt; with updated plugin for 2014.&lt;/li&gt;
		&lt;li&gt;Export images to 2D using built in export option in Sketchup.
		&lt;a href=&quot;http://www.artofraz.com/free-art-tutorials/papercraft-with-google-sketchup/&quot; target=&quot;_blank&quot;&gt;This site&lt;/a&gt; has more info.&lt;/li&gt;
		&lt;li&gt;To make sure images were printed to scale, I added markers in sketchup with dimensions before printing, then in Microsoft Publisher, scaled the images until the dimensions matched the print size. You can use a multitude of image editors to accomplish this though. &lt;/li&gt;
		&lt;li&gt;Printer (any kind will do)&lt;/li&gt;
		&lt;li&gt;Lots of cardboard! I recommend recycling old shipping boxes if you order a lot from Amazon and other online retailers.&lt;/li&gt;
		&lt;li&gt;Exacto or other sharp knife to cut out cardboard (hobby knife recommended)&lt;/li&gt;
		&lt;li&gt;Hot glue gun and lots of hot glue sticks&lt;/li&gt;
	&lt;/ol&gt;&lt;/span&gt;
&lt;/div&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Wireless Water Feature Meter</title><link>https://joshuatz.com/projects/electronics/wireless-water-feature-meter/</link><guid isPermaLink="true">https://joshuatz.com/projects/electronics/wireless-water-feature-meter/</guid><description>My first programming and electronics project: a wireless water meter for an outside stream. There are two Arduino units that communicate over 2.4Ghz using the inexpensive nRF24L01+ radios, and send information about the water level of the outside stream, temperature, and humidity. Click the picture above for the full project writeup.</description><pubDate>Mon, 13 Jan 2014 01:00:07 GMT</pubDate><content:encoded>&lt;!-- WP HTML export, not converted to MD --&gt;
&lt;div id=&quot;main_post_wrapper&quot;&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;/div&gt;
&lt;div style=&quot;text-align: left;&quot;&gt;
&lt;span style=&quot;font-size: large;&quot;&gt;&lt;b&gt;&lt;u&gt;Video Overview:&lt;/u&gt;&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
This is was a long and complex project, so a video is going  to be the easiest way for me to give a very quick overview of this project. Please watch it if you have the time.
&lt;div id=&quot;video&quot; style=&quot;text-align: center;&quot;&gt;
&lt;iframe allowfullscreen frameborder=&quot;0&quot; height=&quot;315&quot; src=&quot;//www.youtube-nocookie.com/embed/q-WZ2rgpMAU?rel=0&quot; width=&quot;560&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
All content on this page is protected under full copyright (all rights reserved),  except for the images, which I will allow anyone to use as long as you follow &lt;a href=&quot;https://creativecommons.org/licenses/by/3.0/us/&quot; target=&quot;_blank&quot;&gt;Creative Commons 3.0  Attribution License&lt;/a&gt; (basically, just link to this webpage).
&lt;p&gt;Note: Click on (almost) any image on this page to see it full-sized!&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;b&gt;&lt;u&gt;Table of Contents:&lt;/u&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
 	&lt;li&gt;&lt;a href=&quot;#the_why&quot;&gt;The “Why?”&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#the_end_product&quot;&gt;The End Product&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#bill_of_materials&quot;&gt;Bill of Materials&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#beginning_of_technical_section&quot;&gt;Beginning of Technical Section&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#source_code&quot;&gt;Source Code&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#how_it_works&quot;&gt;How It Works&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#my_background&quot;&gt;My Background&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#how_it_started&quot;&gt;How It Started: The Idea&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#procedure&quot;&gt;Procedure&lt;/a&gt;&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;&lt;a href=&quot;#prototyping&quot;&gt;Prototyping&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#building&quot;&gt;Building&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#programming&quot;&gt;Programming&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;&lt;a href=&quot;#issues&quot;&gt;The Issues&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#things_i_could_have_done_differently&quot;&gt;Things I Could Have Done  Differently&lt;/a&gt;&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;#thanks&quot;&gt;Thanks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;the_why&quot; name=&quot;The_Why&quot;&gt;&lt;b&gt;&lt;u&gt;The  “Why?”&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
I’m a frequent reader of technology news sites and blogs, so  I have been hearing about Arduino and microcontrollers for a while now.  However, I have never had a class on circuits, or on programming, nor did I see  a use for them for myself, so I didn’t see much reason to start using them.  However, I decided that at some point I would give them a try, but only if I  could find a project that would actually be beneficially and serve some actual  purpose, rather than creating code that simply flashes lights and looks pretty  (a common first project for those learning to use microcontrollers).
&lt;!--more--&gt;
&lt;p&gt;Then, in July of 2013, roughly half a year ago, my parents  (mostly my dad) built a running stream in the back of their yard.&lt;/p&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://4.bp.blogspot.com/-98UaZzqydNU/UtM1Anqrk7I/AAAAAAAAAdI/QjgaZ2ZoCEk/s000/IMG_4508.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;240&quot; src=&quot;http://4.bp.blogspot.com/-98UaZzqydNU/UtM1Anqrk7I/AAAAAAAAAdI/QjgaZ2ZoCEk/s320/IMG_4508.JPG&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;u&gt;&lt;b&gt;The Stream (from above)&lt;/b&gt;&lt;/u&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;/div&gt;
The water flows  down a long bed of rocks, and at the end, flows into a large 3-4 feet deep  hole. At the bottom of that hole is an electric pump, which pulls the water up  and out of the hole (which I will now refer to as the “tank”), through a hose,  and back the top of the stream, where it starts the cycle all over again with a  very pretty waterfall. The risk in how this system works is that if the water  level got low enough, to where the pump at the bottom of the tank was pulling  in air instead of water, the motor would rather quickly burn out, and pumps are  not cheap. As it was the summertime, my dad said that this was worrying him, as  water was evaporating rather quickly from the large exposed surface of the  stream. To make matters worse, the hole at the end of the stream is covered  with flat rocks, which although makes it pretty to look at, makes it near  impossible to check the water level of the stream at a glance.
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://4.bp.blogspot.com/-8gxqtDpJTss/UtMxAkgn07I/AAAAAAAAAbw/6Ga9oFKk8X8/s000/DSC_1654%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;213&quot; src=&quot;http://4.bp.blogspot.com/-8gxqtDpJTss/UtMxAkgn07I/AAAAAAAAAbw/6Ga9oFKk8X8/s320/DSC_1654%2520%2528Custom%2529.JPG&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;The end of the stream&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
It was out of  this worry and inconvenience that I decided to build something to help them. I  decided to create a device that would let them check the exact (or as close to  exact) water level of the stream, from the comfort of inside their house.
&lt;p&gt;Summer classes got in the way of getting work done on the  project immediately, so there was some delay, but eventually I started putting  time towards it. I first brainstormed different ways I could read the water  level of return tank at the bottom of the stream. I did some research online  and dismissed many ways that were possible, but I felt weren’t the best. If you  want to see my brainstorming and build process more and detail, skip down below  to the more technical section, or keep reading and you will get there  eventually. Anyways, I eventually landed on the somewhat unique idea of using a  long pole with a common anode running the entire length of the pole, with 8  nodes (screws in the pole) that would be connected to 8 analog inputs on an  Arduino board.&lt;/p&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://3.bp.blogspot.com/-GA-6rBlYqy8/UtM2oZ40JTI/AAAAAAAAUTU/S076U401hKU/s1600/Illustration+of+Setup2.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;300&quot; src=&quot;http://3.bp.blogspot.com/-GA-6rBlYqy8/UtM2oZ40JTI/AAAAAAAAUTU/S076U401hKU/s400/Illustration+of+Setup2.png&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;A very bad illustration of my idea&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
When the pole gets submerged in water, any node that is  underwater is going to be connected to the common anode, since water conducts  electricity, and will read a positive voltage (my setup is somewhat similar to  t&lt;a href=&quot;http://www.instructables.com/id/Water-Level-Indicator-with-Alarm/&quot; target=&quot;_blank&quot;&gt;his  instructables&lt;/a&gt;). The way it works is a bit more complicated than that, but  again, if you want a more in depth explanation of the project, it will come  later, in the bottom section.
&lt;p&gt;Originally, I just planned to only measure the water level  of the stream, wirelessly transmit it from the outside unit to a box sitting  inside, and then have the inside box display the water level by way of 8 little  LED lights that would either be on or off, depending on the level of the water.  However, as I started to design my project, I started to realize that there  were more and more features I could easily add with just a few lines of code,  and I also found out how incredibly cheap electronics are direct from China  using eBay. At a certain point I should have stopped myself and resisted the  slippery slope of “feature creep”, but I didn’t, so the end product is way more  advanced than I originally planned.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;the_end_product&quot; name=&quot;The_End_Product&quot;&gt;&lt;b&gt;&lt;u&gt;The End Product&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
In terms of the programming that is in the end product, I  won’t go into great detail, but will say that I wrote over a thousand lines of  C for the program that runs on the microcontroller (if I were to print it out  on paper it would use roughly &lt;b&gt;24 pages&lt;/b&gt;)  and there are currently no (that’s right, zero) bugs to report.&lt;/p&gt;
&lt;p&gt;Time: About 4 days to solder everything and get the  enclosures completed, 2 hours to program the external unit, a few days to  program the internal unit, and then a few more days to debug all the code.  Probably somewhere around 1.5 weeks total of actual work.&lt;/p&gt;
&lt;p&gt;What I like about this project, and why I made it, is that  there is no commercial product (at least not that I could find) that even comes  close to comparison. Most water alarms either use one point of electrical  contact to sense whether water is present or not, but not the amount present  (such as a buzzer that you might put in your basement to warn of flooding) or  something called a “float”, which usually only measure to 2 levels of accuracy  – whether the tank is full or near empty. Pretty much all of those commercial  products also don’t work wirelessly – the alarm has to be where the water is.  Finally, even units that only measure full or empty wirelessly are very  expensive – this one is not even wireless, but uses a tethered alarm to alert  to a tank being full or empty, but &lt;a href=&quot;http://www.amazon.com/gp/product/B0061P2FR4/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B0061P2FR4&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;it starts at around 400 dollars&lt;/a&gt; .&lt;/p&gt;
&lt;p&gt; My end product&lt;/p&gt;
&lt;ul&gt;
 	&lt;li&gt;measures how full the tank is in accuracy of  1/8ths (12.5%)&lt;/li&gt;
 	&lt;li&gt;communicates over 2.4Ghz radio signals (can  communicate through walls, has a theoretical range up to 2000 feet, etc.)&lt;/li&gt;
 	&lt;li&gt;reports outside temperature, outside humidity, and  the temperature of the water&lt;/li&gt;
 	&lt;li&gt;keeps the current time, date, and minutes the  system has been running for&lt;/li&gt;
 	&lt;li&gt;has both an auditory and visible alarm&lt;/li&gt;
 	&lt;li&gt;has configurable alarm settings and refresh intervals&lt;/li&gt;
 	&lt;li&gt;Can provide alarms for water level, temperature  (for example, if the water temperature is below freezing), and humidity&lt;/li&gt;
 	&lt;li&gt;What I call “logic checking” – code that makes  sure the information it is receiving makes sense&lt;/li&gt;
&lt;/ul&gt;
Considering all these features, that units with only one or  two of my 20+ features cost hundreds, and that there is no manufacturer that  makes anything commercial that even comes close to what my units do, you might  be amazed to learn that all my parts together cost me… drumroll please….&lt;b&gt; &lt;/b&gt;Less than 80$!!!!! Seriously!!!
&lt;p&gt;The end product is really two units. One unit is installed  outside and should never have to be touched again, and the other unit is a box  that sits inside, that has a user interface and displays all the information  collected by the outside box. The outside unit is an &lt;a href=&quot;http://www.amazon.com/gp/product/B00ASSQ9S6/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B00ASSQ9S6&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;Iduino  Nano&lt;/a&gt; (an Arduino clone board), with an ATmega328, stuffed inside a  weather-tight enclosure.&lt;/p&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://4.bp.blogspot.com/-DbOWZKNBo7w/UtMxBPwDiWI/AAAAAAAAAb0/tabTfAkxXn4/s000/DSC_1655%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://4.bp.blogspot.com/-DbOWZKNBo7w/UtMxBPwDiWI/AAAAAAAAAb0/tabTfAkxXn4/s400/DSC_1655%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;The External Unit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
 It has a 2.4Ghz radio attached to it, for  communication to the inside unit, a temperature/humidity sensor inside the  enclosure, various electronic parts such as resistors, and is powered by 5v  supplied by the same outlet for the water pump, and has a long cable coming out  of the enclosure that leads to a PVC pipe. The PVC has all the electronics on  it that provide information about the water; depth and temperature.
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://3.bp.blogspot.com/-oVaoDJ4loG4/UtMwS5LlLAI/AAAAAAAAAME/flfXGWrquE8/s000/DSC_1367%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://3.bp.blogspot.com/-oVaoDJ4loG4/UtMwS5LlLAI/AAAAAAAAAME/flfXGWrquE8/s400/DSC_1367%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;The PVC Probe for the External Unit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
In  addition, the box that sits outside has a bright flashing LED that lights up  when the PVC pipe is “electrified”, which is visible even from our second floor  deck during daylight.
&lt;p&gt;The internal unit is what took most of my time to build and  code for, and what the user (my parents) will see at least a few times a day.  It is a rather small box, but most of the front surface of it is occupied by an  LCD screen, which serves to display all the information about the stream.&lt;/p&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-wmZV_hATF8k/UtMwzyDc7rI/AAAAAAAAAXM/g6pM9xvKngU/s000/DSC_1615222%2520%2528Custom%2529.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;223&quot; src=&quot;http://2.bp.blogspot.com/-wmZV_hATF8k/UtMwzyDc7rI/AAAAAAAAAXM/g6pM9xvKngU/s400/DSC_1615222%2520%2528Custom%2529.jpg&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-size: small;&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-size: x-small;&quot;&gt;The Internal Unit&lt;/span&gt;&lt;b&gt;
&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
It  has four buttons above the screen, and one on the side, as well as a power  jack, a USB jack for reprogramming, a video jack (in case someone wants to use  the LCD screen on the box to display video from some other source), and 9 (yes,  nine) bright LEDS that flash in coordination with a buzzer to alert the user if  there is something they should know about (such as the water level reaching too  low of a level). Inside the unit there is a &lt;a href=&quot;http://www.amazon.com/gp/product/B006GX8IAY/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B006GX8IAY&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;SainSmart  UNO&lt;/a&gt; (another Arduino knockoff, albeit a great one), a transistor, a 9V  battery, a “Real-Time Clock” (RTC) with a backup battery, and a buzzer. And of  course, the other 2.4Ghz radio that communicates with the outside unit.
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-rMBi20O6buk/UtMw2gvIJ3I/AAAAAAAAAYU/bZNkg9i5s70/s000/DSC_1618%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://2.bp.blogspot.com/-rMBi20O6buk/UtMw2gvIJ3I/AAAAAAAAAYU/bZNkg9i5s70/s400/DSC_1618%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-size: small;&quot;&gt;Diagnostics Screen&lt;/span&gt;&lt;/u&gt;&lt;/td&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-size: small;&quot;&gt;
&lt;/span&gt;&lt;/u&gt;&lt;/td&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-size: small;&quot;&gt;
&lt;/span&gt;&lt;/u&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://3.bp.blogspot.com/-8dcjWsBG93Q/UtMw15x0-_I/AAAAAAAAAYA/sKi9Oue_uVs/s000/DSC_1616%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://3.bp.blogspot.com/-8dcjWsBG93Q/UtMw15x0-_I/AAAAAAAAAYA/sKi9Oue_uVs/s400/DSC_1616%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Clock Screen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;bill_of_materials&quot; name=&quot;Bill_of_Materials&quot;&gt;&lt;b&gt;&lt;u&gt;List of “most” materials: (please note that this list is not so that  someone can recreate what I built, just to give a starting point for ideas)&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
&lt;ul&gt;
 	&lt;li&gt;A “weatherproof” electrical junction box, such  as &lt;a href=&quot;http://www.homedepot.com/p/Carlon-6-in-x-4-in-Junction-Box-E987RR/100404096&quot; target=&quot;_blank&quot;&gt;this&lt;/a&gt;, &lt;a href=&quot;http://www.homedepot.com/p/Carlon-8-in-x-4-in-Junction-Box-E989N-CAR/100404099&quot; target=&quot;_blank&quot;&gt;this&lt;/a&gt;,  or &lt;a href=&quot;http://www.homedepot.com/p/Carlon-4-in-x-2-in-Junction-Box-E989NNJ-CAR/100404097&quot; target=&quot;_blank&quot;&gt;this&lt;/a&gt;.  However, I actually am not very satisfied with mine (I believe I have the 6x4).&lt;/li&gt;
 	&lt;li&gt;An aluminum flat bar for the common anode (I  used &lt;a href=&quot;http://www.homedepot.com/p/Crown-Bolt-3-4-in-x-48-in-Aluminum-Flat-Bar-with-1-8-in-Thick-44650/100337842&quot; target=&quot;_blank&quot;&gt;this  one&lt;/a&gt; and cut it to match the length of my PVC probe).&lt;/li&gt;
 	&lt;li&gt;Lots of hookup wire (either &lt;a href=&quot;http://www.amazon.com/gp/product/B00B4ZRPEY/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B00B4ZRPEY&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;22&lt;/a&gt; or 20 gauge, I recommend solid core, not stranded).&lt;/li&gt;
 	&lt;li&gt;Arduino Uno (either &lt;a href=&quot;http://www.amazon.com/gp/product/B006H06TVG/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B006H06TVG&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;name  brand&lt;/a&gt;, or &lt;a href=&quot;http://www.amazon.com/gp/product/B006GX8IAY/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B006GX8IAY&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;generic  SainSmart&lt;/a&gt;)&lt;/li&gt;
 	&lt;li&gt;Cheap auto backup/reverse LCD screen, &lt;a href=&quot;http://www.amazon.com/gp/product/B006MPRFJQ/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B006MPRFJQ&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;such  as this one&lt;/a&gt; that I used&lt;/li&gt;
 	&lt;li&gt;(2) 2.4Ghz nRF24L01+ RF Transceiver Modules (&lt;a href=&quot;http://www.amazon.com/gp/product/B007ZZANPA/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B007ZZANPA&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;Amazon&lt;/a&gt;,  or you can find them much cheaper on eBay, but shipping might take 2-4 weeks)&lt;/li&gt;
 	&lt;li&gt;Arduino Nano (or clone, &lt;a href=&quot;http://www.amazon.com/gp/product/B00ASSPI60/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B00ASSPI60&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;such  as this one that I used&lt;/a&gt;)&lt;/li&gt;
 	&lt;li&gt;Tools: Soldering iron, solder, desoldering wick,  hotglue gun, hotglue, &lt;a href=&quot;http://www.amazon.com/gp/product/B000ALJ4NS/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B000ALJ4NS&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;epoxy&lt;/a&gt; (for weather sealing), rotary tool for cutting enclosures (I highly recommend &lt;a href=&quot;http://www.amazon.com/gp/product/B000MUSLCC/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B000MUSLCC&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;this  one&lt;/a&gt;), wirestripper and wirecutters, wrench, screwdriver&lt;/li&gt;
 	&lt;li&gt;Components: transistors, multicolored LEDs, LED  holders, momentary pushbuttons, sliding switches, perfboard (I used some random  boards from RadioShack, but &lt;a href=&quot;http://www.amazon.com/s/?_encoding=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;linkCode=ur2&amp;#x26;pageMinusResults=1&amp;#x26;suo=1389551420322&amp;#x26;tag=jwbstz-20&amp;#x26;url=search-alias%3Daps#/ref=nb_sb_ss_sc_0_7?url=search-alias%3Daps&amp;#x26;field-keywords=22%20gauge%20wire&amp;#x26;sprefix=22+guag%2Caps%2C178&amp;#x26;rh=i%3Aaps%2Ck%3A22%20gauge%20wire&amp;#x26;sepatfbtf=true&amp;#x26;tc=1389551424781&quot; target=&quot;_blank&quot;&gt;Amazon  also sells them&lt;/a&gt;), buzzer, 9v battery, &lt;a href=&quot;http://www.amazon.com/gp/product/B003UC4FSS/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B003UC4FSS&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;various  resistors&lt;/a&gt;, &lt;a href=&quot;https://www.radioshack.com/product/index.jsp?productId=2103233&quot; target=&quot;_blank&quot;&gt;terminal  strips&lt;/a&gt;, RCA plug/cable, galvanized bolts and nuts (available at hardware  stores), weatherproof PVC pipe (also at hardware stores)&lt;/li&gt;
 	&lt;li&gt;Special parts: DHT22 (&lt;a href=&quot;http://www.amazon.com/gp/product/B00CDHH3WQ/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B00CDHH3WQ&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;Amazon&lt;/a&gt;,  or cheaper on eBay), DS18B20 waterproof temperature sensor (&lt;a href=&quot;http://www.amazon.com/gp/product/B008HODWBU/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B008HODWBU&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;Amazon&lt;/a&gt; or eBay), Real Time Clock “RTC” module (&lt;a href=&quot;http://www.amazon.com/gp/product/B00E37VTWY/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B00E37VTWY&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot;&gt;Amazon&lt;/a&gt;,  or cheaper on eBay), rubber stopper/plug for creating the seal around the  cables going into the external unit (I got mine from Ace Hardware, but most  hardware stores should carry them)&lt;/li&gt;
 	&lt;li&gt;Weatherproof landscape lighting cable (I  recommend 16 gauge or thicker – lower numbers are thicker) for the water depth  probe and extending the USB power cable for the outside unit (&lt;a href=&quot;http://www.amazon.com/s/?_encoding=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;field-keywords=landscape%20cable&amp;#x26;linkCode=ur2&amp;#x26;tag=jwbstz-20&amp;#x26;url=search-alias%3Daps&quot; target=&quot;_blank&quot;&gt;Amazon&lt;/a&gt;,  HomeDepot, or local hardware store)&lt;/li&gt;
 	&lt;li&gt;&lt;a href=&quot;http://www.amazon.com/gp/product/B0073FE1F0/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B0073FE1F0&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;AC-USB  Adapter&lt;/a&gt; for external unit, AC-DC barrel adapter for inside Arduino UNO unit  and to power screen (needs to be 5.5x2.1mm center positive, at least 2 amps to  power both UNO and LCD screen, &lt;a href=&quot;http://www.amazon.com/gp/product/B006QYRVU6/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B006QYRVU6&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;such  as this one&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;span style=&quot;font-size: large;&quot;&gt;
&lt;/span&gt;
&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;beginning_of_technical_section&quot; name=&quot;Beginning_of_Technical_Section&quot;&gt;&lt;b&gt;&lt;u&gt;The “Technical”&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
If you are viewing this page because I asked you to on FB,  or one of my parents shared it with you, or for whatever other reason you might  have, but you are not interested in electronics or software, or at least not in  this application, or if the above sections confused you, this might be where  you want to stop reading. I thank you for reading so far, and I realize that not  everyone finds the same subjects interesting (I stop reading articles about  sports generally after reading the title). Now, if you are interested in  technology/microcontrollers/software etc., and you want to see more of how this  was built and what exactly went into it, &lt;b&gt;this  is the section you want;&lt;/b&gt; here is where I will go into the nitty-gritty, and  even be completely honest about the issues I encountered.
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;source_code&quot; name=&quot;Source_Code&quot;&gt;&lt;b&gt;&lt;u&gt;Source  Code:&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
As of right now, I have no plans to ever release my source  code (either for the internal or external units). There are a few reasons why.  First, this was my first programming/electronics project. As such, although my  code works, it is messy and I would be embarrassed to have strangers look at  it. Second, it would likely be of no help to anyone. I haven’t really provided  enough information in this writeup so that someone could actually duplicate my  project, and every stream is going to be different, so there is no way someone  could duplicate the electronics in my project exactly, to where my code would  actually run the same.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;how_it_works&quot; name=&quot;How_It_Works&quot;&gt;&lt;b&gt;&lt;u&gt;How  it works:&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
Before covering how I made it, maybe first I should address,  in a more technical manner than I did above, how the units work. The inside  unit has an LCD screen that displays information about the air and water  outside. If the user presses a certain button, or a preset amount of time  passes (right now set to 20 minutes), the inside unit will request an update  from outside. It does this by sending an integer value over 2.4Ghz using the  incredibly cheap nRF24L01+ Radio (can be purchased on eBay for as low as $1 per  radio). The outside unit is constantly listening, and when it hears that  integer over the radio, it starts to gather data to send back. It quickly  supplies 5v to the common anode and then does analog reads of the 8 screws to  get the water depth, measure the temperature and humidity of the air inside the  enclosure by using a DHT22 sensor, and the temperature of the water by polling  a DS18b20 waterproof temperature sensor. It gathers all that data in a matter  of milliseconds, packs it into an array, and sends it back to the inside unit,  which is now switched to a listening mode and is awaiting the updated  information. Upon receiving the new information by way of the radio  transmission, the internal unit inside the house rips apart the array and  stores its values into variables in memory. It then reports back the values to  the user by displaying them on the GUI. If one of the new values is outside of  a preset range (that is adjustable), for example, if the water is below  freezing, than the alarm LEDs will flash and an alarm buzzer will go off (that  unfortunately sounds identical to our dishwashing machine, which means everyone  will ignore it).&lt;/p&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://1.bp.blogspot.com/-haQuIOasBfw/UtMw3yTOLuI/AAAAAAAAAYw/bwSUFKdtfCg/s000/DSC_1622%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://1.bp.blogspot.com/-haQuIOasBfw/UtMw3yTOLuI/AAAAAAAAAYw/bwSUFKdtfCg/s400/DSC_1622%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-size: small;&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-size: x-small;&quot;&gt;A sample error screen&lt;/span&gt;&lt;b&gt;
&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
Additionally, since there is too much information to display  on the screen at once, the user can press two additional buttons to get extra  information. Pressing one button with a   clock icon shows the current date in DD/MM/YYYY format, as well as the  current time in 12-hour format.
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://3.bp.blogspot.com/-8dcjWsBG93Q/UtMw15x0-_I/AAAAAAAAAYA/sKi9Oue_uVs/s000/DSC_1616%2520%2528Custom%2529.JPG&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://3.bp.blogspot.com/-8dcjWsBG93Q/UtMw15x0-_I/AAAAAAAAAYA/sKi9Oue_uVs/s400/DSC_1616%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Clock Screen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
It gets that information by talking a “Real-Time  Clock”, which is a separate module that only functions to keep precise track of  the current time and has a backup battery to ensure that it continues to keep  the current time even in case of a power failure (real time clocks are in most  computers and in many personal electronics). If the user presses another  button, with the icon of a stethoscope, a diagnostics screen is displayed.
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-rMBi20O6buk/UtMw2gvIJ3I/AAAAAAAAAYU/bZNkg9i5s70/s000/DSC_1618%2520%2528Custom%2529.JPG&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://2.bp.blogspot.com/-rMBi20O6buk/UtMw2gvIJ3I/AAAAAAAAAYU/bZNkg9i5s70/s400/DSC_1618%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-size: small;&quot;&gt;Diagnostics Screen&lt;/span&gt;&lt;/u&gt;&lt;/td&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-size: small;&quot;&gt;
&lt;/span&gt;&lt;/u&gt;&lt;/td&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;u&gt;&lt;span style=&quot;font-size: small;&quot;&gt;
&lt;/span&gt;&lt;/u&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
The  diagnostics screen reads out the amount of free SRAM on the Arduino board, the  data rate of the wireless 2.4Ghz radio, the CRC length for the radio, the PA  level of the radio, the humidity of the outside enclosure (since the humidity  sensor is inside the enclosure), as well as the precise analog values that the  outside unit got when it read the analog pins. The main GUI, which is always  displayed on the screen unless there is an error message to display or one of  the buttons is being pressed or an update being fetched, displays the current  level of the tank visually, through a drawing of a tank divided into 8 sections,  displays the minutes since the inside unit last received an update from the  outside unit, the level of the tank in percentage, the outside temperature of  the air, and the outside temperature of the water.
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;my_background&quot; name=&quot;My_Background&quot;&gt;&lt;b&gt;&lt;u&gt;My  Background&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
I’m a 21 year old male, living in Seattle, a full-time  student, and a complete coffee addict.
Keep in mind, particularly while reading this section, that  I am completely new, not just to Arduino, but coding and electronics in  general. Prior to this project my only experience with code was a summer camp  as a child learning ActionScript (the code that is behind Adobe Flash), a  robotics class in Highschool, that didn’t even use code (it used drag and drop  blocks, which I actually was able to dig around and figure out it turned into  plain C before compiling and sending to the robot), and basic HTML/CSS for some  web sites.  My only experience with  electronics had been taking stuff apart (which I loved to do, sometimes to the  horror of my parents as I dismantled electronic gifts and items I had scrimped  and saved for) and maybe once or twice soldering super simple wire connections  on something that had broken. I have had no classes on C, no classes on even  basic circuits (except for maybe a few days in elementary school and a few days  in AP Physics), and certainly no experience or education in microcontrollers.&lt;/p&gt;
&lt;p&gt;For most people like me, who came to the table with little  to no experience with electronics and software, the recommendation is usually  to take things slow and use free online courses, video lectures, practice  homework, and build-it-yourself kits (such as from RadioShack). You might take  a few weeks to learn the basics of C programming, then another week or two to  study basic circuits, then another week to combine the two to create some “hello  world” programs and flash some LED bulbs. However, those that know me well also  know that patience is not a virtue I really have. I was going to have NONE of  that. So I didn’t. I knew what I wanted my end product to be, so I just made a  mad dash to get to it.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;how_it_started&quot; name=&quot;How_it_Started&quot;&gt;&lt;b&gt;&lt;u&gt;How it Started: The Idea&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
I already outline the rough idea at the beginning of this  webpage – to be able to wirelessly check the water level of the outside tank –  but how I chose to do that was a hard process. I started by breaking it down  into parts; the first idea to tackle was the most difficult; how do I measure  the water level? I brainstormed different ideas, as well as did online  research, and eventually came up with a list of possibilities, only one of  which met my needs. Possibles were:&lt;/p&gt;
&lt;ul&gt;
 	&lt;li&gt;A float. These are insanely common in measuring  water level and are used just about everywhere. The &lt;a href=&quot;http://www.amazon.com/gp/product/B00AQ9NI36/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B00AQ9NI36&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;Keurig  pod coffee makers&lt;/a&gt; use magnetic floats in their reservoirs to know if there  is enough water left to brew another cup, or if the warning light needs to be  lit up. You can also buy commercial units with a float attached to an alarm.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;The biggest problem with floats is their lack of  accuracy. Most floats act basically as a giant switch – they are either on or  off – there is no in-between. Considering how cheap they are, this is  absolutely wonderful if you just want to be alerted when the water reaches one  specific level that you set, or to have it turn something on, but it is bad if  you want more points of accuracy. I believe continuous floats exist, that can  output multiple values, but my guess would be that they are primarily for industrial  use and probably cost hundreds, if not thousands.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;A waterproof sonar sensor pointed downwards  towards the bottom of the tank. The sound should ping back off the surface of  the water and the sensor should be able to, very accurately, return the current  level of the water. Here is an online example: &lt;a href=&quot;http://www.freaklabs.org/index.php/Tutorials/Software/Tutorial-How-to-Fix-Leaky-Radioactive-Water-Tanks.html&quot; target=&quot;_blank&quot;&gt;link&lt;/a&gt;.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;Problem: The accuracy of this sensor would be  awesome to have. I could probably know the depth of the water down to a few  millimeters. However, a truly waterproof one would likely be pricy, and most  importantly, since there are rocks covering the top of the tank, and the tank  is really a non-uniform hole in the ground, filled with a pump and various  floating debris, it would not only be near impossible to mount the sensor in a  place where it can echo off the water, but it is highly likely that debris in  the water would mess with it and cause it to give back incorrect readings. A  great idea for an actual “water tank”, in which water is relatively stagnant,  but probably not good for a tank where water is continuously rushing in and  out.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;Pressure Differentials: I didn’t do much  research here, but what I found made it sound difficult to work with in my  scenario, as again, I am not really working with a “tank”, but rather a hole in  the ground.&lt;/li&gt;
 	&lt;li&gt;There are a few interesting ideas on &lt;a href=&quot;http://forum.allaboutcircuits.com/showthread.php?t=64889&quot; target=&quot;_blank&quot;&gt;this forum page&lt;/a&gt;,  but not exactly what I ended up doing. Still useful to look at though.&lt;/li&gt;
 	&lt;li&gt;Strings of Resistors. This is a very interesting  concept that I did not come up with during brainstorming, but found online on a  few different sites (&lt;a href=&quot;https://lifeboatfarm.wordpress.com/2009/12/28/arduino-water-level-gauge/&quot; target=&quot;_blank&quot;&gt;link  1&lt;/a&gt;, &lt;a href=&quot;http://pompie-arduino.blogspot.com/2007_01_01_archive.html&quot; target=&quot;_blank&quot;&gt;link  2&lt;/a&gt;). It is actually a pretty neat way of measuring water – a bunch of  resistors are in series, from a power source, to an analog input on the Arduino  (also of course, back to ground). As the string dips below the water, the  resistors start shorting out to the return path, and the resistance changes –  this provides the Arduino with a variable resistance level that is directly  correlational to the depth of the water.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;Why didn’t I use it? Well, for one, after coming  up with the next idea, this one seemed a bit complicated and convoluted for its  purpose. Most importantly though – when electrical current is passed through a  metal conductor under water, oxidation takes place, which results in the  corrosion of the metal conductor. I could find ways to slow this down, but it  would complicate my build further – the idea, without modifications, would  likely result in failure in under a year, considering the thickness of the  metal wires coming out of most resistors.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;&lt;b&gt;The  chosen idea: &lt;/b&gt;A basic circuit. There is a flat aluminum stick that runs the  entire length of my PVC probe and acts as a common anode, supplying a positive  5v (which is generated by the Arduino) to the tank. Also on the PVC pipe/probe  are 8 screws, to which wires are attached. Each of these 8 wires goes to a  separate analog input pin on the Arduino outside. If a screw is submerged in  water, a circuit is created between the analog pin and the 5v of the common  anode, so the analog pin reads a rather high voltage/value There is also a  pulldown-resistor circuit connected to each of the analog input pins to reduce  electrical noise and ensure that when a screw is not submerged in water, the  pin only reads “0”. This was really my first or second idea, but I still did my  online research just to confirm that I should go with it. After looking at the  alternatives, it is the simplest, cheapest, and can provide me with as many  accuracy levels as analog pins I have (I chose to just go with 8), although you  could theoretical get even more levels by using digital logic circuits to  return various levels to the Arduino depending on the level of the tank. My way  works well though, and I didn’t over-engineer.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;What is important to note, since I mentioned it  as a negative for another idea, is that when metal is conducting electricity  under water, oxidation occurs and the metal will rather quickly corrode. This  method is no exception. However, I took several steps to ensure that it will  occur at an extremely slow rate, to the point that it will likely fail because  of some other component, such as an AC transformer failing, long before the PVC  probe ever fails. The first step was to use a thick (1/8” thick) aluminum flat  as the common anode to conduct the positive voltage. There were several  different types of flats available, and at least according to my research,  aluminum was less likely to corrode than the others, at least at my price  point. I found the flat at Home Depot, but I know for a fact that Ace Hardware  also carries them (as well as probably most hardware stores). For the  conductive screws serving as “nodes”, I used galvanized screws as opposed to  plain zinc screws – galvanized screws are more expensive and look duller, but  they will resist corrosion longer while still conducting electricity. Again,  easy to find at almost any hardware store. Finally, for the wires used, I used  “low-voltage landscape lighting cable”, which is cheap for really long lengths  (I needed about 80 feet), has a copper conductor mixed with other metal strands  (great for electrical conductivity), is really thick (typically 16-gauge, great  for slowing down corrosion failure), and is covered with a really thick PVC  insulation, which means it can actually be buried underground and will survive  exposure to harsh weather. I bought 50 feet of it at Home Depot, and then when  I ran out of that, I bought another 100 feet at Ace Hardware (of which I used  very little). It was much cheaper to buy at Ace, but your experience may  differ. Of course, you can also always &lt;a href=&quot;http://www.amazon.com/gp/product/B000QD5JUM/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B000QD5JUM&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;buy  it online with Amazon.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;procedure&quot; name=&quot;Procedure&quot;&gt;&lt;b&gt;&lt;u&gt;Procedure:&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
Once I knew what I wanted to do, and how I was going to do  it, my first concern was whether it could stand up to constant use. In  particular, since I was going to be passing current through a metal conductor  underwater, I was worried about how quickly my metal anode and cathodes would  corrode, and if the project was even worth attempting, if it was only going to  fail in a month or two. I tested this by attaching the heavy gauge landscape  cable I bought to the terminals of a 9V battery, stripping their PVC protection  off, and then leaving the ends submerged at least an inch or two deep in a see  through cup of tap water. I left this set up on a table, for about 3 days,  continuously watching to see how quickly the wire corroded. After three days,  of an electrical current continuously being passed through the water and the  cables, I evaluated the results and took some pictures (below). All in all, it  wasn’t terrible. There was visible oxidation after just a few hours of  submersion (visible bubbles on the ends of the wires) and after a full three  days, bits of both ends of wires had corroded away and the ends had become  blackened. At the bottom of the cup was a layer of what looked like blue  powder.
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://4.bp.blogspot.com/-L4x9i3TsTEU/UtMwiZkNm1I/AAAAAAAAARY/Nn4Aq_VnCxQ/s000/DSC_1442%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://4.bp.blogspot.com/-L4x9i3TsTEU/UtMwiZkNm1I/AAAAAAAAARY/Nn4Aq_VnCxQ/s400/DSC_1442%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-size: x-small;&quot;&gt;&lt;u&gt;Blue Powder Substance&lt;/u&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;font-size: small;&quot;&gt;&lt;u&gt;
&lt;/u&gt;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
Basic inference and a quick Google search confirmed that it  was indeed copper metal that had corroded away (now you know to keep an eye out  for blue stains around copper pipes in your house!). This might sound bad, but  I was expecting much worse. You see, instead of constantly running a current  through my common anode, my plan was always to only run electricity through the  water when getting an update for the internal unit, which I planned to make a  default of 30 minutes (of course, this is user configurable). It is very  unlikely that the water level would ever change drastically in less than 30 minutes,  so it makes sense to only get updates that often. In addition, each time it  gets an update, it only actually passes current through the tank for about 1-2  seconds, just long enough to get decent analog readings. If you want to see the  actual duration, watch one of my videos and watch for the flashing LED on the  external box.
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-roVPR3qaZ_U/UtMwnoF8ufI/AAAAAAAAATA/ZgA0l4xKHr4/s000/DSC_1471%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;213&quot; src=&quot;http://2.bp.blogspot.com/-roVPR3qaZ_U/UtMwnoF8ufI/AAAAAAAAATA/ZgA0l4xKHr4/s320/DSC_1471%2520%2528Custom%2529.JPG&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-size: x-small;&quot;&gt;&lt;u&gt;Flashing LED&lt;/u&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
When it lights up, electricity is passing through the tank.
&lt;p&gt;After being convinced that my idea would stand the test of  time, I thought it best to design the hardware first. At first, I wanted to  have the internal unit have individual LEDs, 8 of them in a vertical column, to  light up to represent the water level of the outside tank (eg: if it was full  to three levels, then light up the bottom 3 LEDs – &lt;a href=&quot;http://www.instructables.com/id/Water-Level-Indicator-with-Alarm/&quot; target=&quot;_blank&quot;&gt;this instructables&lt;/a&gt; is the closest to what I originally had in mind). I quickly discovered a few issues  with this plan though, mainly that as I added more sensors and information to  my idea (“I SHOULD HAVE IT RECORD HUMIDITY!!! YEAH FOR FEATURE BLOAT!”) I would  have to have a small LCD panel to display the information, and then a user  would have to press a button multiple times to display whatever page of  information they want to see.&lt;/p&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://4.bp.blogspot.com/-dzRCzhPGuGw/UtMvtlQAT3I/AAAAAAAAACA/pe9hpcTLXRA/s000/DSC_1094%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://4.bp.blogspot.com/-dzRCzhPGuGw/UtMvtlQAT3I/AAAAAAAAACA/pe9hpcTLXRA/s400/DSC_1094%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-size: x-small;&quot;&gt;&lt;u&gt;My original idea for display&lt;/u&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
As I was thinking about these issues, as well as others (see  the &lt;a href=&quot;#issues&quot;&gt;issues section&lt;/a&gt;), I stumbled across some information about a  unique and frankly amazing Arduino library that someone had developed. I am not  sure if I found it through a &lt;a href=&quot;http://hackaday.com/&quot; target=&quot;_blank&quot;&gt;Hackaday&lt;/a&gt; post  or a “related video” section on Youtube when I was looking at videos about  Arduinos, but however I found it, I immediately was astounded at the amount of  functions the library provided and how it works. The library is called &lt;a href=&quot;http://code.google.com/p/arduino-tvout/&quot; target=&quot;_blank&quot;&gt;“arduino-tvout” or “TVOut”&lt;/a&gt;,  and uses &lt;b&gt;&lt;u&gt;just 2 pins&lt;/u&gt;&lt;/b&gt; to  generate a full NTSC or PAL video signal. &lt;b&gt;&lt;u&gt;JUST  2 PINS&lt;/u&gt;&lt;/b&gt; (in addition to a ground connection)!!! The second I saw the  demo and the &lt;a href=&quot;http://code.google.com/p/arduino-tvout/&quot; target=&quot;_blank&quot;&gt;GoogleCode page&lt;/a&gt;,  I knew I had to use it. It could display all the information I wanted to, on a  single screen, and I could actually create my very own GUI (graphical user  interface)!
&lt;p&gt;Sometime around discovering TVOut I also started acquiring a  lot more parts. I visited RadioShack and got transistors, resistors, and other  various small components (make sure to ask for a student discount if you have a  student ID – an extra 10% off &lt;b&gt;&lt;u&gt;all&lt;/u&gt;&lt;/b&gt; purchases!), shopped at Home Depot and picked up the copper wire, aluminum  flat, and weatherproof enclosures, and finally, started purchasing the bulk of  my supplies from either Amazon or eBay, eBay being my preferred retail website  for electronic components, as they all are pretty much with free shipping and  at a fraction of what I would pay at Amazon. The downside to ordering off eBay  is that you are usually ordering directly from distributors or manufacturers in  China or elsewhere in Asia, so you can expect anywhere between 1-4 weeks for  delivery. It is very hard to go from Amazon Prime to having to wait 3 weeks for  a package.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;prototyping&quot; name=&quot;prototyping&quot;&gt;&lt;b&gt;&lt;u&gt;Prototyping&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
Around this time, I was also looking for ways I could organize  my ideas about the hardware for the project and experiment with configurations  without having to prototype everything out on breadboards. Enter “&lt;a href=&quot;http://fritzing.org/home/&quot; target=&quot;_blank&quot;&gt;Frtizing&lt;/a&gt;”, one of the coolest programs I  have every used. Fritzing is a little hard to describe, but is close to  analogous to website wireframing software, such as the &lt;a href=&quot;http://pencil.evolus.vn/&quot; target=&quot;_blank&quot;&gt;Pencil Project&lt;/a&gt; – Fritzing allows you to  easily create graphical representations of electronic circuits and prototype  your ideas and save them.&lt;/p&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-mbyePEODjlY/UtMwPk9HVaI/AAAAAAAAALA/v1JlLRqJTE0/s000/DSC_1291%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://2.bp.blogspot.com/-mbyePEODjlY/UtMwPk9HVaI/AAAAAAAAALA/v1JlLRqJTE0/s400/DSC_1291%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-size: x-small;&quot;&gt;&lt;u&gt;Looking at Fritzing on my laptop&lt;/u&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;font-size: x-small;&quot;&gt;&lt;u&gt;
&lt;/u&gt;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
It is really great for a beginner like me, because it  does not require that you understand or desire to use circuit  schematics/electrical diagrams, only that you can drag and drop components and  arrange wires. The downside is that if you &lt;b&gt;&lt;i&gt;really&lt;/i&gt;&lt;/b&gt; don’t know what you are  doing, for example if you don’t know how to work a basic transistor, Fritzing  might do you more harm than good. Fritzing doesn’t actually test your circuit  in any way, it is just way of representing your circuit in a digital format, so  it can’t tell you if your design will work or not. If you are unsure, it is  best to go back and forth between Fritzing and an actual breadboard prototype,  that way you can be sure your circuit works every step of the way. In my case,  I did not do that – I designed it all in Fritzing without doing any testing and  then soldered both units in a single night – a very stupid stupid thing to do,  but I got incredibly lucky and all of my designs worked (except for a  transistor mistake and a broken radio – see the &lt;a href=&quot;#issues&quot;&gt;issues section&lt;/a&gt;).
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;building&quot; name=&quot;building&quot;&gt;&lt;b&gt;&lt;u&gt;Building&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
Shortly before I did all the soldering work, I cut out parts  of the box to go inside the house with a rotary tool. I cut out a section of  the front to allow room for the LCD panel (which is really just an &lt;a href=&quot;http://www.amazon.com/gp/product/B006MPRFJQ/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B006MPRFJQ&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;unmodified  car LCD screen I bought on Amazon&lt;/a&gt;, designed to be attached to reversing  cameras), holes for LED holders and LEDs, holes for the 5 momentary  pushbuttons, a hole for the extra RCA video plug, a hole for a sliding switch,  and final two holes for the connectors for the Arduino Uno inside the box, one  for the USB connector and the other for the barrel power connector.&lt;/p&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://1.bp.blogspot.com/-GQiKkynZmcs/UtMwL52viaI/AAAAAAAAAeY/WnXhbqYP_Ew/s000/DSC_1276%2520%2528Custom%2529.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;213&quot; src=&quot;http://1.bp.blogspot.com/-GQiKkynZmcs/UtMwL52viaI/AAAAAAAAAeY/WnXhbqYP_Ew/s320/DSC_1276%2520%2528Custom%2529.jpg&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Cutting the Enclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;/div&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-KEdWMM7Xd28/UtMwfC9p5uI/AAAAAAAAAQY/VsjGIbTZCgM/s000/DSC_1427%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://2.bp.blogspot.com/-KEdWMM7Xd28/UtMwfC9p5uI/AAAAAAAAAQY/VsjGIbTZCgM/s400/DSC_1427%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Messy wires!!!&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
I was able  to make super precise cuts with the Black and Decker RTX-B 3-speed rotary tool  I picked up from Target; so precise in fact, that many of my components, such  as the screen and buttons, stayed in without glue (I did end up using glue, but  just as a precaution). Not to sound like a sales pitch, but I really do love  the rotary tool and although it does not carry the Dremel name, I think it  actually has more power and torque than the Dremels I have seen, and it really  is my new favorite tool.&lt;a href=&quot;http://www.amazon.com/gp/product/B000MUSLCC/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B000MUSLCC&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt; Amazon has it for exactly the same price as Target and with free two-day  shipping&lt;/a&gt; (with Prime).
&lt;p&gt;After cutting my enclosures and doing the majority of the  solder work, I started work on the water probe. I used weather-resistant PVC (I  am not sure if that is exactly what it is called, but it is grey and available  at most hardware stores. It is stronger than the plain white PVC and has a  higher rating.) and cut it to about the height of the water tank at the end of  the stream. I then also cut the aluminum flat to the same length as the PVC (&lt;a href=&quot;http://www.amazon.com/gp/product/B000MUSLCC/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B000MUSLCC&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;I  used my Black and Decker rotary tool&lt;/a&gt; with a cut off dics to cut the  aluminum flat). I cut a hole in the bottom of the pole and secured my DS18b20  waterproof temperature sensor in it, and then cut 8 more holes for the 8 analog  sensors. I ran galvanized bolts through the 8 holes, using nuts to secure them,  and threaded the ends of my 8 analog wires around the nuts, using mostly  tension to hold them in place. I also used two bolts to secure the aluminum  flat to the PVC pipe. To understand my design easier, I recommend looking at  the pictures.&lt;/p&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://3.bp.blogspot.com/-oVaoDJ4loG4/UtMwS5LlLAI/AAAAAAAAAME/flfXGWrquE8/s000/DSC_1367%2520%2528Custom%2529.JPG&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://3.bp.blogspot.com/-oVaoDJ4loG4/UtMwS5LlLAI/AAAAAAAAAME/flfXGWrquE8/s400/DSC_1367%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;The PVC Probe for the External Unit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
 The build process for the external unit was much more  difficult than for the internal unit. I had to figure out a way to get 14  cables (2 for power and ground to board, 8 for analog signals, 1 for common  anode, and 3 for DS18b20 waterproof temperature probe), all of which are a  thick 16-guage, organized and fed into a tiny enclosure box. I ended up cutting  a large hole in the waterproof sealed enclosure box, then buying a plug/stopper  at Ace Hardware made out of some plastic material and using it to plug the  hole. I then carefully drilled out the center of the plastic stopper, until it  was just a hair larger than all the cables bundled together. Then, I threaded  all the 14 cables through the rubber stopper and attached them to multiple 5  position terminal strips inside the enclosure.
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-CVw7bvRfs34/UtMwk1gAD8I/AAAAAAAAASM/zPdQJrxFrJc/s000/DSC_1460%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://2.bp.blogspot.com/-CVw7bvRfs34/UtMwk1gAD8I/AAAAAAAAASM/zPdQJrxFrJc/s400/DSC_1460%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Starting to attach all the wires to the terminal strips&lt;/td&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://1.bp.blogspot.com/-vfqI-UK-lmc/UtMwmYqUJ8I/AAAAAAAAASo/jct0ErVs4mk/s000/DSC_1467%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://1.bp.blogspot.com/-vfqI-UK-lmc/UtMwmYqUJ8I/AAAAAAAAASo/jct0ErVs4mk/s400/DSC_1467%2520%2528Custom%2529.JPG&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;More wires attached!&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
Since the rubber stopper opening  was made just barely large enough for the cables, the friction alone held them  in place and kept them from getting yanked out of the enclosure, but I also  added a very thick layer of Gorilla Glue Epoxy and regular hot glue around the  stopper and the cables, to stop any water from leaking into the enclosure. I  then connected the terminal strips to the Arduino pins, by way of thinner wire;  I believe 22-guage (solid core, not threaded).
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://3.bp.blogspot.com/-roqvqus_KbY/UtM-VeQKknI/AAAAAAAAAdw/c1isNO4pbW4/s000/124321353214132.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;266&quot; src=&quot;http://3.bp.blogspot.com/-roqvqus_KbY/UtM-VeQKknI/AAAAAAAAAdw/c1isNO4pbW4/s400/124321353214132.jpg&quot; width=&quot;400&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Cables attached, seal expoxied, almost done with external unit!&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
 Finally, I wanted to add an  indicator light to the external unit that could be visible from far away and  would let the user know when there is electrical current running through the  water, so I cut a small hole in the enclosure and placed an LED holder and a  multicolored LED in it and soldered it to wires that attached to the Arduino.  To protect the LED the tiny gap around it from water, I cut the end off of an  empty RadioShack solder container, which was see through, and placed it over  the LED. I then used plenty of epoxy around it to make sure it would stay  attached and not let any moisture seep in. Instead of cutting the solder  container, I could have also used the clear bubble lid that comes on those  capsules you can get out of quarter vending machines that dispense little toys  and stickers, but I couldn’t find any of those vending machines when I needed  the part, so I improvised even further with the solder container.
&lt;p&gt;Once everything was all wired up, it was time to start work  on…&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;programming&quot; name=&quot;programming&quot;&gt;&lt;b&gt;&lt;u&gt;The  Programming&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
Like I said before, I have no real programming experience,  so take the information I share here with that consideration. That being said,  I ended up writing over a thousand lines of codes, which compile without  errors, and I currently have no real bugs to report.&lt;/p&gt;
&lt;p&gt;I really only used three programs to help my write my code.  The first is the official &lt;a href=&quot;http://arduino.cc/en/main/software&quot; target=&quot;_blank&quot;&gt;Arduino  IDE&lt;/a&gt;, which really provides very little in terms of an interface. There is  no autocomplete or suggestions for functions, no collapsing of sections, and no  debugging. However, it super easy to use, since it just requires one click to  compile and upload to your Arduino board, and it is lightweight, so you can  code without distractions. I would highly recommend starting out with the  official Arduino IDE if you are new to programming, as its lack of features  actually make it better for a beginner; there is let to get confused about and  its limitations force you to learn and understand the code you are producing.  If you are an experienced programmer, you can use pretty much any IDE you care  to use (the most popular usually being &lt;a href=&quot;http://www.eclipse.org/&quot; target=&quot;_blank&quot;&gt;Eclipse&lt;/a&gt;).  I haven’t really gotten into alternative IDEs yet, but I have a feeling I might  at some point, as &lt;a href=&quot;http://www.atmel.com/microsite/atmel_studio6/&quot; target=&quot;_blank&quot;&gt;Atmel  Studio&lt;/a&gt; looks especially promising, considering there are ways to debug and  use breakpoints with it.&lt;/p&gt;
&lt;p&gt;Another piece of software I used was &lt;a href=&quot;http://www.getpaint.net/&quot; target=&quot;_blank&quot;&gt;Paint.net&lt;/a&gt;. If you know what Photoshop is  and what Microsoft Paint is, think of Paint.net as a happy medium between them.  It is way more advanced than Microsoft Paint, and offers things like cloning,  layers, transparency, and more, but stops short of the more advanced features  of Photoshop. By not being quite as advanced as Photoshop, it is able to  maintain a very small installation (currently uses a 3.5 MB installer), and it  runs blazing fast even on the slowest of computers. Normally you would never  need something like this for an Arduino or electronics project, but to  prototype what my GUI would look like on the LCD screen using the TVout  library, I needed a program I could use very easily and quickly to manipulate  images on a pixel-by-pixel level. Paint.net allowed me to do just that and was  integral in allowing me to create different GUI designs and even an animation.&lt;/p&gt;
&lt;p&gt;Finally, I used one of my all time favorite programs, and it  has been one of my favorites since long before I started work on this project, &lt;a href=&quot;http://notepad-plus-plus.org/&quot; target=&quot;_blank&quot;&gt;Notepad++&lt;/a&gt;. Sorry to keep using  comparisons, but if you are familiar with Microsoft Notepad and Microsoft  Visual Studio or Eclipse (or even Microsoft Word), Notepad++ is a somewhere  in-between. It has the minimalist design of notepad and the small footprint and  fast runtime, but it also has many of the features that make a full-fledged IDE  so great, such as syntax highlighting and collapsing of sections, spellcheck  (by way of a free plugin), auto-completion (available, not forced),  multi-tabbed to switch between documents, a document map, zooming, and many  more features. What I liked to do was to work on my Arduino sketches (.ino  files) in the Arduino IDE, but also have them open in Notepad++. Then, if I  wanted to navigate my code once it was really long, it was easier to use  Notepad++ to examine it in great detail and look for bugs.&lt;/p&gt;
&lt;p&gt;Using these three software programs, I was able to create  two Arduino programs (sketches), one that runs on the unit outside, and the  other that runs on the inside unit and displays the user interface. It took  over a thousand lines of code, many hours of debugging, and many cups of coffee,  but in the end the units work amazing and perform much better than I ever  expected them to. They have been running for days now without any software bugs  popping up, and my hope is that they will continue to run for quite some time.  However, the path to this point was not as easy it might sound right now. That  is because I have not yet discussed…&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;issues&quot; name=&quot;issues&quot;&gt;&lt;b&gt;&lt;u&gt;The  Issues!!!&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
 	&lt;li&gt;The first issue I ran into was that my LCD  screen flickered and was making weird patterns. Not only that, but the entire  program was acting strange and values were being reported and sent at values  outside of what they should have been. Sometimes the board would just crash and  nothing would work.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;The fix: This was a memory issue, one that I  would have been more aware of to start with if I had experience with  microcontrollers. Turns out, the Arduino Uno, the board I was using, only has &lt;a href=&quot;http://arduino.cc/en/Tutorial/Memory&quot; target=&quot;_blank&quot;&gt;2 kilobytes of SRAM&lt;/a&gt;, which is  insanely small to today’s standards, or even to yesterday’s standards,  considering that the original &lt;a href=&quot;https://en.wikipedia.org/wiki/Game_Boy#Technical_specifications&quot; target=&quot;_blank&quot;&gt;Gameboy released  in 1989 had 4 times that amount&lt;/a&gt; of both general SRAM and dedicated to  video. When I started writing my code and using the Arduino TVOut library I  followed their suggestion and used a default resolution of 128x96. Can anyone  see a problem with this? I should have. If you think about it for a second,  since the TVout library outputs only black or white, which can be really  thought of as a binary value, it would have to create a frame buffer of 128x96  pixels, with each pixel occupying a single bit of memory. If you use simple  math and multiply 128 by 96, you get 12,288, which is how many bits of SRAM the  code needs (1.5 kilobytes), just to display anything on the screen. Considering  that 2kB is only equal to 16384 bits, using a resolution of 128x96 was leaving  me with only 4,096 bits of SRAM, or half a kilobyte, to work with. Throw in  some more code and the fact that I was using the TVout library to load an  entire bitmap image into memory as a bootscreen, and it was no surprise that I  was maxing out my memory and inducing a buffer overflow. By reducing my  resolution to 120x54, I started using only 6,480 bits for the framebuffer,  cutting my SRAM usage roughly in half. This was a huge amount of saved memory  and I have had plenty of room for my bitmap bootscreen, all my large variables  and arrays, and all the graphic elements.&lt;/li&gt;
 	&lt;li&gt;The flickering / buffer overflow returned once  more, when I completely removed delays that limited the speed at which my  program ran. My guess is that if it runs too quickly, something happens with  the TVOut library and how it generates frames and uses SRAM. I found that a  TV.delay(10), which is 10 milliseconds, is enough to stop the SRAM from getting  chewed up and short enough to not impact any functionality of the devices.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;I couldn’t seem to get the TVout library to  completely fill the LCD screen.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;When I had to reduce the active resolution of my  project to save SRAM I inadvertently solved my problem. I just sat there and  played around with different low resolutions until I found one that occupied  very little SRAM but also filled most of my screen. That ended up being 120x54.  Keep in mind, if you are playing around with values, that the horizontal  resolution (in this case 120 pixels wide) has to be evenly divisible by 8.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;Everything was sideways!!! The TVOut library is  designed to output text and graphics to a screen that is in a landscape  orientation, like all TVs are and computer screens – this means that the screen  is wider than it is tall. However, because I’m a dolt and didn’t check any of  this out before soldering all my connections, cutting my enclosure, and gluing  everything &lt;b&gt;permanently&lt;/b&gt; in place, my  screen ended up being in portrait orientation, which mean that using the TVout  library without any modifications resulted in text being drawn sideways (if you  were to tilt your head 90 degrees clockwise it would become readable, see the  below pictures). &lt;/li&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;/div&gt;
&lt;ul&gt;
 	&lt;li&gt; 
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-uftpLGNi62g/UtMwsKIBDbI/AAAAAAAAAek/sNZ8FmSu2NI/s000/DSC_1487%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;426&quot; src=&quot;http://2.bp.blogspot.com/-uftpLGNi62g/UtMwsKIBDbI/AAAAAAAAAek/sNZ8FmSu2NI/s640/DSC_1487%2520%2528Custom%2529.JPG&quot; width=&quot;640&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Really sideways, not upside down&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-Sq9721WHZLk/UtMwtdA_WEI/AAAAAAAAAVQ/ZpJw09wtjtc/s000/DSC_1491%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;426&quot; src=&quot;http://2.bp.blogspot.com/-Sq9721WHZLk/UtMwtdA_WEI/AAAAAAAAAVQ/ZpJw09wtjtc/s640/DSC_1491%2520%2528Custom%2529.JPG&quot; width=&quot;640&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Now the letters are correct, but it is writing them sideways&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;/div&gt;
&lt;ul&gt;
 	&lt;li&gt; 
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://2.bp.blogspot.com/-H845AMVd3w8/UtMwtrh2QHI/AAAAAAAAAVM/ONaFaTu_4zA/s000/DSC_1492%2520%2528Custom%2529.JPG&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;426&quot; src=&quot;http://2.bp.blogspot.com/-H845AMVd3w8/UtMwtrh2QHI/AAAAAAAAAVM/ONaFaTu_4zA/s640/DSC_1492%2520%2528Custom%2529.JPG&quot; width=&quot;640&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Success!!! YES!!!&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
 	&lt;li&gt;The easiest solution would have also been the  hardest. If I had more experience before starting this project, I’m sure I  could have dug around in the library, found where it creates the frame buffer,  and then modified the library to generate a buffer that was rotated 90 degrees  counter-clockwise. Then I could have used all the TVOut functions as normal and  it would have drawn the screen just as how I wanted it to. Instead, I did  things the difficult way. First, I actually made my own font. Well, not quite  true. I took the 6x8 font that they provided and rotated every character 90  degrees counter clockwise. My dad helped me out by writing a tiny Python script  that rotated the binary chunks, or else I would have had to do it by hand  (which I actually did for a different font). This still doesn’t fix the  problem, as it would now write each character in the correct orientation, but  would write the words in the wrong orientation. See the above pictures for a  better look. The solution than become more convoluted – to write words on the  screen and have them show up readable, I had to individually print out each  character at different pixel locations, sort of like if every time you wanted  to type on your computer you had to use your mouse to set the cursor in a  different place (for every freakin’ character!). &lt;/li&gt;
 	&lt;li&gt; 
&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto; text-align: center;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;http://4.bp.blogspot.com/-GAIHpjMJ4K4/UtM_7sbgRbI/AAAAAAAAAeA/EDEV7_s6ZFk/s000/printing%2520a%2520word.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;http://4.bp.blogspot.com/-GAIHpjMJ4K4/UtM_7sbgRbI/AAAAAAAAAeA/EDEV7_s6ZFk/s000/printing%2520a%2520word.png&quot;&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Printing &quot;Sending&quot;, by printing each character individually&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
 	&lt;li&gt;This also made printing  numbers very difficult, because I had to write snippets of code to parse  variables, find out of a variable was holding a single, double, or triple digit  number, and then parse it into different place values depending on its length,  and then draw each place value in a different pixel location&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;The LCD screen could not be powered off the 5v  pin from the Arduino.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;This isn’t really a bug, and it makes sense. The  LCD screen is bright and obviously needs a decent amount of power. Although 5v  is enough voltage, current is what really matters, and an Arduino board only  receiving a few hundred milliamps cannot source enough amps to power the LCD  screen. To fix this, you can either solder connections from the VCC on the USB  connector before it hits the regulator, or you can do as I did, and solder  wires to the barrel connector on the UNO and solder the other ends to the power  supply for the LCD screen. I then started using a 2 amp &lt;a href=&quot;http://www.amazon.com/gp/product/B006QYRVU6/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B006QYRVU6&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;AC-DC  adapter&lt;/a&gt; with the barrel connector, to ensure that both the board and the  screen received enough current. It worked after that.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;When I got to coding for the nRF24L01+ radios, I  couldn’t get anything to work. I even tried a program that I had successfully  used before with slightly different hardware, and it still didn’t work.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;This was an annoying one. I spent hours checking  my code, then a few more hours tracing every circuit on my PCBs with a  multimeter and then even more time just trying and retrying to get them to  work. Eventually I resorted to purchasing another Arduino to test with, so I  could at least see which of the two units was not using the radio correctly.  Once it arrived, I tested and discovered that the external unit was not  responding to any messages. I traced its connections again and resoldered some  connections, but it still didn’t work. Finally, I realized that this might  actually not be my fault, and I swapped out the nRF24L01+ radio on that unit  with an extra I had lying around (I said they were cheap), and all of a sudden,  the exact same code that wasn’t working days ago, was working just fine. Turns  out, when you pay a dollar for something that competitors charge roughly 20  for, you are bound to get a dud once in a while. It really was just a bad radio  unit. I never really did any further testing to see why that particular radio  stopped working, nor do I really care to find out since I purchased two new  ones to replenish my supply for less than a cup of coffee. Luckily, instead of  soldering the radio directly to the board, I had soldered jumper cables with  female ends onto the board and then just placed the pins of the radio into the  female ends. This meant that to replace the radio I didn’t have to desolder  anything, I just had to pull the pins out of jumper cables and then slide the  jumper cables over the pins of the new radio. What I really want to emphasize  though, through telling this experience, is that sometimes things are really  just broken and it is not your fault, so don’t just look at your own work, look  at every part of the puzzle.&lt;/li&gt;
 	&lt;li&gt;Sometimes the Arduino IDE will not recognize  that a new library has been added and linked to your code, unless you do  &quot;Import library...&quot;, under the &quot;Sketch&quot; option on the top  menu bar&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;I’m not sure if this has been addressed  somewhere online, but it is just a strange, once in every 200 times, bug I have  encountered.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;Another nRF24L01+ radio issue: radio.read gets  nothing. At one point, I was trying to figure out why using the radio.read  function out of the nRF24L01+ radio library was getting stuck and returning  nothing, when it should have been returning a payload that the outside unit was  sending.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;The documentation for the library says to call  “startListening” before attempting a radio read. I did that, however what I  found out is that if it is called multiple times, in rapid succession (such as  during a while or for loop that is true), it will actually interfere with the  radio.read and you will get nothing read. My guess is that “startListening”  actually changes something on a hardware level on the radio and there needs to  be some sort of delay between the hardware change and the radio attempting a  read. Making sure that “startListening” was only called once and not  repetitively/rapidly, fixed my problem.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;After changing the datarate of the nRF24L01+  radios, nothing would work!&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;The documentation for the nRF24L01+ says that if  you need a longer distance range between two radios, you can decrease the data  rate of the transmissions. I did this by using “radio.setDataRate(RF24_250KBPS);”,  but after doing so, nothing would work. I figured out, and I am not sure if  this is addressed in the documentation, that you cannot set the data rate of  the radio before using “radio.begin”. This should be kind of intuitive as to  why, but I missed it the first time around, so worth warning about.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;My biggest and most challenging issue to debug  and fix: random lockup every 60 minutes. This was such a strange bug; every 60  minutes, pretty much on the dot, the GUI screen would freeze and say it had been  29 minutes since last updated (at that time, my code was set with 30 minute  intervals between reads) and it would stay that way indefinitely. Even  stranger, none of the buttons on the box responded to presses, even though  there was no while or for loops that could cause their keypresses not to be  read. The board couldn’t have actually crashed, or else nothing would be on the  screen; for there to be an image on the screen, the TVOut library has to be  actively functioning.&lt;/li&gt;
&lt;ul&gt;
 	&lt;li&gt;I initially thought this was a timing issue,  since it was occurring every 60 minutes like clockwork. I tried changing the  interval between reads from 30 minutes to 20 minutes, but then it just started  failing every 3 read instead of every 2nd.  Obviously, this was not the issue.&lt;/li&gt;
 	&lt;li&gt;I next jumped to low RAM as a possibility. As I  mentioned before, the SRAM of the Arduino UNO is extremely limited and it has  caused trouble before. However, I was able to determine that this was not the  case, as it had plenty of free SRAM.&lt;/li&gt;
 	&lt;li&gt;Finally, I remembered that the TVOut library is  insanely dependent on interrupts. This is why you cannot use serial at the same  time as using TVOut. I started looking through the different libraries I was  using and discovered that the library used by the Real Time Clock and the Wire  library (which is necessary to talk to the RTC) also used interrupts. I didn’t  want to completely stop using the library, so what I did was move the reading  of the RTC out of the very fast looping main loop and into a subroutine, that  is only ran once a user presses a button to display the current calendar date  and time. Since I could no longer check unixtime using the RTC in the main  loop, I replaced my uses of unixtime with TV.millis, which returns the number  of milliseconds since the board has started running. Unfortunately, TVOut’s  dependency on interrupts also screws with the boards timing, and so the  TV.millis returns a rather inaccurate number. I was able to compensate by  finding a factor I could multiple the number by to get a more accurate representation  of the number of milliseconds the board has been running. Anyways, moving the  reads of the RTC and the Wire.begin outside of the main loop fixed the problem.  It has now been running for multiple days on end and has not frozen up once.&lt;/li&gt;
&lt;/ul&gt;
 	&lt;li&gt;There is one issue that I am sad to say has not  yet quite been resolved: humidity. As you should be able to see from the  pictures, I build the external enclosure in a sealed electrical box from  HomeDepot. It has a gasket that runs between the lid and the box, and I screwed  the lid on tight enough to seal it. However, every day, since I first plugged  it in outside, the humidity has been steadily going up on the readings on the inside  unit. It started somewhere around 40 I think, and after about a week and half,  had reading 90%. I freaked out, as the alarm kept going off and I had to keep  raising the alert level so the high humidity wouldn’t trigger it, but most  importantly, I was worried that the high humidity might damage the Arduino  board (I’ll remind you that the humidity is measured from &lt;b&gt;inside&lt;/b&gt; the enclosure, not the air outside). I live in Seattle  Washington, so one fact that I cannot change is that it is very damp here and  we get frequent rain (especially right now, when I am having the humidity  issue). My current temporary fix was to go outside, put the entire enclosure in  a ziplock bag with a bunch of silica gel packets (that absorb moisture in the  air), then seal the bag with rubber bands, then cover the bag and the enclosure  with saran wrap, and then secure the saran wrap with more rubber bands. After  doing that, the humidity readings stabilized, and then dropped a few  percentages, but they are still way too high, hovering around 90%. Once it  stops raining (which might take a while here), I’ll unwrap it, maybe let it dry  in the sun or take a blow drier out on low heat, and add some new silica gel  packs and a better seal.&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;things_i_could_have_done_differently&quot; name=&quot;things_i_could_have_done_differently&quot;&gt;&lt;b&gt;&lt;u&gt;Things I could have done differently:&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
&lt;ul&gt;
 	&lt;li&gt;Instead of using the TVOut library, I could have  used an LCD that was designed to be used with Arduino (such as one mounted on a  shield), however, those use more pins and I am actually glad I went with TVOut  since I learned more using it&lt;/li&gt;
 	&lt;li&gt;I could have used a different Arduino with more  RAM, such as the MEGA, or I could have used a completely different kind of  microcontroller platform, such as the &lt;a href=&quot;http://www.pjrc.com/store/teensy3.html&quot; target=&quot;_blank&quot;&gt;Teensy board&lt;/a&gt;, which actually  has a much more powerful 32 bit ARM processor for about the same price, or I  could have gone with a &lt;a href=&quot;http://www.amazon.com/gp/product/B009SQQF9C/ref=as_li_ss_tl?ie=UTF8&amp;#x26;camp=1789&amp;#x26;creative=390957&amp;#x26;creativeASIN=B009SQQF9C&amp;#x26;linkCode=as2&amp;#x26;tag=jwbstz-20&quot; target=&quot;_blank&quot;&gt;Raspberry  Pi&lt;/a&gt;. Using a more powerful board would have meant I could have used a higher  resolution with the TVOut library, as well as store more information in RAM. As  it is, I actually really look how the display looks with the low resolution setting,  as it has a nice retro feel and is easily visible from far away. For future  projects I might try different boards, but I am glad I went with the UNO for  the internal unit.&lt;/li&gt;
&lt;/ul&gt;
&lt;span style=&quot;font-size: large;&quot;&gt;&lt;a href=&quot;#&quot; id=&quot;thanks&quot; name=&quot;thanks&quot;&gt;&lt;b&gt;&lt;u&gt;Thanks&lt;/u&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;
If you read to here, I feel sorry for you. That was a lot to  read and I really went overboard in writing this. I thank you though, for  taking the time to read it. If you didn’t read all the way to here, and just  skipped to the bottom of the page, I can’t say I blame you.
&lt;p&gt;Finally, I want to say thanks to my parents for raising me  and encouraging me to do stuff like this, and especially my dad for sparking  the original idea, helping me debug a few things, and letting me use his tools  and workroom.&lt;/p&gt;
&lt;/div&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item><item><title>Virtual Tour Site: TheOldLW.com</title><link>https://www.theoldlw.com/</link><guid isPermaLink="true">https://www.theoldlw.com/</guid><description>For my senior project in high school, I took close to 10,000 photos of the inside of the old Lake Washington High School, before it was demolished, stitched them together into panoramic images, and joined the panoramic files into a giant interactive tour. I al...</description><pubDate>Mon, 12 Sep 2011 07:00:00 GMT</pubDate><content:encoded>&lt;!-- 302 REDIRECT TO externally_hosted_full_page_url --&gt;</content:encoded><dc:creator>Joshua Tzucker</dc:creator></item></channel></rss>