<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Danny Guo</title>
    <description>The latest articles on DEV Community by Danny Guo (@dguo).</description>
    <link>https://dev.to/dguo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F80025%2F1038dad0-4950-44d4-aa18-54deeec803b0.jpeg</url>
      <title>DEV Community: Danny Guo</title>
      <link>https://dev.to/dguo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dguo"/>
    <language>en</language>
    <item>
      <title>How to Fix instanceof Not Working For Custom Errors in TypeScript</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Wed, 12 May 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/dguo/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript-4amp</link>
      <guid>https://dev.to/dguo/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript-4amp</guid>
      <description>&lt;p&gt;In JavaScript, you can create custom errors by extending the built-in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error"&gt;Error object&lt;/a&gt; (ever since &lt;a href="https://en.wikipedia.org/wiki/ECMAScript#6th_Edition_%E2%80%93_ECMAScript_2015"&gt;ES 2015&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can do the same thing in &lt;a href="https://www.typescriptlang.org/"&gt;TypeScript&lt;/a&gt;, but there is an important caveat if your &lt;code&gt;tsconfig.json&lt;/code&gt; has a compilation &lt;a href="https://www.typescriptlang.org/tsconfig#target"&gt;target&lt;/a&gt; of ES3 or ES5. In that case, &lt;code&gt;instanceof&lt;/code&gt; doesn’t work, which breaks any logic that is based on whether or not an error is a case of the custom error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unique constraint violation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// prints "true"&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// incorrectly prints "false"&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can try &lt;a href="https://www.typescriptlang.org/play?target=1&amp;amp;ts=4.2.3#code/MYGwhgzhAEAiYBcwCNIFMCiAnLB7L0aAHgmgHYAmM2eBA3gL4BQTwuZEChO+0AvNDJoA7nEQp0NfAAoARAFUyASwCOAVzTQ2HBFjBKyXAG5Lc4BKbKyAlAG4WAegfQADlgMIYs3Rtmt2EGZoAHQguADm0mg8BAacYGTAaLgAZtBSWHZMTtAGbDhowAggAJ6u7oZeKWAgEGh+2oEgIWGR0bS5OglJqWJIqHUZdkA"&gt;this out yourself&lt;/a&gt; in the TypeScript playground. This is a &lt;a href="https://github.com/microsoft/TypeScript/issues/13965"&gt;known issue&lt;/a&gt; that started with &lt;a href="https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work"&gt;TypeScript version 2.1&lt;/a&gt;. The recommended fix is to manually set the prototype in the constructor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setPrototypeOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unique constraint violation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// both print "true" now&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any custom errors which further extend &lt;code&gt;DatabaseError&lt;/code&gt; still need the same adjustment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setPrototypeOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;DatabaseConnectionError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setPrototypeOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DatabaseConnectionError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;DatabaseConnectionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid credentials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// all print "true"&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;DatabaseError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;DatabaseConnectionError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Upgrade the Compilation Target
&lt;/h2&gt;

&lt;p&gt;Remember that this is only an issue if your compilation target is ES3 or ES5. Instead of having to remember to set the prototype, you could consider upgrading your target to ES 2015 or even later. ES 2015 has &lt;a href="https://caniuse.com/es6"&gt;over 97% browser support&lt;/a&gt;, so it may be a reasonable choice for you, especially if you are okay with dropping support for Internet Explorer.&lt;/p&gt;

</description>
      <category>typescript</category>
    </item>
    <item>
      <title>What I Learned by Relearning HTML</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Fri, 07 May 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/dguo/what-i-learned-by-relearning-html-21eg</link>
      <guid>https://dev.to/dguo/what-i-learned-by-relearning-html-21eg</guid>
      <description>&lt;p&gt;I’ve worked on websites for several years, both professionally and for side projects. One day, I reflected on the fact that all of my web development education had come from actually making websites. In most cases, I’d have a specific problem, Google how to solve it, and learn something new in the process.&lt;/p&gt;

&lt;p&gt;I wondered what I was missing by never learning HTML in a comprehensive way. Forget CSS and JavaScript. I’m just talking about raw HTML. It might seem silly to go back to such a basic aspect of web development after a decent amount of experience, but it’s easy to become overconfident with a skill just because you know enough to do a few useful things.&lt;/p&gt;

&lt;p&gt;So I decided to relearn HTML and discover my &lt;a href="https://en.wikipedia.org/wiki/There_are_known_knowns"&gt;unknown unknowns&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experience
&lt;/h2&gt;

&lt;p&gt;For context, I made my first website in middle school for a class project. We learned basic HTML, and embedding a MP3 song felt like magic. But I didn’t touch web development again until college. I made a lightweight news aggregator called &lt;a href="https://www.dailylore.com/"&gt;The Daily Lore&lt;/a&gt; that’s still running (I preserved the &lt;a href="https://www.dailylore.com/legacy"&gt;original version&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Since then, I’ve worked on two websites professionally, one website for a &lt;a href="https://sublimefund.org/"&gt;nonprofit&lt;/a&gt;, my &lt;a href="https://www.dannyguo.com/"&gt;personal website&lt;/a&gt;, and a few small websites for side projects, such as &lt;a href="https://www.makeareadme.com/"&gt;Make a README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to HTML5
&lt;/h2&gt;

&lt;p&gt;I don’t consider myself to be a web development expert based on just that experience, but I surely had far more knowledge than the typical student for &lt;a href="https://www.coursera.org/"&gt;Coursera&lt;/a&gt;’s &lt;a href="https://www.coursera.org/learn/html"&gt;Introduction to HTML5&lt;/a&gt; course. I started the course expecting to know a lot of the content already, since it was designed for complete beginners with no programming backgrounds.&lt;/p&gt;

&lt;p&gt;As I went through the material, I did in fact know a lot of it already, but it was still a good refresher for two points in particular: the importance of using semantic elements and what to think about in terms of accessibility.&lt;/p&gt;

&lt;p&gt;I’ve always had a bad habit of using generic &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; elements to make what I need, rather than semantic elements that represent specific content, like the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header"&gt;header&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/footer"&gt;footer&lt;/a&gt; elements.&lt;/p&gt;

&lt;p&gt;Accessibility was also something I had never considered in depth. I knew that images should have &lt;code&gt;alt&lt;/code&gt; descriptions, and that was about it. One of the course’s key points is that using the appropriate semantic elements is important to making a site more accessible.&lt;/p&gt;

&lt;p&gt;For example, people who use screen readers can jump around using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements"&gt;heading&lt;/a&gt; elements (&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; through &lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt;), so it’s important to use them and make sure they’re in the correct order. It’s wrong to use them only to make text bigger because their real purpose is to define the structure of the content. They’re like a table of contents.&lt;/p&gt;

&lt;p&gt;Instead of headings, we could use &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; elements and alter their font sizes with CSS to create a website that looks identical, but it’d be less semantic and less accessible. There is more to web development than making websites look the way we want. It’s important to make the content &lt;em&gt;mean&lt;/em&gt; what we want as well.&lt;/p&gt;

&lt;p&gt;Accessibility isn’t just about improving how websites work with screen readers. We should think about font size, font style, and color contrast for people who have visual impairments or color blindness. We should consider that people who have hearing loss may have a harder time recognizing that audio or video is playing. We should make tab navigation work well for people who rely primarily on the keyboard, perhaps because they have a difficult time using a mouse. When we add animations, we should take care to avoid ones that make it more difficult for someone to actually use the website, such as animations that change the page layout in the middle of interactions. And we should consider when a page is overloaded with too much information or too many elements, making it hard for people to understand things or how to actually use the website.&lt;/p&gt;

&lt;p&gt;It’s easy to forget about accessibility, but we should strive to make websites work well for as many people as possible. Accessibility also goes hand in hand with usability and search engine optimization. The course points out that improving one frequently means improving all the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the Documentation
&lt;/h2&gt;

&lt;p&gt;I have a &lt;a href="https://azeemba.com/"&gt;friend&lt;/a&gt; who is probably the only person I know who has read the entire &lt;a href="https://operations.nfl.com/the-rules"&gt;NFL rulebook&lt;/a&gt; (the 2020 version is 87 pages long). Watching football with him was fun because he was so good at understanding nuances to the game and weird situations. I figured there was a similar opportunity for me with HTML.&lt;/p&gt;

&lt;p&gt;The strict equivalent would have been to read the &lt;a href="https://html.spec.whatwg.org/"&gt;HTML standard&lt;/a&gt; for every HTML element, but I decided to read the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element"&gt;MDN documentation&lt;/a&gt; for every element instead since MDN has a lot of information about browser compatibility and using elements in practice. I read the entire page for each element, took notes, and made &lt;a href="https://apps.ankiweb.net/"&gt;Anki&lt;/a&gt; cards for the bits that I wanted to commit to memory.&lt;/p&gt;

&lt;p&gt;There were many deprecated elements that I only skimmed through, and I didn’t bother to take notes for those, but dozens of standardized elements and attributes were totally new to me.&lt;/p&gt;

&lt;p&gt;I didn’t intend to come out of this experience as a master of HTML, and I still have to apply what I’ve learned (including to this website), but I find it useful just to be aware of what is available. Even though I can’t recall all the details about using a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture"&gt;picture&lt;/a&gt; element, I know it exists now, and I can always look up the details later during implementation. It’s a categorical difference from not being aware of it at all and using a plain &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; for all cases because I don’t know any better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observations
&lt;/h2&gt;

&lt;p&gt;As I read the documentation, some things were particularly interesting to me, and I had some observations.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address"&gt;address&lt;/a&gt; element is for contact information in general, not just physical mailing addresses.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dfn"&gt;definition&lt;/a&gt; element represents the term that is being defined, rather than the definition itself.&lt;/p&gt;

&lt;p&gt;There is a whole set of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ruby"&gt;ruby&lt;/a&gt; elements that are primarily used to show the pronunciations of East Asian characters.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track"&gt;track&lt;/a&gt; element provides a standard way to embed timed text tracks for video and audio. I had never heard of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API"&gt;WebVTT&lt;/a&gt; (Web Video Text Tracks) format before.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map"&gt;map&lt;/a&gt; element seems like an anachronism, especially considering that it isn’t responsive.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data"&gt;data&lt;/a&gt; element provides a machine-readable translation for content. This seems likes it could help screen scraping, which some websites like &lt;a href="https://www.theverge.com/2019/9/10/20859399/linkedin-hiq-data-scraping-cfaa-lawsuit-ninth-circuit-ruling"&gt;LinkedIn have been actively trying to prevent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There’s subtlety when it comes to correctly choosing to use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong"&gt;strong&lt;/a&gt; versus &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em"&gt;em&lt;/a&gt; versus &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i"&gt;i&lt;/a&gt; versus &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u"&gt;u&lt;/a&gt; versus &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b"&gt;b&lt;/a&gt; versus &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark"&gt;mark&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There are a few elements that seem redundant. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend"&gt;legend&lt;/a&gt; element represents a caption for a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset"&gt;fieldset&lt;/a&gt; element, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption"&gt;caption&lt;/a&gt; element represents a caption for a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table"&gt;table&lt;/a&gt; element, and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption"&gt;figcaption&lt;/a&gt; element represents a caption for a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure"&gt;figure&lt;/a&gt; element. I don’t know why one element couldn’t do the job for all three, since the meaning could be derived from the parent element.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Future of HTML
&lt;/h3&gt;

&lt;p&gt;As I read through the documentation, it kept making me consider the question of how HTML should evolve. Browsers keeps gaining more and more functionality, to the point that they are becoming more like operating systems. There’s even an experimental &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API"&gt;API for connecting to Bluetooth devices&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Wikipedia is the perfect website for what HTML was originally designed for: mostly static documents that are connected through hyperlinks. But now we use the browser to deliver full on applications, like &lt;a href="https://www.figma.com/"&gt;Figma&lt;/a&gt;, which is a design tool that &lt;a href="https://www.figma.com/blog/webassembly-cut-figmas-load-time-by-3x/"&gt;effectively runs C++ code in the browser by compiling it to WebAssembly&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;HTML has added a few elements and attributes that make interactivity possible without JavaScript. For example, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details"&gt;details&lt;/a&gt; element creates a widget that can be toggled between open and closed states.&lt;/p&gt;

&lt;p&gt;But as your use case become more advanced, it quickly becomes difficult to rely solely on what HTML provides. For example &lt;a href="https://getbootstrap.com/"&gt;Bootstrap&lt;/a&gt;’s progress bar &lt;a href="https://getbootstrap.com/docs/5.0/components/progress/"&gt;doesn’t use&lt;/a&gt; the HTML &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress"&gt;progress&lt;/a&gt; element.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We don’t use the HTML5 &amp;lt;progress&amp;gt; element, ensuring you can stack progress bars, animate them, and place text labels over them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another example is the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table"&gt;table&lt;/a&gt; element. Pure HTML tables &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr#advanced_styling"&gt;can be pretty sophisticated&lt;/a&gt; in terms of displaying data, but there’s no built-in support for interactive functionality like sorting, filtering, and pagination.&lt;/p&gt;

&lt;p&gt;Browser support also becomes an issue when an element does become more advanced. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input"&gt;input&lt;/a&gt; element is one of the most complex elements because it supports so many combinations of input types and attributes. In theory, you could use it to easily collect a date and a time, using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local"&gt;datetime-local type&lt;/a&gt;. But not all browsers support it, and there is variation in how it works even among those that do.&lt;/p&gt;

&lt;p&gt;Some elements are also difficult to style, such as the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select"&gt;select&lt;/a&gt; element. So website developers may want to rely on standard functionality instead of using a library or implementing a feature themselves, but then they have to worry about it not working well in certain browsers or stylistic inconsistency with the rest of the website.&lt;/p&gt;

&lt;p&gt;I’m eager to see if &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components"&gt;Web Components&lt;/a&gt; become more popular and provide a good solution to these problems. If they do, the situation could become similar to programming languages, where different languages take difference stances on the question of how much functionality should be included in the standard library (HTML) so that the community has a lesser or greater tendency to rely on third party libraries (Web Components).&lt;/p&gt;

&lt;p&gt;Web Components do seem to be picking up some momentum. &lt;a href="https://github.com"&gt;GitHub&lt;/a&gt; has &lt;a href="https://github.blog/2021-05-04-how-we-use-web-components-at-github/"&gt;started to use them&lt;/a&gt;, and they publish their components to &lt;a href="https://www.webcomponents.org/"&gt;WebComponents.org&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It was easy to feel confident with HTML after doing web development for several years. Yet I found plenty of value in going back to learn it in a more rigorous manner. I learned about many improvements I can make to the websites I work on, and I have a better big picture view of HTML and how it might develop. While I still think learning by doing is highly effective, this experience has made me want to go back and relearn other things with a bottom up approach.&lt;/p&gt;

</description>
      <category>html</category>
      <category>learning</category>
      <category>a11y</category>
    </item>
    <item>
      <title>How to Concatenate Strings in Lua</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Tue, 29 Dec 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/dguo/how-to-concatenate-strings-in-lua-3hai</link>
      <guid>https://dev.to/dguo/how-to-concatenate-strings-in-lua-3hai</guid>
      <description>&lt;p&gt;The most straightforward way to concatenate (or combine) strings in &lt;a href="https://www.lua.org/about.html"&gt;Lua&lt;/a&gt; is to use the dedicated &lt;a href="https://www.lua.org/pil/3.4.html"&gt;string concatenation operator&lt;/a&gt;, which is two periods (&lt;code&gt;..&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hello, "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;"world!"&lt;/span&gt;
&lt;span class="c1"&gt;-- message equals "Hello, World!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Numbers are coerced to strings. For fine-grained control over number formatting, use &lt;a href="https://www.lua.org/manual/5.4/manual.html#pdf-string.format"&gt;string.format&lt;/a&gt;, which behaves mostly like C's &lt;a href="https://www.cplusplus.com/reference/cstdio/printf/"&gt;printf&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The count is: "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;-- message equals "The count is: 42"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trying to concatenate other &lt;a href="https://www.lua.org/pil/2.html"&gt;types&lt;/a&gt;, like nil or a table, will result in an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The count is: "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;-- results in an "attempt to concatenate a nil value" error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that Lua &lt;a href="https://stackoverflow.com/q/20091779/1481479"&gt;doesn't have&lt;/a&gt; syntactic sugar for &lt;a href="https://en.wikipedia.org/wiki/Augmented_assignment"&gt;augmented assignment&lt;/a&gt;. The following is invalid syntax.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hello, "&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;..=&lt;/span&gt; &lt;span class="s2"&gt;"world!"&lt;/span&gt;
&lt;span class="c1"&gt;-- results in a "syntax error near '..'" error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strings in Lua are &lt;a href="https://www.lua.org/pil/2.4.html"&gt;immutable&lt;/a&gt;, so the concatenation result (&lt;code&gt;message&lt;/code&gt; in this example) is a brand new string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hello, "&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;"world!"&lt;/span&gt;
&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bye, "&lt;/span&gt;
&lt;span class="c1"&gt;-- message still equals "Hello, World!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  table.concat
&lt;/h2&gt;

&lt;p&gt;If you need to perform many concatenation operations, using the concatenation operator can be slow because Lua has to keep reallocating memory to create new strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a result, it &lt;a href="https://www.reddit.com/r/lua/comments/1t6ois/tableconcat_is_fast/"&gt;can be much faster&lt;/a&gt; to use &lt;a href="https://www.lua.org/manual/5.4/manual.html#6.6"&gt;table.concat&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;table.concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a benchmark comparsion (using &lt;a href="https://github.com/sharkdp/hyperfine"&gt;hyperfine&lt;/a&gt;) from running the &lt;code&gt;..&lt;/code&gt; example as &lt;code&gt;slow.lua&lt;/code&gt; and running the &lt;code&gt;table.concat&lt;/code&gt; example as &lt;code&gt;fast.lua&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hyperfine &lt;span class="s1"&gt;'lua slow.lua'&lt;/span&gt;
&lt;span class="c"&gt;# Benchmark #1: lua slow.lua&lt;/span&gt;
&lt;span class="c"&gt;#   Time (mean ± σ):      1.287 s ±  0.115 s    [User: 1.120 s, System: 0.078 s]&lt;/span&gt;
&lt;span class="c"&gt;#   Range (min … max):    1.187 s …  1.528 s    10 runs&lt;/span&gt;
hyperfine &lt;span class="s1"&gt;'lua fast.lua'&lt;/span&gt;
&lt;span class="c"&gt;# Benchmark #1: lua fast.lua&lt;/span&gt;
&lt;span class="c"&gt;#   Time (mean ± σ):      39.3 ms ±   3.8 ms    [User: 34.6 ms, System: 2.8 ms]&lt;/span&gt;
&lt;span class="c"&gt;#   Range (min … max):    35.3 ms …  58.3 ms    48 runs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference probably doesn't matter in most cases, but it's a good optimization to be aware of.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;table.concat&lt;/code&gt; can also be easier to use because it can take a separator argument to add between elements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;table.concat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;-- message equals "12345"&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;table.concat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- message equals "1, 2, 3, 4, 5"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can also take start and end indexes. Keep in mind that Lua arrays &lt;a href="https://www.lua.org/pil/11.1.html"&gt;start with index 1&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;table.concat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- message equals "2, 3, 4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Direct Approach
&lt;/h2&gt;

&lt;p&gt;Depending on your use case, you might be able to save some memory usage over &lt;code&gt;table.concat&lt;/code&gt; by generating the result directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;io.stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>lua</category>
    </item>
    <item>
      <title>Fixing MacBook Pro Thermal Performance Issues</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Sun, 02 Aug 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/dguo/fixing-macbook-pro-thermal-performance-issues-46hk</link>
      <guid>https://dev.to/dguo/fixing-macbook-pro-thermal-performance-issues-46hk</guid>
      <description>&lt;p&gt;&lt;em&gt;I may earn commissions from purchases made through affiliate links in this post.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Over the past several months, my 2018 15" MacBook Pro developed thermal performance issues. Here's what I did to fix the problem. The two key changes were that I switched to using the right side &lt;a href="https://en.wikipedia.org/wiki/Thunderbolt_(interface)#Thunderbolt_3"&gt;Thunderbolt 3&lt;/a&gt; ports instead of the left side ones, and I cleaned the dust buildup in the internal fans.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;The problem first started when I noticed my CPU usage spiking to near 100%. In the process list, I saw &lt;code&gt;kernel_task&lt;/code&gt; taking up most of the CPU.&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://support.apple.com/en-us/HT207359"&gt;learned&lt;/a&gt; that macOS does this intentionally to prevent the CPU from overheating.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One of the functions of kernel_task is to help manage CPU temperature by &amp;gt; making the CPU less available to processes that are using it intensely. In &amp;gt; other words, kernel_task responds to conditions that cause your CPU to become &amp;gt; too hot, even if your Mac doesn't feel hot to you. It does not itself cause &amp;gt; those conditions. When the CPU temperature decreases, kernel_task &amp;gt; automatically reduces its activity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fan Speed
&lt;/h2&gt;

&lt;p&gt;For many years, I've used &lt;a href="https://github.com/hholtmann/smcFanControl"&gt;smcFanControl&lt;/a&gt; to manually increase my fan speed because I don't mind fan noise, especially if it means getting better performance out of my laptop or being able to comfortably put it on my lap.&lt;/p&gt;

&lt;p&gt;However, it stopped working reliably after a macOS update. I purchased &lt;a href="https://bjango.com/mac/istatmenus/"&gt;iStat Menus&lt;/a&gt;, which comes with a well supported fan control feature. Note that you have to buy it from their own website to get fan control. The Mac App Store version &lt;a href="https://bjango.com/help/istatmenus6/macappstore/"&gt;doesn't include that particular feature&lt;/a&gt; for some reason.&lt;/p&gt;

&lt;p&gt;I turned the fans up to max regularly in an attempt to preempt the &lt;code&gt;kernel_task&lt;/code&gt; issue, but it still kept popping up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thunderbolt 3 Ports
&lt;/h2&gt;

&lt;p&gt;I found &lt;a href="https://apple.stackexchange.com/a/363933/275342"&gt;this excellent StackExchange answer&lt;/a&gt; that recommends using the right side Thunderbolt 3 ports instead of the left side ones. Apparently the ports aren't equal. The left ones can cause thermal issues. Also, using both ports on either side can be problematic.&lt;/p&gt;

&lt;p&gt;I use a &lt;a href="https://www.amazon.com/CalDigit-TS3-Plus-Thunderbolt-Dock/dp/B07CZPV8DF/ref=as_li_ss_tl?crid=223U28DX1402C&amp;amp;dchild=1&amp;amp;keywords=caldigit+ts3+plus&amp;amp;qid=1595940705&amp;amp;sprefix=caldigit+,aps,223&amp;amp;sr=8-2&amp;amp;th=1&amp;amp;linkCode=ll1&amp;amp;tag=thdalo00-20&amp;amp;linkId=016d783904cae8cac9d952a3c58d816b&amp;amp;language=en_US"&gt;Thunderbolt 3 dock&lt;/a&gt; for a single cable solution that handles charging and all my peripherals, including monitors. I tended to plug it into a left port. I switched it over to a right port and saw an immediate difference in temperature. iStat Menus reports the temperatures from various sensors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TkNdpcJz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/2oiPxjF.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TkNdpcJz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/2oiPxjF.png" alt="iStat Menus temperature readings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I tested and verified that using a left port caused the CPU temperature to go up much higher than using a right port.&lt;/p&gt;

&lt;p&gt;For a few months, this simple change was enough to resolve the issue. Luckily, I was able to continue using a single cable, and I didn't have to plug in my dock on one side and then a separate charger on the other side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolution Scaling
&lt;/h2&gt;

&lt;p&gt;Then the problem started happening again. I also experienced issues playing &lt;a href="https://en.wikipedia.org/wiki/Tabletop_Simulator"&gt;Tabletop Simulator&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Counter-Strike:_Global_Offensive"&gt;Counter-Strike&lt;/a&gt;, and &lt;a href="https://en.wikipedia.org/wiki/Valorant"&gt;Valorant&lt;/a&gt; (on Windows through Boot Camp), all games that my machine should be able to handle without any problems. It would stutter even after I turned the resolution and graphical settings all the way down.&lt;/p&gt;

&lt;p&gt;I thought it might have something to do with my monitors. In my desk setup, I have two external monitors that I run at a scaled resolution that looks like 2560 x 1440.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7QUPjotc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/xPz7nXG.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7QUPjotc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/xPz7nXG.png" alt="external display resolution"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note the warning that "Using a scaled resolution may affect performance." The problem is that macOS &lt;a href="https://github.com/kovidgoyal/kitty/issues/1043"&gt;achieves this resolution&lt;/a&gt; by rendering everything in double the size and then scaling it down. So my MacBook is essentially doing the rendering work for two 5K monitors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FMDaPpXS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/0oroxGb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FMDaPpXS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/0oroxGb.png" alt="System Information displays"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://forums.macrumors.com/threads/4k-monitor-at-1440p-scaling-performance.2232164/"&gt;this thread&lt;/a&gt; or &lt;a href="https://apple.stackexchange.com/a/338581/275342"&gt;this StackExchange answer&lt;/a&gt; for more details.&lt;/p&gt;

&lt;p&gt;I considered turning the resolution down to look like 1920 x 1080, which isn't a scaled resolution, but then everything looked too big.&lt;/p&gt;

&lt;p&gt;I was pretty frustrated at this point, especially because &lt;a href="https://zoom.us/"&gt;Zoom&lt;/a&gt; video calls started freezing up on me. I got into the habit of unplugging my monitors before joining video calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dust
&lt;/h2&gt;

&lt;p&gt;This entire time, I thought the problem was my MacBook producing too much heat. Beyond making the fans run at max more often, I never considered that the problem could be that it stopped being able to get rid of heat.&lt;/p&gt;

&lt;p&gt;Then I read &lt;a href="https://quanticdev.com/articles/cleaning-macbook-after-16800-hours-of-use/"&gt;this post&lt;/a&gt; from someone who cleaned the inside of a MacBook after 7 years of use. I was initially skeptical that clogged fans were my problem because my MacBook was only about a year and a half old. But the more I thought about it, the more it made sense, and I didn't have anything to lose by trying to clean it.&lt;/p&gt;

&lt;p&gt;To open my MacBook, I followed &lt;a href="https://www.ifixit.com/Guide/MacBook+Pro+15-Inch+Touch+Bar+2018+Lower+Case+Replacement/121426"&gt;this iFixit guide&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  AutoBoot
&lt;/h3&gt;

&lt;p&gt;I learned that by default, macOS turns on the MacBook when you press any button (not just the power button) or open the lid. This was very surprising to me. I did recall being confused in the past when my MacBook turned on even though I didn't press the power button. When it first happened, I thought there was something wrong. At least from Apple's perspective, it turns out &lt;a href="https://www.wired.com/story/its-not-a-bug-its-a-feature/"&gt;this is a feature, not a bug&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So to make sure the MacBook stays off through the process, you have to run this in a terminal to turn off booting on lid open.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nvram &lt;span class="nv"&gt;AutoBoot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To turn the setting back on later, run this. Though I just left mine off after I was done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nvram &lt;span class="nv"&gt;AutoBoot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%03
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Removing the Bottom
&lt;/h3&gt;

&lt;p&gt;The next step was to take off the screws. Apple uses &lt;a href="https://en.wikipedia.org/wiki/Pentalobe_security_screw"&gt;pentalobe screws&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hPohVzEc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/FQndl8j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hPohVzEc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/FQndl8j.jpg" alt="pentalobe screw"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've opened MacBooks before to replace &lt;a href="https://www.reddit.com/r/spicypillows"&gt;swollen batteries&lt;/a&gt;, so I was prepared with an &lt;a href="https://www.amazon.com/gp/product/B072HNBL9Z/ref=as_li_ss_tl?ie=UTF8&amp;amp;psc=1&amp;amp;linkCode=ll1&amp;amp;tag=thdalo00-20&amp;amp;linkId=09c787c5245a192a816fd263da8ad7ea&amp;amp;language=en_US"&gt;electronics screwdriver set&lt;/a&gt; that includes pentalobe bits.  I also had a &lt;a href="https://www.amazon.com/Endust-Electronics-Compressed-bitterant-11407/dp/B00HX7VZ5M/ref=as_li_ss_tl?dchild=1&amp;amp;keywords=endust+duster&amp;amp;qid=1596317241&amp;amp;sr=8-4&amp;amp;th=1&amp;amp;linkCode=ll1&amp;amp;tag=thdalo00-20&amp;amp;linkId=4bb9cd9f61cc6a4a158ec8dfbfb00e3d&amp;amp;language=en_US"&gt;can of compressed air&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--K_gpyopU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/JWhg3vL.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--K_gpyopU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/JWhg3vL.jpg" alt="screwdriver and duster"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I removed the screws and learned that Apple has made the bottom harder to open because now you need a suction cup. I repurposed the suction cup from my &lt;a href="https://www.amazon.com/gp/product/B07GKXPL6M/ref=as_li_ss_tl?ie=UTF8&amp;amp;psc=1&amp;amp;linkCode=ll1&amp;amp;tag=thdalo00-20&amp;amp;linkId=a201bd337a2a7814cff67674269ea94f&amp;amp;language=en_US"&gt;car dash cell phone holder&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zKt8CgNO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/ewlAsGh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zKt8CgNO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/ewlAsGh.jpg" alt="cell phone holder with suction cup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You have to use the suction cup to lift part of the bottom enough to slide in something to release a few internal latches. I used a library card.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FfDNQdnH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/tDEaslF.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FfDNQdnH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/tDEaslF.jpg" alt="lifting the bottom cover"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning the Dust
&lt;/h3&gt;

&lt;p&gt;Once I got my MacBook open, it was obvious that dust was the main cause of my performance issues. Just taking the bottom off caused a cloud of dust to come out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6kMtF5j9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/CN5oz3j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6kMtF5j9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/CN5oz3j.jpg" alt="dust on the bottom cover"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There was also cat hair from this troublemaker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A890oBar--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Feill8c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A890oBar--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Feill8c.jpg" alt="my cat"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both fans were totally gunked up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--shAcf-Qx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/IAGcS0j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--shAcf-Qx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/IAGcS0j.jpg" alt="dust on the inside"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UXPm9e_u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/BF8Sinf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UXPm9e_u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/BF8Sinf.jpg" alt="dust on the left fan"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oKqoC5mT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/gX2FxJ5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oKqoC5mT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/gX2FxJ5.jpg" alt="dust on the right fan"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used compressed air to blow out the dust. I also used a toothpick to get some off the fans that wouldn't come off from blowing alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VQmZJP1Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/BxW6OOp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VQmZJP1Q--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/BxW6OOp.jpg" alt="overview after cleaning"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5DsURiOG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/OMmeXnG.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5DsURiOG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/OMmeXnG.jpg" alt="left fan after cleaning"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Q4rldQvR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/E60RF3J.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Q4rldQvR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/E60RF3J.jpg" alt="fan closeup after cleaning"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I put the cover back on and haven't had any more thermal performance issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrospective
&lt;/h2&gt;

&lt;p&gt;It would be nice if macOS could provide some actionable advice when &lt;code&gt;kernel_task&lt;/code&gt; is trying to stop the MacBook from overheating. I would have appreciated an OS suggestion to use the right side Thunderbolt 3 ports.&lt;/p&gt;

&lt;p&gt;But I also never considered that my laptop fans could require cleaning. In retrospect, that's foolish because I've seen how dirty it can get in desktop PCs, and I know that heat is an inhibitor to computer performance. It's just like running in humid weather. Your body starts shutting down because its main mechanism for getting rid of heat (sweating) stops working because humidity prevents sweat from evaporating.&lt;/p&gt;

&lt;p&gt;It would be cool if the machine could detect and alert when the fans are no longer effective, perhaps with a sensor that looks for a lack of airflow at the vents.&lt;/p&gt;

&lt;p&gt;To prevent the issue from occurring again, I've set up a recurring task in &lt;a href="https://todoist.com/r/danny_guo_xlyrub"&gt;Todoist&lt;/a&gt; to remind me to clean my computers once a year.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found an error or typo? Feel free to open a pull request on &lt;a href="https://github.com/dguo/dannyguo.com/blob/master/content/blog/fixing-macbook-pro-thermal-performance-issues.md"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macbook</category>
    </item>
    <item>
      <title>Clearing Mac Storage Space</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Wed, 15 Jul 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/dguo/clearing-mac-storage-space-257i</link>
      <guid>https://dev.to/dguo/clearing-mac-storage-space-257i</guid>
      <description>&lt;p&gt;I recently needed to clear a large amount of space on my Mac's hard drive in order to install Windows through &lt;a href="https://en.wikipedia.org/wiki/Boot_Camp_(software)"&gt;Boot Camp&lt;/a&gt;. After deleting files and applications, I still had large amounts of space taken up in ways that I didn't understand. Here is what I did to fix the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Easy Methods
&lt;/h2&gt;

&lt;p&gt;The obvious thing to start with was deleting known files and applications. In particular, I removed some built-in macOS applications, like GarageBand, that I can always re-download from the Mac App Store.&lt;/p&gt;

&lt;p&gt;I also cleared the trash. As a tip, you probably want to set it to &lt;a href="https://support.apple.com/guide/mac-help/delete-files-and-folders-on-mac-mchlp1093/mac"&gt;automatically delete items after 30 days&lt;/a&gt;. I don't understand why that's considered an advanced feature in Finder when it seems like it should be the default setting. It &lt;a href="https://osxdaily.com/2017/02/10/automatically-empty-trash-mac/"&gt;wasn't even an option&lt;/a&gt; until &lt;a href="https://en.wikipedia.org/wiki/MacOS_Sierra"&gt;Sierra&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then I went into my Google Backup and Sync (for &lt;a href="https://www.google.com/drive/"&gt;Google Drive&lt;/a&gt;) settings and &lt;a href="https://www.remosoftware.com/info/how-to-configure-google-drive-sync-settings-to-sync-specific-folders-or-file-types"&gt;unsynced some folders&lt;/a&gt; that I don't need to be available locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Storage
&lt;/h2&gt;

&lt;p&gt;After those easy wins, I wanted to get a better idea of what was taking up space. macOS has a built in storage analyzer, but it's pretty basic. The overview gives a rough breakdown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qiQLP1Lh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/ZbajDJM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qiQLP1Lh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/ZbajDJM.png" alt="macOS storage overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking manage gives you a more detailed view.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hGOe5_EM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Jl509hi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hGOe5_EM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/Jl509hi.png" alt="macOS storage management"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But note that "System" is greyed out! There's no way to drill down into it to see what's actually taking up space.&lt;/p&gt;

&lt;p&gt;This was on &lt;a href="https://en.wikipedia.org/wiki/MacOS_Mojave"&gt;Mojave&lt;/a&gt;.  At least &lt;a href="https://www.apple.com/macos/catalina/"&gt;Catalina&lt;/a&gt; splits it up into "System" and "Other," though you still can't drill down into either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disk Space Analysis Programs
&lt;/h2&gt;

&lt;p&gt;I'm familiar with using &lt;code&gt;du -sh&lt;/code&gt; in the terminal to show folder sizes, but I thought a GUI program might be better for this task. I looked into third party programs to find out what folders and files were taking up space. The first one I tried was &lt;a href="http://www.derlien.com/"&gt;Disk Inventory X&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It was useful but took a while (around 10 minutes) to produce results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ilis8vK---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/VW6aebM.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ilis8vK---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/VW6aebM.png" alt="Disk Inventory X results"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I looked into alternatives. The most helpful resources were this &lt;a href="https://apple.stackexchange.com/q/81568/275342"&gt;Stack Exchange question&lt;/a&gt; and this &lt;a href="https://macpaw.com/how-to/best-disk-space-analyzers-mac"&gt;article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I tried &lt;a href="https://www.omnigroup.com/more"&gt;OmniDiskSweeper&lt;/a&gt;. It doesn't have a tree view like Disk Inventory X does, but I didn't find that very useful anyway. OmniDiskSweeper was able to show results as they came, rather than all at once at the end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0w5N7ZYo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/t9N8zcf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0w5N7ZYo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/t9N8zcf.png" alt="OmniDiskSweeper results"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It doesn't update in real time, so I did have to re-analyze the disk after deleting files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker
&lt;/h2&gt;

&lt;p&gt;The first thing I noticed was Docker's &lt;a href="https://docs.docker.com/docker-for-mac/space/"&gt;disk image file&lt;/a&gt;. Docker for Mac stores containers and images in that single, large file. I ran &lt;a href="https://docs.docker.com/engine/reference/commandline/system_prune/"&gt;docker system prune&lt;/a&gt; to delete unnecessary containers and images.&lt;/p&gt;

&lt;p&gt;Then I decreased the size limit for the disk image file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vxUhS-uT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/FNrnAtf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vxUhS-uT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/FNrnAtf.png" alt="Docker disk image size"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Spotify
&lt;/h2&gt;

&lt;p&gt;The biggest surprise to me was discovering that Spotify can take a large amount of space to cache files. Mine was over 6 GB. Even Firefox's cache was only 2 GB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--70eHNRqn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/4oUnYL7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--70eHNRqn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/4oUnYL7.png" alt="Spotify cache size"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Furthermore, there doesn't seem to be a way to control this cache size in Spotify settings, not even buried in some advanced settings. This setting &lt;a href="https://community.spotify.com/t5/Social/What-happened-to-the-cache-limit-setting-on-Mac-desktop-player/td-p/4658524"&gt;might have existed before&lt;/a&gt;, but it doesn't now. Luckily, &lt;a href="https://community.spotify.com/t5/Desktop-Mac/How-to-limit-cache-size/td-p/2907725"&gt;someone discovered&lt;/a&gt; that you can set a limit manually by editing the Spotify preferences file at &lt;code&gt;~/Library/Application\ Support/Spotify/prefs&lt;/code&gt; and adding a line like &lt;code&gt;storage.size=2048&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I set the limit to 2048 MB, restarted Spotify, and confirmed that the cache folder dropped to the expected size, saving me over 4 GB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; ~/Library/Caches/com.spotify.client/Data
6.4G    Data

&lt;span class="c"&gt;# restart Spotify&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; ~/Library/Caches/com.spotify.client/Data
2.0G    Data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Yarn
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://yarnpkg.com/"&gt;Yarn&lt;/a&gt; seemed to have old cache folders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Library/Caches/Yarn &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;ls &lt;/span&gt;v2 v4 v6

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; v2 v4 v6
  0B    v2
1.2G    v4
1.2G    v6

&lt;span class="nv"&gt;$ &lt;/span&gt;yarn cache &lt;span class="nb"&gt;dir&lt;/span&gt; /Users/dguo/Library/Caches/Yarn/v6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deleted the v2 and v4 folders. My cache is fairly small, but &lt;a href="https://github.com/yarnpkg/yarn/issues/6037"&gt;other people&lt;/a&gt; have ended up with gigantic caches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git
&lt;/h2&gt;

&lt;p&gt;I found some &lt;a href="https://git-scm.com/"&gt;Git&lt;/a&gt; repositories that I didn't need anymore. Deleting these saved around 1 GB in total. Git repos can take up a large amount of space since by default, they contain the entire history of a project. Though in my case, most of the space savings was due to deleting &lt;code&gt;node_modules&lt;/code&gt; with each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;For years, I've used &lt;a href="https://freemacsoft.net/appcleaner/"&gt;AppCleaner&lt;/a&gt; to uninstall applications because I knew most of them leave files behind if you only delete the application itself. This experience reminded me that when I install an application, I generally have no idea where it's going to write files and how large they can get. It would be nice if operating systems could make it easier for users to be more in control of hard drive usage, instead of letting applications treat the hard drive like an unlimited resource.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found an error or typo? Feel free to open a pull request on &lt;a href="https://github.com/dguo/dannyguo.com/blob/master/content/blog/clearing-mac-storage-space.md"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mac</category>
    </item>
    <item>
      <title>Morning Joy - wake up to a happy text every day</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Fri, 01 May 2020 06:59:19 +0000</pubDate>
      <link>https://dev.to/dguo/morning-joy-wake-up-to-a-happy-text-every-day-mk8</link>
      <guid>https://dev.to/dguo/morning-joy-wake-up-to-a-happy-text-every-day-mk8</guid>
      <description>&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Morning Joy lets you start each day on a good note by texting you an uplifting news story and a picture of something cute.&lt;/p&gt;

&lt;p&gt;Following the news can be stressful or even depressing. This is one small way to remember that good things are happening too. &lt;/p&gt;

&lt;h3&gt;
  
  
  Category Submission
&lt;/h3&gt;

&lt;p&gt;Interesting Integrations&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fK9zIQt9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/pdsq2j6bh4f16ul6660l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fK9zIQt9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/pdsq2j6bh4f16ul6660l.jpg" alt="demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Link to Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--i3JOwpme--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-ba8488d21cd8ee1fee097b8410db9deaa41d0ca30b004c0c63de0a479114156f.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/dguo"&gt;
        dguo
      &lt;/a&gt; / &lt;a href="https://github.com/dguo/morning-joy"&gt;
        morning-joy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Wake up to a happy text every day!
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I signed up for Twilio and followed their &lt;a href="https://www.twilio.com/docs/sms/quickstart/node#install-nodejs-and-the-twilio-module"&gt;quickstart guide for Node.js&lt;/a&gt;. I bought a phone number and tested sending a text to myself. This was painless. I probably spent more time picking the phone number than I did writing the code.&lt;/p&gt;

&lt;p&gt;Next, I needed to find a source for the content that I can count on to be renewed on a daily basis and to be of high quality. I decided to use Reddit's &lt;a href="https://www.reddit.com/r/aww/"&gt;r/aww&lt;/a&gt; and &lt;a href="https://www.reddit.com/r/UpliftingNews/"&gt;r/UpliftingNews&lt;/a&gt; subreddits. They are two of the largest subreddits, so I should never get duplicate content. I also decided to grab the first post when sorting by top for today, which means the community has heavily upvoted the post.&lt;/p&gt;

&lt;p&gt;I looked into the best way to fetch posts from Reddit. At first I thought I would use their &lt;a href="https://www.reddit.com/dev/api/"&gt;API&lt;/a&gt;, but it &lt;a href="https://www.reddit.com/r/javascript/comments/8yg6ig/adding_json_onto_the_end_of_most_reddit_urls/"&gt;turns out&lt;/a&gt; that Reddit has a convenient feature where you can simply append &lt;code&gt;.json&lt;/code&gt; to the end of a subreddit URL to receive the data as JSON: &lt;code&gt;https://www.reddit.com/r/aww.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This worked well, but I didn't know how to sort it by top for today rather than hot. I &lt;a href="https://www.reddit.com/r/redditdev/comments/1470nj/what_is_the_json_link_for_getting_differently/"&gt;discovered&lt;/a&gt; that a query parameter does the trick: &lt;code&gt;https://www.reddit.com/r/aww/top.json?t=day&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I wrote a script to grab the top posts for both subreddits and then text them using Twilio.&lt;/p&gt;

&lt;p&gt;Lastly, I needed a way to schedule the script to run every morning. To keep it simple, I converted my script into an &lt;a href="https://aws.amazon.com/lambda/"&gt;AWS Lambda&lt;/a&gt; function and set up a CloudWatch Events rule that &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/Create-CloudWatch-Events-Scheduled-Rule.html"&gt;triggers on a schedule&lt;/a&gt; to fire the function every day at 10 a.m. UTC. The schedule expression is: &lt;code&gt;cron(0 10 * * ? *)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This was a relatively simple project, but I'm looking forward to seeing what I get every morning!&lt;/p&gt;

</description>
      <category>twiliohackathon</category>
      <category>reddit</category>
      <category>lambda</category>
    </item>
    <item>
      <title>Migrating From Authy to Bitwarden for 2FA Codes</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Wed, 25 Mar 2020 00:00:00 +0000</pubDate>
      <link>https://dev.to/dguo/migrating-from-authy-to-bitwarden-for-2fa-codes-1nch</link>
      <guid>https://dev.to/dguo/migrating-from-authy-to-bitwarden-for-2fa-codes-1nch</guid>
      <description>&lt;p&gt;I've used &lt;a href="https://authy.com/"&gt;Authy&lt;/a&gt; for several years to generate my time-based one-time passwords (&lt;a href="https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm"&gt;TOTP&lt;/a&gt;) for two-factor authentication (&lt;a href="https://en.wikipedia.org/wiki/Multi-factor_authentication"&gt;2FA&lt;/a&gt;). For various reasons, I recently migrated to using &lt;a href="https://bitwarden.com/"&gt;Bitwarden&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Authenticator Issues
&lt;/h2&gt;

&lt;p&gt;Many services recommend using &lt;a href="https://en.wikipedia.org/wiki/Google_Authenticator"&gt;Google Authenticator&lt;/a&gt; for 2FA. I originally used it before switching to Authy, but I switched for a reason that is still valid today: it doesn't have any sort of backup or syncing functionality.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&amp;amp;showAllReviews=true"&gt;reviews&lt;/a&gt; to get a sense of how often people get burned by switching to a new phone for whatever reason and realizing they've lost all their codes or need to go through each service one by one and set up 2FA again.&lt;/p&gt;

&lt;p&gt;Google Authenticator is also a neglected app. The &lt;a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"&gt;Android app&lt;/a&gt; was last updated on September 27, 2017, and the &lt;a href="https://apps.apple.com/us/app/google-authenticator/id388497605"&gt;iOS app&lt;/a&gt; was last updated on September 12, 2018. You could argue that these are relatively simple apps that don't need frequent updates, but take a look at what other apps like &lt;a href="https://play.google.com/store/apps/details?id=org.shadowice.flocke.andotp"&gt;andOTP&lt;/a&gt; and &lt;a href="https://getaegis.app/"&gt;Aegis&lt;/a&gt; offer in terms of functionality that Google Authenticator doesn't have, like being able to search for a service instead of having to scroll though the entire list to find it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authy Issues
&lt;/h2&gt;

&lt;p&gt;While I have happily used Authy for several years, I also have some issues with it that caused me to look for a replacement.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Browser Extension
&lt;/h3&gt;

&lt;p&gt;Authy doesn't have a browser extension for &lt;a href="https://www.mozilla.org/firefox/"&gt;Firefox&lt;/a&gt;, my primary browser. This is a problem because an extension can offer some protection against &lt;a href="https://en.wikipedia.org/wiki/Phishing"&gt;phishing&lt;/a&gt;, one of the main &lt;a href="https://www.scottbrady91.com/Authentication/Software-Tokens-Wont-Save-You"&gt;security weaknesses&lt;/a&gt; of using TOTP for 2FA. If the extension fails to find an entry that matches the current domain, that can alert me to a possible phishing attempt.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://chrome.google.com/webstore/detail/authy-chrome-extension/fhgenkpocbhhddlgkjnfghpjanffonno"&gt;Chrome extension&lt;/a&gt; also hasn't been updated in two and a half years and will &lt;a href="https://authy.com/blog/authy-chrome-app-extension-end-life/"&gt;no longer be supported&lt;/a&gt; going forward.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Web Client
&lt;/h3&gt;

&lt;p&gt;Authy doesn't have a web client. While this could be considered a security feature, I'd rather have the option to access my codes through any browser in an emergency. It's a security vs. usability tradeoff that I'm willing to make.&lt;/p&gt;

&lt;h3&gt;
  
  
  No CLI Client
&lt;/h3&gt;

&lt;p&gt;Authy doesn't have a &lt;a href="https://en.wikipedia.org/wiki/Command-line_interface"&gt;CLI&lt;/a&gt; client. I have some ideas for personal browser automation projects that could be easier to implement with programmatic access to my TOTP codes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mac CPU Usage
&lt;/h3&gt;

&lt;p&gt;I use the Mac desktop program, but when it has a code open, the program uses significantly more CPU. Here's the CPU usage when it's just displaying the list of services.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_mAXVVf---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/0a7vZMW.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_mAXVVf---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/0a7vZMW.png" alt="Authy desktop CPU usage during list view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's the CPU usage when it's showing the TOTP code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eKm8x1u1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/5kVBCbB.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eKm8x1u1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/5kVBCbB.png" alt="Authy desktop CPU usage during code view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since I don't want the program to unnecessarily drain my laptop battery, I try to remember to press the back button after copying the code.  There's no option to automatically go back on copy or to just copy the code from the list view without even seeing the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication and Recovery
&lt;/h3&gt;

&lt;p&gt;When you create an Authy account, you have to provide a phone number rather than an email address or username. I didn't like this to begin with since I want as few things tied to my phone number as possible, given how often phone numbers &lt;a href="https://www.wired.com/story/sim-swap-attack-defend-phone/"&gt;get hijacked&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Authy then &lt;a href="https://support.authy.com/hc/en-us/articles/360016317013-Enable-or-Disable-Authy-Multi-Device"&gt;encourages&lt;/a&gt; you to add the app to your other devices and then disable the multi-device feature. This means that your codes will keep working on your existing devices, but to add Authy to a new device, you need access to one of your old ones to temporarily re-enable multi-device and to grant access to the new device. If you don't have access to an old device, you have to go through a 24 hour &lt;a href="https://support.authy.com/hc/en-us/articles/115012672088-Restoring-Authy-Access-on-a-New-Lost-or-Inaccessible-Phone#new_app"&gt;account recovery process&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, I want to be able to regain access to my 2FA codes, even if I've lost access to all my devices. For example, I could be in a foreign country without my laptop and then lose my phone. I want to have a good contingency plan for this kind of situation.&lt;/p&gt;

&lt;p&gt;Note that Authy doesn't support an account level password. It does support a password for your encrypted backups, but you don't enter that until after you log in.&lt;/p&gt;

&lt;p&gt;Authy also doesn't support TOTP codes or &lt;a href="https://en.wikipedia.org/wiki/Universal_2nd_Factor"&gt;U2F&lt;/a&gt; security keys for protecting itself. Its sole authentication mechanism (beyond account recovery processes) is access to an old device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Yubico Authenticator
&lt;/h2&gt;

&lt;p&gt;I considered using my &lt;a href="https://www.yubico.com/"&gt;YubiKeys&lt;/a&gt; to generate TOTP codes using &lt;a href="https://www.yubico.com/products/services-software/download/yubico-authenticator/"&gt;Yubico Authenticator&lt;/a&gt;, but a YubiKey &lt;a href="https://www.reddit.com/r/yubikey/comments/ataxt3/only_32_oathtotp_entries/"&gt;can only store 32&lt;/a&gt; TOTP secrets, and I already have 49 of them since I enable TOTP-based 2FA whenever possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bitwarden
&lt;/h2&gt;

&lt;p&gt;I currently use &lt;a href="https://www.lastpass.com/"&gt;LastPass&lt;/a&gt; to manage my passwords, but I am going to switch to &lt;a href="https://1password.com/"&gt;1Password&lt;/a&gt; soon. I decided to use Bitwarden as well but solely for TOTP codes. 1Password can also &lt;a href="https://support.1password.com/one-time-passwords/"&gt;handle TOTP codes&lt;/a&gt;, but I am willing to deal with the hassle of having two password managers to avoid using the same service for both passwords and 2FA.&lt;/p&gt;

&lt;p&gt;By using a password manager for TOTP, I get broad cross-platform support with a web client, browser extensions, desktop programs, mobile apps, and even a CLI client. I also get standard authentication mechanisms, including 2FA support.&lt;/p&gt;

&lt;p&gt;This does mean that I am treating my TOTP codes more like secondary passwords (&lt;a href="https://en.wikipedia.org/wiki/Multi-factor_authentication#Knowledge_factors"&gt;something I know&lt;/a&gt;) rather than as &lt;a href="https://en.wikipedia.org/wiki/Multi-factor_authentication#Possession_factors"&gt;something I have&lt;/a&gt;. Authy's requirement to have access to an old device better fits the latter principle. This is a deliberate choice on my part.&lt;/p&gt;

&lt;p&gt;Note that Bitwarden requires a premium account that costs $10 a year in order to generate TOTP codes. A premium account also adds U2F support, which I wanted as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication Strategy
&lt;/h2&gt;

&lt;p&gt;U2F support is the last component of my authentication strategy. Going forward, it will be like this: I'll store passwords in 1Password and TOTP secrets in Bitwarden. I'll use separate, &lt;a href="https://xkcd.com/936/"&gt;high entropy&lt;/a&gt; master passwords that will only exist in my head.&lt;/p&gt;

&lt;p&gt;1Password requires a &lt;a href="https://support.1password.com/secret-key/"&gt;secret key&lt;/a&gt; in conjunction with the master password in order to log in on a new device. Since I can't memorize it, I plan to store my secret key as a &lt;a href="https://support.yubico.com/support/solutions/articles/15000006480-understanding-core-static-password-features"&gt;static password&lt;/a&gt; on my YubiKeys. This means that if I touch the metal contact for a few seconds, it will type out the secret key for me.&lt;/p&gt;

&lt;p&gt;For both services, I'll add all my YubiKeys for 2FA. This means that all I need is one of my YubiKeys (one of which is on my keychain) and the master passwords in my head to regain full access to all of my accounts.&lt;/p&gt;

&lt;p&gt;However, I can't guarantee that I'll be able to use my YubiKey on every device. For example, Bitwarden &lt;a href="https://help.bitwarden.com/article/setup-two-step-login-u2f/"&gt;doesn't support&lt;/a&gt; U2F in its mobile apps. I would also be paranoid about feeling like I need two YubiKeys when I travel in case I lose one.&lt;/p&gt;

&lt;p&gt;My plan to deal with these issues is to also set up TOTP-based 2FA for both 1Password and Bitwarden. I'll print those TOTP secrets, along with the 1Password secret key, on a small card and laminate it. I can make multiple copies to put in my wallet and my bag. Sometimes being overly prepared is fun in itself, even though it might be overkill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration
&lt;/h2&gt;

&lt;p&gt;To migrate to Bitwarden, I went through my Authy list one by one. In theory, I'd be able to just copy the TOTP secret to Bitwarden, but Authy doesn't expose the secret.&lt;/p&gt;

&lt;p&gt;For each account, I logged in and reset 2FA to add the secret to Bitwarden. Then I deleted the account from Authy. Authy marks it for deletion and then waits 48 hours before actually deleting it in case you made a mistake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MIOAOrT0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/58A2QCW.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MIOAOrT0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/58A2QCW.png" alt="Accounts marked for deletion in Authy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I did have trouble with adding some services, such as &lt;a href="https://www.algolia.com/"&gt;Algolia&lt;/a&gt; and &lt;a href="https://www.npmjs.com/"&gt;npm&lt;/a&gt;, that only show the QR code and don't have an option to display the TOTP secret. The QR codes encode URIs that look like this, as &lt;a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format"&gt;documented&lt;/a&gt; in the Google Authenticator wiki:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&amp;amp;issuer=Example 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried using my phone camera's built-in QR scanner, but I couldn't see the full URI and opening it would open Authy, with no other option. I used &lt;a href="https://lens.google.com/"&gt;Google Lens&lt;/a&gt; instead to grab the secret. In retrospect, I was only having trouble because I was adding the services to Bitwarden through the browser extension. I should have installed the mobile app from the beginning and used that because it has an option to scan QR codes.&lt;/p&gt;

&lt;p&gt;I also had trouble with adding &lt;a href="https://www.twitch.tv/"&gt;Twitch&lt;/a&gt;, which has a specific integration with Authy instead of providing a generic QR code. To get around the issue, I followed &lt;a href="https://medium.com/@dubistkomisch/set-up-2fa-two-factor-authentication-for-twitch-with-google-authenticator-or-other-totp-client-f19af32df68a"&gt;this guide&lt;/a&gt;. You can use the deprecated &lt;a href="https://chrome.google.com/webstore/detail/authy/gaedmjdfmmahhbjefcbgaolhhanlaolb/related"&gt;Authy Chrome app&lt;/a&gt; to retrieve the TOTP secrets and configurations. This method entails using Chrome's developer tools to execute &lt;a href="https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93"&gt;custom code&lt;/a&gt; to print the information.&lt;/p&gt;

&lt;p&gt;This revealed that Twitch uses 7 digit codes instead of the standard 6 and 10 second intervals instead of the standard 30.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;otpauth://totp/Twitch?secret=XXXXXXXX&amp;amp;digits=7&amp;amp;period=10 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, I thought I hit a Bitwarden limitation because I mistakenly assumed that the extension would only take the TOTP secret in the authenticator key field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--L3a6DPWH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/WU3MYY5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L3a6DPWH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/WU3MYY5.png" alt="Bitwarden add login form"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, I discovered that Bitwarden &lt;a href="https://community.bitwarden.com/t/support-totp-auth-parameters/37"&gt;supports&lt;/a&gt; putting the full URI with configuration into that field. I tested it and was able to log in to Twitch using the code generated by Bitwarden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Migrating to Bitwarden took me about a full day, but I'm happy with the result. I've been using the Bitwarden browser extension to log in to accounts for the past week, and it is much nicer than using the Authy desktop program. Next up is migrating from LastPass to 1Password.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found an error or typo? Feel free to open a pull request on &lt;a href="https://github.com/dguo/dannyguo.com/blob/master/content/blog/migrating-from-authy-to-bitwarden-for-2fa-codes.md"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>2fa</category>
      <category>authentication</category>
    </item>
    <item>
      <title>How to Add Copy to Clipboard Buttons to Code Blocks in Hugo</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Fri, 22 Mar 2019 00:00:00 +0000</pubDate>
      <link>https://dev.to/dguo/how-to-add-copy-to-clipboard-buttons-to-code-blocks-in-hugo-1331</link>
      <guid>https://dev.to/dguo/how-to-add-copy-to-clipboard-buttons-to-code-blocks-in-hugo-1331</guid>
      <description>&lt;p&gt;A small quality of life improvement for programming-related websites is to add copy to clipboard buttons to code blocks. When a visitor wants to copy a code example or a shell command, it's nice to be able to just hit a button rather than manually select the text, right click, and press copy.&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt; to build my &lt;a href="https://www.dannyguo.com/" rel="noopener noreferrer"&gt;personal website&lt;/a&gt;. While Hugo has built-in support for &lt;a href="https://gohugo.io/content-management/syntax-highlighting/" rel="noopener noreferrer"&gt;syntax highlighting&lt;/a&gt;, it doesn't support copy buttons. Here is how I added the feature to my website. The end result looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2F8SBpdIT.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2F8SBpdIT.png" alt="code block with a copy button"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the buttons
&lt;/h2&gt;

&lt;p&gt;I inspected the source of a page with code blocks and found that Hugo generates each block with markup like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"highlight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code blocks without syntax highlighting have the same structure but without the surrounding highlight div. To account for both cases, I selected for code elements that are children of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre" rel="noopener noreferrer"&gt;pre&lt;/a&gt; elements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pre &amp;gt; code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;codeBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy-code-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;pre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;codeBlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;highlight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;highlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For many implementations of copy code buttons that I've seen, the button is located in the top right or bottom right corner of the code block. However, I've noticed that the button can cover up some of the code if the line is too long, especially on mobile. To avoid this possibility, I placed each button before the entire code block.&lt;/p&gt;

&lt;p&gt;Some implementations only show the button when the user hovers over the code block, but for discoverability, I left the buttons always visible.&lt;/p&gt;

&lt;p&gt;For styling the buttons, I used this CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.copy-code-button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#272822&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#FFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#272822&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c"&gt;/* right-align */&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.copy-code-button&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#F2F2F2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.copy-code-button&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;/* Avoid an ugly focus outline on click in Chrome,
       but darken the button for accessibility.
       See https://stackoverflow.com/a/25298082/1481479 */&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#E6E6E6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.copy-code-button&lt;/span&gt;&lt;span class="nd"&gt;:active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#D9D9D9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.highlight&lt;/span&gt; &lt;span class="nt"&gt;pre&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;/* Avoid pushing up the copy buttons. */&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interacting with the clipboard
&lt;/h2&gt;

&lt;p&gt;Next, I investigated how to &lt;a href="https://stackoverflow.com/q/400212/1481479" rel="noopener noreferrer"&gt;copy to the clipboard using JavaScript&lt;/a&gt;. The most popular library for doing so is &lt;a href="https://clipboardjs.com/" rel="noopener noreferrer"&gt;clipboard.js&lt;/a&gt;, but I wanted to avoid bringing in a dependency if possible.&lt;/p&gt;

&lt;p&gt;One way is to use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand" rel="noopener noreferrer"&gt;execCommand&lt;/a&gt; with &lt;code&gt;document.execCommand('copy')&lt;/code&gt;, which copies the current text selection. &lt;a href="https://github.com/zenorocha/clipboard.js" rel="noopener noreferrer"&gt;Under the hood&lt;/a&gt;, clipboard.js uses this method.&lt;/p&gt;

&lt;p&gt;However, there is a newer approach, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API" rel="noopener noreferrer"&gt;Clipboard API&lt;/a&gt;. It has &lt;a href="https://developers.google.com/web/updates/2018/03/clipboardapi" rel="noopener noreferrer"&gt;several advantages&lt;/a&gt;: it's asynchronous, takes arbitrary text/data (so it doesn't have to already exist on the page), and has a better story for dealing with permissions. Chrome, Firefox, and Opera &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#Browser_compatibility" rel="noopener noreferrer"&gt;support it&lt;/a&gt; already. For other browsers, there is a &lt;a href="https://github.com/lgarron/clipboard-polyfill" rel="noopener noreferrer"&gt;polyfill&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I put the code in a function and added click handlers. I used &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText" rel="noopener noreferrer"&gt;innerText&lt;/a&gt; to get the code to be copied. After the copy operation, the button displays either an error message or a success message that lasts for two seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addCopyButtons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pre &amp;gt; code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;codeBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy-code-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;codeBlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="cm"&gt;/* Chrome doesn't seem to blur automatically,
                   leaving the button in a focused state. */&lt;/span&gt;
                &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

                &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Copied!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

                &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;pre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;codeBlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;highlight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;highlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I added a check for whether or not the browser supports the Clipboard API. If not, the script loads the polyfill from &lt;a href="https://cdnjs.com/libraries/clipboard-polyfill" rel="noopener noreferrer"&gt;CDNJS&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;addCopyButtons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdnjs.cloudflare.com/ajax/libs/clipboard-polyfill/2.7.0/clipboard-polyfill.promise.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;integrity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256-waClS2re9NUbXRsryKoof+F9qc1gjjIhc2eT7ZbIv94=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;addCopyButtons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;After the Clipboard API becomes ubiquitous, I'll remove the polyfill code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart loading with Hugo
&lt;/h2&gt;

&lt;p&gt;After I got the functionality to work, I thought about how to include the script. I had three options. The first was to indiscriminately include it on every page. The script is small, but for optimization, I wanted to only include it when it's actually needed, saving a bit of bandwidth and a network request (or two, if the polyfill is needed).&lt;/p&gt;

&lt;p&gt;The second option was to use a &lt;a href="https://gohugo.io/content-management/front-matter/#user-defined" rel="noopener noreferrer"&gt;custom Hugo front matter variable&lt;/a&gt;. With this method, I'd set a flag on every post that has a code block.  The template could then check for this flag. However, this approach involves manual work and runs the risk of me forgetting to do it.&lt;/p&gt;

&lt;p&gt;The third option was to find a way to use Hugo to figure out which pages have at least one code block. A regex seemed like the way to go. I used Hugo's &lt;a href="https://gohugo.io/functions/findre/" rel="noopener noreferrer"&gt;findRE&lt;/a&gt; function to determine if the HTML seems to contain a &lt;code&gt;pre&lt;/code&gt; element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;findRE&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;pre"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/js/copy-code-button.js"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I passed it a limit parameter of &lt;code&gt;1&lt;/code&gt; because I only care if the page &lt;em&gt;has&lt;/em&gt; a code block or not, not the total number of code blocks.&lt;/p&gt;

&lt;p&gt;Keep in mind that this script should be loaded after the page content, preferably at the end of the body so that it doesn't block rendering. Otherwise, the selector might run before the code blocks actually exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Non-Hugo websites
&lt;/h2&gt;

&lt;p&gt;This solution should easily work for non-Hugo websites as well. The only part of the script that is specific to Hugo is the &lt;code&gt;pre &amp;gt; code&lt;/code&gt; selector. Modifying the selector and possibly where the button is inserted should be all that is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  CodeCopy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/zenorocha/codecopy" rel="noopener noreferrer"&gt;CodeCopy&lt;/a&gt; is a browser extension for &lt;a href="https://chrome.google.com/webstore/detail/codecopy/fkbfebkcoelajmhanocgppanfoojcdmg" rel="noopener noreferrer"&gt;Chrome&lt;/a&gt; and &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/codecopy/" rel="noopener noreferrer"&gt;Firefox&lt;/a&gt; that adds copy buttons to code blocks on many websites that are likely to have them, such as &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and &lt;a href="https://stackoverflow.com/" rel="noopener noreferrer"&gt;Stack Overflow&lt;/a&gt;. It's made by the &lt;a href="https://zenorocha.com/" rel="noopener noreferrer"&gt;same person&lt;/a&gt; behind clipboard.js.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found an error or typo? Feel free to open a pull request on &lt;a href="https://github.com/dguo/dannyguo.com/blob/master/content/blog/how-to-add-copy-to-clipboard-buttons-to-code-blocks-in-hugo.md" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>hugo</category>
      <category>webdev</category>
      <category>ux</category>
    </item>
    <item>
      <title>Animated Multiline Link Underlines with CSS</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Thu, 20 Dec 2018 21:33:06 +0000</pubDate>
      <link>https://dev.to/dguo/animated-multiline-link-underlines-with-css-hk0</link>
      <guid>https://dev.to/dguo/animated-multiline-link-underlines-with-css-hk0</guid>
      <description>&lt;p&gt;One benefit of building my &lt;a href="https://www.dannyguo.com/" rel="noopener noreferrer"&gt;personal website&lt;/a&gt; from scratch instead of using a &lt;a href="https://themes.gohugo.io/" rel="noopener noreferrer"&gt;theme&lt;/a&gt; made by someone else is that I can start from the browser's defaults and gradually add my own flourishes. I strive to keep my site lean, but making it personal is also kind of the point. There is a spectrum of gratuitous touches between the spartan pages of &lt;a href="https://news.ycombinator.com/" rel="noopener noreferrer"&gt;Hacker News&lt;/a&gt; and &lt;a href="https://newyork.craigslist.org/" rel="noopener noreferrer"&gt;Craigslist&lt;/a&gt; on one end and the sensory overload of &lt;a href="https://news.codecademy.com/myspace-coding-legacy/" rel="noopener noreferrer"&gt;old MySpace&lt;/a&gt; on the other.&lt;/p&gt;

&lt;p&gt;I ran across a site that had fancy, animated underlines for links, and I wanted to add a similar effect to my site. It was important to me to use a pure CSS solution. For something as frivolous as this, I didn't want to add JavaScript that has a risk of causing a performance issue or frustrating behavior (see &lt;a href="https://envato.com/blog/scroll-hijacking/" rel="noopener noreferrer"&gt;scroll hijacking&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Here's what the effect looks like now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fyf8as7sn1ovzkdp2zx5x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fyf8as7sn1ovzkdp2zx5x.gif" alt="preview of the underline effect"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The topic of drawing lines under text on the web can be &lt;a href="https://medium.design/crafting-link-underlines-on-medium-7c03a9274f9" rel="noopener noreferrer"&gt;surprisingly complicated&lt;/a&gt;, depending on how far you are willing to &lt;a href="https://css-tricks.com/styling-underlines-web/" rel="noopener noreferrer"&gt;stray&lt;/a&gt; from &lt;code&gt;text-decoration: underline&lt;/code&gt; and which details (like &lt;a href="https://stackoverflow.com/q/40008990/1481479" rel="noopener noreferrer"&gt;clearing descenders&lt;/a&gt;) you care about.&lt;/p&gt;

&lt;p&gt;I investigated a few approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="http://tobiasahlin.com/blog/css-trick-animating-link-underlines/" rel="noopener noreferrer"&gt;Animating Link Underlines&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.cssportal.com/blog/css-animated-underline-links/" rel="noopener noreferrer"&gt;CSS Animated Underline Links&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both of them essentially remove the default &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration" rel="noopener noreferrer"&gt;text-decoration&lt;/a&gt; and add a simulated border using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements" rel="noopener noreferrer"&gt;pseudo-elements&lt;/a&gt;. The border is then animated by &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions" rel="noopener noreferrer"&gt;CSS transitions&lt;/a&gt;. Unfortunately, these solutions have one drawback: they don't work properly if the link spans more than one line. The underline only appears on the first line.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fx6a51n5ai9i0ow83c95x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fx6a51n5ai9i0ow83c95x.gif" alt="broken for multiline links"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I eventually found a &lt;a href="https://codepen.io/shshaw/pen/pdyJBW" rel="noopener noreferrer"&gt;CodePen&lt;/a&gt; by &lt;a href="https://twitter.com/shshaw" rel="noopener noreferrer"&gt;Shaw&lt;/a&gt; that doesn't have this flaw. I modified the CSS until I got to a solution that looks good to me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F9m2i9sjm9g43mxbn387g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F9m2i9sjm9g43mxbn387g.gif" alt="working for multiline links"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the relevant code. You can use &lt;a href="https://repl.it/@dyguo/animated-multiline-link-underlines" rel="noopener noreferrer"&gt;this repl&lt;/a&gt; to play around with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;background-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-repeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;no-repeat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;background-size&lt;/span&gt; &lt;span class="m"&gt;.3s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go through this approach part by part. First, we turn off the default &lt;code&gt;text-decoration&lt;/code&gt; for links.&lt;/p&gt;

&lt;p&gt;We want to use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/background-image" rel="noopener noreferrer"&gt;background-image&lt;/a&gt; because it can span multiple lines. While we could supply an actual image, we only want to draw a line, so we use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient" rel="noopener noreferrer"&gt;linear-gradient&lt;/a&gt;, which generates an image for us. Normally, it's used to create a gradient between two different colors, but I want the underline to just be the same color as the link, so I use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#currentColor_keyword" rel="noopener noreferrer"&gt;currentColor&lt;/a&gt; for both the beginning and the end of the gradient. &lt;code&gt;currentColor&lt;/code&gt; tells the browser to use the element's computed &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color" rel="noopener noreferrer"&gt;color&lt;/a&gt; property.&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/background-position" rel="noopener noreferrer"&gt;background-position&lt;/a&gt; to place the image in the bottom left corner. &lt;code&gt;0%&lt;/code&gt; sets the horizontal position, and &lt;code&gt;100%&lt;/code&gt; sets the vertical position.&lt;/p&gt;

&lt;p&gt;We turn off &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat" rel="noopener noreferrer"&gt;background-repeat&lt;/a&gt; to prevent it from creating multiple instances of the image to fill the entire background of the link.&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/background-size" rel="noopener noreferrer"&gt;background-size&lt;/a&gt; to make the image zero pixels wide and two pixels tall. It has zero width because the underline should only appear on hover.&lt;/p&gt;

&lt;p&gt;We set a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/transition" rel="noopener noreferrer"&gt;transition&lt;/a&gt; on &lt;code&gt;background-size&lt;/code&gt;, so any change to the property will take &lt;code&gt;0.3&lt;/code&gt; seconds to complete.&lt;/p&gt;

&lt;p&gt;On link &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:hover" rel="noopener noreferrer"&gt;hover&lt;/a&gt;, we change the width of the image to &lt;code&gt;100%&lt;/code&gt;, creating a full underline, and &lt;code&gt;transition&lt;/code&gt; takes care of the animation. As &lt;a href="https://www.reddit.com/r/web_design/comments/a7y701/animated_multiline_link_underlines_with_css/ec6pwel/" rel="noopener noreferrer"&gt;nickels55 suggests&lt;/a&gt;, we also want the effect to happen on focus for those who use keyboard navigation. Thanks to nickels55 for keeping &lt;a href="https://en.wikipedia.org/wiki/Web_accessibility" rel="noopener noreferrer"&gt;accessibility&lt;/a&gt; in mind.&lt;/p&gt;

&lt;p&gt;And that's it! I was very happy with how small the &lt;a href="https://github.com/dguo/dannyguo.com/commit/14e51391329163fa414ac55d77fdf6da521ab644" rel="noopener noreferrer"&gt;commit&lt;/a&gt; ended up being. If you'd like to add something similar to your site, feel free to take this implementation, or check out some other &lt;a href="https://speckyboy.com/underline-text-effects-css/" rel="noopener noreferrer"&gt;animated underline effects&lt;/a&gt; for inspiration.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.dannyguo.com/blog/animated-multiline-link-underlines-with-css/" rel="noopener noreferrer"&gt;www.dannyguo.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found an error or typo? Feel free to open a pull request on &lt;a href="https://github.com/dguo/dannyguo.com/blob/master/content/blog/animated-multiline-link-underlines-with-css.md" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
    </item>
    <item>
      <title>Using Mailgun for a Free Custom Domain Email Address</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Fri, 07 Dec 2018 23:37:37 +0000</pubDate>
      <link>https://dev.to/dguo/using-mailgun-for-a-free-custom-domain-email-address-fom</link>
      <guid>https://dev.to/dguo/using-mailgun-for-a-free-custom-domain-email-address-fom</guid>
      <description>&lt;p&gt;I've been using &lt;a href="https://www.google.com/gmail/about/" rel="noopener noreferrer"&gt;Gmail&lt;/a&gt; for my primary email address for over 13 years. I should thank my sister for making me use my name and not my now embarrassing &lt;a href="https://en.wikipedia.org/wiki/AIM_(software)" rel="noopener noreferrer"&gt;AIM&lt;/a&gt; username. Here's a look at the welcome email back in 2005. Gmail wasn't even a year old yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FVRHg6jX.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FVRHg6jX.png" alt="Gmail welcome email in 2005"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gmail has served me well since then, but mostly for fun (and partly because of &lt;a href="https://theoatmeal.com/comics/email_address" rel="noopener noreferrer"&gt; vanity&lt;/a&gt;), I recently set up an email address on &lt;a href="https://dannyguo.com" rel="noopener noreferrer"&gt;my own domain&lt;/a&gt;. I ended up going with a free solution that involves &lt;a href="https://www.mailgun.com/" rel="noopener noreferrer"&gt;Mailgun&lt;/a&gt; and is good enough for my purposes. I wanted to still use my Gmail account to actually receive and send the custom domain emails. It isn't important to me right now to actually have a separate inbox or to stop using Google for email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Options
&lt;/h2&gt;

&lt;p&gt;Several years ago, a common choice was Google Apps (now known as &lt;a href="https://gsuite.google.com/" rel="noopener noreferrer"&gt;G Suite&lt;/a&gt;) because it was free for up to five users.  G Suite &lt;a href="https://support.google.com/a/answer/2855120" rel="noopener noreferrer"&gt;got rid&lt;/a&gt; of the free edition and &lt;a href="https://gsuite.google.com/pricing.html" rel="noopener noreferrer"&gt;costs&lt;/a&gt; $5 per month now, so I investigated alternatives.&lt;/p&gt;

&lt;p&gt;I've read about &lt;a href="https://www.zoho.com/mail/" rel="noopener noreferrer"&gt;Zoho Mail&lt;/a&gt; as a free alternative.  Unfortunately, it stopped offering &lt;a href="https://en.wikipedia.org/wiki/Post_Office_Protocol" rel="noopener noreferrer"&gt;POP&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol" rel="noopener noreferrer"&gt;IMAP&lt;/a&gt; access for its free plan &lt;a href="https://help.zoho.com/portal/community/topic/zoho-free-tier-pop-imap-activesync-no-longer-free" rel="noopener noreferrer"&gt;earlier this year&lt;/a&gt;, so the only way to access my email would be through Zoho's website or apps. It would &lt;a href="https://www.zoho.com/workplace/pricing.html" rel="noopener noreferrer"&gt;cost&lt;/a&gt; $3 per month to add POP and IMAP support. I don't fault Zoho for making this change, but I would prefer to access all my email through my Gmail account. I have also read some anecdotes that Zoho Mail can be slow in sending and receiving emails.&lt;/p&gt;

&lt;p&gt;Next, I looked at &lt;a href="https://www.fastmail.com/" rel="noopener noreferrer"&gt;FastMail&lt;/a&gt; and &lt;a href="https://protonmail.com/" rel="noopener noreferrer"&gt;ProtonMail&lt;/a&gt;, which are frequently cited by people who have chosen to &lt;a href="https://nomoregoogle.com/" rel="noopener noreferrer"&gt;avoid Google services&lt;/a&gt;. FastMail &lt;a href="https://www.fastmail.com/pricing/" rel="noopener noreferrer"&gt;costs&lt;/a&gt; $5 per month or $50 per year for a plan that includes custom domain support. ProtonMail &lt;a href="https://protonmail.com/support/knowledge-base/custom-domain-support/" rel="noopener noreferrer"&gt;requires a paid plan&lt;/a&gt; for the feature, and the cheapest &lt;a href="https://protonmail.com/pricing" rel="noopener noreferrer"&gt;paid plan&lt;/a&gt; is $5 per month or $48 per year.&lt;/p&gt;

&lt;p&gt;Another option was to use &lt;a href="https://domains.google/" rel="noopener noreferrer"&gt;Google Domains&lt;/a&gt;. It includes a free &lt;a href="https://support.google.com/domains/answer/3251241" rel="noopener noreferrer"&gt;email forwarding feature&lt;/a&gt; that allows for sending an email from the alias as well. However, the feature is only available when you &lt;a href="https://support.google.com/domains/answer/3251241" rel="noopener noreferrer"&gt;use Google's name servers&lt;/a&gt;. My site runs on &lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt;, and I use their &lt;a href="https://www.netlify.com/docs/dns/" rel="noopener noreferrer"&gt;DNS service&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I also looked at mailboxes from other domain registrars.  &lt;a href="https://www.gandi.net" rel="noopener noreferrer"&gt;Gandi&lt;/a&gt; includes &lt;a href="https://www.gandi.net/en/domain/email" rel="noopener noreferrer"&gt;two mailboxes&lt;/a&gt; with every domain, but Gandi's domain prices also &lt;a href="https://tld-list.com/" rel="noopener noreferrer"&gt;seem more expensive&lt;/a&gt; than elsewhere, especially when considering that &lt;a href="https://www.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;'s &lt;a href="https://blog.cloudflare.com/cloudflare-registrar/" rel="noopener noreferrer"&gt;at-cost domain registrar service&lt;/a&gt; is scheduled to be available soon.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.namecheap.com" rel="noopener noreferrer"&gt;Namecheap&lt;/a&gt; has very affordable &lt;a href="https://www.namecheap.com/hosting/email.aspx" rel="noopener noreferrer"&gt;email hosting&lt;/a&gt; that only costs $9.88 a year. I was ready to set it up when I stumbled upon one last solution, deep in search results for "custom domain email." Some people use &lt;a href="https://www.mailgun.com/" rel="noopener noreferrer"&gt;Mailgun&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mailgun
&lt;/h2&gt;

&lt;p&gt;Essentially, incoming emails go to Mailgun. With a routing rule, I can forward those emails to my Gmail, and with some additional configuration, I can also send emails from my Gmail but have them appear as if they are sent from my custom domain.&lt;/p&gt;

&lt;p&gt;Mailgun's &lt;a href="https://www.mailgun.com/pricing" rel="noopener noreferrer"&gt;free tier&lt;/a&gt; is way more than enough.  It allows for 10,000 emails per month. That includes both incoming and outgoing emails, but for a personal email account, I'll never even come close to that limit.  Even if I do manage to go over, it only costs $0.50 for every additional thousand emails.&lt;/p&gt;

&lt;p&gt;The plan does restrict you from sending to email addresses that you haven't explicitly added to an "authorized recipients" list. However, Mailgun lifts this restriction if you provide a payment method, which puts you on the "concept" &lt;a href="https://help.mailgun.com/hc/en-us/articles/203068914-What-are-the-differences-between-the-free-and-concept-plans-" rel="noopener noreferrer"&gt;plan&lt;/a&gt;.  I was happy to do so.&lt;/p&gt;

&lt;p&gt;Do note that Mailgun &lt;a href="https://help.mailgun.com/hc/en-us/articles/203306710-Can-I-use-Mailgun-for-my-personal-email-address-" rel="noopener noreferrer"&gt;does not officially recommend&lt;/a&gt; using the service for personal email, but they do not forbid it. They recommend using hosted email services like G Suite instead, which I would definitely do for something like a business.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Instructions
&lt;/h3&gt;

&lt;p&gt;Here are the step by step instructions. &lt;a href="https://signup.mailgun.com/" rel="noopener noreferrer"&gt;Sign up&lt;/a&gt; for a Mailgun account, and provide a payment method unless you're okay with the sending restriction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FiF4KJKP.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FiF4KJKP.png" alt="Mailgun's payment info form"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go the "&lt;a href="https://app.mailgun.com/app/domains" rel="noopener noreferrer"&gt;Domains&lt;/a&gt;" tab to &lt;a href="https://app.mailgun.com/app/domains/new" rel="noopener noreferrer"&gt;add a new domain&lt;/a&gt;. The page recommends using a subdomain, like "mail.yourdomain.com" instead of just "yourdomain.com". The reason for this recommendation is that in general, you don't want to risk hurting your root domain's &lt;a href="https://www.sparkpost.com/resources/email-explained/email-sender-reputation/" rel="noopener noreferrer"&gt;email sender reputation&lt;/a&gt;.  Companies will sometimes &lt;a href="https://help.mailgun.com/hc/en-us/articles/202256730-How-do-I-pick-a-domain-name-for-my-Mailgun-account-" rel="noopener noreferrer"&gt;use different domains or subdomains&lt;/a&gt; for sending transactional versus marketing emails.&lt;/p&gt;

&lt;p&gt;For the purpose of a personal email address, use the root domain if you want to receive email there (&lt;a href="mailto:ben@yourdomain.com"&gt;ben@yourdomain.com&lt;/a&gt;) rather than at a subdomain (&lt;a href="mailto:ben@mail.yourdomain.com"&gt;ben@mail.yourdomain.com&lt;/a&gt;). The risk of harming your sender reputation is much lower because you presumably aren't going to use your personal email to email tons of strangers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FNnkx9eP.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FNnkx9eP.png" alt="Mailgun's add domain page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, go to wherever you manage your website's DNS settings (like your domain registrar), and add the DNS records provided by Mailgun. The &lt;a href="https://en.wikipedia.org/wiki/MX_record" rel="noopener noreferrer"&gt;MX records&lt;/a&gt; are for setting Mailgun's servers as the recipient for emails. The &lt;a href="https://en.wikipedia.org/wiki/TXT_record" rel="noopener noreferrer"&gt;TXT records&lt;/a&gt; are for setting up &lt;a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework" rel="noopener noreferrer"&gt;SPF&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail" rel="noopener noreferrer"&gt;DKIM&lt;/a&gt;, which are two ways for recipients to verify an email's authenticity. You don't need to add the &lt;a href="https://en.wikipedia.org/wiki/CNAME_record" rel="noopener noreferrer"&gt;CNAME record&lt;/a&gt; unless you want to track what your recipients do with your emails, like opening them and clicking links inside them.&lt;/p&gt;

&lt;p&gt;It might take a while for the DNS changes to propogate, though mine only took about a minute. Tell Mailgun to check the DNS records. Once they are verified, you can go to the "&lt;a href="https://app.mailgun.com/app/routes" rel="noopener noreferrer"&gt;Routes&lt;/a&gt;" tab to &lt;a href="https://app.mailgun.com/app/routes/new" rel="noopener noreferrer"&gt;create&lt;/a&gt; a &lt;a href="https://mailgun-documentation.readthedocs.io/en/latest/api-routes.html#routes" rel="noopener noreferrer"&gt;routing rule&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Change the expression type to "Match Recipient," and set the custom domain address that you want to use. Then check the "Forward" action, and set the address where you actually want to receive the emails (like a Gmail account). You could set up more sophisticated rules, or even add additional addresses that all forward to the same email address.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2Fk39ZI2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2Fk39ZI2e.png" alt="Mailgun's create route page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, receiving emails should work. Go ahead, and try it out. I also created a &lt;a href="https://support.google.com/mail/answer/118708" rel="noopener noreferrer"&gt;Gmail label&lt;/a&gt;, and set up a &lt;a href="https://support.google.com/mail/answer/6579" rel="noopener noreferrer"&gt;filter&lt;/a&gt; so that all emails from my custom domain get tagged with it, making it easy for me to view only those emails if I want to.&lt;/p&gt;

&lt;p&gt;To make sending work, you'll need some additional information from Mailgun. Go back to the domains tab, and open your domain. You'll need the values for "SMTP Hostname," "Default SMTP Login," and "Default Password."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FDlcFo5y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FDlcFo5y.png" alt="Mailgun's domain information page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go to Gmail settings, and open the "Accounts and Import" tab. In the "Send mail as" section, add the custom domain email address. I unchecked the "Treat as an alias" option because I want to automatically reply with whichever address is the intended recipient. I don't want to have to manually change the sender. Normally, &lt;a href="https://support.google.com/a/answer/1710338" rel="noopener noreferrer"&gt;unchecking the option&lt;/a&gt; means that emails sent to the other address won't show up in Gmail, but in this case, Mailgun is handling that part.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FoRGTT8R.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FoRGTT8R.png" alt="Gmail's add email address popup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go to the next step, and fill in the info from Mailgun. The Mailgun "SMTP Hostname" corresponds to the Gmail "SMTP Server" field, and the "Default SMTP Login"" corresponds to the "Username" field. Leave the SMTP port as 587, and make sure to fill in the password.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FkDjArk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FkDjArk5.png" alt="Gmail's SMTP info popup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press the "Add Account" button, and you should get a confirmation email from Gmail that contains a link you can use to verify the request. At this point, sending emails should work too! When you compose an email, you can click on the "From" field to change who you are sending as.&lt;/p&gt;

&lt;p&gt;Try it out. Your recipient should receive the email, and the email details should have both the "mailed-by" and "signed-by" fields set to your custom domain. From their perspective, they don't know that you actually used Gmail to send the email.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FzLYaRvL.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FzLYaRvL.png" alt="Gmail email details"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One last thing you can do is to set a monthly sending limit with your Mailgun account. The option is in your &lt;a href="https://app.mailgun.com/app/account/settings" rel="noopener noreferrer"&gt;account settings&lt;/a&gt;. Look for the "Custom Message Limit" field. Just in case someone manages to get access to your Gmail and tries to use your custom domain address to send email, Mailgun will stop processing emails once the limit is hit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;If Mailgun does change in a way that makes it an unreasonable solution, I will probably switch to a Namecheap or ProtonMail mailbox. Until then, I'm happy to have found a free solution for a custom domain email address.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.dannyguo.com/blog/using-mailgun-for-a-free-custom-domain-email-address/" rel="noopener noreferrer"&gt;www.dannyguo.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found an error or typo? Feel free to open a pull request on &lt;a href="https://github.com/dguo/dannyguo.com/blob/master/content/blog/using-mailgun-for-a-free-custom-domain-email-address.md" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>email</category>
    </item>
    <item>
      <title>I Published My AWS Secret Key to GitHub</title>
      <dc:creator>Danny Guo</dc:creator>
      <pubDate>Wed, 15 Aug 2018 00:00:00 +0000</pubDate>
      <link>https://dev.to/dguo/i-published-my-aws-secret-key-to-github-44fk</link>
      <guid>https://dev.to/dguo/i-published-my-aws-secret-key-to-github-44fk</guid>
      <description>&lt;p&gt;One of my earliest programming projects was a basic news headlines aggregator that would display the top three headlines for each news source. I called it &lt;a href="https://dailylore.com/"&gt;The Daily Lore&lt;/a&gt;. The &lt;a href="https://www.dailylore.com/legacy/"&gt;original site&lt;/a&gt; was &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html"&gt;hosted on AWS S3 as a static website&lt;/a&gt;.  At the time, I didn't know how to make a dynamic, server-generated website, so my hacky solution for updating the headlines was to run a Python script that would generate a JavaScript file with the headlines hardcoded inside. The script then updated the JS file in the S3 bucket. To automate the updates, I ran the script on a micro EC2 instance in a cron job. Crucially, that means the script needed an AWS secret key.&lt;/p&gt;

&lt;p&gt;Eventually, AWS terminated my instance for some reason, and I abandoned the website for a while. I revived it a few years later with a &lt;a href="https://github.com/dguo/dailylore"&gt;new repo&lt;/a&gt;. Last month, I was cleaning up my Google Drive when I came across the old source code. I had stored the &lt;code&gt;.git&lt;/code&gt; folder as well, so I decided to push the &lt;a href="https://github.com/dguo/headlines"&gt;repo&lt;/a&gt; up to GitHub just for fun.&lt;/p&gt;

&lt;p&gt;A few minutes later, I got an email:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Amazon Web Services has opened case ******** on your behalf.&lt;/p&gt;

&lt;p&gt;The details of the case are as follows:&lt;/p&gt;

&lt;p&gt;Case ID: ********&lt;/p&gt;

&lt;p&gt;Subject: Your AWS account ******** is compromised&lt;/p&gt;

&lt;p&gt;Severity: Urgent&lt;/p&gt;

&lt;p&gt;Correspondence: Dear AWS Customer,&lt;/p&gt;

&lt;p&gt;Your AWS Account is compromised! Please review the following notice and take&lt;br&gt;
immediate action to secure your account.&lt;/p&gt;

&lt;p&gt;Your security is important to us. We have become aware that the AWS Access&lt;br&gt;
Key ******** along with the corresponding Secret Key is publicly available&lt;br&gt;
online at ********.&lt;/p&gt;

&lt;p&gt;This poses a security risk to your account and other users, could lead to&lt;br&gt;
excessive charges from unauthorized activity or abuse, and violates the AWS&lt;br&gt;
Customer Agreement.&lt;/p&gt;

&lt;p&gt;Please delete the exposed credentials from your AWS account by using the&lt;br&gt;
instructions below and take steps to prevent any new credentials from being&lt;br&gt;
published in this manner again. Unfortunately, deleting the keys from the&lt;br&gt;
public website and/or disabling them is NOT sufficient to secure your&lt;br&gt;
account.&lt;/p&gt;

&lt;p&gt;To additionally protect your account from excessive charges, we have&lt;br&gt;
temporarily limited your ability to create some AWS resources. Please note&lt;br&gt;
that this does not make your account secure, it just partially limits the&lt;br&gt;
unauthorized usage for which you could be charged.&lt;/p&gt;

&lt;p&gt;Detailed instructions are included below for your convenience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Immediate actions
&lt;/h2&gt;

&lt;p&gt;First, I let out a loud groan. I immediately knew the consequences of what I had done. I've read &lt;a href="https://medium.com/@nagguru/exposing-your-aws-access-keys-on-github-can-be-extremely-costly-a-personal-experience-960be7aad039"&gt;plenty of stories&lt;/a&gt; of nefarious people &lt;a href="https://www.theregister.co.uk/2015/01/06/dev_blunder_shows_github_crawling_with_keyslurping_bots/"&gt;scraping AWS keys&lt;/a&gt; and spinning up tons of EC2 instances for purposes like cryptocurrency mining.&lt;/p&gt;

&lt;p&gt;I deleted the GitHub repo, but like the email cautions, I knew that wasn't enough. Once a secret is compromised once, it's compromised forever. I went into the AWS console and invalidated the key. Luckily, this was on my personal AWS account, and nothing else depended on that key. I got another AWS email a few minutes later confirming that the compromised key had been deleted.&lt;/p&gt;

&lt;p&gt;Then I went through each AWS region to make sure nothing suspicious was present, like newly created servers or IAM users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning up and republishing
&lt;/h2&gt;

&lt;p&gt;I still wanted to publish the repo, but I also wanted to scrub the secrets.  Even though they were no longer usable, I didn't want anyone to see them in the future and get the impression that committing secrets is okay. It wouldn't be enough to just remove them in a new commit because they would still be in the Git history. This part was the silver lining because I knew about &lt;a href="https://rtyley.github.io/bfg-repo-cleaner/"&gt;the BFG&lt;/a&gt; (I assume the name is a reference to the &lt;a href="https://en.wikipedia.org/wiki/BFG_%28weapon%29"&gt;weapon&lt;/a&gt; in &lt;a href="https://en.wikipedia.org/wiki/Doom_(1993_video_game)"&gt;Doom&lt;/a&gt;, but I'm not sure) but had never gotten a chance to use it before. It made it very easy to remove the secrets from the entire history.&lt;/p&gt;

&lt;p&gt;I did a more thorough inspection and found other things in the repo that shouldn't have ever been committed. There were &lt;code&gt;.pyc&lt;/code&gt; files. There was a PDF explaining how SVGs work. There was an Excel spreadsheet containing passwords for services that I used for the site. At the time, I wasn't using a password manager. Fortunately, I had changed all the passwords when I did start using a password manager, so I didn't need to do so again. Lastly, there was even a SSH private key for logging into the EC2 instance, and a script for doing so that contained the host.&lt;/p&gt;

&lt;p&gt;I used the BFG to remove all of these files. I double checked to make sure it worked as expected, and then I re-pushed the repo to GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrospective
&lt;/h2&gt;

&lt;p&gt;I should have done a manual inspection of the files before pushing. I also should have used a tool that is designed to find secrets in Git repos. There are several of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/awslabs/git-secrets"&gt;git-secrets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/zricethezav/gitleaks"&gt;gitleaks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dxa4481/truffleHog"&gt;Truffle Hog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I should have distrusted my past self. While it's secondhand nature to me now to use environment variables for secrets, to think about how they can be leaked, and to be aware of which files should and shouldn't be committed to Git, I forgot that at one point, I didn't know any of that. Lesson learned.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found an error or typo? Feel free to open a pull request on &lt;a href="https://github.com/dguo/dannyguo.com/blob/master/content/blog/I-published-my-aws-secret-key-to-github.md"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
    </item>
  </channel>
</rss>
