<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Michael Uloth</title>
        <link>https://michaeluloth.com/</link>
        <description>Software engineer helping scientists discover new medicines at Recursion.</description>
        <lastBuildDate>Thu, 01 Jan 2026 00:59:50 GMT</lastBuildDate>        <language>en-ca</language>
        <copyright>All rights reserved 2026, Michael Uloth</copyright>
        <item>
            <title><![CDATA[Ignoring files you've already committed]]></title>
            <link>https://michaeluloth.com/git-ignore-tracked-files/</link>
            <guid permalink="true">https://michaeluloth.com/git-ignore-tracked-files/</guid>
            <pubDate>Mon, 08 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Cache invalidation is tricky to automate.]]></description>
            <content:encoded><![CDATA[<p>If you try to <code>.gitignore</code> files <em>after</em> committing them, you'll notice it doesn't work: <code>git</code> still tracks changes to those files and they still appear in your remote repo. What to do?</p>
<p>You need to remove those files from <code>git</code>'s cache.</p>
<p>You can remove specific files or folders:</p>
<pre><code>git rm --cached &lt;file&gt;
git rm -r --cached &lt;folder&gt;</code></pre>
<p>Or just clear the whole cache:</p>
<pre><code>git rm -r --cached .</code></pre>
<p>Then commit your changes so <code>git</code> knows which files it <em>should</em> track going forward:</p>
<pre><code>git add .
git commit -m &quot;fix: stop tracking ignored files&quot;</code></pre>
<h2>Related</h2>
<ul><li><a href="https://gist.github.com/ainsofs/2b80771a5582b7528d9e">Clear .gitignore cache</a> • Ainsof So'o</li><li><a href="https://stackoverflow.com/questions/1139762/ignore-files-that-have-already-been-committed-to-a-git-repository">Ignore files that have already been committed to a Git repository</a> • Stack Overflow</li><li><a href="https://stackoverflow.com/questions/1274057/how-do-i-make-git-forget-about-a-file-that-was-tracked-but-is-now-in-gitignore">How do I make Git forget about a file that was tracked, but is now in .gitignore?</a> • Stack Overflow</li></ul>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Converting a list of JS objects into a parent-child tree]]></title>
            <link>https://michaeluloth.com/javascript-objects-nested/</link>
            <guid permalink="true">https://michaeluloth.com/javascript-objects-nested/</guid>
            <pubDate>Fri, 27 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[A fun nesting puzzle.]]></description>
            <content:encoded><![CDATA[<p>My previous approach to displaying my notes on this site was to render them as a tree of infinitely nested topics and subtopics:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1735331880/mu/notes-index-nested.png" alt="" />
<h2>Parents and children</h2>
<p>To achieve that nesting, my solution was to manually give each note that represented a subtopic (or subsubsubtopic) a <code>parent</code> value (via markdown frontmatter) and then move any child notes (i.e. notes with a <code>parent</code> value) into their parent's <code>children</code> array:</p>
<pre><code>/**
 * Given an array of collection items, returns the array with child items nested under their parents.
 */
function nestChildren(collection: Note[]): Note[] {
  // Step 1: Create a mapping of item IDs to item data
  const slugToNodeMap = collection.reduce(
    (nodesBySlug, item): Record&lt;string, Note&gt; =&gt; {
      // Add an empty children array to the item's existing data
      nodesBySlug[item.id.toLowerCase()] = { ...item, data: { ...item.data, children: [] } }
      return nodesBySlug
    },
    {} as Record&lt;string, Note&gt;,
  )

  // Step 2: Build the nested item tree
  const tree = collection.reduce((roots, item): Note[] =&gt; {
    // Find the node matching the current collection item
    const node = slugToNodeMap[item.id.toLowerCase()]

    if (item.data.parent) {
      // If the note has a parent...
      const parentNode = slugToNodeMap[item.data.parent.toLowerCase()]

      if (parentNode) {
        // ...add the item's data to the parent's children array
        parentNode.data.children.push(node)
      } else {
        console.error(`Parent slug &quot;${item.data.parent}&quot; not found (this should never happen).`)
      }
    } else {
      // If the item has no parent, treat it as a new root-level note
      roots.push(node)
    }

    // Return the updated tree and keep iterating
    return roots
  }, [] as Note[])

  // Return the final, nested tree
  return tree
}

/**
 * Returns all notes with child notes nested under their parents.
 */
export const getNestedNotes = async (): Promise&lt;Note[]&gt; =&gt;
  nestChildren(await getCollection('writing', note =&gt; isNote(note)))</code></pre>
<h2>Incremental left padding</h2>
<p>My solution for visually indicating how those notes related to each other was to multiply each note's <code>left-padding</code> by <code>1.4x</code> its descendent level:</p>
<pre><code>&lt;ul class=&quot;list-notes&quot;&gt;
  {
    notes.map(note =&gt; {
      const getLinkText = (item: Note): string =&gt; item.data.title || item.id

      const getChildren = (item: Note, level: number) =&gt;
        item.data.children &amp;&amp; (
          &lt;ul&gt;
            {item.data.children
              .sort((a: Note, b: Note) =&gt; a.data.title.localeCompare(b.data.title))
              .map((child: Note) =&gt; (
                &lt;li
                  class=&quot;before:content-['└'] before:pe-[0.3rem] mt-[0.1rem]&quot;
                  style={`padding-left: ${level === 0 ? 0 : 1.4}rem`}
                &gt;
                  &lt;a href={`/${child.slug}/`} class=&quot;link&quot;&gt;
                    {getLinkText(child)}
                  &lt;/a&gt;

                  {/* Recursively render grandchildren, etc */}
                  {getChildren(child, level + 1)}
                &lt;/li&gt;
              ))}
          &lt;/ul&gt;
        )

      return (
        &lt;li class=&quot;mb-2 break-inside-avoid-column&quot;&gt;
          &lt;a href={`/${note.slug}/`} class=&quot;link&quot;&gt;
            {getLinkText(note)}
          &lt;/a&gt;

          {getChildren(note, 0)}
        &lt;/li&gt;
      )
    })
  }
&lt;/ul&gt;</code></pre>
<h2>Maintaining note hierarchies is a chore</h2>
<p>That was a fun programming puzzle to solve, but unfortunately it made my note-taking more difficult by forcing me to decide where each new thought belonged in the hierarchy before I could jot it down.</p>
<p>To remove that friction, I've adopted to a flat (and eventually searchable and filterable) approach to note-taking instead. But I wanted to share this former nested-topics solution in case anyone else finds it useful.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[A "!" prefix makes any Tailwind CSS class important]]></title>
            <link>https://michaeluloth.com/tailwindcss-important-modifier/</link>
            <guid permalink="true">https://michaeluloth.com/tailwindcss-important-modifier/</guid>
            <pubDate>Thu, 26 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[A delightfully succinct bit of syntax.]]></description>
            <content:encoded><![CDATA[<p>You can add <code>!important</code> to any Tailwind CSS declaration by putting a &quot;!&quot; <a href="https://tailwindcss.com/docs/configuration#important-modifier">at the beginning of the class name</a>:</p>
<pre><code>&lt;p class=&quot;!my-0&quot;&gt;Really, really no vertical margins&lt;/p&gt;</code></pre>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Tmux's version command is -V]]></title>
            <link>https://michaeluloth.com/tmux-version/</link>
            <guid permalink="true">https://michaeluloth.com/tmux-version/</guid>
            <pubDate>Wed, 18 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Probably the last option you’d think to try.]]></description>
            <content:encoded><![CDATA[<p>If you want to see which version of <code>tmux</code> is installed, you're <a href="https://stackoverflow.com/questions/26705755/tmux-how-do-i-find-out-the-currently-running-version-of-tmux">looking for</a>...</p>
<pre><code>tmux -V</code></pre>
<p>Not <code>tmux -v</code>.</p>
<p>Not <code>tmux --version</code>.</p>
<p>Not <code>tmux version</code>.</p>
<p>Why you gotta be so special, tmux?</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[SELECT DISTINCT outputs a column's unique values]]></title>
            <link>https://michaeluloth.com/sql-select-distinct/</link>
            <guid permalink="true">https://michaeluloth.com/sql-select-distinct/</guid>
            <pubDate>Thu, 12 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[A very useful keyword when you’re trying to understand sets of values.]]></description>
            <content:encoded><![CDATA[<p>Here's <a href="https://www.dbvis.com/thetable/sql-distinct-a-comprehensive-guide/">how</a> to see every value in a column with SQL:</p>
<pre><code>SELECT DISTINCT column
FROM table_name</code></pre>
<p>I found that helpful today when I wanted to understand the significance of a value I was looking at by comparing it to the alternatives.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[It's tricky to statically type a "pipe" function in Python]]></title>
            <link>https://michaeluloth.com/python-fp-pipe/</link>
            <guid permalink="true">https://michaeluloth.com/python-fp-pipe/</guid>
            <pubDate>Thu, 28 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Less tricky once you understand overloads better.]]></description>
            <content:encoded><![CDATA[<p>I wanted a type-safe <code>pipe</code> utility to help me write Python in a more functional style, but unfortunately <a href="https://toolz.readthedocs.io/en/latest/api.html#toolz.functoolz.pipe">toolz.pipe</a> and <a href="https://returns.readthedocs.io/en/latest/pages/pipeline.html#flow">returns.pipelines.flow</a> both output <code>Any</code>.</p>
<p>Happily, it turns out creating your own pipe mechanism is a one-liner:</p>
<pre><code>from functools import reduce

result = reduce(lambda acc, f: f(acc), (fn1, fn2, fn3), value)</code></pre>
<p>Which you can reuse by wrapping it in a function:</p>
<pre><code>from functools import reduce
from typing import Callable, TypeVar

_A = TypeVar(&quot;A&quot;)
_B = TypeVar(&quot;B&quot;)

def pipe(value: _A, *functions: Callable[[_A], _A]) -&gt; _A:
    &quot;&quot;&quot;Pass a value through a series of functions that expect one argument of the same type.&quot;&quot;&quot;
    return reduce(lambda acc, f: f(acc), functions, value)</code></pre>
<p>And calling it with any number of functions:</p>
<pre><code>assert pipe(&quot;1&quot;, int, float, str) == &quot;1.0&quot;
# =&gt; i.e. str(float(int('1')))
# =&gt; i.e. int(&quot;1&quot;) -&gt; float(1) -&gt; str(1.0) -&gt; &quot;1.0&quot;</code></pre>
<p>So you can stop thinking up names for throwaway variables like these:</p>
<pre><code>def str_to_float_str(value: string):
    as_integer = int(value)
    as_float = float(as_integer)
    as_string = str(as_float)
    return as_string

assert str_to_float_str(&quot;1&quot;) == &quot;1.0&quot;</code></pre>
<p>Credit to <a href="https://earldouglas.com/mypy-lists.html">Statically-Typed Functional Lists in Python with Mypy</a> by James Earl Douglas and the <a href="https://returns.readthedocs.io/en/latest/_modules/returns/_internal/pipeline/flow.html#flow">returns.pipeline.flow</a> source code for the inspiration.</p>
<h3>Update: Nov 29, 2024</h3>
<p>While the <code>pipe</code> function above with the <code>Callable[[A], A]</code> type hint works fine if every function in the pipeline outputs the same type (<code>A</code>), the example I showed above doesn't actually work out very well! Mypy notices that some of the functions (<code>int</code> and <code>float</code>) output a different type than we started with (<code>str</code>), so we aren't actually passing <code>A</code> all the way through.</p>
<p>After trying a number of workarounds (and getting some good advice on <a href="https://www.reddit.com/r/Python/comments/1h2esxo/creating_a_typesafe_pipe_function_in_python/">Reddit</a>), I learned that you can either tell Mypy what's going on by painstakingly articulating every possible overload:</p>
<pre><code>_A = TypeVar(&quot;A&quot;)
_B = TypeVar(&quot;B&quot;)
_C = TypeVar(&quot;C&quot;)
_D = TypeVar(&quot;D&quot;)
_E = TypeVar(&quot;E&quot;)

@overload
def pipe(value: _A) -&gt; _A: ...

@overload
def pipe(value: _A, f1: Callable[[_A], _B]) -&gt; _B: ...

@overload
def pipe(
    value: _A,
    f1: Callable[[_A], _B],
    f2: Callable[[_B], _C]
) -&gt; _C: ...

@overload
def pipe(
    value: _A,
    f1: Callable[[_A], _B],
    f2: Callable[[_B], _C],
    f3: Callable[[_C], _D]
) -&gt; _D: ...

@overload
def pipe(
    value: _A,
    f1: Callable[[_A], _B],
    f2: Callable[[_B], _C],
    f3: Callable[[_C], _D],
    f4: Callable[[_D], _E],
) -&gt; _E: ...


def pipe(value: Any, *functions: Callable[[Any], Any]) -&gt; Any:
    return reduce(lambda acc, f: f(acc), functions, value)
</code></pre>
<p>Or, you can just use <a href="https://github.com/dbrattli/Expression">expression</a>, which already <a href="https://github.com/dbrattli/Expression/blob/main/expression/core/pipe.py#L43">does this for you</a>.</p>
<p>I'm going to do the latter, but this was a fun exercise in the meantime. 😎</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[An iOS Shortcut can add data to a Google Sheet]]></title>
            <link>https://michaeluloth.com/ios-shortcut-save-form-responses-to-google-sheet/</link>
            <guid permalink="true">https://michaeluloth.com/ios-shortcut-save-form-responses-to-google-sheet/</guid>
            <pubDate>Wed, 20 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Workflows you can complete from your phone are seriously underrated.]]></description>
            <content:encoded><![CDATA[<p>I followed <a href="https://thiagoalves.ai/sending-data-to-google-sheets-using-ios-shortcuts/">this short guide</a> by Thiago Alves for sending arbitrary data to Google Sheets from iOS via an iOS Shortcut and a Google Form and it worked like a charm.</p>
<p>In brief, here are the steps:</p>
<ol><li>Create a <a href="https://docs.google.com/forms">Google Form</a></li><li>Link it to a Google Sheet</li><li>Copy the pre-filled link to your form and edit it to end with <code>&amp;submit=Submit</code> and say <code>formResponse</code> instead of <code>viewform</code></li><li>Use a <strong>Text</strong> shortcut action to insert your data into that link</li><li>Use a <strong>Get contents of URL</strong> action to send your data to your form results sheet</li></ol>
<p>A response link for a form with one question will look something like this:</p>
<pre><code>https://docs.google.com/forms/d/e/&lt;form-id&gt;/formResponse?usp=pp_url&amp;entry.&lt;question-id&gt;=&lt;answer&gt;&amp;submit=Submit</code></pre>
<p>Here's what my shortcut to save the current URL looks like:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1732163784/mu/rss-shortcut-redacted.png" alt="" />]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[How to query a BigQuery table from Python]]></title>
            <link>https://michaeluloth.com/bigquery-query-from-python/</link>
            <guid permalink="true">https://michaeluloth.com/bigquery-query-from-python/</guid>
            <pubDate>Tue, 19 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Doesn’t take much to pull in a whole lotta data.]]></description>
            <content:encoded><![CDATA[<h2>1. Install the SDK</h2>
<pre><code>dependencies = [
	&quot;google-cloud-bigquery[bqstorage, pandas]&quot;,
]</code></pre>
<h2>2. Create a BigQuery client</h2>
<pre><code>from google.cloud import bigquery

bq_client = bigquery.Client(project=&quot;project&quot;)</code></pre>
<h2>3. Query your data</h2>
<pre><code>query = &quot;&quot;&quot;
SELECT column_one, column_two
FROM bq_table
WHERE column_three = true
&quot;&quot;&quot;

df = bq_client.query(query).to_dataframe()</code></pre>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[CloudFlare sells domain names at cost]]></title>
            <link>https://michaeluloth.com/cloudflare-domain-registration-at-cost/</link>
            <guid permalink="true">https://michaeluloth.com/cloudflare-domain-registration-at-cost/</guid>
            <pubDate>Sat, 16 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Consumer pricing doesn’t often go down.]]></description>
            <content:encoded><![CDATA[<p>I thought of a domain I wanted to grab.</p>
<p>I looked it up on my usual domain registrar (Hover): <code>$20.99</code></p>
<p>I looked it up on CloudFlare: <code>$12.99</code></p>
<p>Sold.</p>
<p>(The <a href="https://developers.cloudflare.com/registrar/about/">free security features</a> don't hurt either.)</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Homebrew packages might install dependencies]]></title>
            <link>https://michaeluloth.com/homebrew-package-dependencies/</link>
            <guid permalink="true">https://michaeluloth.com/homebrew-package-dependencies/</guid>
            <pubDate>Wed, 13 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Brew packages install brew packages.]]></description>
            <content:encoded><![CDATA[<p>I wondered why <code>brew list</code> showed so many packages I didn't install myself.</p>
<p>Then, I discovered those are <a href="https://stackoverflow.com/questions/69994716/brew-list-shows-many-things-i-did-not-install-why-if-something-installed-dep">the dependencies of the packages I installed</a>. And the dependencies of those dependencies. And so on.</p>
<p>Seems kinda obvious now.</p>
<pre><code># Which packages and casks are installed?
brew list</code></pre>
<pre><code># Why is &lt;package-name&gt; installed? Which packages are using it?
brew uses &lt;package-name&gt; --installed</code></pre>
<pre><code># Which dependencies came with &lt;package-name&gt;?
brew deps &lt;package-name&gt; --tree</code></pre>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Shell functions don’t need parentheses]]></title>
            <link>https://michaeluloth.com/bash-functions-parentheses-optional/</link>
            <guid permalink="true">https://michaeluloth.com/bash-functions-parentheses-optional/</guid>
            <pubDate>Mon, 11 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Just in case you see this in the wild and are confused.]]></description>
            <content:encoded><![CDATA[<p>Copilot surprised me by generating a <code>zsh</code> function that looked like this:</p>
<pre><code>function act {
  # do stuff
}</code></pre>
<p>Instead of like this:</p>
<pre><code>act() {
  # do stuff
}</code></pre>
<p>It turns out <a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Functions.html">function parentheses are optional in bash</a> and <a href="https://zsh.sourceforge.io/Doc/Release/Shell-Grammar.html#index-function">zsh</a>. Though, using parentheses <a href="https://stackoverflow.com/a/4654730/8802485">is more POSIX compliant</a> if that's relevant for your use case.</p>
<p>In fact, all of these variations are equivalent:</p>
<pre><code>function act () {
  command
}

function act() {
  command
}

function act {
  command
}

act () {
  command
}

act() {
  command
}

act () command

act() command</code></pre>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[You can run shell scripts in tmux.conf]]></title>
            <link>https://michaeluloth.com/tmux-conf-run-shell-script/</link>
            <guid permalink="true">https://michaeluloth.com/tmux-conf-run-shell-script/</guid>
            <pubDate>Tue, 05 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[It’s a nice way to integrate whatever behaviour you like.]]></description>
            <content:encoded><![CDATA[<h2>1. Create a shell script</h2>
<pre><code>#!/usr/bin/env bash

echo &quot;♥&quot; $(pmset -g batt | grep -Eo '[0-9]+%')</code></pre>
<h2>2. Make the file executable</h2>
<pre><code>chmod +x battery.sh</code></pre>
<h2>3. Call it from <code>tmux.conf</code></h2>
<pre><code>set -g status-right &quot;#($HOME/.config/tmux/battery.sh)&quot;</code></pre>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Why unknown types are useful]]></title>
            <link>https://michaeluloth.com/programming-types-unknown-why-useful/</link>
            <guid permalink="true">https://michaeluloth.com/programming-types-unknown-why-useful/</guid>
            <pubDate>Mon, 29 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Sometimes you want the type checker to help you avoid making assumptions.]]></description>
            <content:encoded><![CDATA[<p>When external data enters your program, you can't <em>really</em> be certain of its type unless you validate it. That library output, that API response, and (most of all) that user input...are you sure it is what you think it is?</p>
<p>Until you check, the most accurate type to assign that data is one that means &quot;I don't actually know&quot;.</p>
<p>(This post is a response to questions I received <a href="https://www.reddit.com/r/Python/comments/1e942fx/comment/leepku0/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button">on Reddit</a> suggesting we always know a given value's data type. For any data coming from outside your program, I think that assumption is risky!)</p>
<h2>Making Assumptions</h2>
<p>Let's say you're taking input from a user and expecting it to be a string. A naive approach would be to assume the data will always be what you expect:</p>
<pre><code>const getUserInput = (): string =&gt; {/*...*/}

const unsafe = () =&gt; {
  const data = getUserInput()
  data.toUpperCase()
}
</code></pre>
<p>No warnings, no problem, right? Not exactly... There are no warnings because we told the type checker the user input is always a <code>string</code>. That's why it's happy to let us call string methods on <code>data</code>.</p>
<p>But what if <code>data</code> is sometimes <code>undefined</code> (or anything other than <code>string</code>)? In that case, this code will experience an uncaught <code>TypeError</code> at runtime saying, <code>Cannot read properties of undefined (reading 'toUpperCase')</code>, which may leave your program in a broken state.</p>
<p>This is where &quot;unknown&quot; can help — and unfortunately where a lot of people reach for an &quot;any&quot; type. Be careful! &quot;any&quot; is the opposite of &quot;unknown&quot; and effectively disables type checking by telling the type checker all assumptions about a value are safe. You probably don't want that.</p>
<h2>Unknown to the Rescue</h2>
<p>Some languages explicitly include a type called &quot;unknown&quot; (e.g. <a href="https://www.typescriptlang.org/docs/handbook/2/functions.html#unknown">TypeScript has one</a>), while others have a type you can treat similarly (e.g. Python's <code>object</code> type <a href="https://michaeluloth.com/python-types-object-as-unknown/">effectively means &quot;unknown&quot;</a>).</p>
<p>Whatever your language gives you, the general approach is the same:</p>
<ol><li>Assign the &quot;unknown&quot; type to the unverified data</li><li>Explicitly validate the data's relevant characteristics before you use them</li><li>Be happy when your tooling warns you about unsafe assumptions you're making</li></ol>
<h2>No Assumptions</h2>
<p>Let's ask the type checker to help us be more careful:</p>
<pre><code>// 🤞 Should be a string, but who knows...
const getUserInput = (): unknown =&gt; {/*...*/}

const unsafe = () =&gt; {
  const data = getUserInput()
  data.toUpperCase()
  // 🚨 'data' is of type 'unknown'
}</code></pre>
<p>Perfect! We want those type warnings. We're calling a <code>string</code> method (<code>toUpperCase</code>) on a value we haven't confirmed is a <code>string</code>. That's risky.</p>
<p>To resolve the warning, we need to validate the assumption we're making about <code>data</code>'s type:</p>
<pre><code>const getUserInput = (): unknown =&gt; {/*...*/}

const safe = () =&gt; {
  const data = getUserInput()

  if (typeof data === 'string') {
    data.toUpperCase() // confirmed safe
  } else {
    // handle invalid input
  }
}</code></pre>
<p>With each assumption you validate, the type checker &quot;widens&quot; its understanding of your data from its narrow starting point (<code>unknown</code>) to a type with more characteristics (e.g. <code>string</code>).</p>
<p>And now you can be sure what type of data you have.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Using "object" as an unknown type in Python]]></title>
            <link>https://michaeluloth.com/python-types-object-as-unknown/</link>
            <guid permalink="true">https://michaeluloth.com/python-types-object-as-unknown/</guid>
            <pubDate>Thu, 25 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Python's base class (and base type) can help keep you safe when your data could be anything.]]></description>
            <content:encoded><![CDATA[<p>TypeScript has an <a href="https://www.typescriptlang.org/docs/handbook/2/functions.html#unknown">unknown</a> type, which can be <a href="https://michaeluloth.com/programming-types-unknown-why-useful/">quite useful</a>. Python doesn't have &quot;unknown&quot;, but its <code>object</code> type can be used the same way.</p>
<h2>The <code>object</code> type</h2>
<p>The <a href="https://docs.python.org/3/library/functions.html#object">object</a> type in Python &quot;is a base for all classes&quot; containing the &quot;methods that are common to all instances of Python classes.&quot; And since in Python &quot;types&quot; and &quot;classes&quot; are effectively the same thing, <code>object</code> is both Python's base class and base type.</p>
<p>While Python doesn't explicitly advertise <code>object</code> as the language's &quot;use when you're not sure&quot; type, it works well for that use case because it only includes the features all types share.¹ Beyond that, it makes no assumptions about what type features a value might have.</p>
<p>In practice, using the <code>object</code> type tells Mypy to warn you if you start making unvalidated assumptions about what you can safely do with that value:</p>
<pre><code>def unsafe(value: object) -&gt; None:
    value.get(&quot;some_key&quot;)
    # 🚨 &quot;object&quot; has no attribute &quot;get&quot;
</code></pre>
<p>To pass the type-checker, you need to confirm <code>value</code> has a <code>get</code> method before you use it:</p>
<pre><code>def safe(value: object) -&gt; None:
    if not isinstance(value, dict):
        return None

    value.get(&quot;some_key&quot;)
    # other logic...
</code></pre>
<p>You won't need <code>object</code> for data you create yourself, but it can make validating external data much safer and easier.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Defining a custom unknown type in Python]]></title>
            <link>https://michaeluloth.com/python-types-unknown/</link>
            <guid permalink="true">https://michaeluloth.com/python-types-unknown/</guid>
            <pubDate>Sun, 21 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[How to replicate TypeScript's unknown type with a generic type variable.]]></description>
            <content:encoded><![CDATA[<p>Python doesn't include a built-in <code>unknown</code> type like <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type">TypeScript's</a>. Since <code>unknown</code> can be so helpful, it may be worth creating one yourself.</p>
<p>You can do that by <a href="https://stackoverflow.com/questions/51828930/unknown-variable-for-python-typehint/51830033#51830033">defining a generic type variable</a>:</p>
<pre><code>from typing import TypeVar

unknown = TypeVar(&quot;unknown&quot;)</code></pre>
<p>This is useful anywhere you want <a href="https://mypy-lang.org/">Mypy</a> to remind you to make no assumptions about a value's type:</p>
<pre><code>def unsafe(value: unknown) -&gt; None:
    value.get(&quot;some_key&quot;)
    # 🚨 Mypy: &quot;unknown&quot; has no attribute &quot;get&quot;

def safe(value: unknown) -&gt; None:
    if not isinstance(value, dict):
        return None

    value.get(&quot;some_key&quot;)</code></pre>
<p>Using a <a href="https://docs.python.org/3/library/typing.html#generics">Generic</a> to mimic <code>unknown</code> is a safer option than <a href="https://docs.python.org/3/library/typing.html#the-any-type">Any</a>, which effectively disables type-checking by assuming anything you try to do with a value will work.</p>
<h2>Update</h2>
<p>Thanks to <a href="https://www.reddit.com/r/Python/comments/1e942fx/comment/leca3ja/">this helpful discussion</a> of this post, I've learned that Python's built-in <code>object</code> type already works as an &quot;unknown&quot; type, so there's no need to resort to hacking generics to replicate one.</p>
<p>I'll leave this post up to avoid a broken link, but let's all agree to <a href="https://michaeluloth.com/python-types-object-as-unknown/">use &quot;object&quot; instead</a>!</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Undoing a merge to your main git branch]]></title>
            <link>https://michaeluloth.com/git-undo-merge-to-main/</link>
            <guid permalink="true">https://michaeluloth.com/git-undo-merge-to-main/</guid>
            <pubDate>Wed, 20 Dec 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[How to roll back changes by reverting commits]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1703127894/mu/u-turn.jpg" alt="Undoing a merge to your main git branch" />
<img src="https://res.cloudinary.com/ooloth/image/upload/v1703127894/mu/u-turn.jpg" alt="" />
<p>You merge your PR only to discover you accidentally missed a bug and now prod is broken. Gah! Is there a quick way to undo that merge?</p>
<p>Yes! What you need to do is “revert” your PR. Here’s how to do that using the GitHub UI.</p>
<h2>Reverting a PR</h2>
<ol><li>In the browser, go to your merged PR</li><li>Click the “Revert” button near the bottom of the page to create a new <code>revert-</code> branch that reverses your changes</li><li>When prompted, open a PR for that <code>revert-</code> branch</li><li>Merge the PR</li></ol>
<p>That’s it. 😀 Crisis averted. Your main branch is back to how it was before your feature.</p>
<h2>Restoring your original feature branch</h2>
<p>If you want to debug and re-open an improved version of your original PR, here’s what you need to do:</p>
<ol><li>Click the “Revert” button at the bottom of the <em>reversion</em> PR you just merged to create a new <code>revert-revert-</code> branch that includes the same changes as your original feature branch (i.e. revert the reversion)</li><li>Pull that <code>revert-revert-</code> branch locally (don't open a PR yet) and make any changes you like to this new copy of your feature branch (not your old one!)</li><li>When ready, push your changes and open a new PR for the <code>revert-revert-</code> branch with the repaired version of your feature</li><li>Merge when ready</li></ol>
<h2>Reverting a commit</h2>
<p>So far, these steps have assumed the change that broke prod came from a PR. But what if you committed directly to <code>main</code>?</p>
<p>That's a simpler fix. You'll just need to revert that bad commit (no PR required):</p>
<pre><code>git checkout main
git log # find the hash of your bad commit (&quot;q&quot; to exit)
git revert HASH
git log # admire your new &quot;Revert X&quot; commit (optional)
git push</code></pre>
<p>Or using <a href="https://github.com/jesseduffield/lazygit">Lazygit</a>:</p>
<ol><li>Go to the Branches panel and check out <code>main</code></li><li>Go to the Commits panel and highlight the bad commit</li><li>Press <code>t</code> to revert your commit (you'll see a new commit appear reversing your bad one)</li><li>Press <code>P</code> to push the new commit to your remote branch</li></ol>
<h2>Try, try again</h2>
<p>These steps have helped me out of multiple “oh 💩” moments at work. I hope they help you too!</p>
<p>Reverting your reversion may seem a bit strange the first time you do it, but hopefully it will make sense as you think about it. 🙂</p>
<h2>Related</h2>
<ul><li><a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/reverting-a-pull-request">Reverting a pull request</a> • GitHub Docs</li><li><a href="https://stackoverflow.com/questions/27852143/how-to-pr-and-merge-again-after-reverting-pr-using-github-revert-button">How to PR and merge again after reverting PR using Github Revert Button</a> • StackOverflow</li><li><a href="https://justinjoyce.dev/undo-git-commit/">Undo a Git Commit</a> • Justin Joyce</li></ul>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Switching configs in Neovim]]></title>
            <link>https://michaeluloth.com/neovim-switch-configs/</link>
            <guid permalink="true">https://michaeluloth.com/neovim-switch-configs/</guid>
            <pubDate>Thu, 07 Sep 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[How to maintain multiple Neovim configurations and switch between them]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1694064460/mu/ratchet-set.jpg" alt="Switching configs in Neovim" />
<img src="https://res.cloudinary.com/ooloth/image/upload/v1694064460/mu/ratchet-set.jpg" alt="" />
<p>Learning how to configure <a href="https://neovim.io/">Neovim</a> can be overwhelming. One way to get started is to install a few pre-built configurations (like <a href="https://www.lazyvim.org/">LazyVim</a>, <a href="https://nvchad.com/">NvChad</a>, <a href="https://astronvim.com/">AstroNvim</a>, <a href="https://www.lunarvim.org/">LunarVim</a> or the official Neovim <a href="https://github.com/nvim-lua/kickstart.nvim">Kickstart</a>) and see what you like.</p>
<p>Most installation instructions <a href="https://astronvim.com/#installation">will</a> <a href="https://www.lazyvim.org/installation">tell</a> <a href="https://nvchad.com/docs/quickstart/install">you</a> to replace everything in your <code>~/.config/nvim</code> directory with the new configuration. But once you do, you lose the ability to launch Neovim with your previous config.</p>
<p>With that approach, you can only have one Neovim config installed at a time.</p>
<p>But what if you want to compare two configs? Or maintain different configs for different purposes (e.g. one for work and one for personal projects)?</p>
<h2>Install each config in its own directory</h2>
<p>To be able to use more than one config, you'll need to make a couple changes to your setup:</p>
<ol><li>Instead of installing a new configuration in <code>~/.config/nvim</code>, install it in a custom <code>~/.config</code> subdirectory</li><li>Each time you open Neovim, specify which config you want by setting the <code>NVIM_APPNAME</code> environment variable in your launch command</li></ol>
<p>For example, assuming you've installed <a href="https://www.lazyvim.org/">LazyVim</a> in <code>~/.config/nvim-lazyvim</code>, you'd launch it with this command:</p>
<pre><code>$ NVIM_APPNAME=nvim-lazyvim nvim</code></pre>
<p>Neovim uses <code>NVIM_APPNAME</code> to determine which config directory to load. If you don't include it (or set it to an invalid value), Neovim will use the default config in <code>~/.config/nvim</code>.</p>
<h2>Switching configs using <code>alias</code>, <code>select</code> or <code>fzf</code></h2>
<p>Lets assume your <code>~/.config</code> directory includes these subdirectories:</p>
<pre><code>~/.config
├── nvim-astrovim
│   └── init.lua
├── nvim-kickstart
│   ├── init.lua
│   └── lua
│       ├── custom
│       └── kickstart
├── nvim-lazyvim
│   ├── init.lua
│   └── lua
│       ├── config
│       └── plugins
├── nvim-lunarvim
│   └── config.lua
└── nvim-nvchad
│   ├── init.lua
│   └── lua
│       ├── core
│       ├── custom
│       └── plugins
└── nvim</code></pre>
<p>To quickly open Neovim using each config, you could create an <code>alias</code> for each launch command:</p>
<pre><code>alias v='nvim' # default Neovim config
alias vz='NVIM_APPNAME=nvim-lazyvim nvim' # LazyVim
alias vc='NVIM_APPNAME=nvim-nvchad nvim' # NvChad
alias vk='NVIM_APPNAME=nvim-kickstart nvim' # Kickstart
alias va='NVIM_APPNAME=nvim-astrovim nvim' # AstroVim
alias vl='NVIM_APPNAME=nvim-lunarvim nvim' # LunarVim</code></pre>
<p>Or use <code>select</code> to list your configs so you can choose one:</p>
<pre><code>vv() {
  select config in lazyvim kickstart nvchad astrovim lunarvim
  do NVIM_APPNAME=nvim-$config nvim $@; break; done
}</code></pre>
<p>Which would produce a menu like this:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1694061561/mu/nvim-config-switcher-select.png" alt="" />
<p>Or you could get fancy and use a fuzzy finder like <a href="https://github.com/junegunn/fzf">fzf</a> to do the same thing:</p>
<pre><code>vv() {
  # Assumes all configs exist in directories named ~/.config/nvim-*
  local config=$(fd --max-depth 1 --glob 'nvim-*' ~/.config | fzf --prompt=&quot;Neovim Configs &gt; &quot; --height=~50% --layout=reverse --border --exit-0)

  # If I exit fzf without selecting a config, don't open Neovim
  [[ -z $config ]] &amp;&amp; echo &quot;No config selected&quot; &amp;&amp; return

  # Open Neovim with the selected config
  NVIM_APPNAME=$(basename $config) nvim $@
}</code></pre>
<p>Here's what that <code>fzf</code> menu would look like:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1694061562/mu/nvim-config-switcher-fzf.png" alt="" />
<h2>Conclusion</h2>
<p>Configuring Neovim can be daunting, but being able to install a few configs and compare them is a great way to gather inspiration for your own custom config.</p>
<p>Good luck!</p>
<h2>Related</h2>
<ul><li><a href="https://neovim.io/doc/user/starting.html#%24NVIM_APPNAME">NVIM_APPNAME</a> • Neovim docs</li><li><a href="https://www.youtube.com/watch?v=LkHjJlSgKZY">Neovim Config Switcher</a> • Elijah Manor</li><li><a href="https://gist.github.com/elijahmanor/b279553c0132bfad7eae23e34ceb593b">Neovim Switcher Gist</a> • Elijah Manor</li><li><a href="https://lazyman.dev/">Lazyman: Neovim Configuration Manager</a> • Ronald Record</li><li><a href="https://www.youtube.com/watch?v=6qSzFWRz6Ck">You Should Use a Neovim Distro If You Are New</a> • ThePrimeagen</li></ul>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Adding a pull request template to your GitHub repo]]></title>
            <link>https://michaeluloth.com/github-repo-pr-template</link>
            <guid permalink="true">https://michaeluloth.com/github-repo-pr-template</guid>
            <pubDate>Thu, 15 Dec 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[How to pre-fill the blank description with helpful prompts.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1671128799/mu/blank-page.jpg" alt="Adding a pull request template to your GitHub repo" />
<img src="https://res.cloudinary.com/ooloth/image/upload/v1671128799/mu/blank-page.jpg" alt="" />
<p>If you spend time wondering what to write in the blank “Description” field every time you open a pull request, adding a PR template to your repo will speed you up.</p>
<p>By pre-filling that “Description” field with a few quick prompts to answer, you’ll always know what to write and your reviewers will always get the info they need.</p>
<h2>How to add a PR template</h2>
<ol><li>Create a folder called <code>.github</code> at the root of your repo</li><li>Create a file called <code>.github/pull_request_template.md</code></li><li>Use <a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet">GitHub’s Markdown syntax</a> to add any prompts you think will be useful to that file</li></ol>
<p>That’s it! Each time you open a pull request, the description section will now be pre-filled with your template.</p>
<h2>What to include in your template?</h2>
<p>Feel free to add any prompts you like! I prefer to keep it simple:</p>
<pre><code>## ✅ What

&lt;!-- A brief description of the changes in this PR. --&gt;

## 🤔 Why

&lt;!-- A brief description of why we want these changes. --&gt;

## 👩‍🔬 How to validate

&lt;!-- Step-by-step instructions for how reviewers can verify these changes work as expected. --&gt;

## 🔖 Related

- [Jira task](url)
- [Slack thread](url)</code></pre>
<p>(Those comments are only visible while editing.)</p>
<h2>Don’t overdo it</h2>
<p>Whatever you do, don’t make your PR template too long. If you notice your team keeps skipping certain prompts, don’t hesitate to remove them. Only include prompts that actually save everyone time.</p>
<p>If your reviewers always have to ask which ticket each PR relates to, adding a ticket link prompt to the template is probably a good idea. But adding 50 well-intentioned checkboxes will only teach your team to ignore the template entirely.</p>
<p>So, keep it short. 😊</p>
<h2>Related</h2>
<ul><li><a href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository">Creating a pull request template for your repository</a> • GitHub Docs</li><li><a href="https://egghead.io/lessons/github-create-a-github-pr-template">Create a GitHub PR Template</a> • egghead.io</li><li><a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet">GitHub Markdown Cheatsheet</a> • Adam Pritchard</li><li><a href="https://docs.github.com/en/get-started/writing-on-github">Writing on GitHub</a> • Github Docs</li></ul>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Dramatically reducing video file size using FFmpeg]]></title>
            <link>https://michaeluloth.com/how-to-shrink-video-file-size-using-ffmpeg</link>
            <guid permalink="true">https://michaeluloth.com/how-to-shrink-video-file-size-using-ffmpeg</guid>
            <pubDate>Thu, 10 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[How to shrink a video so it’s small enough to upload. ]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1668101610/mu/video-camera.jpg" alt="Dramatically reducing video file size using FFmpeg" />
<img src="https://res.cloudinary.com/ooloth/image/upload/v1668101610/mu/video-camera.jpg" alt="" />
<p>Sometimes you want to upload a video, but you can’t because its file size is too large. When that happens, you can use <code>ffmpeg</code> to shrink the video size with one command.</p>
<p>Here’s how:</p>
<h2>1. Install <a href="https://brew.sh/">Homebrew</a></h2>
<pre><code>/bin/bash -c &quot;$(curl -fsSL &lt;https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&gt;)&quot;</code></pre>
<h2>2. Install <code>ffmpeg</code> </h2>
<pre><code>brew install ffmpeg</code></pre>
<p>(Or <a href="https://ffmpeg.org/download.html">download FFmpeg</a> for your OS.)</p>
<h3>3. Run <code>ffmpeg</code> on your file</h3>
<pre><code># General pattern
ffmpeg -i path/to/original/file path/to/new/file

# Example
ffmpeg -i &quot;Desktop/Screen Recording 2022-11-08 at 4.30.40 PM.mov&quot; Desktop/code-walkthrough.mp4</code></pre>
<p>That’s it — I was able to shrink a 10-minute macOS screencast from <code>1.4 GB</code> to <code>148 MB</code> just by running that command and waiting 6 minutes.</p>
<h2>Where to go from here</h2>
<p>This approach works best when your final video quality doesn’t need to be very high.</p>
<p>If you’re creating high-resolution content, you’ll want to dive deeper into <code>ffmpeg</code>’s <a href="https://ffmpeg.org/ffmpeg.html">configuration options</a> or use a tool like <a href="https://handbrake.fr/docs/en/1.5.0/">Handbrake</a> to help you select the quality you need.</p>
<h2>Related</h2>
<ul><li><a href="https://linuxhint.com/how-reduce-video-size-with-ffmpeg/">How to Reduce Video Size With FFmpeg</a> — John Otieno</li></ul>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[The translateZ trick]]></title>
            <link>https://michaeluloth.com/translate-z</link>
            <guid permalink="true">https://michaeluloth.com/translate-z</guid>
            <pubDate>Thu, 01 Sep 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[How to smoothly animate CSS filters in Safari by engaging the GPU.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1662045633/mu/simon-abrams-blurry-lights.jpg" alt="The translateZ trick" />
<h2>CSS filters are great</h2>
<p>CSS filters are super cool. They can take something that looks like this…</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1662044963/mu/css-filter-blur-0.png" alt="" />
<p>…and make it look like this:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1662044970/mu/css-filter-blur-4px.png" alt="" />
<p>Love it. Effects like these are the fun part of writing CSS.</p>
<p>And <code>blur()</code> is just one of many fun <code>filter</code> options. The rest are listed <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/filter">here</a>. </p>
<p>CSS filters become especially useful when you want to animate items on and off the page, or in and out of the foreground. For example, you may want to gradually blur items as they animate out. Or gradually blur the page background when a user opens a dialog or modal.</p>
<h2>Safari woes</h2>
<p>Unfortunately, once you start animating the CSS <code>filter</code> property, you’re going to notice your animation is awfully choppy in Safari.</p>
<p>I ran into this issue at work while animating CSS filters. And I recently heard Scott Tolinski lament on <a href="https://twitter.com/stolinski/status/1532745802174578691">Twitter</a> and the Syntax podcast that he was excited about using CSS filters but was forced to abandon them because of this Safari rendering issue.</p>
<p>But there’s a fix! That’s why I’m writing this post, if only to help Scotty. 😎</p>
<h2>The fix</h2>
<p>To fix the issue in Safari, take this…</p>
<pre><code>.blurry {
  filter: blur(4px);
  transition: filter 0.3s ease-in-out;
}</code></pre>
<p>…and change it to this:</p>
<pre><code>.blurry {
  filter: blur(4px);
  transition: filter 0.3s ease-in-out;
  transform: translateZ(0);
}</code></pre>
<p>That’s it. Your animations will run smoothly in Safari now.</p>
<h2>How does translateZ help?</h2>
<p>The basic reason <code>translateZ(ANY_VALUE)</code> makes the animation run smoothly is that it tells Safari to render the animation using the GPU instead of the CPU. </p>
<p>Without Without <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/translateZ">translateZ</a> (or a similar hint like <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/translate3d">translate3d</a>), Safari will try to animate your <code>blur()</code> filter using the CPU, which isn’t nearly as good at rendering complicated graphics. For graphics-intensive tasks like animating a filter effect, the GPU will always do a much better job.</p>
<h2>Help us out, Safari</h2>
<p>Chrome and other non-Safari browsers automatically hardware-accelerate animations like this by engaging the GPU, but Safari still requires you to add extra declarations like <code>translateZ()</code> to get the same result.</p>
<p>Hopefully the Safari team will fix that soon so Scott can animate all the CSS filters he likes without needing workarounds like this one.</p>
<p>Until then, <code>translateZ()</code> is your friend.</p>
<h2>Related</h2>
<ul><li><a href="https://developer.chrome.com/blog/css-filter-effects-landing-in-webkit/#support">CSS Filter Effects landing in WebKit</a> — mentions the need for <code>translateZ()</code> back in 2011 • Chrome Developers</li><li><a href="https://blog.teamtreehouse.com/increase-your-sites-performance-with-hardware-accelerated-css">Increase Your Site’s Performance with Hardware-Accelerated CSS</a> • Treehouse</li><li><a href="https://www.notion.so/The-filter-Boolean-trick-889b30c18eed4d71ba7d439fb4eb8f61">Improving HTML5 App Performance with GPU Accelerated CSS Transitions</a> • Urban Insight</li><li><a href="https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/">CSS GPU Animation: Doing It Right</a> • Smashing Magazine</li><li><a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Fundamentals#specific_coding_tips_for_application_performance">Web Performance Fundamentals</a> — calls out that <code>translateZ()</code> is still needed to get hardware accelerated CSS animations on some platforms • MDN Web Docs</li><li><a href="https://twitter.com/andyngo/status/1263056084719202304?s=20">Andy Ngo tweet</a> — suggests <code>translate3d()</code> version of same trick</li></ul>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[What is a Factory Function?]]></title>
            <link>https://michaeluloth.com/what-is-a-factory-function</link>
            <guid permalink="true">https://michaeluloth.com/what-is-a-factory-function</guid>
            <pubDate>Sun, 27 Feb 2022 05:00:00 GMT</pubDate>
            <description><![CDATA[Kyle Shevlin on how to use factory functions as an alternative to classes.]]></description>
            <content:encoded><![CDATA[<p>I hadn’t heard of the factory function technique until I read <a href="https://kyleshevlin.com/what-is-a-factory-function">What is a Factory Function?</a> by Kyle Shevlin:</p>
<blockquote>A factory function is a function that returns a new object. The key feature of a factory is that its only job is to pump out those items, just like an actual factory.</blockquote>
<blockquote>The place I use factories most often is when I want to return an object with methods but use closures to create private methods and values. Even better, I never have to even think about the this{:js} keyword.</blockquote>
<p>In other words, you can write them instead of classes!</p>
<p>I found a good use case for this technique today while adding extra properties and helper methods to blocks I’d fetched from the Notion API, so I wanted to give Kyle a shout-out for the timely assist.</p>
<p>Check out <a href="https://kyleshevlin.com/all-posts">Kyle's blog</a> for lots of other interesting coding tips.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Writing Great Alt Text]]></title>
            <link>https://michaeluloth.com/writing-great-alt-text</link>
            <guid permalink="true">https://michaeluloth.com/writing-great-alt-text</guid>
            <pubDate>Thu, 17 Feb 2022 05:00:00 GMT</pubDate>
            <description><![CDATA[Jake Archibald on how to write better image alt text.]]></description>
            <content:encoded><![CDATA[<p>I agree with Jake Archibald's argument in <a href="https://jakearchibald.com/2021/great-alt-text/">Writing great alt text: Emotion matters</a> that when we write image alt text, we should aim to evoke the same emotions as the image visuals do. Otherwise, screen reader users miss out.</p>
<p>If an image makes you laugh or cry, ideally the alt text will do the same:</p>
<blockquote>The relevant parts of an image aren't limited to the cold hard facts. Images can make you feel a particular way, and that's something that should be made available to a screen reader user.</blockquote>
<blockquote>It isn't just a head-shot of me. I'm doing a thing. I'm peering from behind a plant and pulling a bit of a silly face. There's humour expressed in the image. I'm not saying that it's going to win any comedy awards, but the image expresses a particular tone, and that matters. So, it should go in the alt: “Head-shot of me, a pale white guy, wearing glasses, grinning slightly, and partially hiding behind a plant”.</blockquote>
<p>Let the creative writing begin!</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Levels of Abstraction in Testing]]></title>
            <link>https://michaeluloth.com/levels-of-abstraction-in-testing</link>
            <guid permalink="true">https://michaeluloth.com/levels-of-abstraction-in-testing</guid>
            <pubDate>Tue, 15 Feb 2022 05:00:00 GMT</pubDate>
            <description><![CDATA[Sam Selikoff on how to make unit tests easier to read.]]></description>
            <content:encoded><![CDATA[<p>Sam Selikoff's <a href="https://youtube.com/watch?v=G_0yKeh0Sf0">Levels of abstraction in testing</a> does a nice job demonstrating how to make unit tests easier to read by abstracting the low level setup steps at the beginning of each test into a well-named function.</p>
<p>After that change, the everything in the tests reads at a single level of abstraction (namely, how would a user interact with this component?) rather than alternating between what the computer is doing and what the user is doing. The result is easier to understand.</p>
<blockquote>Basically, you just want to act as if you’re sitting next to the person who is using the system you’re testing…just imagine how you’d explain to them what behaviour you’re testing and then write your tests using similar words.</blockquote>
<p>I think this advice would apply just as well to non-test code. If you notice a code block mixes levels of abstraction, try extracting the lower level bits so the resulting code reads as a continuous train of thought from a single point of view.</p>
<p>For more, check out Sam's <a href="https://youtube.com/watch?v=G_0yKeh0Sf0">complete video</a>.</p>
<p><a href="https://m.youtube.com/watch?v=G_0yKeh0Sf0&t=303s">[Video: https://m.youtube.com/watch?v=G_0yKeh0Sf0&t=303s]</a></p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Using Slack to report data entry errors to content editors]]></title>
            <link>https://michaeluloth.com/slack-alerts-for-cms-errors</link>
            <guid permalink="true">https://michaeluloth.com/slack-alerts-for-cms-errors</guid>
            <pubDate>Mon, 31 May 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[How the ".com" team at ecobee uses Slack as an alert system.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645224835/mu/slack-logo-in-browser.jpg" alt="Using Slack to report data entry errors to content editors" />
<img src="https://res.cloudinary.com/ooloth/image/upload/v1645224835/mu/slack-logo-in-browser.jpg" alt="" />
<p><em>This post originally appeared on the </em><a href="https://www.ecobee.dev/blog/2021-05-21-how-ecobee-uses-slack-to-report-data-entry-errors-to-content-editors/"><em>ecobee engineering blog</em></a><em>.</em></p>
<p>My team at ecobee spent months debugging build failures on the staging site for <a href="https://www.ecobee.com/">ecobee.com</a> until we finally discovered a way to solve them with automated <a href="https://slack.com/">Slack</a> notifications.</p>
<h2>Production vs. staging</h2>
<p><a href="https://www.ecobee.com/">ecobee.com</a> is an e-commerce website, which we populate with content from <a href="https://www.contentful.com/">Contentful</a> and <a href="https://www.shopify.com/">Shopify</a>.</p>
<p>Our designers and writers create new pages by building and assembling “blocks” of content in Contentful. As they work, they can preview their changes on a staging version of our site.</p>
<p>Here’s the live version of <a href="http://ecobee.com/">ecobee.com</a>:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1646344889/mu/dotcom-production.png" alt="" />
<p>And here’s the staging version of <a href="http://ecobee.com/">ecobee.com</a>:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1646344971/mu/dotcom-staging.png" alt="" />
<p>The staging version looks very similar to the live version becae, well, that’s the point. It’s generated using the exact same codebase as the live site and all the content for both versions of the site come from Contentful.</p>
<p>If you aren't familiar, here’s what Contentful looks like:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1646344813/mu/contentful-homepage.png" alt="" />
<p>This example is the “Page” entry that creates the homepage for <a href="http://ecobee.com/">ecobee.com</a>. It’s basically a series of fields: some are required; some are not.</p>
<p>If you mark a field as required, Contentful will confirm that field is populated before it lets you publish the entry. (The same applies to other field validations like max length, number vs. text, etc.) And because the live site only consumes published entries, we know all the fields have been validated (e.g. no required fields are empty), and the data the website receives will match what we expect.</p>
<p>So far, so good.</p>
<p>However, the staging site is a little different: it connects to the same production Contentful data as the live site, but also includes any draft entries that have not yet been published.</p>
<p>That’s by design, so that editors can preview their in-progress changes on staging as they go.</p>
<p>But those draft entries have one major problem: their fields have not been validated.</p>
<h2>Broken staging builds</h2>
<p>The challenge we faced was that we kept seeing errors like this:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1646344834/mu/netlify-build-error.png" alt="" />
<p>This the deploy log for a build of our staging website on <a href="https://www.netlify.com/">Netlify</a>. And over and over, we encountered errors saying <code>Cannot read property X of null</code> or <code>Cannot read property X of undefined</code>, which indicated that our codebase trusted a value existed (because its Contentful field was required) and tried to use it, but failed because that value was unexpectedly empty.</p>
<p>So, why didn’t Contentful make sure those required fields were populated?</p>
<h2>Draft entries cannot be trusted</h2>
<p>The basic reason is that unpublished draft entries (which staging consumes) are not validated by Contentful because Contentful only applies validations when an entry is published.</p>
<p>So, while our production builds were stable (since the live site only consumes validated, published entries), our staging builds were constantly exposed to the risk that a field coming from a draft entry could contain invalid data (or no data at all).</p>
<p>This issue was minor when our team only had one or two content editors. But as our team grew to include half a dozen or more editors working separately and constantly triggering new builds, incomplete data began to be hit staging on a daily basis.</p>
<h2>Debugging in the dark</h2>
<p>When one of these errors occurred, the staging website would stop updating, and one or more devs would have to put aside their work and jump into the build logs to locate an error like the one shown above.</p>
<p>Next came the hard part: trying to figure out which field in Contentful was responsible for the problem.</p>
<p>For example, all the error in the screenshot above says is that the <code>Link</code> component was passed an empty <code>href</code>. It doesn’t say which field in Contentful that empty value came from.</p>
<p>Trying to track down those empty fields was no fun. Each hunt took up a lot of time and involved a lot of head scratching and rooting through Contentful, searching for the root of each problem. Work ground to a halt for editors and developers while we hunted, sometimes for up to an hour.</p>
<p>We were to determined to free ourselves from these thankless debugging sessions.</p>
<h2>Custom null checks everywhere</h2>
<p>To do that, we started adding defensive <code>null</code> checks all over our codebase.</p>
<p>Even though we didn’t have to worry about the existence of required values in production, for the sake of our editors having a working preview site to look at, we realized we needed to treat required fields as unreliable and start guarding against their lack of existence.</p>
<p>So, we started adding things like this all over our codebase that intercepted empty values and identified exactly which Contentful field to fix:</p>
<pre><code>const ReviewsResolver = ({
  data: { productReference, header, name = '' },
}: BlockResolverProps&lt;ReviewsDataType&gt;) =&gt; {
  const logError = useLogError()

  if (!productReference) {
    logError(
      `The Reviews entry named &quot;${name}&quot; cannot be displayed because its required field &quot;Product reference&quot; is empty.`,
    )

    return null
  }

  return &lt;Reviews product={productReference} /&gt;
}</code></pre>
<h2>Slack to the rescue</h2>
<p>We also created a helper, called <code>logError()</code>, to decide whether each error message should appear just in the console or also be posted to Slack:</p>
<pre><code>// If this is prod, report the issue and break the build
if (context === 'PRODUCTION') {
  const prodImpact = 'The live site will not update until this issue is resolved.'

  const prodMessage = formatMessage(
    `🚨 PRODUCTION ERROR 🚨\\n\\n${message}\\n\\n${prodImpact}`,
  )

  postToSlack(prodMessage).then(() =&gt; {
    throw new Error(prodMessage)
  })
}

// If this is staging, report the issue and continue the build
if (context === 'STAGING') {
  const stagingImpact =
    'The staging site will continue to update while this issue gets resolved.'

  const stagingMessage = formatMessage(
    `⚠️ STAGING ERROR ⚠️\\n\\n${message}\\n\\n${stagingImpact}`,
  )

  postToSlack(stagingMessage)
  console.error(stagingMessage)

  return
}

// In all other environments, just log the issue and continue the build
console.error(formatMessage(`\\n\\n⚠️ ${message}\\n\\n`))</code></pre>
<p>So, we ask, “Are we on the production site? Or staging? Or just in development?”, and based on the answer we decide if we should to stop the build (production only) and/or post the error to Slack (production and staging only). In all environments, we also log the error to the console.</p>
<p>We then created a dedicated Slack channel and invited all our editors to it, created a <a href="https://slack.com/intl/en-ca/help/articles/115005265703-Create-a-bot-for-your-workspace">Slack bot</a>, and used the <a href="https://slack.dev/node-slack-sdk/web-api">Slack Web API</a> to automatically post these Slack messages for us:</p>
<pre><code>import type { WebClient } from '@slack/web-api'

import { isTest } from '../../src/utils/getEnv'

const isSSR = typeof window === 'undefined'

async function postToSlack(text: string, channel = '#dotcom-cms-errors') {
  // Only post during a build that isn't part of a test run
  if (!isSSR || isTest || !global.slackClient) {
    return
  }

  try {
    await global.slackClient.chat.postMessage({ channel, text })
  } catch (error) {
    console.error(`\\n[postToSlack]: ${error}\\n`)
  }
}</code></pre>
<p>Now, whenever a build error occurs and is caused by a Contentful data entry problem we know about, the message goes straight to that editor-focused channel so they can can hop into Contentful and fix the issue without dev assistance:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1646344975/mu/slack-notification-v1.png" alt="" />
<h2>An endless process</h2>
<p>But, we weren’t done.</p>
<p>We started noticing that after we had fixed the 10 or so errors we had seen over and over, there were 10 or so new ones that we hadn’t previously seen because they never had a chance to appear because the other errors were happening first.</p>
<p>It slowly dawned on us that with our current approach of inserting custom <code>null</code> checks and error messages in individual components, this problem would never end. We’d have to <code>null</code> check every single value we considered required and continue doing that whenever we wrote new code.</p>
<p>That was not going to be scalable for us in the future.</p>
<h2>Automated validation step</h2>
<p>So, we came up with a better idea.</p>
<p>To avoid having to spread <code>null</code> checks all over our codebase and worry about this forever, early in our build we now query all of the data from Contentful that our website will use along with the rules for every field, and then compare the two.</p>
<p>We take each field and its rules and pass it to a function that asks if it’s invalid. For example, for requiredness, we check if the field’s rules say it’s required and if its value is empty. If both are true at the same time, the field is invalid:</p>
<pre><code>function isEmptyRequiredField(fieldRules: ContentFields, fieldValue: unknown) {
  const isRequired = 'required' in fieldRules &amp;&amp; !!fieldRules.required

  // We don't want to check falsiness here, since that would flag fields intentionally set to 0
  const isEmpty = fieldValue == null

  return isRequired &amp;&amp; isEmpty
}

function detectFieldInvalidity(
  rules: ContentTypeIdsAndFields,
  contentTypeId: string,
  fieldName: string,
  fieldValue: unknown,
): string | null {
  const fieldRules = getFieldRules(rules, contentTypeId, fieldName)

  if (isEmptyRequiredField(fieldRules, fieldValue)) {
    return 'is required but empty'
  }

  if (isInvalidRichTextField(fieldValue)) {
    return 'contains invalid HTML'
  }

  return null
}</code></pre>
<p>Then, we have a single place where we call our <code>logError()</code> helper as many times as necessary:</p>
<pre><code>const isInvalidReason = detectFieldInvalidity(
  rules,
  contentTypeId,
  fieldName,
  entry[fieldName],
)

if (isInvalidReason) {
  const contentTypeName = startCase(contentTypeId)

  const entryName = entry?.name || entry?.title || ''

  const contentTypeAndEntryName = entryName
    ? `The Contentful ${contentTypeName} entry named &quot;${entryName}&quot;`
    : `An unnamed Contentful ${contentTypeName} entry with entry ID &quot;${entry.contentful_id}&quot;`

  const locale = entry?.node_locale
    ? (String(entry.node_locale).toLowerCase() as 'en-us' | 'en-ca')
    : undefined

  const message = `${contentTypeAndEntryName} cannot be displayed because its field &quot;${fieldName}&quot; ${isInvalidReason}.\\n\\nPlease update this field and publish your changes.`

  logError(message, locale, entry.contentful_id)
}</code></pre>
<p>The updated version of these Slack error notifications looks like this:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1646344813/mu/slack-notification-v2.png" alt="" />
<p>The updated template tells editors the name of the broken Contentful entry, which field in the entry has the problem, what the problem is, how to fix it, a link to the entry, and context about the environment where the build error occurred.</p>
<h2>Happy editors and developers</h2>
<p>We've been living with this solution for the past few months and our editors and developers are both much happier!</p>
<p>There are no more confusing debugging sessions. Whenever a staging build breaks, the solution is posted to Slack a second later and an editor can fix it themselves within a minute.</p>
<p>This has eliminated an annoying source context switching and restored hours of developer and editor productivity each week.</p>
<h2>Please share your ideas!</h2>
<p>So, that’s how the &quot;.com&quot; team at ecobee is currently tackling the challenge of using the same codebase for production and staging but sending that codebase different data in each environment.</p>
<p>If you’ve encountered this issue before and have tips or best practices that might help us, please let us know! We realize this is likely a common problem for sites with a content management system, so we’d love to hear how you approached solving this issue.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[The filter(Boolean) trick]]></title>
            <link>https://michaeluloth.com/filter-boolean</link>
            <guid permalink="true">https://michaeluloth.com/filter-boolean</guid>
            <pubDate>Mon, 01 Jun 2020 00:00:00 GMT</pubDate>
            <description><![CDATA[How to remove empty values from an array.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1646239412/mu/be-careful-austin-distel.jpg" alt="The filter(Boolean) trick" />
<img src="https://res.cloudinary.com/ooloth/image/upload/v1646239412/mu/be-careful-austin-distel.jpg" alt="" />
<p>Here's a trick I often find helpful.</p>
<h2>Bad array. Very, very bad.</h2>
<p>You have an array of whatever:</p>
<pre><code>const array = [{ stuff }, { moreStuff }, ...]</code></pre>
<p>But hiding in that array are some unusable <code>null</code> or <code>undefined</code> values:</p>
<pre><code>const array = [{ good }, null, { great }, undefined]</code></pre>
<p>Those empty values might be a sneaky little gift from an API. Or you may have left them there yourself while validating the original data. Either way, you've got a problem.</p>
<h2>Looping over null data</h2>
<p>If you try to perform actions on every item in the array, you'll run into errors when you hit those <code>null</code> and <code>undefined</code> items:</p>
<pre><code>const newArray = array.map(item =&gt; {
  // Of course this will work, wheeee...
  const assumption = item.thing
})

// Oh noooo...
// 🚨 Error: Cannot read property &quot;thing&quot; of undefined.
// 🚨 Error: Cannot read property &quot;thing&quot; of null.</code></pre>
<p>Illegal! Now you're a criminal. Before you interact with an item, you need to make sure it exists.</p>
<h2>Null checks?</h2>
<p>You could confirm each item exists by performing a <code>null</code> check before you use it:</p>
<pre><code>const newArray = array.map(item =&gt; {
  // Life has made me cautious.
  if (!item) {
    return item // Just forget it
  }

  // If we get this far, item exists.
  const assumption = item.thing
})</code></pre>
<p>Buuut, now your code is getting cluttered. And worse, those dangerous empty values will be passed along to <code>newArray</code>. So when <code>newArray</code> is used, another round of suspicious <code>null</code> checks will be needed.</p>
<h2>The truth and only the truth</h2>
<p>Want something better?</p>
<p>Here's my favourite way to quickly remove all empty items from an array:</p>
<pre><code>const array = [{ good }, null, { great }, undefined]

const truthyArray = array.filter(Boolean)
// truthyArray = [{ good }, { great }]</code></pre>
<p>The <code>filter(Boolean)</code> step does the following:</p>
<ol><li>Passes each item in the array to the <code>Boolean()</code> object</li><li>The <code>Boolean()</code> object <a href="https://developer.mozilla.org/en-US/docs/Glossary/Type_coercion">coerces</a> each item to <code>true</code> or <code>false</code> depending on whether it's <a href="https://developer.mozilla.org/en-US/docs/Glossary/Truthy">truthy</a> or <a href="https://developer.mozilla.org/en-US/docs/Glossary/Falsy">falsy</a></li><li>If the item is truthy, we keep it</li></ol>
<h2>Where did item go?</h2>
<p>I love how concise <code>filter(Boolean)</code> is, but it might look strange that we aren't explicitly passing <code>item</code> to <code>Boolean()</code>.</p>
<p>The main thing to know is that, in JavaScript, this:</p>
<pre><code>array.filter(item =&gt; Boolean(item))</code></pre>
<p>is exactly the same as this:</p>
<pre><code>array.filter(Boolean)</code></pre>
<p>The second version is just written in a <a href="https://en.wikipedia.org/wiki/Tacit_programming">&quot;tacit&quot; or &quot;point-free&quot; style</a>. We don't name each item and pass it into <code>Boolean()</code>, but JavaScript understands that <code>Boolean()</code> takes one argument, so it takes the argument <code>filter()</code> exposes and <a href="https://mostly-adequate.gitbooks.io/mostly-adequate-guide/ch02.html">passes it to Boolean for you</a>.</p>
<p>If you find the first version easier to understand, use it! Readable code is more important than fancy tricks.</p>
<h2>Safer mapping</h2>
<p>With our new tool, we can remove the <code>null</code> checks from above and chain a filtering step instead:</p>
<pre><code>const newArray = array.filter(Boolean).map(item =&gt; {
  // Item is always truthy!
  const assumption = item.thing
})</code></pre>
<p>Now, our <code>map()</code> can focus on what it's trying to do, and we've removed the empty values from our pipeline forever.</p>
<p>Hope that helps!</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Using GraphQL with Gatsby]]></title>
            <link>https://michaeluloth.com/using-graphql-with-gatsby</link>
            <guid permalink="true">https://michaeluloth.com/using-graphql-with-gatsby</guid>
            <pubDate>Mon, 03 Jun 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[How to write GraphQL queries in a Gatsby project.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718545/mu/gatsby-10-using-graphql-with-gatsby.png" alt="Using GraphQL with Gatsby" />
<p>This is the tenth video in our <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><a href="https://michaeluloth.com/what-is-gatsby">What is Gatsby?</a></li><li><a href="https://michaeluloth.com/starting-a-new-gatsby-project">Starting a New Gatsby Project</a></li><li><a href="https://michaeluloth.com/gatsbys-default-files-and-folders">Gatsby's Default Files and Folders</a></li><li><a href="https://michaeluloth.com/writing-html-in-a-gatsby-project">Writing HTML in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-pages-to-a-gatsby-project">Adding Pages to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Wrapping Pages in a Layout Component</a></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Writing CSS-in-JS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Adding Content to a Gatsby Project</a></li><li><em><strong>Using GraphQL with Gatsby (this post)</strong></em></li></ol>
<p>In <a href="https://youtu.be/IaorT4-efuU">this video</a>, we explore how GraphQL works and practice using it with Gatsby to query our content.</p>
<p><a href="https://www.youtube.com/watch?v=IaorT4-efuU">[Video: https://www.youtube.com/watch?v=IaorT4-efuU]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Adding content to a Gatsby project]]></title>
            <link>https://michaeluloth.com/adding-content-to-a-gatsby-project</link>
            <guid permalink="true">https://michaeluloth.com/adding-content-to-a-gatsby-project</guid>
            <pubDate>Mon, 27 May 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[The pros and cons of different approaches.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718545/mu/gatsby-9-adding-content-to-a-gatsby-project.png" alt="Adding content to a Gatsby project" />
<p>This is the ninth video in our <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><a href="https://michaeluloth.com/what-is-gatsby">What is Gatsby?</a></li><li><a href="https://michaeluloth.com/starting-a-new-gatsby-project">Starting a New Gatsby Project</a></li><li><a href="https://michaeluloth.com/gatsbys-default-files-and-folders">Gatsby's Default Files and Folders</a></li><li><a href="https://michaeluloth.com/writing-html-in-a-gatsby-project">Writing HTML in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-pages-to-a-gatsby-project">Adding Pages to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Wrapping Pages in a Layout Component</a></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Writing CSS-in-JS in a Gatsby Project</a></li><li><em><strong>Adding Content to a Gatsby Project (this post)</strong></em></li><li><a href="https://michaeluloth.com/using-graphql-with-gatsby">Using GraphQL with Gatsby</a></li></ol>
<p>In <a href="https://youtu.be/MPqEU9ywgW8">this video</a>, we explore a few ways we can add content to a Gatsby project and discuss the advantages and disadvantages of each approach.</p>
<p><a href="https://youtu.be/MPqEU9ywgW8">[Video: https://youtu.be/MPqEU9ywgW8]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Writing CSS-in-JS in a Gatsby project]]></title>
            <link>https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project</link>
            <guid permalink="true">https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project</guid>
            <pubDate>Mon, 18 Mar 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[How CSS-in-JS libraries like CSS Modules and Styled Components make it easier to safely add and remove styles.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718543/mu/gatsby-8-writing-css-in-js-in-a-gatsby-project.png" alt="Writing CSS-in-JS in a Gatsby project" />
<p>This is the eighth video in our <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><a href="https://michaeluloth.com/what-is-gatsby">What is Gatsby?</a></li><li><a href="https://michaeluloth.com/starting-a-new-gatsby-project">Starting a New Gatsby Project</a></li><li><a href="https://michaeluloth.com/gatsbys-default-files-and-folders">Gatsby's Default Files and Folders</a></li><li><a href="https://michaeluloth.com/writing-html-in-a-gatsby-project">Writing HTML in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-pages-to-a-gatsby-project">Adding Pages to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Wrapping Pages in a Layout Component</a></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS in a Gatsby Project</a></li><li><em><strong>Writing CSS-in-JS in a Gatsby Project (this post)</strong></em></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Adding Content to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/using-graphql-with-gatsby">Using GraphQL with Gatsby</a></li></ol>
<p>In <a href="https://youtu.be/RwUn0Trhyyk">this video</a>, we explore CSS-in-JS and how libraries like CSS Modules and Styled Components can make it easier to add and remove styles from your project.</p>
<p><a href="https://youtu.be/RwUn0Trhyyk">[Video: https://youtu.be/RwUn0Trhyyk]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Writing CSS in a Gatsby project]]></title>
            <link>https://michaeluloth.com/writing-css-in-a-gatsby-project</link>
            <guid permalink="true">https://michaeluloth.com/writing-css-in-a-gatsby-project</guid>
            <pubDate>Mon, 11 Mar 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[How to write traditional CSS in a Gatsby project.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718543/mu/gatsby-7-writing-css-in-a-gatsby-project.png" alt="Writing CSS in a Gatsby project" />
<p>This is the seventh video in our <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><a href="https://michaeluloth.com/what-is-gatsby">What is Gatsby?</a></li><li><a href="https://michaeluloth.com/starting-a-new-gatsby-project">Starting a New Gatsby Project</a></li><li><a href="https://michaeluloth.com/gatsbys-default-files-and-folders">Gatsby's Default Files and Folders</a></li><li><a href="https://michaeluloth.com/writing-html-in-a-gatsby-project">Writing HTML in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-pages-to-a-gatsby-project">Adding Pages to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Wrapping Pages in a Layout Component</a></li><li><em><strong>Writing CSS in a Gatsby Project (this post)</strong></em></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS-in-JS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Adding Content to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/using-graphql-with-gatsby">Using GraphQL with Gatsby</a></li></ol>
<p>In <a href="https://youtu.be/hAzt8BIwXiE">this video</a>, we explore traditional approaches to writing CSS, like inline styles, vanilla CSS, Sass, and PostCSS, and walk through how each one works in a Gatsby project.</p>
<p><a href="https://youtu.be/hAzt8BIwXiE">[Video: https://youtu.be/hAzt8BIwXiE]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Wrapping pages in a layout component]]></title>
            <link>https://michaeluloth.com/wrapping-pages-in-a-layout-component</link>
            <guid permalink="true">https://michaeluloth.com/wrapping-pages-in-a-layout-component</guid>
            <pubDate>Mon, 04 Mar 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[How to share common content and styling between pages.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718544/mu/gatsby-6-wrapping-pages-in-a-layout-component.png" alt="Wrapping pages in a layout component" />
<p>This is the sixth video in our <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><a href="https://michaeluloth.com/what-is-gatsby">What is Gatsby?</a></li><li><a href="https://michaeluloth.com/starting-a-new-gatsby-project">Starting a New Gatsby Project</a></li><li><a href="https://michaeluloth.com/gatsbys-default-files-and-folders">Gatsby's Default Files and Folders</a></li><li><a href="https://michaeluloth.com/writing-html-in-a-gatsby-project">Writing HTML in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-pages-to-a-gatsby-project">Adding Pages to a Gatsby Project</a></li><li><em><strong>Wrapping Pages in a Layout Component (this post)</strong></em></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Writing CSS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS-in-JS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Adding Content to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/using-graphql-with-gatsby">Using GraphQL with Gatsby</a></li></ol>
<p>In <a href="https://youtu.be/dvorpyrTI2g">this video</a>, we learn how to use a Layout component to share common sections and styling between pages.</p>
<p><a href="https://youtu.be/dvorpyrTI2g">[Video: https://youtu.be/dvorpyrTI2g]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Adding pages to a Gatsby project]]></title>
            <link>https://michaeluloth.com/adding-pages-to-a-gatsby-project</link>
            <guid permalink="true">https://michaeluloth.com/adding-pages-to-a-gatsby-project</guid>
            <pubDate>Thu, 21 Feb 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[How to add pages to a Gatsby project and navigate between them.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718544/mu/gatsby-5-adding-pages-to-a-gatsby-project.png" alt="Adding pages to a Gatsby project" />
<p>This is the fifth video in our <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><a href="https://michaeluloth.com/what-is-gatsby">What is Gatsby?</a></li><li><a href="https://michaeluloth.com/starting-a-new-gatsby-project">Starting a New Gatsby Project</a></li><li><a href="https://michaeluloth.com/gatsbys-default-files-and-folders">Gatsby's Default Files and Folders</a></li><li><a href="https://michaeluloth.com/writing-html-in-a-gatsby-project">Writing HTML in a Gatsby Project</a></li><li><em><strong>Adding Pages to a Gatsby Project (this post)</strong></em></li><li><a href="https://michaeluloth.com/wrapping-pages-in-a-layout-component">Wrapping Pages in a Layout Component</a></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Writing CSS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS-in-JS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Adding Content to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/using-graphql-with-gatsby">Using GraphQL with Gatsby</a></li></ol>
<p>In <a href="https://youtu.be/ktHshp6SKXc">this video</a>, we learn how to add new pages to a Gatsby project and how to navigate between them using Gatsby's Link component.</p>
<p><a href="https://youtu.be/ktHshp6SKXc">[Video: https://youtu.be/ktHshp6SKXc]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Writing HTML in a Gatsby project]]></title>
            <link>https://michaeluloth.com/writing-html-in-a-gatsby-project</link>
            <guid permalink="true">https://michaeluloth.com/writing-html-in-a-gatsby-project</guid>
            <pubDate>Fri, 15 Feb 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[How to use React to split HTML into reusable components.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718543/mu/gatsby-4-writing-html-in-a-gatsby-project.png" alt="Writing HTML in a Gatsby project" />
<p>This is the fourth video in our <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><a href="https://michaeluloth.com/what-is-gatsby">What is Gatsby?</a></li><li><a href="https://michaeluloth.com/starting-a-new-gatsby-project">Starting a New Gatsby Project</a></li><li><a href="https://michaeluloth.com/gatsbys-default-files-and-folders">Gatsby's Default Files and Folders</a></li><li><em><strong>Writing HTML in a Gatsby Project (this post)</strong></em></li><li><a href="https://michaeluloth.com/adding-pages-to-a-gatsby-project">Adding Pages to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/wrapping-pages-in-a-layout-component">Wrapping Pages in a Layout Component</a></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Writing CSS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS-in-JS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Adding Content to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/using-graphql-with-gatsby">Using GraphQL with Gatsby</a></li></ol>
<p>In <a href="https://youtu.be/MMFvFtpyoIQ">this video</a>, we take our first look at React and how we can use it to split our HTML markup into reusable components.</p>
<p><a href="https://youtu.be/MMFvFtpyoIQ">[Video: https://youtu.be/MMFvFtpyoIQ]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Gatsby's default files and folders]]></title>
            <link>https://michaeluloth.com/gatsbys-default-files-and-folders</link>
            <guid permalink="true">https://michaeluloth.com/gatsbys-default-files-and-folders</guid>
            <pubDate>Thu, 31 Jan 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[What's going on inside each folder in a Gatsby project.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718544/mu/gatsby-3-gatsbys-default-files-and-folders.png" alt="Gatsby's default files and folders" />
<p>This is the third video in our <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><a href="https://michaeluloth.com/what-is-gatsby">What is Gatsby?</a></li><li><a href="https://michaeluloth.com/starting-a-new-gatsby-project">Starting a New Gatsby Project</a></li><li><em><strong>Gatsby's Default Files and Folders (this post)</strong></em></li><li><a href="https://michaeluloth.com/writing-html-in-a-gatsby-project">Writing HTML in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-pages-to-a-gatsby-project">Adding Pages to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/wrapping-pages-in-a-layout-component">Wrapping Pages in a Layout Component</a></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Writing CSS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS-in-JS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Adding Content to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/using-graphql-with-gatsby">Using GraphQL with Gatsby</a></li></ol>
<p>In <a href="https://youtu.be/o06e7TIJR9Y">this video</a>, we walk through each file and folder that comes with a new Gatsby project and talk about what each one is for so you can start customizing your site.</p>
<p><a href="https://youtu.be/o06e7TIJR9Y">[Video: https://youtu.be/o06e7TIJR9Y]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Starting a new Gatsby project]]></title>
            <link>https://michaeluloth.com/starting-a-new-gatsby-project</link>
            <guid permalink="true">https://michaeluloth.com/starting-a-new-gatsby-project</guid>
            <pubDate>Thu, 24 Jan 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[How to start a Gatsby project on your computer.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718544/mu/gatsby-2-starting-a-new-gatsby-project.png" alt="Starting a new Gatsby project" />
<p>This is the second video in our <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><a href="https://michaeluloth.com/what-is-gatsby">What is Gatsby?</a></li><li><em><strong>Starting a New Gatsby Project (this post)</strong></em></li><li><a href="https://michaeluloth.com/gatsbys-default-files-and-folders">Gatsby's Default Files and Folders</a></li><li><a href="https://michaeluloth.com/writing-html-in-a-gatsby-project">Writing HTML in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-pages-to-a-gatsby-project">Adding Pages to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/wrapping-pages-in-a-layout-component">Wrapping Pages in a Layout Component</a></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Writing CSS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS-in-JS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Adding Content to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/using-graphql-with-gatsby">Using GraphQL with Gatsby</a></li></ol>
<p>In <a href="https://youtu.be/afeqjonYgs8">this video</a>, we see how easy it is to get a new Gatsby project up and running on your computer.</p>
<p><a href="https://youtu.be/afeqjonYgs8">[Video: https://youtu.be/afeqjonYgs8]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[What is Gatsby?]]></title>
            <link>https://michaeluloth.com/what-is-gatsby</link>
            <guid permalink="true">https://michaeluloth.com/what-is-gatsby</guid>
            <pubDate>Thu, 17 Jan 2019 00:00:00 GMT</pubDate>
            <description><![CDATA[How Gatsby combines the best features of React and HTML.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645718543/mu/gatsby-1-what-is-gatsby.png" alt="What is Gatsby?" />
<p>This video kicks off a new <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">beginner series</a> exploring GatsbyJS and how to use it to easily build performant apps and websites:</p>
<ol><li><em><strong>What is Gatsby? (this post)</strong></em></li><li><a href="https://michaeluloth.com/starting-a-new-gatsby-project">Starting a New Gatsby Project</a></li><li><a href="https://michaeluloth.com/gatsbys-default-files-and-folders">Gatsby's Default Files and Folders</a></li><li><a href="https://michaeluloth.com/writing-html-in-a-gatsby-project">Writing HTML in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-pages-to-a-gatsby-project">Adding Pages to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/wrapping-pages-in-a-layout-component">Wrapping Pages in a Layout Component</a></li><li><a href="https://michaeluloth.com/writing-css-in-a-gatsby-project">Writing CSS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/writing-css-in-js-in-a-gatsby-project">Writing CSS-in-JS in a Gatsby Project</a></li><li><a href="https://michaeluloth.com/adding-content-to-a-gatsby-project">Adding Content to a Gatsby Project</a></li><li><a href="https://michaeluloth.com/using-graphql-with-gatsby">Using GraphQL with Gatsby</a></li></ol>
<p>The <a href="https://youtu.be/jAa1wh5ATm0">first video</a> explores the Gatsby ecosystem and how Gatsby simplifies web development by combining the best features of React and static HTML.</p>
<p><a href="https://youtu.be/jAa1wh5ATm0">[Video: https://youtu.be/jAa1wh5ATm0]</a></p>
<p>For more, check out <a href="https://www.youtube.com/watch?v=jAa1wh5ATm0&list=PLHBEcHVSROXQQhXpNhmiVKKcw72Cc0V-U">the entire playlist</a> on YouTube.</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[Introducing Gatsby Tutorials]]></title>
            <link>https://michaeluloth.com/introducing-gatsby-tutorials</link>
            <guid permalink="true">https://michaeluloth.com/introducing-gatsby-tutorials</guid>
            <pubDate>Fri, 30 Nov 2018 05:00:00 GMT</pubDate>
            <description><![CDATA[A new, community-updated directory of Gatsby.js learning resources.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645626987/mu/boy-surprised-by-book.jpg" alt="Introducing Gatsby Tutorials" />
<img src="https://res.cloudinary.com/ooloth/image/upload/v1645626987/mu/boy-surprised-by-book.jpg" alt="" />
<p>Earlier this month, I launched <a href="https://www.gatsbytutorials.com/">Gatsby Tutorials</a>. 🎉</p>
<p>Gatsby Tutorials is a filterable and sortable directory of all the <a href="https://www.gatsbyjs.org/">GatsbyJS</a> learning resources currently available online. It aims to gather every video, audio and written Gatsby tutorial into one place so you can easily find it when you need it.</p>
<p>Oh, and it’s free and open source.</p>
<h2>Umm…why did you make this?</h2>
<p>Four main reasons:</p>
<ol><li>Gatsby is awesome and I want to help other coders learn it. 👍</li><li>I'd like high-quality Gatsby tutorials to reach a wider audience. 🎙️</li><li>It can be hard to locate useful resources when you need them. 🧐</li><li>Subscribing separately to every Gatsby-related Twitter feed, blog and YouTube channel can be overwhelming. 😩</li></ol>
<img src="https://res.cloudinary.com/ooloth/image/fetch/https://media.giphy.com/media/3o7TKs2XTu7R5DefUk/giphy.gif" alt="" />
<h2>How does it work?</h2>
<p>Gatsby Tutorials gathers every Gatsby learning resource in one place so you don’t have to scroll through endless feeds or wander the internet for help.</p>
<p>Instead, just visit the directory and search or filter the list for the topic you’re interested in. The tutorials you need will magically appear:</p>
<img src="https://res.cloudinary.com/ooloth/image/upload/v1645627440/mu/gatsby-tutorials-search-results.jpg" alt="" />
<p>Gatsby Tutorials also assists new tutorial creators by making their content more discoverable—a win-win for content creators and Gatsby learners alike.</p>
<h2>How do I contribute?</h2>
<p>If you notice a resource is missing from the list, please add it! This project will work best if the community helps keep it up to date. 🙌</p>
<p>Just click the &quot;<a href="https://github.com/ooloth/gatsby-tutorials#how-do-i-add-a-tutorial">Add a tutorial</a>&quot; link and you’ll be taken to the quick instructions.</p>
<p>If you’d like to suggest a tutorial without adding it yourself, that's helpful too! Just copy the link into a <a href="https://github.com/ooloth/gatsby-tutorials/issues/new">new issue</a> and I’ll add it for you.</p>
<img src="https://res.cloudinary.com/ooloth/image/fetch/https://media.giphy.com/media/l0HlGPNqhq07VzPeU/giphy.gif" alt="" />
<h2>Roadmap</h2>
<p>Gatsby Tutorials currently includes 157 video, audio and written tutorials that can be discovered by browsing, searching, or filtering by format, topic, author and source.</p>
<p>Here are some additional features coming soon:</p>
<h3>Better filters 🏷</h3>
<ul><li>Filter tutorials by language</li><li>Filter by multiple topics at once</li><li>Cancel a filter by clicking it again</li></ul>
<h3>Better performance ⚡️</h3>
<ul><li>Paginate the tutorials (and search results)</li><li>Generate the filter lists at build time</li></ul>
<h3>Better search 🕵️‍♂️</h3>
<ul><li>Implement fuzzy search to enable non-sequential search terms</li><li>Highlight search string in search results</li></ul>
<h2>Wrap-up</h2>
<p>If you have questions or ideas for making Gatsby Tutorials more useful, please share your thoughts on Twitter or by opening a <a href="https://github.com/ooloth/gatsby-tutorials/issues/new">new issue</a> in the Gatsby Tutorials repo.</p>
<p>Happy coding!</p>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
        <item>
            <title><![CDATA[How to set up a Mac for web development]]></title>
            <link>https://michaeluloth.com/how-to-set-up-a-mac-for-web-development</link>
            <guid permalink="true">https://michaeluloth.com/how-to-set-up-a-mac-for-web-development</guid>
            <pubDate>Sat, 20 Oct 2018 04:00:00 GMT</pubDate>
            <description><![CDATA[What to install on your Mac before starting a JavaScript project.]]></description>
            <content:encoded><![CDATA[<img src="https://res.cloudinary.com/ooloth/image/upload/v1645232039/mu/laptop-with-code.jpg" alt="How to set up a Mac for web development" />
<img src="https://res.cloudinary.com/ooloth/image/upload/v1645232039/mu/laptop-with-code.jpg" alt="" />
<p>While you can build basic websites with nothing more than a text editor and browser, you may want to up your game by adding a JavaScript framework like <a href="https://reactjs.org/">React</a> or <a href="https://vuejs.org/">Vue</a> and useful tools like <a href="https://git-scm.com/">Git</a> to your workflow.</p>
<p>But wait! Your Mac isn’t ready. Before diving in, you’ll need to install a few items to that will save you from confusing errors later. 😩</p>
<p>This article will guide you through the minimum setup you'll need to get up and running with JavaScript-based web development on your Mac.</p>
<p>Let’s go!</p>
<h2>Update Your Mac</h2>
<p>Before installing any new software, follow <a href="https://support.apple.com/en-ca/HT201541">these instructions</a> from Apple to upgrade macOS and your current software to the latest version.</p>
<h2>Choose a Terminal App</h2>
<p>Since you'll be interacting with your Mac using the command line in this article, you'll need a terminal app.</p>
<p>Any of the following are good options:</p>
<ul><li><a href="https://www.iterm2.com/">iTerm2</a></li><li><a href="https://sw.kovidgoyal.net/kitty/">Kitty</a></li><li><a href="https://alacritty.org/">Alacritty</a></li><li><a href="https://wezfurlong.org/wezterm/">WezTerm</a></li><li><a href="https://code.visualstudio.com/docs/editor/integrated-terminal">Visual Studio Code</a>'s integrated terminal</li><li><a href="https://support.apple.com/en-ca/guide/terminal/welcome/mac">Terminal</a> (the default app that comes with your Mac)</li></ul>
<p>If you aren’t sure which one to pick, start with iTerm2.</p>
<h2>Command Line Developer Tools</h2>
<p>The first thing you'll need to install from the command line are your Mac's command line developer tools. Installing these now will prevent <a href="https://stackoverflow.com/questions/32893412/command-line-tools-not-working-os-x-el-capitan-macos-sierra-macos-high-sierra">weird errors</a> later.</p>
<p>To check if the tools are already installed, type the following command in your terminal app and hit return:</p>
<pre><code>xcode-select --version</code></pre>
<p>If the result is not a version number, install the tools with this command:</p>
<pre><code>xcode-select --install</code></pre>
<p>A dialog will appear asking if you'd like to install the tools. Click <strong>Install</strong> and the package will download and install itself.</p>
<p>When the installation finishes, confirm the tools are now installed by rerunning the first command:</p>
<pre><code>xcode-select --version</code></pre>
<p>The result should now be a version number.</p>
<h2>Homebrew</h2>
<p>Instead of installing the next few tools by going to each tool's website, finding the download page, clicking the download link, unzipping the files, and manually running the installer, we’re going to use <a href="https://brew.sh/">Homebrew</a>.</p>
<p>Homebrew is a tool that lets you install, update and uninstall software on your Mac from the command line. This is faster and <a href="https://blog.teamtreehouse.com/install-node-js-npm-mac">safer</a> than the manual approach and generally makes your development life easier.</p>
<p>First, check if Homebrew is already installed:</p>
<pre><code>brew --version</code></pre>
<p>If you don't see a version number, install Homebrew with this command:</p>
<pre><code>/bin/bash -c &quot;$(curl -fsSL &lt;https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&gt;)&quot;</code></pre>
<p>I promise that's the weirdest command you'll see in this article! 😅 Thanks to Homebrew, the rest are simple.</p>
<p>Before moving on, confirm Homebrew is now installed:</p>
<pre><code>brew --version</code></pre>
<h2>Node and npm</h2>
<p><a href="https://nodejs.org/">Node.js</a> is a tool that allows your Mac to run JavaScript code outside of a web browser. If you want to run a JavaScript framework like React or Vue on your Mac, you’ll need to have Node installed.</p>
<p>Node also includes <a href="https://www.npmjs.com/">npm</a> (the Node Package Manager), which gives you access to a giant library of free code you can download and use in your projects.</p>
<p>First, check if <code>node</code> is already installed:</p>
<pre><code>node --version</code></pre>
<p>If not, install it with Homebrew:</p>
<pre><code>brew install node</code></pre>
<p>Finally, confirm <code>node</code> and <code>npm</code> are now installed:</p>
<pre><code>node --version</code></pre>
<pre><code>npm --version</code></pre>
<h2>Git</h2>
<p><a href="https://git-scm.com/">Git</a> is a tool that helps you track changes to your code and work with other developers on shared projects.</p>
<p>Using Git on all every project is a great habit to develop and will prepare you for future projects where Git may be required. Some tools (like <a href="https://www.gatsbyjs.org/">Gatsby</a>) also depend on Git being installed on your Mac, so you’ll need it even if you don’t plan to add it to your workflow.</p>
<p>Once again, start by checking if <code>git</code> is already installed:</p>
<pre><code>git --version</code></pre>
<p>If not, install it:</p>
<pre><code>brew install git</code></pre>
<p>And confirm the installation worked:</p>
<pre><code>git --version</code></pre>
<h2>Update Everything</h2>
<p>Once in a while, run the following command and everything you’ve installed with Homebrew will update automatically:</p>
<pre><code>brew update &amp;&amp; brew upgrade &amp;&amp; brew cleanup &amp;&amp; brew doctor</code></pre>
<p>That one command is all you need to keep your system up to date. 🙌 I usually run it each time I start a new project, but feel free to do so whenever you like.</p>
<p>(When you run this command, if Homebrew suggests additional commands for you to run, go ahead and run them. If a command begins with <code>sudo</code> and you are prompted for a password, use your Mac’s admin password.)</p>
<p>That’s it for the command line!</p>
<h2>Code Editor</h2>
<p>While you can write code in any text editor, using one that highlights and validates your code will make your life much easier.</p>
<p>Any of the following are good options:</p>
<ul><li><a href="https://code.visualstudio.com/">Visual Studio Code</a></li><li><a href="https://atom.io/">Atom</a></li><li><a href="https://www.sublimetext.com/">Sublime Text</a></li></ul>
<p>If you’re just getting started, choose Visual Studio Code.</p>
<h2>Browser</h2>
<p>As you code, it helps to view the app or website you’re building in a browser to confirm it works. While you can use any browser for this, some include extra developer tools that show you details about your code and how to improve it.</p>
<p>Either of the following are good options:</p>
<ul><li><a href="https://www.google.com/chrome/">Chrome</a></li><li><a href="https://www.mozilla.org/firefox/">Firefox</a></li></ul>
<p>If you’re just getting started, choose Chrome.</p>
<h2>Finder</h2>
<p>A quick tip here: you’ll want to show the files your Mac hides by default. (For example, git files are automatically hidden, but sometimes you’ll want to edit them.)</p>
<p>The easiest way to show your Mac’s hidden files is with the keyboard shortcut <code>command-shift-period</code>. This will alternate between showing and hiding these files so you can access them when you need them.</p>
<h2>Conclusion</h2>
<p>You're all set! 🎉</p>
<p>That’s all you need to get up and running with JavaScript-based frontend development on your Mac.</p>
<p>To prevent confusion, I left out any items that aren’t strictly required. If you'd like to dive deeper into optional ways you can further customize your Mac for web development, check out the links below.</p>
<h2>Further Reading</h2>
<ul><li><a href="https://www.taniarascia.com/setting-up-a-brand-new-mac-for-development/">Setting up a Brand New Mac for Development</a> • Tania Rascia</li><li><a href="https://www.bhnywl.com/blog/setting-up-a-macbook-for-frontend-development/">Setting up a MacBook for Front-End Development</a> • Ben Honeywill</li></ul>]]></content:encoded>
      <author>Michael Uloth</author>
        </item>
    </channel>
</rss>