<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  
  <title>Graham F. Scott: Blog</title>
  <subtitle>Personal blog for Graham F. Scott</subtitle>
  <link href="https://gfscott.com/blog/feed.xml" rel="self" />
  <link href="https://gfscott.com/blog/" />
  <updated>2026-01-01T18:00:00Z</updated>
  <id>https://gfscott.com/blog/</id>
  <author>
    <name>Graham F. Scott</name>
  </author>
  <entry>
    <title>The books I read in 2025</title>
    <link href="https://gfscott.com/blog/books-2025/" />
    <updated>2026-01-01T18:00:00Z</updated>
    <id>https://gfscott.com/blog/books-2025/</id>
    <content type="html">&lt;p&gt;It wasn&#39;t my intent, but quite a few of these turned out to be about work, labour, and power. In every sense of the word, folks were on the hustle, either head-on as in &lt;em&gt;The Hammer&lt;/em&gt;, &lt;em&gt;Offshore&lt;/em&gt;, and &lt;em&gt;The Soul of a New Machine&lt;/em&gt;, or more obliquely, as in Niko Stratis&#39;s &lt;em&gt;The Dad Rock That Made Me a Woman&lt;/em&gt; or Colson Whitehead&#39;s &lt;em&gt;Crook Manifesto&lt;/em&gt;.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;Ancillary Mercy&lt;/em&gt;, by Ann Leckie&lt;/li&gt;&lt;li&gt;&lt;em&gt;Ancillary Sword&lt;/em&gt;, by Ann Leckie&lt;/li&gt;&lt;li&gt;&lt;em&gt;Audition&lt;/em&gt;, by Katie Kitamura&lt;/li&gt;&lt;li&gt;&lt;em&gt;Crook Manifesto&lt;/em&gt;, by Colson Whitehead&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Dad Rock That Made Me a Woman&lt;/em&gt;, by Niko Stratis&lt;/li&gt;&lt;li&gt;&lt;em&gt;Everything Must Go: The Stories We Tell About the End of the World&lt;/em&gt;, by Dorian Lynskey&lt;/li&gt;&lt;li&gt;&lt;em&gt;Fellow Travelers&lt;/em&gt;, by Thomas Mallon&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Hammer: Power, Inequality, and the Struggle for the Soul of Labor&lt;/em&gt;, by Hamilton Nolan&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Library: A Fragile History&lt;/em&gt;, by Andrew Pettegree and Arthur der Weduwen&lt;/li&gt;&lt;li&gt;&lt;em&gt;Offshore: Stealth Wealth and the New Colonialism&lt;/em&gt;, by Brooke Harrington&lt;/li&gt;&lt;li&gt;&lt;em&gt;Pick a Colour&lt;/em&gt;, by Souvankham Thammavongsa&lt;/li&gt;&lt;li&gt;&lt;em&gt;Proto: How One Ancient Language Went Global&lt;/em&gt;, By Laura Spinney&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Quakers: A Very Short Introduction&lt;/em&gt;, by Pink Dandelion&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Soul of a New Machine&lt;/em&gt;, by Tracy Kidder&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Theory and Craft of Digital Preservation&lt;/em&gt;, by Trevor Owens&lt;/li&gt;&lt;li&gt;&lt;em&gt;Way Down Deep in the Belly of the Beast: A Memoir of the Seventies&lt;/em&gt;, by Douglas Fetherling&lt;/li&gt;&lt;li&gt;&lt;em&gt;When Women Ran Fifth Avenue: Glamour and Power at the Dawn of American Fashion&lt;/em&gt;, by Julie Satow&lt;/li&gt;&lt;li&gt;&lt;em&gt;Will There Ever Be Another You&lt;/em&gt;, by Patricia Lockwood&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;notes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/books-2025/#notes&quot;&gt;Notes&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;In &lt;em&gt;Way Down Deep...&lt;/em&gt;, Douglas Fetherling calls Toronto of the 1970s “the world’s most cosmopolitan and least sophisticated city” and unfortunately it still feels witheringly accurate today.&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Hammer&lt;/em&gt; and &lt;em&gt;Offshore&lt;/em&gt; make for a bracing, though somewhat depressing, pairing. The decline of unionized labour and the rise of the (latest) plutocrat class are obviously linked in the abstract, but both of these books zoom in to the human-level dynamics — how people actually organize their workplaces now (or fail to), and how wealth managers actually conceal the tidal flows of capital that slosh around the globe from one tax haven to the next. They’re also both satisfyingly lean, cutting to the chase to make their arguments and not outstaying their welcome.&lt;/li&gt;&lt;li&gt;Brooke Harrington also makes a point about “New Colonialism” in &lt;em&gt;Offshore&lt;/em&gt; that seems obvious once she points it out: it’s not a coincidence that so many of these tiny island tax havens around the world are former British colonies. While the UK’s formal colonial structure crumbled or withdrew from these far-flung holdings, many imperial institutions seamlessly continued their function of covertly channeling the world’s wealth, largely still to the benefit of London, but now in service of the City instead of the Crown.&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Dad Rock That Made Me a Woman&lt;/em&gt; is a powerful mix of memoir and criticism, but the essay that’s really stuck with me is the one about Bruce Springsteen’s “Dancing in the Dark.” Positioned mid-point in the book, it describes a moment of extreme emotional darkness that becomes the turning point toward something better, encapsulated by Bruce’s snarl into the mirror: “wanna change my clothes, my hair, my face.” I’d never really heard the depth of anguish packed into that line, or its corresponding potential for transformational change. Having read this book, I&#39;ll never hear that song the same way again (complimentary).&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Good boots: Salomon X-Ultra 5 Trail Shoes</title>
    <link href="https://gfscott.com/blog/good-boots-salomon-x-ultra-5/" />
    <updated>2025-12-29T18:00:00Z</updated>
    <id>https://gfscott.com/blog/good-boots-salomon-x-ultra-5/</id>
    <content type="html">&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/salomon-x-ultra-5s-BRcJwR67kN8A-640w.avif 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/salomon-x-ultra-5s-BRcJwR67kN8A-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/salomon-x-ultra-5s-BRcJwR67kN8A-640w.webp 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/salomon-x-ultra-5s-BRcJwR67kN8A-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/salomon-x-ultra-5s-BRcJwR67kN8A-640w.jpeg&quot; alt=&quot;Salomon X-Ultra 5s&quot; width=&quot;928&quot; height=&quot;698&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/salomon-x-ultra-5s-BRcJwR67kN8A-640w.jpeg 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/salomon-x-ultra-5s-BRcJwR67kN8A-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;p&gt;After more than 10 years, it’s finally the end of the trail for my Salomon X-Ultra 5 hiking boots. They go to their reward having safely delivered me across the deserts of Utah, the mountains of the Yukon, and the mossy cliffs of the Faroe Islands, among many other places.&lt;/p&gt;&lt;p&gt;They were good boots because they were both sturdy and lightweight, and provided a good balance between support and flexibility. Almost every time I wear my mid-rise equivalents, which come up past the ankle, I wish I was wearing these instead. However, the tread has worn off in places and there are holes at several flex points now, so it’s time.&lt;/p&gt;&lt;p&gt;I was wary at first of the &lt;a href=&quot;https://www.salomon.com/sg/a/how-to-use-the-salomon-quicklace-system&quot;&gt;Quicklace™&lt;/a&gt; system, where the lace is a closed loop that you tighten with a cleat. After all, if the proprietary shoelace breaks out in the middle of nowhere, that could be a long hobble back, and replacement would be more involved. However, I’ve come around because I’ve never had such a problem in a decade of steady wear, and it’s so much faster and more convenient than traditional shoelaces that I think it’s worth the tradeoff.&lt;/p&gt;&lt;p&gt;I intend to replace them with the contemporary equivalent, and I hope to cover just as much ground in them.&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/iceland-Y12GVcbQOoOi-640w.avif 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/iceland-Y12GVcbQOoOi-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/iceland-Y12GVcbQOoOi-640w.webp 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/iceland-Y12GVcbQOoOi-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/iceland-Y12GVcbQOoOi-640w.jpeg&quot; alt=&quot;Looking down at hiking boots on black volcanic sand in Iceland.&quot; width=&quot;928&quot; height=&quot;696&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/iceland-Y12GVcbQOoOi-640w.jpeg 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/iceland-Y12GVcbQOoOi-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/scotland-oBn3_9AB9tUf-640w.avif 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/scotland-oBn3_9AB9tUf-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/scotland-oBn3_9AB9tUf-640w.webp 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/scotland-oBn3_9AB9tUf-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/scotland-oBn3_9AB9tUf-640w.jpeg&quot; alt=&quot;Looking down at hiking boots on a muddy gravel trail in Scotland.&quot; width=&quot;928&quot; height=&quot;696&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/scotland-oBn3_9AB9tUf-640w.jpeg 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/scotland-oBn3_9AB9tUf-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/utah-zIeBifgysvti-640w.avif 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/utah-zIeBifgysvti-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/utah-zIeBifgysvti-640w.webp 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/utah-zIeBifgysvti-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/utah-zIeBifgysvti-640w.jpeg&quot; alt=&quot;Me wearing the hiking boots, seen from behind with Canyonlands National Park in the background.&quot; width=&quot;928&quot; height=&quot;696&quot; srcset=&quot;https://gfscott.com/blog/good-boots-salomon-x-ultra-5/utah-zIeBifgysvti-640w.jpeg 640w, https://gfscott.com/blog/good-boots-salomon-x-ultra-5/utah-zIeBifgysvti-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;figcaption&gt;The boots, pictured in Iceland, Scotland, and the United States.&lt;/figcaption&gt;&lt;/figure&gt;</content>
  </entry>
  <entry>
    <title>Eleventy pagination with Vento</title>
    <link href="https://gfscott.com/blog/eleventy-pagination-vento/" />
    <updated>2025-08-29T00:00:00Z</updated>
    <id>https://gfscott.com/blog/eleventy-pagination-vento/</id>
    <content type="html">&lt;p&gt;I recently switched my Eleventy site from &lt;a href=&quot;https://mozilla.github.io/nunjucks/&quot;&gt;Nunjucks&lt;/a&gt; to &lt;a href=&quot;https://vento.js.org&quot;&gt;Vento&lt;/a&gt; for templating. Mostly I just wanted to try it out, but I find I do prefer the &lt;code&gt;{{ }}&lt;/code&gt; syntax over &lt;code&gt;{% %}&lt;/code&gt;, and the ability to run vanilla JavaScript inside tags is nice.&lt;/p&gt;&lt;p&gt;I won&#39;t rehash Chris Kirk Nielsen&#39;s &lt;a href=&quot;https://chriskirknielsen.com/blog/from-nunjucks-to-vento-in-eleventy-migration-guide/&quot;&gt;thorough post on migrating to Vento&lt;/a&gt;, which came in very handy. But I&#39;ll share what I learned the hard way about Eleventy pagination in Vento.&lt;/p&gt;&lt;p&gt;Eleventy documents a good &lt;a href=&quot;https://www.11ty.dev/docs/pagination/nav/#put-it-all-together&quot;&gt;example&lt;/a&gt; of how to handle pagination with Nunjucks. Following that example, my original pagination component looked like this:&lt;/p&gt;&lt;pre class=&quot;language-njk&quot;&gt;&lt;code class=&quot;language-njk&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;token tag keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;pagination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;nav&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;pagination&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;span&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;span&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;pageEntry&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;pagination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;pages&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;pagination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;hrefs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;index0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;span&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;pg&quot;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;aria&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;page&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;span&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{{ pagination.hrefs[ loop.index0 ] }}&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;endif&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;endfor&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;nav&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;endif&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Migrating this component was the one place where I ran into real trouble, and the reason is that the documented method relies on “&lt;a href=&quot;https://mozilla.github.io/nunjucks/templating.html#for:~:text=Inside%20loops%2C%20you%20have%20access%20to%20a%20few%20special%20variables%3A&quot;&gt;special variables&lt;/a&gt;” that are peculiar to Nunjucks. That magic &lt;code&gt;loop.index0&lt;/code&gt; value — which represents the current zero-indexed iteration of the &lt;code&gt;for&lt;/code&gt; loop — is a nifty shortcut, but it doesn’t exist outside Nunjucks. You can easily swap the syntax but, of course, those special variables no longer mean anything.&lt;/p&gt;&lt;p&gt;After some blundering around, I figured out that Vento can in fact give you the index of a &lt;code&gt;for&lt;/code&gt; loop (although the docs don’t exactly &lt;a href=&quot;https://vento.js.org/syntax/for/#get-the-key-and-value&quot;&gt;articulate it&lt;/a&gt; that way). I suspect others might run into similar issues and find this useful.&lt;/p&gt;&lt;p&gt;Here&#39;s the equivalent Vento pagination component that I eventually landed on:&lt;/p&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pagination&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;nav &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;pagination&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;Page&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;span&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;# &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;pagination.pages&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; is an array &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; arrays&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; so &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; here acts &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; a counter #&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; index&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _content &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; pagination&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pages &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; pagination&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hrefs&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;span &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;pg&quot;&lt;/span&gt; aria&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;current&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;page&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;index &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;span&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;a href&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{{pagination.hrefs[index]}}&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;index &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;nav&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that &lt;code&gt;_content&lt;/code&gt; isn&#39;t used at all, but if you don&#39;t declare it, the array isn&#39;t destructured and everything breaks.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The books I read in 2024</title>
    <link href="https://gfscott.com/blog/books-2024/" />
    <updated>2025-01-01T14:00:00Z</updated>
    <id>https://gfscott.com/blog/books-2024/</id>
    <content type="html">&lt;p&gt;Only 13 total this year, although one of them was a biggie.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;The Age of Innocence&lt;/em&gt;, by Edith Wharton&lt;/li&gt;&lt;li&gt;&lt;em&gt;American Midnight: The Great War, a Violent Peace, and Democracy’s Forgotten Crisis&lt;/em&gt;, by Adam Hochschild&lt;/li&gt;&lt;li&gt;&lt;em&gt;Everything and More: A Compact History of ∞&lt;/em&gt;, by David Foster Wallace&lt;/li&gt;&lt;li&gt;&lt;em&gt;How Infrastructure Works: Inside the Systems That Shape Our World&lt;/em&gt;, by Deb Chachra&lt;/li&gt;&lt;li&gt;&lt;em&gt;Hyperobjects: Philosophy and Ecology after the End of the World&lt;/em&gt;, by Timothy Morton&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Late Americans&lt;/em&gt;, by Brandon Taylor&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Maniac&lt;/em&gt;, by Benjamín Labatut&lt;/li&gt;&lt;li&gt;&lt;em&gt;Middlemarch&lt;/em&gt;, by George Eliot&lt;/li&gt;&lt;li&gt;&lt;em&gt;Parable of the Sower&lt;/em&gt;, by Octavia E. Butler&lt;/li&gt;&lt;li&gt;&lt;em&gt;Parable of the Talents&lt;/em&gt;, by Octavia E. Butler&lt;/li&gt;&lt;li&gt;&lt;em&gt;Super-Infinite: The Transformations of John Donne&lt;/em&gt;, by Katherine Rundell&lt;/li&gt;&lt;li&gt;&lt;em&gt;Warez: The Infrastructure and Aesthetics of Piracy&lt;/em&gt;, by Martin Paul Eve&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Wounded World: W. E. B. Du Bois and the First World War&lt;/em&gt;, by Chad L. Williams&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;notes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/books-2024/#notes&quot;&gt;Notes&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;Middlemarch&lt;/em&gt; was the major project for this year and it was a delightful surprise. I went in knowing basically nothing about it and it took some time to acclimate to the Victorian prose, but it&#39;s beautifully funny, wise, and humane. I admit I glazed over at some of the political specifics, but the social dynamics are so finely observed that they’re perfectly legible to a modern reader.&lt;/li&gt;&lt;li&gt;&lt;em&gt;Warez&lt;/em&gt; was a learning experience in more ways than one, since I read it in the process of &lt;a href=&quot;https://github.com/gfscott/warez-the-infrastructure-and-aesthetics-of-piracy&quot;&gt;converting it&lt;/a&gt; from PDF to EPUB format (ironically, for a book about piracy, with the permission of the author).&lt;/li&gt;&lt;li&gt;An unexpected theme of &lt;em&gt;American Midnight&lt;/em&gt;, but one that resonated this year, was how much the &lt;em&gt;New York Times&lt;/em&gt; sucks, and always has. Whether it&#39;s peddling braindead jingoism in the leadup to the USA’s entry into the First World War, laundering hysterical Red-baiting in its aftermath, or simply being an easy mark for manipulative ghouls like J. Edgar Hoover, the &lt;em&gt;Times&lt;/em&gt; has a long and inglorious history of witless bootlicking.&lt;/li&gt;&lt;li&gt;Originally published in 1993 and structured as a series of diary entries, &lt;em&gt;Parable of the Sower&lt;/em&gt; begins on July 20, 2024 — coincidentally around when I picked it up. Reading it today, its grim predictions about climate collapse, social breakdown, and political dysfunction feel eerily on the nose.&lt;/li&gt;&lt;li&gt;Finally picked up and finished two books that I’d stalled out on in prior years: &lt;em&gt;Everything and More&lt;/em&gt;, and &lt;em&gt;Hyperobjects&lt;/em&gt;. Both dense and demanding, but ultimately rewarding, and with some surprising intersections.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>100 stars</title>
    <link href="https://gfscott.com/blog/100-stars/" />
    <updated>2024-12-08T19:11:55Z</updated>
    <id>https://gfscott.com/blog/100-stars/</id>
    <content type="html">&lt;p&gt;This week, 1,779 days after publishing the &lt;a href=&quot;https://www.npmjs.com/package/eleventy-plugin-youtube-embed/v/1.0.0&quot;&gt;first version&lt;/a&gt; of the first plugin, &lt;a href=&quot;https://gfscott.com/embed-everything/&quot;&gt;Embed Everything&lt;/a&gt; earned its &lt;a href=&quot;https://github.com/gfscott/eleventy-plugin-embed-everything/stargazers&quot;&gt;100th GitHub star&lt;/a&gt;. It’s a nice little milestone, and a good opportunity to take stock.&lt;/p&gt;&lt;h2 id=&quot;by-the-numbers&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/100-stars/#by-the-numbers&quot;&gt;By the numbers&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;A snapshot of key stats about the project as of today:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;1,512 commits&lt;/li&gt;&lt;li&gt;20,455 unit tests&lt;/li&gt;&lt;li&gt;1,924,929 npm downloads&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://gfscott.com/blog/100-stars/#fn1&quot; id=&quot;fnref1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;More important than the vanity metrics of GitHub stars or npm downloads is the &lt;a href=&quot;https://github.com/gfscott/eleventy-plugin-embed-everything/network/dependents&quot;&gt;dependents graph&lt;/a&gt;, which, to me, provides a better picture of how the plugin is actually used in the wild. There are 572 public repos using it, and 6 packages that include it as a sub-dependency.&lt;/p&gt;&lt;p&gt;I&#39;ve spent quite a lot of time scrolling through the dependents list, snooping into people’s codebases to see how they use this tool. One thing I notice, and that I believe is a good sign, is that many of them add no configuration at all. To me, that&#39;s a positive signal that I’ve chosen the right defaults, and that the plugin fits users’ mental models — it behaves pretty much the way they expect it to.&lt;/p&gt;&lt;p&gt;It’s likely there’s a sizeable contingent of users who don’t know the plugin exists at all. It’s been bundled into a few 11ty starter templates such as &lt;a href=&quot;https://pack11ty.dev&quot;&gt;Pack11ty&lt;/a&gt;, &lt;a href=&quot;https://elva.scott.ee&quot;&gt;Elva&lt;/a&gt;, and &lt;a href=&quot;https://spacebook.app&quot;&gt;Spacebook&lt;/a&gt;, so for some people the core functionality Just Works™ and they neither know nor care how it happens. I find that oddly gratifying! It was the whole point, after all — to remove a few needless mouse-clicks, so you can stay focused on the task at hand. It &lt;em&gt;should&lt;/em&gt; be invisible.&lt;/p&gt;&lt;h2 id=&quot;the-future&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/100-stars/#the-future&quot;&gt;The future&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;At this point the plugin is pretty mature; the pace of development is naturally going to be slower, and I think that’s fine. Still, there are things to do.&lt;/p&gt;&lt;h3 id=&quot;end-to-end-testing&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/100-stars/#end-to-end-testing&quot;&gt;End-to-end testing&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;I added some end-to-end testing with Playwright a few years back but then &lt;a href=&quot;https://github.com/gfscott/eleventy-plugin-embed-everything/pull/221&quot;&gt;removed it&lt;/a&gt; because my implementation was kind of flaky. The plan was always to add it back in, and I still intend to do that.&lt;/p&gt;&lt;h3 id=&quot;vitest&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/100-stars/#vitest&quot;&gt;Vitest&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Still thinking about this one. I’ve worked with &lt;a href=&quot;https://vitest.dev&quot;&gt;Vitest&lt;/a&gt; recently and I like it. In particular, I think it makes sense because, compared to &lt;a href=&quot;https://github.com/avajs/ava&quot;&gt;Ava&lt;/a&gt;, it includes better built-in support for mocking, and that’s important for testing oEmbed implementations. But it would be quite the chore to switch. We’ll see!&lt;/p&gt;&lt;h3 id=&quot;esm&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/100-stars/#esm&quot;&gt;ESM&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;I’m in no rush on this one, but eventually. Now that Node.js 22.12.0 has &lt;a href=&quot;https://nodejs.org/en/blog/release/v22.12.0#requireesm-is-now-enabled-by-default&quot;&gt;turned on&lt;/a&gt; &lt;code&gt;require(esm)&lt;/code&gt; by default, I think we&#39;re in the final throes of the CJS/ESM wars. Overall, if tradeoffs arise between developer ergonomics for me versus stability for end users, I intend to choose the latter. I expect the codebase to remain CJS for at least a couple years yet.&lt;/p&gt;&lt;h3 id=&quot;preprocessor-implementation&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/100-stars/#preprocessor-implementation&quot;&gt;Preprocessor implementation&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;An intriguing feature added in Eleventy 3.0 was &lt;a href=&quot;https://www.11ty.dev/docs/config-preprocessors/&quot;&gt;Preprocessors&lt;/a&gt;, which allows you to manipulate markdown content earlier in the data pipeline. I&#39;ve got a little test implementation working and I think it&#39;s promising. Among other things, it means not having to parse HTML with RegEx, and the ability to override the config on a per-post basis using just the frontmatter. This looks to me like the most logical path to a 2.0. More to come.&lt;/p&gt;&lt;hr class=&quot;footnotes-sep&quot;&gt;&lt;section class=&quot;footnotes&quot;&gt;&lt;ol class=&quot;footnotes-list&quot;&gt;&lt;li id=&quot;fn1&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;This is the total for all the plugins combined, so there&#39;s some double counting, since the &lt;code&gt;everything&lt;/code&gt; package downloads all the others. &lt;a href=&quot;https://gfscott.com/blog/100-stars/#fnref1&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Archiving your Twitter account with Eleventy</title>
    <link href="https://gfscott.com/blog/eleventy-twitter-archive/" />
    <updated>2024-11-20T23:30:00Z</updated>
    <id>https://gfscott.com/blog/eleventy-twitter-archive/</id>
    <content type="html">&lt;p&gt;Last week, I finally deleted all my tweets&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://gfscott.com/blog/eleventy-twitter-archive/#fn1&quot; id=&quot;fnref1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;. I haven&#39;t used Twitter for some time, and hadn’t posted in years, but the latest round of &lt;a href=&quot;https://www.theregister.com/2024/10/18/x_train_data/&quot;&gt;vaguely sinister&lt;/a&gt; terms-of-service updates prompted me to pull the plug for good.&lt;/p&gt;&lt;p&gt;However, I wanted to keep an archive of my account, on my own terms. I wanted it to be lo-fi, low-maintenance, and low-cost. I&#39;m happy to report that it worked: I’ve now got a complete record at &lt;a href=&quot;https://twitter.gfscott.com&quot;&gt;twitter.gfscott.com&lt;/a&gt;. Here&#39;s how I did it.&lt;/p&gt;&lt;h2 id=&quot;how-it-works&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-twitter-archive/#how-it-works&quot;&gt;How it works&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The site is an &lt;a href=&quot;https://11ty.dev&quot;&gt;Eleventy&lt;/a&gt; project, which uses the &lt;code&gt;tweets.js&lt;/code&gt; file that&#39;s part of your &lt;a href=&quot;https://help.x.com/en/managing-your-account/how-to-download-your-x-archive&quot;&gt;Twitter archive download&lt;/a&gt;. Basically it just churns through that file, &lt;a href=&quot;https://www.11ty.dev/docs/pages-from-data/&quot;&gt;generating a permalink&lt;/a&gt; for each tweet. The final product is just a giant pile of static HTML files.&lt;/p&gt;&lt;p&gt;I&#39;ve deployed my personal archive to &lt;a href=&quot;https://pages.cloudflare.com&quot;&gt;Cloudflare Pages&lt;/a&gt;, so it costs me nothing and could easily be moved if needed.&lt;/p&gt;&lt;h2 id=&quot;create-your-own&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-twitter-archive/#create-your-own&quot;&gt;Create your own&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;If you&#39;d like to make your own version, you can use &lt;a href=&quot;https://github.com/gfscott/twitter-archive-eleventy&quot;&gt;this GitHub template&lt;/a&gt;. All the instructions are in the readme:&lt;/p&gt;&lt;blockquote&gt;&lt;ol&gt;&lt;li&gt;Download your Twitter archive.&lt;/li&gt;&lt;li&gt;On the GitHub template repo, click Use this template, then select Create a new repository. Fill in the required details to create your own repo.&lt;/li&gt;&lt;li&gt;Copy the contents of &lt;code&gt;{archive}/data/tweets.js&lt;/code&gt; into &lt;code&gt;./src/_data/__tweets.js&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Change &lt;code&gt;window.YTD.tweets.part0 =&lt;/code&gt; to &lt;code&gt;export default&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Copy image files from &lt;code&gt;{archive}/data/tweets_media/&lt;/code&gt; into &lt;code&gt;./src/static/images/twitter/&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Run &lt;code&gt;npm run dev&lt;/code&gt; to preview.&lt;/li&gt;&lt;/ol&gt;&lt;/blockquote&gt;&lt;h2 id=&quot;caveats-and-limitations&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-twitter-archive/#caveats-and-limitations&quot;&gt;Caveats and limitations&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;My aging desktop outputs about 1,000 tweets/sec, so expect build-times to scale with how prolific you were.&lt;/li&gt;&lt;li&gt;The conversational context is incomplete. You can see &lt;em&gt;who&lt;/em&gt; you were replying to, but not &lt;em&gt;what&lt;/em&gt; they said. So it&#39;s a one-sided conversation.&lt;/li&gt;&lt;li&gt;No threads. Given the available data, I don&#39;t see a way to group posts together, but perhaps others have ideas. Happy to discuss in an &lt;a href=&quot;https://github.com/gfscott/twitter-archive-eleventy/issues&quot;&gt;issue&lt;/a&gt; or PR.&lt;/li&gt;&lt;li&gt;The site replaces t.co links with the actual URL when possible, but (especially in the early days of Twitter) there were a lot of link shortener services that have since vanished, leaving irretrievably broken links behind. Sad!&lt;/li&gt;&lt;li&gt;I don&#39;t &lt;em&gt;think&lt;/em&gt; there&#39;s any truly sensitive data in the &lt;code&gt;tweets.js&lt;/code&gt; file you get in the archive download, but I&#39;m keeping mine in a private repo all the same.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;alternatives&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-twitter-archive/#alternatives&quot;&gt;Alternatives&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Marco Maroni has created a &lt;a href=&quot;https://github.com/marcomaroni-github/twitter-to-bluesky&quot;&gt;Twitter-to-Bluesky&lt;/a&gt; tool, which imports and backdates tweets into your Bluesky account. I decided that I wanted a clean break, so I haven&#39;t tried it out, but &lt;a href=&quot;https://bsky.app/profile/minamarkh.am/post/3lazlc4efn22i&quot;&gt;others have&lt;/a&gt; and it seems pretty cool.&lt;/p&gt;&lt;hr class=&quot;footnotes-sep&quot;&gt;&lt;section class=&quot;footnotes&quot;&gt;&lt;ol class=&quot;footnotes-list&quot;&gt;&lt;li id=&quot;fn1&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;I used &lt;a href=&quot;https://github.com/lucahammer/tweetXer/&quot;&gt;this script&lt;/a&gt;, which worked well and was free. I’m keeping the username, just to prevent it from falling into enemy hands. &lt;a href=&quot;https://gfscott.com/blog/eleventy-twitter-archive/#fnref1&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Save the bike lanes</title>
    <link href="https://gfscott.com/blog/bill-212-bike-lanes/" />
    <updated>2024-10-27T18:00:00Z</updated>
    <id>https://gfscott.com/blog/bill-212-bike-lanes/</id>
    <content type="html">&lt;p&gt;The Environmental Registry of Ontario &lt;a href=&quot;https://ero.ontario.ca/notice/019-9266&quot;&gt;asked for comment&lt;/a&gt; on the province’s &lt;a href=&quot;https://globalnews.ca/news/10825146/toronto-bike-lane-removal-plan-doug-ford/&quot;&gt;plot to kill Toronto’s bike lanes&lt;/a&gt;. Comments are open until November 20, 2024.&lt;/p&gt;&lt;p&gt;Here&#39;s what my submission said:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;As someone who actually lives in a high-density urban environment, I can say from first-hand experience that adding bike lane infrastructure has made my life better and safer. I&#39;m a frequent pedestrian, occasional cyclist, and occasional driver. In all these situations, I appreciate the presence of separated bike lanes. Even if bike lanes slowed city traffic — which they don&#39;t — I would still be in favor.&lt;/p&gt;&lt;p&gt;This government should not backpedal on the significant improvements that have been made to Toronto’s street infrastructure in the last few years. The obvious underlying instinct at work with this proposal — removing existing bike lanes to appease a small number of frustrated drivers who don&#39;t actually live here — shouldn&#39;t be the basis for public policy. A few people’s road rage doesn&#39;t make for good law.&lt;/p&gt;&lt;p&gt;If Queen’s Park insists on tinkering with municipal affairs to improve the city&#39;s traffic situation, here are some alternative policies:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Automated speed cameras, to enforce the speed limit (something the police&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://gfscott.com/blog/bill-212-bike-lanes/#fn1&quot; id=&quot;fnref1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt; have evidently abandoned)&lt;/li&gt;&lt;li&gt;Automated red-light cameras, to penalize reckless driving that endangers pedestrians, cyclists, and other drivers&lt;/li&gt;&lt;li&gt;Automated Block-the-Box cameras, to discourage drivers from blocking cross-traffic in intersections (another neglected police duty)&lt;/li&gt;&lt;li&gt;Enforcing no-stopping zones (yet more police inaction), so the whole street doesn&#39;t grind to a halt for a single Uber or DoorDash driver with their four-way flashers on.&lt;/li&gt;&lt;li&gt;Expanding the no-parking hours on major thoroughfares. There are large segments of the day where Avenue Road, Mount Pleasant Road, Bathurst St, Lawrence Ave, Eglinton Ave, and more effectively lose the use of entire lanes in both directions because of a handful of (legally) parked cars.&lt;/li&gt;&lt;li&gt;More frequent and more reliable GO Train service, to make rail commuting viable for more users, from greater distances&lt;/li&gt;&lt;li&gt;A downtown congestion fee to incentivize more efficient use of city streets, and to pay for the other required improvements&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The behaviour of drivers, not cyclists, has created Toronto’s traffic problems; only changes to driver behaviour can fix them. I suggest you start there.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Now, do I find it &lt;em&gt;extremely galling&lt;/em&gt; to be writing crank letters to the government? Yes I do. Do I think it’s ultimately a futile gesture? Also yes. But it&#39;s a stupid policy, even by the degraded standards of this government, and they deserve to hear it.&lt;/p&gt;&lt;hr class=&quot;footnotes-sep&quot;&gt;&lt;section class=&quot;footnotes&quot;&gt;&lt;ol class=&quot;footnotes-list&quot;&gt;&lt;li id=&quot;fn1&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;There are very few problems to which I belive the solution is “more cops.” But honestly, traffic enforcement is one of them. &lt;a href=&quot;https://gfscott.com/blog/bill-212-bike-lanes/#fnref1&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>The books I read in 2023</title>
    <link href="https://gfscott.com/blog/books-2023/" />
    <updated>2024-01-01T17:00:00Z</updated>
    <id>https://gfscott.com/blog/books-2023/</id>
    <content type="html">&lt;p&gt;This year I focused on completing some book series, while tracing back the original influences for others. Overall number is down this year, so something to work on.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;Ada, the Enchantress of Numbers&lt;/em&gt;, by Betty Alexandra Toole&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Dark Forest&lt;/em&gt;, by Liu Cixin&lt;/li&gt;&lt;li&gt;&lt;em&gt;Death’s End&lt;/em&gt;, by Liu Cixin&lt;/li&gt;&lt;li&gt;&lt;em&gt;Dinosaurs&lt;/em&gt;, by Lydia Millet&lt;/li&gt;&lt;li&gt;&lt;em&gt;Eileen&lt;/em&gt;, by Otessa Moshfegh&lt;/li&gt;&lt;li&gt;&lt;em&gt;In Lonely Places: Film Noir Beyond the City&lt;/em&gt;, by Imogen Sara Smith&lt;/li&gt;&lt;li&gt;&lt;em&gt;La Belle Sauvage&lt;/em&gt;, by Philip Pullman&lt;/li&gt;&lt;li&gt;&lt;em&gt;Maurice&lt;/em&gt;, by E. M. Forster&lt;/li&gt;&lt;li&gt;&lt;em&gt;Midnight in the Garden of Good and Evil&lt;/em&gt;, by John Berendt&lt;/li&gt;&lt;li&gt;&lt;em&gt;A Paradise Built in Hell: The Extraordinary Communities That Arise in Disaster&lt;/em&gt;, by Rebecca Solnit&lt;/li&gt;&lt;li&gt;&lt;em&gt;Paradise Lost&lt;/em&gt;, by John Milton&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Secret Commonwealth&lt;/em&gt;, by Philip Pullman&lt;/li&gt;&lt;li&gt;&lt;em&gt;Stay True&lt;/em&gt;, by Hua Hsu&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;notes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/books-2023/#notes&quot;&gt;Notes&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;After reading &lt;em&gt;The Three Body Problem&lt;/em&gt; &lt;a href=&quot;https://gfscott.com/blog/books-2022/&quot;&gt;last year&lt;/a&gt;, I kept going with Liu Cixin’s follow-ups, &lt;em&gt;The Dark Forest&lt;/em&gt; and &lt;em&gt;Death’s End&lt;/em&gt;. I appreciated the ambition of the narrative, which spans millennia and sprawls across the cosmos, but the human-scale characterization suffers as a result, and in the end I found its worldview to be too bleak and misanthropic.&lt;/li&gt;&lt;li&gt;In contrast, it felt great to slip back into Philip Pullman’s world with &lt;em&gt;La Belle Sauvage&lt;/em&gt; and &lt;em&gt;The Secret Commonwealth&lt;/em&gt;, which expand on the original &lt;em&gt;His Dark Materials&lt;/em&gt; trilogy (with a third on the way). He smuggles in his political agenda more artfully than Liu does, and it’s a more humane, generous vision overall, disguised as a swift adventure yarn.&lt;/li&gt;&lt;li&gt;I followed the thread back from Pullman to his source, &lt;em&gt;Paradise Lost&lt;/em&gt;, and man, what a banger. I see why it was such a splash at the time, and why it’s endured for centuries.&lt;/li&gt;&lt;li&gt;I’ve been searching for a proper, thorough biography of Ada Lovelace, and it seems like a curious blank spot. &lt;em&gt;Ada, the Enchantress of Numbers&lt;/em&gt; comes close, but it’s an odd specimen, more a series of biographical sketches sprinkled with excerpts from letters and other assorted commentary. It’s good, but still not quite the thing I’m looking for. The search continues.&lt;/li&gt;&lt;li&gt;I’ll say the same thing about &lt;em&gt;A Paradise Built in Hell&lt;/em&gt; that I do about many books, which is that it&#39;s very good, and would be even better at a third the length. Most non-fiction books are too long! The book is sort of a mirror image of Naomi Klein’s &lt;em&gt;Shock Doctrine&lt;/em&gt;, where crisis again acts as a catalyst for change — but in this case a positive one, where disasters allow people to glimpse the utopian potential for collective action, mutual aid, and community solidarity. Which is perfectly interesting — but the thesis is not bolstered by many, many extra examples illustrating this same point over a couple hundred pages.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Good song: “How We Drift Away” by Tim Heidecker and Weyes Blood</title>
    <link href="https://gfscott.com/blog/good-song-how-we-drift-away-tim-heidecker-weyes-blood/" />
    <updated>2023-11-07T21:00:00Z</updated>
    <id>https://gfscott.com/blog/good-song-how-we-drift-away-tim-heidecker-weyes-blood/</id>
    <content type="html">&lt;div id=&quot;UVrk7AdCaTU&quot; class=&quot;eleventy-plugin-youtube-embed&quot; style=&quot;position:relative;width:100%;padding-top: 56.25%;&quot;&gt;&lt;iframe style=&quot;position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;&quot; width=&quot;100%&quot; height=&quot;100%&quot; frameborder=&quot;0&quot; title=&quot;Embedded YouTube video&quot; src=&quot;https://www.youtube-nocookie.com/embed/UVrk7AdCaTU&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;The first reason I like this song is its ’70s throwback vibe. That melodic bassline, the steel pedal guitar wailing in the middle distance, Natalie Mering’s Quaalude-deadpan delivery. Big-time Carpenters vibes. Here for it.&lt;/p&gt;&lt;p&gt;The second reason is that it explores comparatively uncommon emotional terrain for pop music: the vague melancholy of an eroding platonic friendship. I think most adults can relate to the sensation of meeting up with someone you once felt close to and finding the connection threadbare. The lyric &lt;em&gt;You can take me out&lt;/em&gt; / &lt;em&gt;of your phone&lt;/em&gt; describes, with deadly specificity and concision, a hypermodern social dynamic. Deleting someone from your phone is trivial, technically. But socially, it’s fraught — the final act of deliberately pulling the plug on a relationship, no matter how feeble its life-signs might have been. That shit is real.&lt;/p&gt;&lt;p&gt;The third and final reason I like this song is the wild narrative turn it takes in the third verse, abruptly jumping 20,000 years into the past: &lt;em&gt;In the Chauvet caves&lt;/em&gt; / &lt;em&gt;women painting on the wall&lt;/em&gt;. I love the grandiosity of this gesture, grasping at common human experiences across the centuries. It reminds me of Terence Malick’s &lt;em&gt;The Tree of Life&lt;/em&gt;, whose kitchen-sink family drama plays out against a backdrop of the literal creation of the universe and the end of the world. By juxtaposing the intimate and the infinite, we glimpse how they’re part of the same whole. Our individual lives are so vanishingly small, and yet, it’s all we have; it’s everything. Some works of art deliver penetrating observations of human nature. Some grapple with cosmic mystery. Few manage to do both at once. To me, this is one of them.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>How to merge multiple Git repos and keep their commit history</title>
    <link href="https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/" />
    <updated>2023-03-18T19:00:00Z</updated>
    <id>https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/</id>
    <content type="html">&lt;h2 id=&quot;background&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/#background&quot;&gt;Background&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I maintain an Eleventy plugin, &lt;a href=&quot;https://gfscott.com/embed-everything/&quot;&gt;Embed Everything&lt;/a&gt;, which aggregates a bunch of other plugins. Until recently, I was keeping a separate Git repository for each package. It was getting to be a hassle, having to track issues, dependencies, and maintenance tasks across nine different repos. So after some experimentation, I decided to combine them all into a single &lt;a href=&quot;https://monorepo.tools&quot;&gt;monorepo&lt;/a&gt;. Here&#39;s how I did it.&lt;/p&gt;&lt;p&gt;This process is based largely on &lt;a href=&quot;https://blog.jdriven.com/2021/04/how-to-merge-multiple-git-repositories/&quot;&gt;this blog post&lt;/a&gt; by Willem Cheizoo. I very slightly modified Willem&#39;s process, however, so I wanted to document it here.&lt;/p&gt;&lt;h2 id=&quot;goals&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/#goals&quot;&gt;Goals&lt;/a&gt;&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;Merge several separate repos into one.&lt;/li&gt;&lt;li&gt;Preserve the complete Git commit history. This was important because I wanted to ensure that volunteers continue to receive credit for their contributions to the codebase.&lt;/li&gt;&lt;/ol&gt;&lt;figure&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Before: two repos&lt;/span&gt;
├── everything
└── youtube&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# After: two packages, one repo&lt;/span&gt;
└── monorepo
    └── packages
        ├── everything
        └── youtube&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h2 id=&quot;overview&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/#overview&quot;&gt;Overview&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;This is an overview of the basic steps. I&#39;ll go through each one in greater detail.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Re-arrange each repo so they can merge without conflicts&lt;/li&gt;&lt;li&gt;Add each repo as a remote source for the monorepo&lt;/li&gt;&lt;li&gt;Merge the repos with &lt;code&gt;--allow-unrelated-histories&lt;/code&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;prepare-each-repo-for-merging&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/#prepare-each-repo-for-merging&quot;&gt;Prepare each repo for merging&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The first task is to get each repo into the target file structure. It&#39;s important to do this ahead of time, to prevent merge conflicts.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Checkout a working branch, so we can bail out safely:&lt;br&gt;&lt;code&gt;$ git checkout -b monorepo-prep&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Create the target folder structure:&lt;br&gt;&lt;code&gt;$ mkdir packages &amp;amp;&amp;amp; mkdir packages/everything&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Move files into that new structure. I like using &lt;code&gt;git mv&lt;/code&gt;:&lt;br&gt;&lt;code&gt;$ git mv package.json packages/everything&lt;/code&gt;&lt;br&gt;Repeat this step as necessary, until all the files are moved.&lt;/li&gt;&lt;/ol&gt;&lt;figure&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Before: &lt;/span&gt;
└── everything
    ├── README.md
    ├── index.js
    ├── package.json
    └── &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# After: &lt;/span&gt;
└── everything 
    └── packages
        └── everything
            ├── README.md
            ├── index.js
            ├── package.json
            └── &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;ol start=&quot;4&quot;&gt;&lt;li&gt;Finally, commit all your changes:&lt;br&gt;&lt;code&gt;$ git add -A&lt;/code&gt;&lt;br&gt;&lt;code&gt;$ git commit -m &amp;quot;Prep for monorepo migration&amp;quot;&lt;/code&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&quot;repeat-for-each-repo&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/#repeat-for-each-repo&quot;&gt;Repeat for each repo&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Follow this same process for each repo that you&#39;re planning to merge. The goal is to ensure that they all have parallel file structures. If they don&#39;t, you&#39;re likely to get git conflicts that are a pain to reconcile.&lt;/p&gt;&lt;p&gt;In my case, I did the same thing for the &lt;code&gt;youtube&lt;/code&gt; repo:&lt;/p&gt;&lt;figure&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Before: &lt;/span&gt;
└── youtube
    ├── README.md
    ├── index.js
    └── package.json&lt;/code&gt;&lt;/pre&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# After: &lt;/span&gt;
└── youtube 
    └── packages
        └── youtube
            ├── README.md
            ├── index.js
            └── package.json&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;We&#39;re now ready to merge the two repos into one.&lt;/p&gt;&lt;h2 id=&quot;connect-the-repos&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/#connect-the-repos&quot;&gt;Connect the repos&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;We&#39;ll allow these separate repos to communicate by connecting them using &lt;code&gt;git remote&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Typically, you&#39;d use the &lt;a href=&quot;https://git-scm.com/docs/git-remote&quot;&gt;git remote&lt;/a&gt; command to connect your local repo to an upstream repo over the network, such as GitHub. But you can use it to track &lt;em&gt;any&lt;/em&gt; repo, including the ones on your local file system:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ./everything&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# -f          = &amp;lt;f&gt;etch the list of branches from the remote repo&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# youtube     = &amp;lt;name&gt; for the remote repo&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# ../youtube  = &amp;lt;url&gt; of the &quot;remote&quot; repo. In this case, just a relative path&lt;/span&gt;

$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; remote &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; youtube &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;/youtube&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So the &lt;code&gt;everything&lt;/code&gt; repo is now tracking the &lt;code&gt;youtube&lt;/code&gt; repo as a remote source. You&#39;re now ready to merge these separate repos into one.&lt;/p&gt;&lt;h2 id=&quot;merge-the-repos&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/#merge-the-repos&quot;&gt;Merge the repos&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;We&#39;ll use &lt;code&gt;git merge&lt;/code&gt; to pull the &lt;code&gt;youtube&lt;/code&gt; repo into the &lt;code&gt;everything&lt;/code&gt; repo, including its complete commit history. The &lt;a href=&quot;https://git-scm.com/docs/git-merge#Documentation/git-merge.txt---allow-unrelated-histories&quot;&gt;&lt;code&gt;--allow-unrelated-histories&lt;/code&gt; flag&lt;/a&gt; is what makes this whole thing work.&lt;/p&gt;&lt;p&gt;By default, a successful &lt;code&gt;merge&lt;/code&gt; command creates a commit. I like using the &lt;a href=&quot;https://git-scm.com/docs/git-merge#Documentation/git-merge.txt---no-commit&quot;&gt;&lt;code&gt;--no-commit&lt;/code&gt; flag&lt;/a&gt; so I can confirm the new file structure before committing the changes, but it&#39;s optional.&lt;/p&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ./everything&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# youtube/monorepo-prep       = &amp;lt;remote repo name&gt;/&amp;lt;branch name&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# --allow-unrelated-histories = It&#39;s OK to smoosh these repos together&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# --no-commit                 = (Optional) Stage instead of committing&lt;/span&gt;

$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; merge youtube/monorepo-prep --allow-unrelated-histories --no-commit&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A this point you can validate that the files were moved as expected:&lt;/p&gt;&lt;pre class=&quot;language-diff&quot;&gt;&lt;code class=&quot;language-diff&quot;&gt;└── everything
&lt;span class=&quot;token unchanged&quot;&gt;&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;   └── packages
&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;       ├── everything&lt;/span&gt;&lt;span class=&quot;token inserted-sign inserted&quot;&gt;&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt;       └── youtube&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can run &lt;code&gt;git log --all&lt;/code&gt; to check that the commit history was merged. (The &lt;a href=&quot;https://git-scm.com/docs/git-log#Documentation/git-log.txt---all&quot;&gt;&lt;code&gt;--all&lt;/code&gt; flag&lt;/a&gt; lets you view the merged commit history &lt;a href=&quot;https://stackoverflow.com/a/78873069/26829947&quot;&gt;before completing the commit&lt;/a&gt;.) The two histories are merged together in reverse chronological order.&lt;/p&gt;&lt;p&gt;If you&#39;re satisfied that everything worked as expected, you can &lt;code&gt;git commit&lt;/code&gt; the results. At this point, you&#39;ve successfully merged the two repos, including their complete Git commit history.&lt;/p&gt;&lt;h2 id=&quot;cleanup&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/#cleanup&quot;&gt;Cleanup&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;There are several things you might want to do at this point. There are some files in each package directory that are likely no longer needed. For example, you can keep a single &lt;code&gt;.gitignore&lt;/code&gt; file in the project root and delete all the individual ones in each package. You can also &lt;code&gt;git remote rm youtube&lt;/code&gt;, since you probably don&#39;t need that remote connection anymore. And there are other tasks required to set everything up as a monorepo, but that&#39;s a different post.&lt;/p&gt;&lt;h2 id=&quot;speedrun&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/merge-git-repos-and-keep-commit-history/#speedrun&quot;&gt;Speedrun&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Here&#39;s a condensed run-through of this full procedure as it looks on the command line:&lt;/p&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ./&lt;/span&gt;
$ &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; everything/
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout &lt;span class=&quot;token parameter variable&quot;&gt;-b&lt;/span&gt; monorepo-prep
$ &lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; packages
$ &lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; packages/everything
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; index.js packages/everything
  &lt;span class=&quot;token comment&quot;&gt;# ... repeat the above step for all files&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Prepping monorepo&quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# switch to the youtube repo&lt;/span&gt;
$ &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;/youtube
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout &lt;span class=&quot;token parameter variable&quot;&gt;-b&lt;/span&gt; monorepo-prep
$ &lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; packages
$ &lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; packages/everything
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mv&lt;/span&gt; index.js packages/youtube
  &lt;span class=&quot;token comment&quot;&gt;# ... repeat the above step for all files&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Prepping monorepo&quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# switch back to the monorepo folder&lt;/span&gt;
$ &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;/everything
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; remote &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; youtube &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;/youtube
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; merge youtube/monorepo-prep --no-commit --allow-unrelated-histories
&lt;span class=&quot;token comment&quot;&gt;# Inspect that everything worked as expected. If so:&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Merging repos&quot;&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; remote &lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; youtube
&lt;span class=&quot;token comment&quot;&gt;# Done!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content>
  </entry>
  <entry>
    <title>The books I read in 2022</title>
    <link href="https://gfscott.com/blog/books-2022/" />
    <updated>2023-01-01T17:00:00Z</updated>
    <id>https://gfscott.com/blog/books-2022/</id>
    <content type="html">&lt;p&gt;I leaned toward novels this year, while belatedly picking up some older non-fiction titles that had been on my to-read list for some time.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;Archives&lt;/em&gt;, by Andrew Lison, Marcell Mars, Tomislav Medak, and Rick Prelinger&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Boy Kings: A Journey Into the Heart of the Social Network&lt;/em&gt;, by Katherine Losse&lt;/li&gt;&lt;li&gt;&lt;em&gt;Cleanness&lt;/em&gt;, by Garth Greenwell&lt;/li&gt;&lt;li&gt;&lt;em&gt;Dark Age Ahead&lt;/em&gt;, by Jane Jacobs&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Heat of the Moment: A Firefighter’s Stories of Life and Death Decisions&lt;/em&gt;, by Sabrina Cohen-Hatton&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Free World: Art and Thought in the Cold War&lt;/em&gt;, by Louis Menand&lt;/li&gt;&lt;li&gt;&lt;em&gt;Harlem Shuffle&lt;/em&gt;, by Colson Whitehead&lt;/li&gt;&lt;li&gt;&lt;em&gt;Lapvona&lt;/em&gt;, by Ottessa Moshfegh&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Listeners&lt;/em&gt;, by Jordan Tannahill&lt;/li&gt;&lt;li&gt;&lt;em&gt;Paper Girls&lt;/em&gt;, By Brian K. Vaughan and Cliff Chiang&lt;/li&gt;&lt;li&gt;&lt;em&gt;Pure Colour&lt;/em&gt;, by Sheila Heti&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Real World of Technology&lt;/em&gt;, by Ursula M. Franklin&lt;/li&gt;&lt;li&gt;&lt;em&gt;Sea State&lt;/em&gt;, by Tabitha Lasley&lt;/li&gt;&lt;li&gt;&lt;em&gt;Second Foundation&lt;/em&gt;, by Isaac Asimov&lt;/li&gt;&lt;li&gt;&lt;em&gt;A Separation&lt;/em&gt;, by Katie Kitamura&lt;/li&gt;&lt;li&gt;&lt;em&gt;Son of Elsewhere: A Memoir in Pieces&lt;/em&gt;, by Elamin Abdelmahmoud&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Three-Body Problem&lt;/em&gt;, by Liu Cixin&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Topeka School&lt;/em&gt;, by Ben Lerner&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Trouble with Brunch: Work, Class, and the Pursuit of Leisure&lt;/em&gt;, by Shawn Micallef&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;notes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/books-2022/#notes&quot;&gt;Notes&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;I picked up &lt;em&gt;The Free World&lt;/em&gt; on a whim and while I had expected it to be about “the CIA invented abstract expressionism!” and so on, it’s more of a series of potted biographies of key midcentury artists and academics; the Cold War is simply a backdrop to it all (which is fine!). Very breezy and engaging.&lt;/li&gt;&lt;li&gt;Feeling so annoyed with humanity that you deliberately provoke alien invasion? Though it was originally written in 2008, &lt;em&gt;The Three-Body Problem&lt;/em&gt; fit my 2022 mood quite well.&lt;/li&gt;&lt;li&gt;After reading &lt;em&gt;Seeing Like a State&lt;/em&gt; &lt;a href=&quot;https://gfscott.com/blog/books-2021/&quot;&gt;last year&lt;/a&gt; and finding it interesting but ponderous, Ursula Franklin’s 1989 Massey Lecture series, &lt;em&gt;The Real World of Technology&lt;/em&gt;, made for an illuminating contrast. It’s lucid and concise, with a similarly sweeping view of history but a more thoughtful moral argument at its core. It shows its age in parts, but is otherwise uncannily relevant to the technological discourse of 2022.&lt;/li&gt;&lt;li&gt;I finished Asimov’s &lt;em&gt;Foundation&lt;/em&gt; trilogy and I think I&#39;m going to leave it at that.&lt;/li&gt;&lt;li&gt;Mixing memoir and reportage is not a new trick, but Tabitha Lasley turns the dial to 11 with &lt;em&gt;Sea State&lt;/em&gt;. Both an account of the brutal labour conditions of North sea offshore oil drilling, and a vivid first-hand encounter with the equally extreme emotional terrain shaped by those conditions. Thrilling and cringe-inducing in the sheer recklessness of the project, with the narrative arc of a novel but the frank prose of a reporter whose beat is her own unraveling life. Strange and compelling.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Denyse Thomasos at the AGO</title>
    <link href="https://gfscott.com/blog/denyse-thomasos-ago/" />
    <updated>2022-11-25T18:00:00Z</updated>
    <id>https://gfscott.com/blog/denyse-thomasos-ago/</id>
    <content type="html">&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/denyse-thomasos-ago/metropolis-2007-denyse-thomasos-LawdGdpVzptf-640w.avif 640w, https://gfscott.com/blog/denyse-thomasos-ago/metropolis-2007-denyse-thomasos-LawdGdpVzptf-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/denyse-thomasos-ago/metropolis-2007-denyse-thomasos-LawdGdpVzptf-640w.webp 640w, https://gfscott.com/blog/denyse-thomasos-ago/metropolis-2007-denyse-thomasos-LawdGdpVzptf-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/denyse-thomasos-ago/metropolis-2007-denyse-thomasos-LawdGdpVzptf-640w.jpeg&quot; alt=&quot;Metropolis (2007) by Denyse Thomasos&quot; width=&quot;928&quot; height=&quot;698&quot; srcset=&quot;https://gfscott.com/blog/denyse-thomasos-ago/metropolis-2007-denyse-thomasos-LawdGdpVzptf-640w.jpeg 640w, https://gfscott.com/blog/denyse-thomasos-ago/metropolis-2007-denyse-thomasos-LawdGdpVzptf-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;figcaption&gt;“Metropolis”, 2007, by Denyse Thomasos. Collection of the Art Gallery of Ontario.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;This week I had a day off and decided to go to the Art Gallery of Ontario, with no particular agenda. And I was absolutely blown away by the exhibit &lt;em&gt;&lt;a href=&quot;https://ago.ca/exhibitions/denyse-thomasos-just-beyond&quot;&gt;Denyse Thomasos: Just Beyond&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;I’d never heard of Thomasos (1964–2012) before, or seen any of her work. That added to the thrill of wandering aimlessly up to the fifth-floor gallery and encountering this art for the first time, without any preconceived ideas.&lt;/p&gt;&lt;p&gt;What I like most about these paintings is the way they skate the line between abstract and figurative, organic and manufactured. Big, drippy expressionist strokes, interlaid with regimented, architectural geometries.&lt;/p&gt;&lt;p&gt;Bone structures, shipping containers, urban infrastructure, prison cells — you get these snatches of recognition without ever quite fixing your eye on a recognizable object. Dense layering means there’s always something pulling your gaze elsewhere on the canvas. They&#39;re easy to get lost in.&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/denyse-thomasos-ago/dos-amigos-1993-virtual-incarceration-1999-denyse-thomasos-GmGWsrKHrrIc-640w.avif 640w, https://gfscott.com/blog/denyse-thomasos-ago/dos-amigos-1993-virtual-incarceration-1999-denyse-thomasos-GmGWsrKHrrIc-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/denyse-thomasos-ago/dos-amigos-1993-virtual-incarceration-1999-denyse-thomasos-GmGWsrKHrrIc-640w.webp 640w, https://gfscott.com/blog/denyse-thomasos-ago/dos-amigos-1993-virtual-incarceration-1999-denyse-thomasos-GmGWsrKHrrIc-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/denyse-thomasos-ago/dos-amigos-1993-virtual-incarceration-1999-denyse-thomasos-GmGWsrKHrrIc-640w.jpeg&quot; alt=&quot;Left: Dos Amigos (Slave Boat), 1993; Right: Virtual Incarceration (1999); both by Denyse Thomasos&quot; width=&quot;928&quot; height=&quot;698&quot; srcset=&quot;https://gfscott.com/blog/denyse-thomasos-ago/dos-amigos-1993-virtual-incarceration-1999-denyse-thomasos-GmGWsrKHrrIc-640w.jpeg 640w, https://gfscott.com/blog/denyse-thomasos-ago/dos-amigos-1993-virtual-incarceration-1999-denyse-thomasos-GmGWsrKHrrIc-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;figcaption&gt;Two paintings by Denyse Thomasos. Left: “Dos Amigos (Slave Boat)”, 1993, Collection of Cadillac Fairview; Right: “Virtual Incarceration”, 1999, Estate of Denyse Thomasos and Olga Korper Gallery.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The political content here is pointed, without ever being didactic. One painting, &lt;em&gt;Virtual Incarceration&lt;/em&gt; (11 feet tall by 20 feet wide) is monumentally large, but still manages to produce a cramped and crowded visual effect, with warped and unsettled perspective planes. I find it rare for abstract painting to achieve this kind of synthesis, where the visual and conceptual content complement one another so clearly.&lt;/p&gt;&lt;p&gt;Anyway, I really liked this show! It’s delightful to stumble over something you’ve never seen before and feel so instantly at home with it. &lt;em&gt;Just Beyond&lt;/em&gt; is at the AGO through February 2023.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The Age of Participation</title>
    <link href="https://gfscott.com/blog/the-age-of-participation/" />
    <updated>2022-06-19T17:00:00Z</updated>
    <id>https://gfscott.com/blog/the-age-of-participation/</id>
    <content type="html">&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/the-age-of-participation/front-rgzo1ssPKVUD-640w.avif 640w, https://gfscott.com/blog/the-age-of-participation/front-rgzo1ssPKVUD-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/the-age-of-participation/front-rgzo1ssPKVUD-640w.webp 640w, https://gfscott.com/blog/the-age-of-participation/front-rgzo1ssPKVUD-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/the-age-of-participation/front-rgzo1ssPKVUD-640w.jpeg&quot; alt=&quot;Front view of a blue T-shirt that reads “Change”&quot; width=&quot;928&quot; height=&quot;928&quot; srcset=&quot;https://gfscott.com/blog/the-age-of-participation/front-rgzo1ssPKVUD-640w.jpeg 640w, https://gfscott.com/blog/the-age-of-participation/front-rgzo1ssPKVUD-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/the-age-of-participation/back-qZbi_CLfVXa_-640w.avif 640w, https://gfscott.com/blog/the-age-of-participation/back-qZbi_CLfVXa_-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/the-age-of-participation/back-qZbi_CLfVXa_-640w.webp 640w, https://gfscott.com/blog/the-age-of-participation/back-qZbi_CLfVXa_-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/the-age-of-participation/back-qZbi_CLfVXa_-640w.jpeg&quot; alt=&quot;Back view of a blue T-shirt that reads “ChangeCamp Change Agent”&quot; width=&quot;928&quot; height=&quot;928&quot; srcset=&quot;https://gfscott.com/blog/the-age-of-participation/back-qZbi_CLfVXa_-640w.jpeg 640w, https://gfscott.com/blog/the-age-of-participation/back-qZbi_CLfVXa_-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;figcaption&gt;Front and back views of a volunteer T-shirt from ChangeCampTO, February 2010.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Recently I was clearing out some old stuff that no longer sparked joy™ and decided the time had come for this T-shirt. Instead of &lt;a href=&quot;https://www.huffpost.com/entry/marie-kondo-saying-thank-you_l_5c49ebc9e4b06ba6d3bb31e6&quot;&gt;thanking the shirt&lt;/a&gt;, however, I thought it was more meaningful to eulogize the era for which it’s personally emblematic.&lt;/p&gt;&lt;p&gt;I got it in exchange for being a volunteer facilitator — a “change agent” — at &lt;a href=&quot;http://web.archive.org/web/20130527144136/http://changecamp.ca/2010/02/changecampto-designing-a-civic-engagement-toolkit/&quot;&gt;ChangeCampTO&lt;/a&gt; in 2010. ChangeCamp was an “&lt;a href=&quot;https://en.wikipedia.org/wiki/Unconference&quot;&gt;unconference&lt;/a&gt;” — a term that dates it to an extremely specific moment in our shared socio-digital fossil record. Here’s how the organizers described it:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;ChangeCamp is an event format, an open community and a set of tools and ideas designed to give citizens and governments the ability to work collaboratively in new ways to make change and to better address real-world challenges in our communities.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The event itself was held at the Metro Toronto Reference Library on February 16, 2010. Since it was an unconference, there was no firm agenda beyond a “design exercise” in which participants, in groups of 8, considered &lt;a href=&quot;http://web.archive.org/web/20100129085931/http://changecamp.ca/about/&quot;&gt;questions like&lt;/a&gt; “How do we re-imagine government and citizenship in the age of participation?” As facilitator for one group, my job was to try to keep my table’s conversation on track, as well as documenting it in real-time through a live-blog format (We used &lt;a href=&quot;https://www.crunchbase.com/organization/scribblelive&quot;&gt;ScribbleLive&lt;/a&gt;). I don’t remember any specifics of that conversation today, but I remember the vibe with perfect clarity.&lt;/p&gt;&lt;p&gt;These ideas were &lt;em&gt;everywhere&lt;/em&gt; in the late ’00s and early ’10s. I’d summarize the ethos this way:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The Internet is, broadly speaking, a democratizing force;&lt;/li&gt;&lt;li&gt;Open Source, which contributed so much to the creation of the Internet, is a viable model for broader sectors of society;&lt;/li&gt;&lt;li&gt;Transparency and openness naturally foster more humane governance;&lt;/li&gt;&lt;li&gt;Innovation and efficiency are core virtues of governments and other public institutions;&lt;/li&gt;&lt;li&gt;When more people are involved in a process, it will naturally be more representative.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Today, every one of these ideas has been shown to be outright wrong at worst, or, at best, hopelessly naïve. But at the time, they were inescapable. And for the “citizens, policy-makers, technologists, design-thinkers, change agents and media creators” (lol) who were the ChangeCamp constituency, they felt like a genuine, widespread, and necessary evolution in our social and civic life.&lt;/p&gt;&lt;p&gt;You can observe strands of this ideology popping up everywhere around this time:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The Arab Spring (2010)&lt;/li&gt;&lt;li&gt;Occupy Wall Street (2011)&lt;/li&gt;&lt;li&gt;“Crowdfunding” services like IndieGoGo (founded 2008) and Kickstarter (2009)&lt;/li&gt;&lt;li&gt;“Sharing economy” (lol) companies like Airbnb (2008) and Uber (2009)&lt;/li&gt;&lt;li&gt;Massive Open Online Courses (MOOCs, peaking around &lt;a href=&quot;http://web.archive.org/web/20220619154611/https://www.nytimes.com/2012/11/04/education/edlife/massive-open-online-courses-are-multiplying-at-a-rapid-pace.html&quot;&gt;2012&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Archive of Our Own (2008), the repository of &lt;del&gt;fan-fiction&lt;/del&gt; “transformative works”&lt;/li&gt;&lt;li&gt;“Collaborative mapping” services like Ushahidi (2008) and its precursor OpenStreetMap (2004)&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://openparliament.ca/about/&quot;&gt;Open Parliament&lt;/a&gt; (2010)&lt;/li&gt;&lt;li&gt;Cryptocurrency (Bitcoin, first minted 2009) also has its roots here&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In very real ways, we’re today living through the “Age of Participation” envisioned by ChangeCamp and all its fellow travellers. I’d say this age is considerably darker than we envisioned, sitting around those tables at the library one February evening in 2010. But it’s still nice to remember the optimism of that moment, and how excited we were about &lt;em&gt;Change&lt;/em&gt;. We probably should have been more specific.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Using the Eleventy Image plugin without a central image folder</title>
    <link href="https://gfscott.com/blog/eleventy-img-without-central-image-directory/" />
    <updated>2022-05-29T16:00:00Z</updated>
    <id>https://gfscott.com/blog/eleventy-img-without-central-image-directory/</id>
    <content type="html">&lt;h2 id=&quot;%F0%9F%9A%A8-update%2C-december-2024&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/#%F0%9F%9A%A8-update%2C-december-2024&quot;&gt;🚨 Update, December 2024&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/@11ty/eleventy-img&quot;&gt;Eleventy Image 5.0+&lt;/a&gt; offers a &lt;a href=&quot;https://www.11ty.dev/docs/plugins/image/#eleventy-transform&quot;&gt;better method&lt;/a&gt; for achieving this same result. It’s better for a few reasons:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Relative image paths just work out of the box.&lt;/li&gt;&lt;li&gt;You use standard image Markdown (&lt;code&gt;![alt](img.src)&lt;/code&gt;), instead of a non-standard shortcode.&lt;/li&gt;&lt;li&gt;In dev mode, images get processed on-demand, so build times are faster.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;strong&gt;I no longer use the custom shortcode method documented below.&lt;/strong&gt; You can eliminate the special Liquid syntax and get basically identical behavior with just a few lines in your Eleventy config file:&lt;/p&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; eleventyImageTransformPlugin &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;@11ty/eleventy-img&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//...&lt;/span&gt;
eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addPlugin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;eleventyImageTransformPlugin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;token literal-property property&quot;&gt;extensions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;token literal-property property&quot;&gt;formats&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;avif&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;webp&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Pick your own formats&lt;/span&gt;
 &lt;span class=&quot;token literal-property property&quot;&gt;widths&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;640&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;928&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;              &lt;span class=&quot;token comment&quot;&gt;// Pick your own srcset widths&lt;/span&gt;
 &lt;span class=&quot;token literal-property property&quot;&gt;defaultAttributes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token literal-property property&quot;&gt;loading&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;lazy&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token literal-property property&quot;&gt;sizes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;100vw&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token literal-property property&quot;&gt;decoding&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;async&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;Original post follows, preserved for posterity only...&lt;/em&gt;&lt;/p&gt;&lt;h2 id=&quot;background&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/#background&quot;&gt;Background&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I don’t use a ton of images on my site, but a while ago I wrote about &lt;a href=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick&quot;&gt;git cherry-picking&lt;/a&gt; and included a screenshot. At the time, I just dropped the image into the same folder as the blog post and kept going, but I knew there were a few problems with that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The original image was huge: more than 2,600 pixels wide, far wider than the maximum width it would ever display at.&lt;/li&gt;&lt;li&gt;All users would download the same 98 kB image, regardless of whether they were on desktop or mobile.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/before-dVf72sxnHqhO-640w.avif 640w, https://gfscott.com/blog/eleventy-img-without-central-image-directory/before-dVf72sxnHqhO-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/before-dVf72sxnHqhO-640w.webp 640w, https://gfscott.com/blog/eleventy-img-without-central-image-directory/before-dVf72sxnHqhO-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/before-dVf72sxnHqhO-640w.png&quot; alt=&quot;Screenshot of Chrome DevTools showing the network tab. The 98 kB image takes 104 milliseconds to download.&quot; width=&quot;928&quot; height=&quot;326&quot; srcset=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/before-dVf72sxnHqhO-640w.png 640w, https://gfscott.com/blog/eleventy-img-without-central-image-directory/before-dVf72sxnHqhO-928w.png 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;p&gt;I knew I wanted to eventually add some better image pre-processing with &lt;a href=&quot;https://www.11ty.dev/docs/plugins/image/&quot;&gt;Eleventy Image&lt;/a&gt;, the framework’s official plugin. This week I finally got around to implementing it.&lt;/p&gt;&lt;p&gt;Overall it was pretty straightforward, but I ran into a few gotchas and wanted to write them up.&lt;/p&gt;&lt;h2 id=&quot;the-goal&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/#the-goal&quot;&gt;The goal&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Here’s what I wanted the plugin to handle for me:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Criterion&lt;/th&gt;&lt;th&gt;Outcome&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Automatically resize and optimize images&lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Automatically support multiple modern image formats&lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Automatically output &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; elements with &lt;code&gt;srcset&lt;/code&gt;&lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;No changes to my existing site file structure&lt;/td&gt;&lt;td&gt;🤔&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;The first three were not a problem; all that stuff is supported by Eleventy Image out of the box. That last one, however, posed an issue.&lt;/p&gt;&lt;h2 id=&quot;my-site-directory-structure&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/#my-site-directory-structure&quot;&gt;My site directory structure&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Supporting my existing file structure turned out to the be hardest part. Instead of keeping images in a single folder at the project root, I like keeping images adjacent to the relevant markdown files, like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;src/
  ├─ blog/
  │   ├─ post-title/
  │   │   └─ index.md
  │   │   └─ image-1.png
  │   │   └─ image-2.png&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Putting on my information architect hat, I prefer this structure for a couple reasons:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;I find it more intuitive to keep related material together.&lt;/li&gt;&lt;li&gt;It doesn’t force me to come up with globally unique names for images; the directory structure itself ensures a unique path.&lt;/li&gt;&lt;li&gt;When writing in markdown, no messing around with &lt;code&gt;&amp;quot;../../../images/file.jpg&amp;quot;&lt;/code&gt;. You don’t have to remember anything about your directory structure in the moment, you can just write &lt;code&gt;&amp;quot;file.jpg&amp;quot;&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;This is speculation on my part and not a personal priority, but theoretically, image SEO is probably better if there are contextual keywords in the image path.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Eleventy Image, however, basically assumes that you’ll always specify image file locations from the project root, and that you want to &lt;a href=&quot;https://www.11ty.dev/docs/plugins/image/#output-directory&quot;&gt;output&lt;/a&gt; the processed images to a single centralized directory, something like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;src/
  ├─ blog/
  │   ├─ post-title.md
  ├─ img/
  │   └─ image-1.png
  │   └─ image-2.png&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is, for the record, a &lt;em&gt;totally reasonable choice&lt;/em&gt; that works for many, many people. But I already have something different and I wanted to support it as-is, without tearing up the entire site structure.&lt;/p&gt;&lt;h2 id=&quot;the-problem&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/#the-problem&quot;&gt;The problem&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I decided to go with with the &lt;a href=&quot;https://www.11ty.dev/docs/plugins/image/#asynchronous-shortcode&quot;&gt;Asynchronous shortcode method&lt;/a&gt;, and opted to implement the &lt;a href=&quot;https://www.11ty.dev/docs/plugins/image/#filter-diy-picture&quot;&gt;do-it-yourself &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; variant&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;With setup complete, I tested by updating an image file from the traditional markdown image syntax to the new Liquid shortcode equivalent:&lt;/p&gt;&lt;pre class=&quot;language-diff&quot;&gt;&lt;code class=&quot;language-diff&quot;&gt;&lt;span class=&quot;token deleted-sign deleted&quot;&gt;&lt;span class=&quot;token prefix deleted&quot;&gt;-&lt;/span&gt; ![Image of a GitHub pull request with a bunch of post-rebase commits that shouldn’t be there](rebase-hell.png)&lt;/span&gt;&lt;span class=&quot;token inserted-sign inserted&quot;&gt;&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt; {% image &#39;rebase-hell.png&#39;, &#39;Image of a GitHub pull request with a bunch of post-rebase commits that shouldn’t be there&#39; %}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But when the server reloaded, I got this error:&lt;/p&gt;&lt;pre class=&quot;language-txt&quot;&gt;&lt;code class=&quot;language-txt&quot;&gt;[11ty] 1. Having trouble rendering liquid template ./src/blog/git-rebase-using-git-cherry-pick/index.md (via TemplateContentRenderError)
[11ty] 2. ENOENT: no such file or directory, stat &#39;rebase-hell.png&#39;, file:./src/blog/git-rebase-using-git-cherry-pick/index.md, line:12, col:1 (via RenderError)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By default, Eleventy Image assumes that the shortcode image path starts relative to your Eleventy &lt;code&gt;inputDir&lt;/code&gt;, so it was looking for a file at &lt;code&gt;src/image.png&lt;/code&gt;, not &lt;code&gt;src/blog/post-title/image.png&lt;/code&gt;. Since there’s no file there, it throws an error and dies.&lt;/p&gt;&lt;h2 id=&quot;the-solution&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/#the-solution&quot;&gt;The solution&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I admit this took me quite a long time to troubleshoot, but eventually I figured out how to adjust the shortcode implementation so it would play nicely with my non-standard file directory setup.&lt;/p&gt;&lt;p&gt;This is a simplified version of the code in &lt;code&gt;eleventy.js&lt;/code&gt;, with the key changes highlighted:&lt;/p&gt;&lt;pre class=&quot;language-diff&quot;&gt;&lt;code class=&quot;language-diff&quot;&gt;const Image = require(&quot;@11ty/eleventy-img&quot;);

async function imageShortcode(src, alt, sizes = &quot;100vw&quot;) {

&lt;span class=&quot;token inserted-sign inserted&quot;&gt;&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt; // Prepend the image src with the full directory `inputPath`:
&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt; let imageSrc = `${path.dirname(this.page.inputPath)}/${src}`;&lt;/span&gt;
&lt;span class=&quot;token deleted-sign deleted&quot;&gt;&lt;span class=&quot;token prefix deleted&quot;&gt;-&lt;/span&gt; let metadata = await Image(src, {&lt;/span&gt;&lt;span class=&quot;token inserted-sign inserted&quot;&gt;&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt; let metadata = await Image(imageSrc, {&lt;/span&gt;&lt;span class=&quot;token unchanged&quot;&gt;&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;   widths: [300, 600],&lt;/span&gt;&lt;span class=&quot;token inserted-sign inserted&quot;&gt;&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt;   // Write processed images to the correct `outputPath`
&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt;   outputDir: path.dirname(this.page.outputPath),
&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt;   // Prepend the correct path to the image `src` value
&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt;   urlPath: this.page.url,&lt;/span&gt;&lt;span class=&quot;token unchanged&quot;&gt;&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt; });&lt;/span&gt;
&lt;span class=&quot;token unchanged&quot;&gt;&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt; return `&amp;lt;picture&gt;
&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;     // picture markup goes here
&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;   &amp;lt;/picture&gt;`;&lt;/span&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now the image plugin can properly fetch image files from their correct location in the file system, and places the processed versions in the equivalent structure in the site build. I don’t have to change my entire image filing practice, and everyone gets properly-sized, optimized images served to their browser, without any extra work on my part.&lt;/p&gt;&lt;p&gt;I’m pretty happy with the results. Most people will get a smaller, faster image now, with no extra work on my part:&lt;/p&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/after-mpJiMbGhfvi3-640w.avif 640w, https://gfscott.com/blog/eleventy-img-without-central-image-directory/after-mpJiMbGhfvi3-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/after-mpJiMbGhfvi3-640w.webp 640w, https://gfscott.com/blog/eleventy-img-without-central-image-directory/after-mpJiMbGhfvi3-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/after-mpJiMbGhfvi3-640w.png&quot; alt=&quot;Screenshot of Chrome DevTools showing the network tab. The 18 kB image takes 18 milliseconds to download.&quot; width=&quot;928&quot; height=&quot;326&quot; srcset=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/after-mpJiMbGhfvi3-640w.png 640w, https://gfscott.com/blog/eleventy-img-without-central-image-directory/after-mpJiMbGhfvi3-928w.png 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;h2 id=&quot;notes-and-caveats&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/eleventy-img-without-central-image-directory/#notes-and-caveats&quot;&gt;Notes and caveats&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;This is a win overall, but I still see some issues:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The solution here is very specific to my particular needs at this particular time. The changes as I’ve made them to the shortcode implementation are mutually exclusive to keeping a centralized image folder, and it shouldn’t necessarily be an either-or. I imagine there’s a more general solve for this issue; I took it only as far as I needed to and no further.&lt;/li&gt;&lt;li&gt;Without adding some very complicated additional logic, the automated processing is pretty one-size fits all, and you can see it in the visual quality. This is especially true for screenshots, where &lt;a href=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/rebase-hell.png&quot;&gt;the original PNG&lt;/a&gt; is very high-fidelity, while &lt;a href=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/rebase-hell-kaae-vmsjpew-928w.png&quot;&gt;the downsized and processed one&lt;/a&gt; shows some blurriness and artifacts.&lt;/li&gt;&lt;li&gt;Even worse, even though the processed PNG file has &lt;em&gt;way&lt;/em&gt; smaller dimensions, it’s nearly 50% bigger in terms of kilobytes served. The .avif and .webp versions are far smaller and a majority of users will be served those versions (WebP is supported by &lt;a href=&quot;https://caniuse.com/webp&quot;&gt;~94%&lt;/a&gt; of browsers, AVIF by &lt;a href=&quot;https://caniuse.com/avif&quot;&gt;~69%&lt;/a&gt;) so this is a tradeoff I’m willing to make. But still kind of irksome.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Implementing dark mode, for real this time</title>
    <link href="https://gfscott.com/blog/dark-mode-for-real/" />
    <updated>2022-02-21T17:00:00Z</updated>
    <id>https://gfscott.com/blog/dark-mode-for-real/</id>
    <content type="html">&lt;p&gt;Years ago I wrote a blog post on &lt;a href=&quot;https://gfscott.com/blog/dark-mode-with-css-variables/&quot;&gt;creating dark mode with CSS variables&lt;/a&gt;. At the time it was more about jotting down the basic idea for my own benefit, but I never actually implemented it. A few weeks ago I felt the itch and decided I wanted to finally follow through for real.&lt;/p&gt;&lt;p&gt;I’m pleased to report that the basic idea works well — you’re looking at it right now — but in putting it into practice, I learned a bunch of new things: some quirks of the &lt;code&gt;matchMedia&lt;/code&gt; object, setting and getting cookie values, and the finer points of HSL color spaces, among others. So this is to follow up on that original post and elaborate on some of the complexities I had originally hand-waved away.&lt;/p&gt;&lt;p&gt;This post is in four parts:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://gfscott.com/blog/dark-mode-for-real/#creating-themes-with-css-variables-variables&quot;&gt;Creating themes with CSS variables {#variables}&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://gfscott.com/blog/dark-mode-for-real/#saving-the-preference-as-a-cookie-cookies&quot;&gt;Saving the preference as a cookie {#cookies}&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://gfscott.com/blog/dark-mode-for-real/#building-the-theme-toggler-toggle&quot;&gt;Building the theme toggler {#toggle}&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://gfscott.com/blog/dark-mode-for-real/#styling-the-checkbox-toggle-styling&quot;&gt;Styling the checkbox toggle {#styling}&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;variables&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-for-real/#variables&quot;&gt;Creating themes with CSS variables&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The strategy here is basically unchanged from my original post: create CSS variables for each colour, plus variations controlled by some selector. In this case, I&#39;m using a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes&quot;&gt;custom HTML attribute&lt;/a&gt; on the &lt;code&gt;body&lt;/code&gt; tag to toggle the theme on and off.&lt;/p&gt;&lt;p&gt;I opted to update my colour declarations to HSL so it was easy to keep things like hue and saturation consistent; I think it makes the colours feel more harmonious overall, both within and between the two themes. I think it’s also slightly easier to reason about compared to hex colours.&lt;/p&gt;&lt;p&gt;Here’s the relevant CSS:&lt;/p&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--hue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 215&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--sat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorBgPrimary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--hue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--sat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 100%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorFgPrimary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--hue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--sat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 20%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorFgSecondary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--hue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 25%&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 45%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorAccentPrimary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--hue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--sat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; * 2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 35%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorAccentSecondary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--hue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--sat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; * 2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 50%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;body[data-theme=&quot;dark&quot;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--hue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 200&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorBgPrimary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--hue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--sat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 15%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorFgPrimary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--hue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--sat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 90%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorFgSecondary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--hue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 25%&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 70%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorAccentPrimary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;30&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--sat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; * 2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 75%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorAccentSecondary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hsl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;30&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;calc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--sat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; * 2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 65%&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;cookies&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-for-real/#cookies&quot;&gt;Saving the preference as a cookie&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I knew I needed some way to save the user’s theme preference in the browser, since the alternatives were bad:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;I’d have to pick one theme to be the default, and if the user wanted something different they’d have to toggle the colour-scheme with every page load.&lt;/li&gt;&lt;li&gt;All pages would load with the colour-scheme preference set at the OS level, and either have to toggle on each page load or have no option to switch at all.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So I knew I needed to set some sort of cookie to persist the user’s preference across page-loads and between sessions. Since I only have two theme options, light and dark, it felt like overkill to use a third-party cookie library, so I went looking for a minimum-viable cookie management strategy. In the end, this was my approach:&lt;/p&gt;&lt;p&gt;The first function reads a cookie value, and comes from &lt;a href=&quot;https://gist.github.com/wpsmith/6cf23551dd140fb72ae7&quot;&gt;this Gist by @wpsmith&lt;/a&gt;:&lt;/p&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCookie&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookie&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; parts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;parts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; parts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;shift&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The second function sets a key-value pair as a cookie. I just hard-coded the options, since my use-case is so limited. This cookie persists for a year (60 seconds, times 60 minutes in an hour, times 24 hours in a day, times 365 days). It also enforces HTTPS and &lt;code&gt;SameSite&lt;/code&gt; which is perhaps a little paranoid, given that the cookie value doesn’t contain information of any value. But then again, why &lt;em&gt;not&lt;/em&gt; default the most secure behaviour?&lt;/p&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setCookie&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;key&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;value&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;; path=/; max-age=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;365&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;; SameSite=Strict; Secure;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Uglified, these two helper functions add up to just over 200 bytes, so I opted to just put them inline in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;&lt;h2 id=&quot;toggle&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-for-real/#toggle&quot;&gt;Building the theme toggler&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The actual theme-toggler affordance is a checkbox, which controls a &lt;code&gt;data-theme&lt;/code&gt; custom attribute on the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag. By default it’s set to &lt;code&gt;light&lt;/code&gt;:&lt;/p&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;data-theme&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;light&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
...
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;theme_toggler&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;checkbox&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;theme_toggler&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;visuallyhidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The checkbox has some extra CSS jazz to make it look nice (see below) but it&#39;s still just a checkbox. The logic for manipulating that checkbox, however, had a few gotchas:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia&quot;&gt;&lt;code&gt;window.matchMedia&lt;/code&gt; browser API&lt;/a&gt; seems undercooked to me. It requires a bunch of hacky string-parsing and generally feels brittle and error-prone.&lt;/li&gt;&lt;li&gt;&lt;code&gt;matchMedia&lt;/code&gt; can also change at the OS level at any time, so that’s an event you need to listen for and (optionally) respond to.&lt;/li&gt;&lt;li&gt;Figuring out which default behaviours to fall back to got me a little twisted up. My first implementation was way too complex because I wasn&#39;t thinking from the default and building up the complexity from there.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I can’t do much about the quirks of &lt;code&gt;matchMedia&lt;/code&gt;, but I can try to explain the theme-switching logic I came up with:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;If there’s no &lt;code&gt;theme&lt;/code&gt; cookie, get the browser setting, use that theme, and set a new cookie.&lt;/li&gt;&lt;li&gt;If there &lt;em&gt;is&lt;/em&gt; a &lt;code&gt;theme&lt;/code&gt; cookie, use that theme.&lt;/li&gt;&lt;li&gt;If the user manually toggles the theme, use that theme, and update the cookie.&lt;/li&gt;&lt;li&gt;If the browser/OS-level theme preference changes mid-session, get the new browser setting, use that theme, and update the cookie.&lt;/li&gt;&lt;li&gt;If all else fails, fall back to the default light mode. If the user has turned off JavaScript, for instance, they’ll see the light theme.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Below is the script. I’ve annotated it with comments to explain what’s going on:&lt;/p&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// The checkbox&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; toggler &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;#theme_toggler&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Caching the matchMedia color-scheme preference as a variable.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; media &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;matchMedia&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;(prefers-color-scheme: dark)&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Function to return the system theme. `matchMedia` returns `matches: true` if&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// the browser setting matches the media query passed above. It returns a&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// string, either &quot;dark&quot; or &quot;light&quot;.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getSystemTheme&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; media&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;matches &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dark&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;light&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Function to perform all the steps involved in setting a new color scheme.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Accepts a color string&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;setTheme&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// set the cookie with the theme preference, as a key-value pair.,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setCookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;theme&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; color&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// update the custom attribute on the body tag with the new theme&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;body&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dataset&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; color&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// update the state of the checkbox to match the new theme&lt;/span&gt;
  toggler&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;checked &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; color &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;dark&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// handle the labelling of the checkbox for accessibility.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; other &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; color &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;dark&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;light&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;dark&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  toggler&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ariaLabel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Switch to &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;other&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; mode&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// If there’s a `theme` cookie, use it to set the color scheme&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;theme&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setTheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getCookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;theme&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// ...Otherwise, set the color scheme based on the browser preference&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setTheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getSystemTheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Listen for changes to the checkbox state &lt;/span&gt;
toggler&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;change&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  toggler&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;checked &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setTheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;dark&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setTheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;light&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Listen for changes to the browser preference state&lt;/span&gt;
media&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;onchange&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setTheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getSystemTheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At this point the toggler was working as intended:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;On initial page load, the user gets their OS theme preference&lt;/li&gt;&lt;li&gt;The toggle reflects the current theme preference&lt;/li&gt;&lt;li&gt;The toggle can be checked on or off to change that preference&lt;/li&gt;&lt;li&gt;The preference is saved as a cookie and persists across page loads and sessions.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;However, it just looked like a checkbox, and I figured I could make it look a little nicer.&lt;/p&gt;&lt;h2 id=&quot;styling&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-for-real/#styling&quot;&gt;Styling the checkbox toggle&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I had two priorities with the more elaborate toggle style:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Keep it accessible by keyboard&lt;/li&gt;&lt;li&gt;Tweak the visuals using only CSS&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Hiding the default checkbox UI while remaining accessible for screen readers and board navigation seems pretty straightforward, using the &lt;a href=&quot;https://www.a11yproject.com/posts/how-to-hide-content/&quot;&gt;“visually hidden” method&lt;/a&gt;:&lt;/p&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* Accessibly hide elements */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.visuallyhidden&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;clip&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;0 0 0 0&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; -1px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;overflow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; hidden&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The final step was deciding what the custom toggle should actually look like. I didn’t want to use a hefty SVG or have to figure out a ton of complex animation, so I’ve opted to style using only Unicode emoji — the &amp;quot;&lt;a href=&quot;https://unicode-table.com/en/1F506/&quot;&gt;High Brightness Symbol&lt;/a&gt;&amp;quot; when in dark mode, and the &amp;quot;&lt;a href=&quot;https://unicode-table.com/en/1F312/&quot;&gt;Waxing Crescent Moon Symbol&lt;/a&gt;&amp;quot; when in light mode.&lt;/p&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* Toggler control, absolute-positioned in the top right of the viewport */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#theme&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; grid&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;place-content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; solid 2px transparent&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 3px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* Toggler focus outline style. The checkbox can still be focused,
   even though it’s visually hidden */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;#theme:focus-within&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;border-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--colorAccentPrimary&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* In dark mode, show brightness symbol. In light mode, show moon symbol */&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;body[data-theme=&quot;dark&quot;] #theme::before&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&#92;1F506&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;body[data-theme=&quot;light&quot;] #theme::before&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&#92;1F312&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Overall, this was a really good exercise in moving from theory to practice, and I’m glad that I (finally!) followed through. Plus I now see some additional threads to pull on, such as moving from just two binary options to a larger selection of themes.&lt;/p&gt;&lt;p&gt;I’ve boiled down the whole setup into a &lt;a href=&quot;https://gist.github.com/gfscott/b581c1fa0cc6485ca2acf6895d977fea&quot;&gt;Gist with the most essential moving parts&lt;/a&gt;. Feel free to comment there with any questions or refinements.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The books I read in 2021</title>
    <link href="https://gfscott.com/blog/books-2021/" />
    <updated>2022-01-01T17:00:00Z</updated>
    <id>https://gfscott.com/blog/books-2021/</id>
    <content type="html">&lt;p&gt;Not a terribly long list this year but I went back to pick up some previous reads that I&#39;d tried and failed to get through, so I’ll take it. In alphabetical order:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;Age of Extremes: The Short Twentieth Century, 1914–1991&lt;/em&gt;, by Eric Hobsbawm&lt;/li&gt;&lt;li&gt;&lt;em&gt;A Children’s Bible&lt;/em&gt;, by Lydia Millet&lt;/li&gt;&lt;li&gt;&lt;em&gt;Death in Her Hands&lt;/em&gt;, by Ottessa Moshfegh&lt;/li&gt;&lt;li&gt;&lt;em&gt;Expressive Design Systems&lt;/em&gt;, by Yesenia Perez-Cruz&lt;/li&gt;&lt;li&gt;&lt;em&gt;Filthy Animals&lt;/em&gt;, by Brandon Taylor&lt;/li&gt;&lt;li&gt;&lt;em&gt;Foundation and Empire&lt;/em&gt;, by Isaac Asimov&lt;/li&gt;&lt;li&gt;&lt;em&gt;Fulfillment: Winning and Losing in One-Click America&lt;/em&gt;, by Alec MacGillis&lt;/li&gt;&lt;li&gt;&lt;em&gt;Homage to Catalonia&lt;/em&gt;, by George Orwell&lt;/li&gt;&lt;li&gt;&lt;em&gt;Intimacies&lt;/em&gt;, by Katie Kitamura&lt;/li&gt;&lt;li&gt;&lt;em&gt;No One Is Talking About This&lt;/em&gt;, by Patricia Lockwood&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Pale King&lt;/em&gt;, by David Foster Wallace&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Rest is Noise: Listening to the Twentieth Century&lt;/em&gt;, by Alex Ross&lt;/li&gt;&lt;li&gt;&lt;em&gt;Seeing Like a State: How Certain Schemes to Improve the Human Condition Have Failed&lt;/em&gt;, by James C. Scott&lt;/li&gt;&lt;li&gt;&lt;em&gt;Super Pumped: The Battle for Uber&lt;/em&gt;, by Mike Isaac&lt;/li&gt;&lt;li&gt;&lt;em&gt;Vesper Flights&lt;/em&gt;, by Helen Macdonald&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;notes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/books-2021/#notes&quot;&gt;Notes&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;A Children’s Bible&lt;/em&gt; is so beautiful, and so bleak. Dramatizing climate change strikes me as something that&#39;s quite hard to do, but by turning it into a parable about parents and children, Lydia Millet focuses what would otherwise be free-floating anxiety into a terse, brutal narrative. I read it early in the year and while the plot hasn&#39;t really stuck with me, the mood remains incredibly vivid.&lt;/li&gt;&lt;li&gt;After saying I didn&#39;t really dig &lt;em&gt;Foundation&lt;/em&gt; &lt;a href=&quot;https://gfscott.com/blog/books-2020/&quot;&gt;last year&lt;/a&gt;, I read the second book in the series anyway, &lt;em&gt;Foundation and Empire&lt;/em&gt;. It&#39;s modestly better, with, you know, some actual characters. Will I keep reading this series I claim not to like despite myself? Stay tuned!&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Pale King&lt;/em&gt; has an understandable reputation for being somewhat impenetrable but once I got into it I was so hooked. The current cultural consensus on David Foster Wallace seems to run from eye-rolls to outright condemnation, all of which I get. But the work: it still sings.&lt;/li&gt;&lt;li&gt;The central idea of &lt;em&gt;Seeing Like a State&lt;/em&gt; — that technocratic authorities wind up distorting the communities they&#39;re meant to serve through over-determined metrics — explains so much about contemporary social dysfunction at every level. But the book is simply too long and trots out too many repetitive examples to prove the point. Good stuff in the home stretch about &lt;em&gt;metis&lt;/em&gt; vs. &lt;em&gt;techne&lt;/em&gt;, and the need to respect local knowledge and craft practices over abstract expertise.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>How we rebuilt Shopify’s developer docs (again)</title>
    <link href="https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/" />
    <updated>2021-11-24T22:30:00Z</updated>
    <id>https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/</id>
    <content type="html">&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;March 2025&lt;/strong&gt; I recently discovered that the &lt;a href=&quot;https://ux.shopify.com&quot;&gt;Shopify UX blog&lt;/a&gt; was shut down and its archives are no longer online. So I&#39;m updating this post with the full text of the original article. Many of the links are to the Wayback Machine, because &lt;a href=&quot;https://shopify.dev&quot;&gt;Shopify.dev&lt;/a&gt; itself has changed again since I originally wrote this.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Last week we &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://twitter.com/ShopifyDevs/status/1461049211554721793&quot;&gt;unveiled Dark Mode&lt;/a&gt; for &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/&quot;&gt;Shopify.dev&lt;/a&gt;, the developer docs hub for the Shopify platform. But that’s just the latest in a series of major design updates we’ve made to Shopify.dev since relaunching the site at &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://unite.shopify.com/&quot;&gt;Unite&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;When we first launched Shopify.dev in February 2020, I wrote about &lt;a href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs&quot;&gt;the design process that led us there&lt;/a&gt; as a Senior Content Designer on the team. But even back then, we knew that what we’d built was Version 1.0, and that the site would need to evolve as we observed how Shopify’s vibrant community of developers used it in the real world.&lt;/p&gt;&lt;p&gt;Throughout 2020, we saw some things working really well — and some that needed a rethink. So I wanted to revisit this subject to talk about some of the updates we’ve made to the site’s information architecture, and why we made them.&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/before-after-hfNa1nvC7lE3-640w.avif 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/before-after-hfNa1nvC7lE3-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/before-after-hfNa1nvC7lE3-640w.webp 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/before-after-hfNa1nvC7lE3-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/before-after-hfNa1nvC7lE3-640w.png&quot; alt=&quot;The original Shopify.dev, left, and the redesigned site today.&quot; width=&quot;928&quot; height=&quot;302&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/before-after-hfNa1nvC7lE3-640w.png 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/before-after-hfNa1nvC7lE3-928w.png 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;figcaption&gt;The original Shopify.dev, left, and the redesigned site today.&lt;/figcaption&gt;&lt;/figure&gt;&lt;h2 id=&quot;what-needed-updating%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/#what-needed-updating%3F&quot;&gt;What needed updating?&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;In the &lt;a href=&quot;https://web.archive.org/web/20220906052332/http://web.archive.org/web/20200302084924/https://shopify.dev/concepts/shopify-introduction&quot;&gt;original design&lt;/a&gt; of Shopify.dev, we sorted the site into four main content buckets:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Concepts:&lt;/strong&gt; Key context about the platform&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Docs:&lt;/strong&gt; References for APIs and other key products&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Tutorials:&lt;/strong&gt; Narrative how-to instructional content&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Tools:&lt;/strong&gt; Software to accelerate the build process&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There were lots of things we liked about this model:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;It was useful to group together high-level information about the platform in ‘Concepts’ — our quantitative and qualitative research both suggested quality user engagement with that part of the site.&lt;/li&gt;&lt;li&gt;Separating Tutorials (step-by-step tasks that you need to complete) from References (comprehensive descriptions of APIs) was the right move. While these types of docs are often used in tandem, users consult them for different reasons, and a clean distinction allowed us to refine the design experience of each document type.&lt;/li&gt;&lt;li&gt;Breaking out ‘Tools’ into their own standalone section made our tooling more findable and reduced duplication in other parts of the docs. For instance, &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/apps/tools/app-bridge&quot;&gt;App Bridge&lt;/a&gt; is required for a variety of development tasks on the platform. Having a single home for the library in the Tools section made it easy and consistent to link to.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;But as we lived with the site over its first year in the wild, we observed some other things that didn’t work so well:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The ‘Docs’ label was confusing. People thought of the whole site as ‘the docs’ and so a section called ‘Docs’ (the largest section by number of pages!) wasn’t very meaningful to them. We needed to refine some of the language and labels we were using.&lt;/li&gt;&lt;li&gt;Grouping by information type — conceptual information, tutorial information, reference information — made perfect sense on paper but didn’t work as well in practice. Tutorial content, in particular, struggled under this model, because sheer volume made it hard to do second-order navigation. If you ever landed on &lt;a href=&quot;https://web.archive.org/web/20220906052332/http://web.archive.org/web/20210411121326/https://shopify.dev/tutorials&quot;&gt;this page&lt;/a&gt;, with hundreds of individual tutorials forming a wall of text, then you felt this pain as you hunted for the particular problem you needed to solve. We experimented with a bunch of design ideas to mitigate this issue but none of them ever broke the logjam.&lt;/li&gt;&lt;/ul&gt;&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/tutorials-5Z-Ow_FjMkJW-640w.avif 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/tutorials-5Z-Ow_FjMkJW-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/tutorials-5Z-Ow_FjMkJW-640w.webp 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/tutorials-5Z-Ow_FjMkJW-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/tutorials-5Z-Ow_FjMkJW-640w.png&quot; alt=&quot;The Tutorials page of the original Shopify.dev.&quot; width=&quot;928&quot; height=&quot;1507&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/tutorials-5Z-Ow_FjMkJW-640w.png 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/tutorials-5Z-Ow_FjMkJW-928w.png 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;figcaption&gt;The Tutorials page of the original Shopify.dev. Dense though it is, this is still only about a third of the page content.&lt;/figcaption&gt;&lt;/figure&gt;&lt;ul&gt;&lt;li&gt;Grouping by information type had a second flaw that was less obvious but became clearer over time as we talked to more developers in the community. What our users needed from us wasn’t just information, but &lt;em&gt;direction&lt;/em&gt;. Our docs were good at describing the different parts of the platform and individual developer tasks, but we hadn’t strung them together into a clear narrative. New users arrived at the site thinking, “I just want to build a theme”, or “I just want to build an app”, and we weren’t clearly laying that process out for them. All the pieces of the puzzle were there, scattered around various parts of the site, but we hadn’t been opinionated enough about Shopify’s vision of what a great app or theme looked like.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Based on these observations, we decided our next design iteration needed to better optimize for real-world developer goals, articulate clearer narratives, and lead by example. We also recruited a small group of developers from the community — some newer to the platform, some quite experienced, and from all around the world—to validate some early design concepts. To get as close as possible to the actual experience of navigating a real website, I built a few low-fidelity versions so our pilot users could click around actual IA prototypes. (Shoutout to &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://www.11ty.dev/&quot;&gt;11ty&lt;/a&gt; and &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://alpinejs.dev/&quot;&gt;Alpine.js&lt;/a&gt; for making it remarkably easy to build these quick-and-dirty experiments.)&lt;/p&gt;&lt;p&gt;Let’s dig into three specific content design decisions we made in pursuit of our goals:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Re-orienting our main navigation from ‘information type’ to ‘developer goal’&lt;/li&gt;&lt;li&gt;Strengthening our ‘Getting Started’ narratives&lt;/li&gt;&lt;li&gt;Scaling up our approach to example code&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;from-%E2%80%98information-type%E2%80%99-to-%E2%80%98developer-goal%E2%80%99&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/#from-%E2%80%98information-type%E2%80%99-to-%E2%80%98developer-goal%E2%80%99&quot;&gt;From ‘information type’ to ‘developer goal’&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;First, we reorganized the site not by the type of information (concepts, references, tutorials), but by the developer’s goal: What are you trying to build?&lt;/p&gt;&lt;p&gt;Today, there are four basic “things” you can build on Shopify’s platform: Apps, Themes, Custom Storefronts, and Marketplaces. These things can overlap in some cases (and lots of developers work across categories, of course ) but in general, they’re distinct development activities.&lt;/p&gt;&lt;p&gt;For instance, Apps and Themes have different primary audiences. Apps are focused on extending and enhancing the platform for merchants, the millions of entrepreneurs who use Shopify to sell their products. Meanwhile, Themes are all about creating refined online shopping experiences for the tens of millions of customers who purchase things from stores running on Shopify.&lt;/p&gt;&lt;p&gt;Apps and Themes are also built using different technologies. Apps are web applications hosted by developers and written in any language, that interact with Shopify through a set of APIs, while Themes are collections of front-end template code, written in Liquid, that are uploaded to Shopify and run on our infrastructure. Apps and Themes have different developer tooling and design paradigms, and the developers who create them have different business models.&lt;/p&gt;&lt;p&gt;All of this may seem obvious — developers know, intuitively, that Apps and Themes are different things. But it was important to be crystal-clear about how we would recognize and enforce these conceptual boundaries so that this information architecture can scale with the platform. Today there are four ‘Things’ on the platform, but soon there are likely to be more, and we wanted to have some guardrails in place so our docs can grow in a way that’s orderly and predictable for everyone.&lt;/p&gt;&lt;h3 id=&quot;the-road-not-taken&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/#the-road-not-taken&quot;&gt;The road not taken&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;This way of organizing the site was not the only option we considered. A strong alternative was to sort our docs, not by the developer’s end goal, but by their stage in a typical software development lifecycle.&lt;/p&gt;&lt;p&gt;This would have led to a site organized similarly to &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://developers.apple.com/&quot;&gt;Apple’s dev docs&lt;/a&gt;. Apple’s developer hub navigation is built around a typical developer journey:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Discover:&lt;/strong&gt; Exploring what’s possible on the platform&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Design:&lt;/strong&gt; Defining the desired user experience&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Develop:&lt;/strong&gt; Realizing that experience in code&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Distribute:&lt;/strong&gt; Selling the final software product to end users&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There’s a lot to like about this model:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;It scales well, in the sense that it can accommodate a wide diversity of software types.&lt;/li&gt;&lt;li&gt;It also scales well in terms of the absolute number of types — this structure works just as well for 3 types or 300.&lt;/li&gt;&lt;li&gt;It proceeds linearly through a typical developer journey, guiding the user through each phase of a successful project.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So why didn’t we take this path?&lt;/p&gt;&lt;p&gt;The first answer is quantitative: we simply don’t (yet!) need to accommodate the sheer scale that Apple does, supporting multiple development ecosystems around multiple hardware and software platforms. That day is coming, but it’s not here yet. (Maybe you’d like to &lt;a href=&quot;https://www.shopify.com/careers&quot;&gt;help us get there&lt;/a&gt;?)&lt;/p&gt;&lt;p&gt;The second answer is qualitative, and it came from the many conversations we had with developer partners as we were evaluating the existing website and planning this round of design updates. Unless prompted, developers simply didn’t talk about their experience with Shopify in terms of which ‘stage of their journey’ they were in; they talked about the stuff they were building. They had no common language for ‘being in the discovery phase’ or ‘starting the distribution process’; they simply didn’t use that vocabulary. But they could always tell us — usually, with great enthusiasm — what they were building, and how those things worked together to solve merchant problems.&lt;/p&gt;&lt;p&gt;Ultimately, we decided this was the right evolution for the site’s information architecture: &lt;em&gt;Build the Thing&lt;/em&gt;. Developers come to Shopify’s platform with an incredibly diverse range of needs, ambitions, experience levels, team structures, and business models. Real-world software development rarely follows a straight, tidy path. What united every developer we talked to, however, was that they had a clear vision of their end goal: to build a great app, or a gorgeous theme, or a unique custom storefront. We reasoned that organizing the site this way would help them get there faster, and keep their eyes on that horizon, regardless of the inevitable bumps in the road or detours along the way.&lt;/p&gt;&lt;h3 id=&quot;the-exception&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/#the-exception&quot;&gt;The exception&lt;/a&gt;&lt;/h3&gt;&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/product-create-1zD3mAcH-5cY-640w.avif 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/product-create-1zD3mAcH-5cY-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/product-create-1zD3mAcH-5cY-640w.webp 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/product-create-1zD3mAcH-5cY-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/product-create-1zD3mAcH-5cY-640w.png&quot; alt=&quot;productCreate mutation API reference screenshot&quot; width=&quot;928&quot; height=&quot;708&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/product-create-1zD3mAcH-5cY-640w.png 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/product-create-1zD3mAcH-5cY-928w.png 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;figcaption&gt;&lt;p&gt;API references, such as this page for the &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/api/admin-graphql/2021-10/mutations/productCreate&quot;&gt;productCreate mutation&lt;/a&gt;, need to support frequent use and repeat visits.&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;We did preserve one important division based on information type, however: API references still reside in a &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/api&quot;&gt;dedicated section&lt;/a&gt; of the site. By studying the usage data, as well as talking with the development community, we know that references are among the most frequently used parts of the docs. That makes sense: once you’ve worked through a tutorial a few times, you’ve probably grasped most of the basic concepts and know the steps involved. It’s not material you need to revisit much.&lt;/p&gt;&lt;p&gt;But a reference is something you’ll come back to repeatedly throughout your build process, regardless of your experience level. We often heard from developers that they keep the &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/api/admin-graphql&quot;&gt;Admin API docs&lt;/a&gt; or the &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/api/liquid&quot;&gt;Liquid reference&lt;/a&gt; open in a browser tab all the time.&lt;/p&gt;&lt;p&gt;As a result, we wanted to ensure that references remain easily and quickly accessible from anywhere on the site. That core principle influenced a bunch of downstream design choices, such as putting the most popular references in a dropdown menu for quicker navigation, and using a shorter, memorable, and easier-to-type URL, &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/api&quot;&gt;shopify.dev/api&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;strengthening-our-narratives&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/#strengthening-our-narratives&quot;&gt;Strengthening our narratives&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;So our first step was reorganizing the docs architecture around the end goal: build an app, a theme, or a custom storefront. It’s important to know your destination. But what about how you get there?&lt;/p&gt;&lt;p&gt;One of the things developers appreciate about Shopify’s platform is how flexible it is — especially when it comes to building apps. After all, most apps in our ecosystem today are web apps that get embedded in the Shopify admin. As an app developer, it’s up to you to pick your back-end programming language, user interface frameworks, and hosting infrastructure. If you really wanted to write your app in COBOL and run it on a cluster of Raspberry Pis in your basement… well, you probably &lt;em&gt;could&lt;/em&gt;, technically! But is that actually the right choice?&lt;/p&gt;&lt;p&gt;In the past, we often weren’t clear enough about what Shopify considers best practice on our platform. It was like we dumped a giant box of Lego on the floor and said, “Go for it.” But we actually do have opinions about how to build for Shopify, and we’re committed to articulating that vision more clearly.&lt;/p&gt;&lt;p&gt;That commitment manifests in a few ways in this latest design iteration.&lt;/p&gt;&lt;p&gt;First, we created more ‘Getting started’ tutorials that are focused on helping you build consistent, baseline implementations. Whether you’re getting started with &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/apps/getting-started&quot;&gt;a minimum-viable boilerplate app&lt;/a&gt;, or adding power and complexity to &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/apps/getting-started&quot;&gt;manage product subscriptions&lt;/a&gt;, we’re embedding Shopify’s opinionated development paths throughout our docs in a clearer and more consistent way. The goal is that all developers should be able to create a baseline experience representing the state of the art in terms of security, privacy, and performance.&lt;/p&gt;&lt;p&gt;Second, we created sections devoted to &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/apps/best-practices&quot;&gt;literally&lt;/a&gt; &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/themes/best-practices&quot;&gt;articulating&lt;/a&gt; what we consider to be best practice on the platform. ‘Best practices’ are all the things that we don’t enforce programmatically through the architecture of the product, nor through human-mediated measures like the app and theme review process.&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/narrative-OYbOKjaUpCdg-640w.avif 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/narrative-OYbOKjaUpCdg-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/narrative-OYbOKjaUpCdg-640w.webp 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/narrative-OYbOKjaUpCdg-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/narrative-OYbOKjaUpCdg-640w.png&quot; alt=&quot;“Building with sections” tutorial screenshot&quot; width=&quot;928&quot; height=&quot;644&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/narrative-OYbOKjaUpCdg-640w.png 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/narrative-OYbOKjaUpCdg-928w.png 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;/figure&gt;&lt;p&gt;We built out new content about what Shopify considers best practices for many common development activities. The aim is to help all developers consistently build more performant, accessible, and maintainable projects.&lt;/p&gt;&lt;p&gt;Consider guidance like &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/apps/best-practices/performance#reduce-javascript-usage&quot;&gt;Reduce JavaScript usage&lt;/a&gt;. The proper amount of JavaScript for your app to load is “just enough to do what it needs to do”. We can’t tell you what that magic number of bytes is. What we &lt;em&gt;can&lt;/em&gt; tell you is what we believe good performance looks like, so that the whole developer community has a common understanding, and merchants and their customers get more consistent, performant user experiences.&lt;/p&gt;&lt;h2 id=&quot;scaling-up-our-approach-to-example-code&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/#scaling-up-our-approach-to-example-code&quot;&gt;Scaling up our approach to example code&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;With the initial launch of Shopify.dev, we had sorted all our tutorial content into one big Tutorials section. One thing that became clear was that we had a lot of things that were called ‘tutorials’ that were really just a list of examples. Take a tutorial called &lt;a href=&quot;https://web.archive.org/web/20220906052332/http://web.archive.org/web/20201130114833/https://shopify.dev/tutorials/use-statuses-to-identify-filter-and-manage-products&quot;&gt;Use statuses to identify, filter, and manage products&lt;/a&gt;. It’s a very long page but its substance is honestly pretty basic: four example API calls to query or edit a product status.&lt;/p&gt;&lt;p&gt;There were two reasons we wanted to cut down on this type of tutorial:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Developers told us that the API reference was a more natural place to look for examples&lt;/li&gt;&lt;li&gt;Maintainability for our internal documentation teams&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;We’ve heard it loud and clear from developers: good examples are the fastest way to get productive with an API, and those examples should be integrated directly into the API reference. When you’re writing code and referring to a reference doc to complete some specific task, it’s less disruptive to have example code immediately available in that browser tab, rather than having to navigate to a different part of the docs to look up an example. So maintaining that flow state and reducing context-switching for developers was a high priority.&lt;/p&gt;&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/example-code-KJ50BUib2jsz-640w.avif 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/example-code-KJ50BUib2jsz-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/example-code-KJ50BUib2jsz-640w.webp 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/example-code-KJ50BUib2jsz-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/example-code-KJ50BUib2jsz-640w.png&quot; alt=&quot;API reference example code screenshot&quot; width=&quot;928&quot; height=&quot;764&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/example-code-KJ50BUib2jsz-640w.png 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/example-code-KJ50BUib2jsz-928w.png 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;/figure&gt;&lt;p&gt;API examples such as this one are now more comprehensive, showing requests in multiple coding languages and sample responses. Some of this content existed before, but it’s now managed in a more scalable fashion.&lt;/p&gt;&lt;p&gt;The second reason was scalability and maintainability for Shopify’s docs team. Those standalone example ‘tutorials’ were fully manually authored, usually by a technical writer working from the API reference. That means that updates were also manual, and required repeatedly reviewing every tutorial with each quarterly API version release to validate that everything still worked. With a few dozen of these tutorials, that’s a hassle, but it’s doable. But now that we’re greatly increasing the volume of examples we provide, it simply wasn’t going to scale.&lt;/p&gt;&lt;p&gt;Shopify’s REST API reference docs are generated by unit tests in the codebase itself, while GraphQL references are generated by the API schema. That means every engineer at the company who works on API functionality documents that feature themselves, in code.&lt;/p&gt;&lt;p&gt;With the redesign of the Admin API references we unveiled in September, all API examples, for both &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/api/admin-rest&quot;&gt;REST&lt;/a&gt; and &lt;a href=&quot;https://web.archive.org/web/20220906052332/https://shopify.dev/api/admin-graphql&quot;&gt;GraphQL&lt;/a&gt;, are now created and managed the same way, and reside directly in the reference codebase.&lt;/p&gt;&lt;p&gt;There are a lot of benefits to this new system:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;We can distribute the workload of creating example code across every platform engineering team. A small incremental effort by each individual creates a huge increase in value for the developer community as a whole.&lt;/li&gt;&lt;li&gt;Examples are now maintained by the same engineering teams who create the API functionality itself. That means they have a detailed understanding of the business problem the functionality solves, and generally create more realistic examples as a result. Plus, they typically know all the edge-cases, nuances, and potential gotchas that developers need to know about.&lt;/li&gt;&lt;li&gt;API functionality and examples are more tightly coupled in the codebase, so updating the API and updating the code examples can be done simultaneously — often as part of the same pull request—instead of having to do the work in two separate code repositories. Code examples are far less likely to get out of sync with the actual product this way.&lt;/li&gt;&lt;li&gt;We can programmatically test example code for correctness as part of our continuous integration and deployment (CI/CD) pipeline. This was never practical in our old system, where example code was manually authored directly inside Markdown files.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Growing our collection of example code in the API reference is definitely a work in progress, but we’ve already been able to dramatically increase the total volume of example code in our new API references, and there’s much more planned in the coming months.&lt;/p&gt;&lt;h2 id=&quot;documentation-is-never-done&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/#documentation-is-never-done&quot;&gt;Documentation is never done&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;It’s been another big year of change for Shopify’s developer docs, and a big leap for the evolution of our content and information architecture.&lt;/p&gt;&lt;p&gt;Shopify’s platform is growing and changing all the time, and Shopify.dev will continue evolving with it. We’ll keep listening and learning, and we hope you’ll continue to share your feedback with us. But for now, go &lt;a href=&quot;https://shopify.dev/&quot;&gt;build the thing&lt;/a&gt;!&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&lt;strong&gt;Previously:&lt;/strong&gt; &lt;a href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/&quot;&gt;How we rebuilt Shopify’s developer docs&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;</content>
  </entry>
  <entry>
    <title>Adventures in web archiving</title>
    <link href="https://gfscott.com/blog/adventures-in-web-archiving/" />
    <updated>2021-10-09T17:00:00Z</updated>
    <id>https://gfscott.com/blog/adventures-in-web-archiving/</id>
    <content type="html">&lt;p&gt;From 2012 to 2018 I worked at &lt;em&gt;Canadian Business&lt;/em&gt; magazine. The publishing business itself was crumbling around us, but I still loved the work and the magazine we were putting out. The dwindling budgets fostered a foxhole spirit among the remaining staff; we had to get creative to keep it all going.&lt;/p&gt;&lt;p&gt;The magazine all but folded in 2017, when Rogers Media ended its print edition. CanadianBusiness.com remained online to host a few remaining sponsored editorial packages and a trickle of CP wire stories. The brand was &lt;a href=&quot;https://www.cbc.ca/news/business/rogers-media-magazines-1.5064054&quot;&gt;sold&lt;/a&gt; to St. Joseph’s Publishing in 2019.&lt;/p&gt;&lt;p&gt;A few months ago we &lt;a href=&quot;https://www.stjoseph.com/news/canadian-business-first-leader-in-residence-geraldine-huse/&quot;&gt;learned&lt;/a&gt; that St. Joe’s was going to relaunch the magazine, in print and online. Good news! Then, a few days ago, I heard through the grapevine that the new CanadianBusiness.com probably wasn’t going to preserve the existing site content. Bad news!&lt;/p&gt;&lt;p&gt;I’d known this day would come sooner or later, and I get that the company needs a fresh start to build something new. But I also didn’t want something that I’d worked on for years to be obliterated outright. So, what to do?&lt;/p&gt;&lt;h2 id=&quot;introduction-to-web-scraping&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/adventures-in-web-archiving/#introduction-to-web-scraping&quot;&gt;Introduction to web scraping&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The site is already &lt;a href=&quot;https://web.archive.org/web/*/https://www.canadianbusiness.com&quot;&gt;fairly well-indexed&lt;/a&gt; in the Internet Archive’s Wayback Machine. But it’s hard to get a good sense from the outside how comprehensive the archive is, and how systematic the crawl. So I decided to look at how to create my own.&lt;/p&gt;&lt;p&gt;My goal was (and is) to ensure that the Wayback Machine has a complete record of CanadianBusiness.com as it existed in the last days before its 2021 relaunch.&lt;/p&gt;&lt;p&gt;A few years ago I ran across &lt;a href=&quot;https://wiki.archiveteam.org&quot;&gt;Archive Team&lt;/a&gt;, a loose network of “rogue archivists” who do huge scrapes of endangered web content, mostly to preserve through the Internet Archive (although they have an arm’s-length relationship to the IA itself.) Archive Team maintains a &lt;a href=&quot;https://wiki.archiveteam.org/index.php/Software&quot;&gt;list of software&lt;/a&gt; for general-purpose web scraping. But the Wayback Machine only ingests &lt;a href=&quot;https://wiki.archiveteam.org/index.php/The_WARC_Ecosystem&quot;&gt;Web Archive (&lt;code&gt;.warc&lt;/code&gt;) files&lt;/a&gt;, which meant straying a little farther into the tall grass.&lt;/p&gt;&lt;p&gt;I ended up installing &lt;a href=&quot;https://github.com/ArchiveTeam/grab-site&quot;&gt;grab-site&lt;/a&gt;, which will crawl a website and produce a series of WARC files containing the results. I started scraping the site on the evening of Monday, October 4, 2021, and it ran for about &lt;strong&gt;72 hours&lt;/strong&gt;. It traversed &lt;strong&gt;516,858&lt;/strong&gt; URLs, and downloaded &lt;strong&gt;26 gigabytes&lt;/strong&gt; total. A little extra searching around led me to some helpful &lt;a href=&quot;https://gist.github.com/Asparagirl/6206247&quot;&gt;directions&lt;/a&gt; for uploading large web archives to the Internet Archive.&lt;/p&gt;&lt;p&gt;Those WARC files are now &lt;a href=&quot;https://archive.org/details/www.canadianbusiness.com-2021-10-04&quot;&gt;freely available on archive.org&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;CanadianBusiness.com &lt;a href=&quot;https://twitter.com/cdnbiz/status/1446494792511332377&quot;&gt;officially relaunched&lt;/a&gt; on the morning of Thursday, October 7. I think it’s a very good looking magazine. Its sitemap &lt;a href=&quot;https://archive.is/m9Mcc&quot;&gt;lists 27 posts&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&quot;now-what%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/adventures-in-web-archiving/#now-what%3F&quot;&gt;Now what?&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I’m not totally sure what the next step is. Apparently only &lt;a href=&quot;https://wiki.archiveteam.org/index.php/Frequently_Asked_Questions#halp_pls_halp&quot;&gt;approved accounts&lt;/a&gt; can put web archives into the Wayback Machine. I’ll need to contact someone at the Internet Archive or on Archive Team to see what it takes.&lt;/p&gt;&lt;p&gt;An important caveat: I have &lt;em&gt;no idea&lt;/em&gt; whether I did this right. I followed the documentation and I can see that I created a big pile of data, but given the short time frame and the fairly steep learning curve, it’s hard to know whether what I produced is actually fit for purpose. It is absolutely possible that I’ve produced something that cannot ever be inserted into the Wayback Machine; I just don’t know yet.&lt;/p&gt;&lt;p&gt;I opted to scrape first and ask questions later, and I think on balance that was still the right choice. But it means I’m still figuring out exactly what the necessary steps are. To be continued!&lt;/p&gt;&lt;h2 id=&quot;stray-thoughts-and-observations&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/adventures-in-web-archiving/#stray-thoughts-and-observations&quot;&gt;Stray thoughts and observations&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;The software ecosystem for web archiving feels very wild and woolly. The tooling can be somewhat arcane and intimidating, largely command-line Python programs. Someone of my experience level can puzzle through, but there’s no hand-holding here.&lt;/li&gt;&lt;li&gt;It’s difficult, as a newcomer, to inspect your work and get a sense of whether you’re doing it right. This is partly due to the industrial-grade nature of the tooling; It’s also due to the scale you have to work at for even a modest-sized site — potentially hundreds of thousands of URLs and gigabytes of data. It’s hard to do a dry run and make sense of the resulting output with any confidence.&lt;/li&gt;&lt;li&gt;The Internet Archive itself strikes me as curiously silent on citizen contributions to the Wayback Machine. You can save a single page at a time through the web interface but there’s otherwise very little official guidance on this type of workflow.&lt;/li&gt;&lt;li&gt;All of the above makes this whole enterprise pretty intimidating to get into, which is a shame. I think further democratizing this ecosystem and lowering the barriers to entry might be healthy overall. But obviously it’s hugely reliant on volunteers who are likely already stretched thin. That’s why I chose to &lt;a href=&quot;https://archive.org/donate&quot;&gt;donate to the Internet Archive&lt;/a&gt; this week. None of my minor gripes about the ergonomics of this process change the fact that the IA does absolutely essential work.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>My year of Eleventy plugins</title>
    <link href="https://gfscott.com/blog/my-year-of-eleventy-plugins/" />
    <updated>2021-01-25T17:00:00Z</updated>
    <id>https://gfscott.com/blog/my-year-of-eleventy-plugins/</id>
    <content type="html">&lt;p&gt;One year ago, I published my first Eleventy plugin (and my first npm package of any kind): &lt;a href=&quot;https://www.npmjs.com/package/eleventy-plugin-youtube-embed&quot;&gt;&lt;code&gt;eleventy-plugin-youtube-embed&lt;/code&gt;&lt;/a&gt;. I’d recently rebuilt my personal site using the &lt;a href=&quot;https://11ty.dev&quot;&gt;Eleventy&lt;/a&gt; static site generator, and wanted a simple way to add embedded YouTube videos in Markdown. I suspected others might want to do the same.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/gfscott/eleventy-plugin-youtube-embed/network/dependents&quot;&gt;Quite a few did&lt;/a&gt;! Today I know that while it’s possible to make this task &lt;em&gt;easy&lt;/em&gt;, it’s far from simple. And that first plugin has since grown into a (tiny) plugin ecosystem of its own, called &lt;a href=&quot;https://gfscott.com/embed-everything&quot;&gt;Embed Everything&lt;/a&gt;, which now covers not just YouTube but Twitch, Vimeo, TikTok, and more.&lt;/p&gt;&lt;p&gt;I thought I’d mark this milestone by sharing some of the things I’ve learned over the last year.&lt;/p&gt;&lt;h3 id=&quot;the-numbers&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/my-year-of-eleventy-plugins/#the-numbers&quot;&gt;The numbers&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;First, some raw statistics. These figures cover &lt;a href=&quot;https://www.npmjs.com/~gfscott&quot;&gt;the whole family&lt;/a&gt; of plugins.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;25,000+&lt;/strong&gt;: Total number of package downloads since launch.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;428&lt;/strong&gt;: Public repos with one or more of my packages as a dependency.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;395&lt;/strong&gt;: Total commits across all repos.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;113&lt;/strong&gt;: Package versions released.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;2&lt;/strong&gt;: Outside code contributors. (Thanks &lt;a href=&quot;https://github.com/gfscott/eleventy-plugin-embed-spotify/pull/7&quot;&gt;Leslie&lt;/a&gt; and &lt;a href=&quot;https://github.com/gfscott/eleventy-plugin-youtube-embed/pull/19&quot;&gt;Scott&lt;/a&gt;! 🎉)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;1&lt;/strong&gt;: &lt;a href=&quot;https://github.com/gfscott/eleventy-plugin-youtube-embed/issues/26&quot;&gt;Feature request&lt;/a&gt; from &lt;a href=&quot;https://www.zachleat.com/&quot;&gt;Mr. Eleventy himself&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Overall, it’s been really rewarding to see people use something I’ve made. It’s also been hard! In fact, the rewarding parts and the hard parts have mostly been the same parts.&lt;/p&gt;&lt;h3 id=&quot;testing%3A-it%E2%80%99s-hard&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/my-year-of-eleventy-plugins/#testing%3A-it%E2%80%99s-hard&quot;&gt;Testing: it’s hard&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;I had never once written a test before starting this project.&lt;/p&gt;&lt;p&gt;I always knew that I &lt;em&gt;should&lt;/em&gt; know more about testing, but (as with so many things) it never clicked until I truly needed it to solve a problem. Which was: I needed to prove, programmatically, that these plugins do what they claim to do, and do so in predictable and reliable ways.&lt;/p&gt;&lt;p&gt;My initial tiptoe into writing tests was simple fire-fighting: I got a couple of GitHub issues filed by early adopters who found the YouTube plugin &lt;a href=&quot;https://github.com/gfscott/eleventy-plugin-youtube-embed/issues/8&quot;&gt;wasn’t working&lt;/a&gt; for them. It quickly became clear that my early haphazard efforts at spotting edge cases was not going to cut it, and I needed a better system. That finally spurred me to make a concerted effort to dig into testing.&lt;/p&gt;&lt;p&gt;I’ve found that learning to write tests (and, to be clear, I consider that learning process very much ongoing) has had the following benefits:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;It improves overall project structure&lt;/strong&gt;&lt;br&gt;Re-writing these codebases for testing has made them more modular and functional, and therefore easier to read and reason about. Breaking functions down into their smallest constituent parts, so that they’re accessible to tests in the first place, makes them more legible and composable — both for me and (I hope) for other contributors.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;It gives you a framework for thinking more deeply&lt;/strong&gt;&lt;br&gt;I’m self-taught; getting code to work &lt;em&gt;at all&lt;/em&gt; is often a triumph for me. Going back to figure out all the ways things could go wrong always just felt like a drag. But I also never had a framework for &lt;em&gt;thinking&lt;/em&gt; about errors. Now, I can sit down and map out expected inputs and outputs, consider possible exceptions, and think logically about how to handle them. It’s not that I couldn’t imagine stuff breaking before; it’s that I didn’t know how to articulate my intuitions. It’s like I could hum the tune but didn’t know musical notation. Now, the known unknowns are much easier to spot and write down.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;It helps you move faster&lt;/strong&gt;&lt;br&gt;Simple trial and error has two drawbacks: First, it’s unreliable because it’s often un-repeatable, with techniques and approaches varying from person to person and machine to machine. Secondly, and as a result, it just &lt;em&gt;takes a long time&lt;/em&gt;. Having tests that can run automatically, regularly, and consistently makes it possible to make changes and updates with greater confidence, and that is &lt;em&gt;so&lt;/em&gt; much faster and more productive. There are definitely updates and features in the plugins today that I would not have had the confidence to send out in the wild if not for the enforced discipline of a continuous integration setup.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For lots of people, all these points will be old news, and it’s clear I have more to learn. But adopting even the basics has been enlightening. And if you’re in the same boat I was with testing, I highly recommend diving in.&lt;/p&gt;&lt;h3 id=&quot;versioning%3A-it%E2%80%99s-hard&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/my-year-of-eleventy-plugins/#versioning%3A-it%E2%80%99s-hard&quot;&gt;Versioning: it’s hard&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;I’m making a good-faith effort to stick to the principles of &lt;a href=&quot;https://semver.org&quot;&gt;semantic versioning&lt;/a&gt;. But it does feel like semver can be overzealous in a way that’s not necessarily helpful to the majority of end users.&lt;/p&gt;&lt;p&gt;Take any of the Embed Everything plugins: their “public APIs” have been quite stable, mostly because what they do is pretty basic: URL in, embed markup out.&lt;/p&gt;&lt;p&gt;If you’re consuming these plugins as packages from npm, in fact, there are many version releases that would have no visible change whatsoever. In general, we &lt;code&gt;.npmignore&lt;/code&gt; test files, the code of conduct, CI config files, and so on, because that stuff really only matters to people contributing to the codebase, not consumers of the package. But those files are also where most of the actual commit action is. So I frequently end up releasing new minor or patch versions that represent no actual change for the vast majority of users.&lt;/p&gt;&lt;p&gt;To me, that doesn’t feel like it’s quite living up to the intended benefit of semantic versioning, which is &lt;em&gt;conveying meaning&lt;/em&gt;. Meaning to whom? Open source software has multiple user bases, and a patch release that’s meaningful to a contributor might be meaningless to an end user drowning in Dependabot notifications.&lt;/p&gt;&lt;p&gt;I don’t know how to fix that, and obviously it’s a complex problem. But it’s been on my mind (since I’m kind of drowning in Dependabot notifications).&lt;/p&gt;&lt;h3 id=&quot;naming-things%3A-it%E2%80%99s-hard&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/my-year-of-eleventy-plugins/#naming-things%3A-it%E2%80%99s-hard&quot;&gt;Naming things: it’s hard&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;In retrospect, I &lt;em&gt;really&lt;/em&gt; wish I’d picked the right naming scheme for the first two plugins. Both the YouTube and Vimeo plugins follow the format &lt;code&gt;eleventy-plugin-{service}-embed&lt;/code&gt;. With the later plugins, I changed it to &lt;code&gt;eleventy-plugin-embed-{service}&lt;/code&gt;, so that they’d cluster together alphabetically in web searches, in the dependency list in &lt;code&gt;package.json&lt;/code&gt;, and so on.&lt;/p&gt;&lt;p&gt;At the time I published the YouTube plugin, I wasn’t thinking about extending the model to other services, so the package name structure didn’t seem that important. The mismatched name pattern has basically zero practical effect, but it just...bugs me. At this point, I think changing it isn’t worth the trouble.&lt;/p&gt;&lt;h3 id=&quot;self-doubt%3A-it%E2%80%99s-hard&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/my-year-of-eleventy-plugins/#self-doubt%3A-it%E2%80%99s-hard&quot;&gt;Self-doubt: it’s hard&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;I’m glad these plugins exist! I’m happy to have contributed them to the Eleventy community, and I intend to keep going. But I definitely also harbour doubts. They include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;br&gt;I’m occasionally haunted by the idea that the fundamental nature of these plugins is...&lt;em&gt;wrong&lt;/em&gt;. They work by running a regular expression on Eleventy’s HTML output. Parsing HTML with regex is &lt;a href=&quot;https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454&quot;&gt;somewhat famously&lt;/a&gt; (though not universally) regarded as a bad idea. I guess the only response I have here is that, given my skills and knowledge today, I’ve made the best thing I’m currently capable of. And yet, I wonder if there’s some dramatically better way — and whether I’d recognize it if I saw it.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance and scale&lt;/strong&gt;&lt;br&gt;Especially with &lt;code&gt;embed-everything&lt;/code&gt;, which aggregates all the &lt;em&gt;individual&lt;/em&gt; plugins, I worry that regexing every single HTML file multiple times is not sustainable, especially as Eleventy matures, people’s sites get bigger, and the number of files grows.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sustainability&lt;/strong&gt;&lt;br&gt;Everyone knows open source has a burnout problem. That’s not a problem for me at the moment, but I am aware that keeping these packages up to date requires periodic maintenance and I want to strike a workable cadence for the long term. I’m not really sure what that looks like yet.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;roadmap-to-v2.0&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/my-year-of-eleventy-plugins/#roadmap-to-v2.0&quot;&gt;Roadmap to v2.0&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;I do have thoughts about the next major release(s). The top-line feature I’m planning is &lt;em&gt;inline options&lt;/em&gt;, so you can change the settings of individual embeds with simple keywords on the same line as the URL in your Markdown.&lt;/p&gt;&lt;p&gt;Here’s what I mean:&lt;/p&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Embed this video with a 4:3 aspect ratio: --&gt;&lt;/span&gt;
https://www.youtube.com/watch?v=mT0RNrTDHkI 4:3

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Autoplay and loop this video: --&gt;&lt;/span&gt;
https://www.youtube.com/watch?v=mT0RNrTDHkI autoplay loop&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The idea is to support features that you likely need to control on a per-video basis, like autoplay or aspect ratio. Some videos require slight tweaks and currently there’s no way to break the mold on a case-by-case basis. I think inline options are the way to handle that.&lt;/p&gt;&lt;p&gt;This represents a breaking change to the existing regular expression structure, which is currently very strict about skipping paragraphs that contain anything other than a URL. But I think the additional flexibility it unlocks is worth it.&lt;/p&gt;&lt;p&gt;My intent is to cut a new major version of all the plugins at the same time, even if the inline options available at 2.0 are limited or non-existent. But I think breaking the RegEx pattern should be done only once across the board, even if the new features come later.&lt;/p&gt;&lt;p&gt;Other things on the radar:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Drop support for Node.js 8. Definitely a 2.0 breaking change.&lt;/li&gt;&lt;li&gt;Support the new &lt;code&gt;aspect-ratio&lt;/code&gt; CSS property launched in Chrome v88.&lt;/li&gt;&lt;li&gt;Standardize all the plugins on an &lt;code&gt;embedMethod&lt;/code&gt; option (name could change!) to make it easier to better support multiple types of HTML output. For example: &lt;code&gt;iframe&lt;/code&gt;, &lt;code&gt;script&lt;/code&gt;, &lt;code&gt;lite&lt;/code&gt;, &lt;code&gt;oembed&lt;/code&gt;, and so on.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The latter two ideas here could be non-breaking 1.x releases too, I haven’t totally settled on how to do it. And all of this is up for discussion — if you have ideas or want to help out, I’m all ears!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The books I read in 2020</title>
    <link href="https://gfscott.com/blog/books-2020/" />
    <updated>2021-01-01T17:00:00Z</updated>
    <id>https://gfscott.com/blog/books-2020/</id>
    <content type="html">&lt;p&gt;You’d think the waking nightmares of 2020 would prompt more escapist reading, but instead I zagged toward nonfiction. I do notice this list of authors is very male: something to work on.&lt;/p&gt;&lt;p&gt;In alphabetical order:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;1491: New Revelations of the Americas Before Columbus&lt;/em&gt;, by Charles C. Mann&lt;/li&gt;&lt;li&gt;&lt;em&gt;Against the Grain: A Deep History of the Earliest States&lt;/em&gt;, by James C. Scott&lt;/li&gt;&lt;li&gt;&lt;em&gt;American Radical: The Life and Times of I.F. Stone&lt;/em&gt;, by D. D. Guttenplan&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Dream Machine&lt;/em&gt;, by M. Mitchell Waldrop&lt;/li&gt;&lt;li&gt;&lt;em&gt;Fight No More&lt;/em&gt;, by Lydia Millet&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Fire Next Time&lt;/em&gt;, by James Baldwin&lt;/li&gt;&lt;li&gt;&lt;em&gt;Fortune’s Children: The Fall of the House of Vanderbilt&lt;/em&gt;, by Arthur T. Vanderbilt II&lt;/li&gt;&lt;li&gt;&lt;em&gt;Foundation&lt;/em&gt;, by Isaac Asimov&lt;/li&gt;&lt;li&gt;&lt;em&gt;Infinite Detail&lt;/em&gt;, by Tim Maughan&lt;/li&gt;&lt;li&gt;&lt;em&gt;Lincoln in the Bardo&lt;/em&gt;, by George Saunders&lt;/li&gt;&lt;li&gt;&lt;em&gt;Nature’s Mutiny: How the Little Ice Age of the Long Seventeenth Century Transformed the West and Shaped the Present&lt;/em&gt;, by Philipp Blom&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Nickel Boys&lt;/em&gt;, by Colson Whitehead&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Night of the Gun&lt;/em&gt;, by David Carr&lt;/li&gt;&lt;li&gt;&lt;em&gt;No Rules Rules: Netflix and the Culture of Reinvention&lt;/em&gt;, by Erin Meyer and Reed Hastings&lt;/li&gt;&lt;li&gt;&lt;em&gt;Something That May Shock and Discredit you&lt;/em&gt;, by Daniel Mallory Ortberg&lt;/li&gt;&lt;li&gt;&lt;em&gt;Strangers: Homosexual Love in the 19th Century&lt;/em&gt;, by Graham Robb&lt;/li&gt;&lt;li&gt;&lt;em&gt;Team of Rivals: The Political Genius of Abraham Lincoln&lt;/em&gt;, by Doris Kearns Goodwin&lt;/li&gt;&lt;li&gt;&lt;em&gt;Uncanny Valley&lt;/em&gt;, by Anna Wiener&lt;/li&gt;&lt;li&gt;&lt;em&gt;Utopia Avenue&lt;/em&gt;, by David Mitchell&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;notes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/books-2020/#notes&quot;&gt;Notes&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I don’t have something to say about every book here, but some stray thoughts:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;em&gt;Fortune’s Children&lt;/em&gt; is a wonderfully indiscreet history of the Vanderbilts, written by a latter-day Vanderbilt nephew. Tons of detail but always breezy and readable.&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Nickel Boys&lt;/em&gt; is a perfectly cut gem. Whitehead’s earlier novels dabble across genres, and here he deploys some of those techniques to give his flinty realism a mythic quality. The result is beautifully haunting.&lt;/li&gt;&lt;li&gt;I read tons of sci-fi as a kid, and yet I never cracked any Asimov, strangely. I can see why &lt;em&gt;Foundation&lt;/em&gt; was influential in its time but all its ideas feel out of date now, and the writing isn’t good enough to bridge the gap.&lt;/li&gt;&lt;li&gt;I audiobooked &lt;em&gt;Lincoln in the Bardo&lt;/em&gt;, which I think was a mistake. The recording has like 150 celebrity voice actors switching in and out like a radio play; I suspect it works better on the page.&lt;/li&gt;&lt;li&gt;&lt;em&gt;The Dream Machine&lt;/em&gt; was out of print for years and very hard to find before Stripe Press reissued it in a beautiful new hardcover volume. Definitely lives up to its billing as a key history of the early computer age, but not a casual read. I stalled out once and came back to it months later. I’m glad I did!&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Embed Everything</title>
    <link href="https://gfscott.com/blog/embed-everything/" />
    <updated>2020-04-28T23:20:00Z</updated>
    <id>https://gfscott.com/blog/embed-everything/</id>
    <content type="html">&lt;p&gt;I recently released an open source Eleventy plugin called &lt;a href=&quot;https://gfscott.com/embed-everything/&quot;&gt;Embed Everything&lt;/a&gt;. It’s actually seven different plugins &lt;a href=&quot;https://www.npmjs.com/~gfscott&quot;&gt;and counting&lt;/a&gt;, bundled together. I think it has potential!&lt;/p&gt;&lt;h2 id=&quot;what-it-does&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/embed-everything/#what-it-does&quot;&gt;What it does&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The plugin turns URLs into embeds. For instance, drop a YouTube URL into a markdown file, and it comes out as the full thing. So this:&lt;/p&gt;&lt;pre class=&quot;language-md&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;https://www.youtube.com/watch?v=SQIYZ-iKuG4&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Becomes this:&lt;/p&gt;&lt;div id=&quot;SQIYZ-iKuG4&quot; class=&quot;eleventy-plugin-youtube-embed&quot; style=&quot;position:relative;width:100%;padding-top: 56.25%;&quot;&gt;&lt;iframe style=&quot;position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;&quot; width=&quot;100%&quot; height=&quot;100%&quot; frameborder=&quot;0&quot; title=&quot;Embedded YouTube video&quot; src=&quot;https://www.youtube-nocookie.com/embed/SQIYZ-iKuG4&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;h2 id=&quot;why-do-this%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/embed-everything/#why-do-this%3F&quot;&gt;Why do this?&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I recently re-built my website using &lt;a href=&quot;https://11ty.dev&quot;&gt;Eleventy&lt;/a&gt;, which I really like. I think it&#39;s a great choice for small personal sites that don’t need to be updated super-often.&lt;/p&gt;&lt;p&gt;One of the reasons I wanted to re-do the site was to &lt;a href=&quot;https://www.theverge.com/2014/8/20/6049259/the-future-of-blogging&quot;&gt;ease (back?) into old-school blogging&lt;/a&gt;: the low-pressure, digital pastoral sort of thing where you can quickly drop some stray thoughts or share something you found online.&lt;/p&gt;&lt;p&gt;In other words, I want the convenience of “quickly sharing a funny YouTube video on Facebook,” but without the Facebook part. Admittedly, no one will see my funny video because a) they’re all on Facebook, not browsing their impeccably groomed lists of RSS feeds; and b) I’m doing all this out of hermetic self-regard anyway, so who cares.&lt;/p&gt;&lt;p&gt;The point is, &lt;a href=&quot;https://gfscott.com/embed-everything/&quot;&gt;Embed Everything&lt;/a&gt; makes it about as easy to do that in this bespoke static blogging framework as it is in Facebook, or Wordpress, or whatever. I get to go LARPing as a netizen of the antediluvian blogosphere, but I don’t have to give up the modern conveniences of platform capitalism. And now, &lt;a href=&quot;https://www.npmjs.com/package/eleventy-plugin-embed-everything&quot;&gt;neither do you&lt;/a&gt;!&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>How we rebuilt Shopify’s developer docs</title>
    <link href="https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/" />
    <updated>2020-02-12T22:30:00Z</updated>
    <id>https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/</id>
    <content type="html">&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;March 2025&lt;/strong&gt; I recently discovered that the &lt;a href=&quot;https://ux.shopify.com&quot;&gt;Shopify UX blog&lt;/a&gt; was shut down and its archives are no longer online. So I&#39;m updating this post with the full text of the original article. Many of the links are to the Wayback Machine, because &lt;a href=&quot;https://shopify.dev&quot;&gt;Shopify.dev&lt;/a&gt; itself has changed a few times since I originally wrote this.&lt;/p&gt;&lt;/blockquote&gt;&lt;figure&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/coding-team-working-on-laptops-9wgpFwM5MuKQ-640w.avif 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/coding-team-working-on-laptops-9wgpFwM5MuKQ-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/coding-team-working-on-laptops-9wgpFwM5MuKQ-640w.webp 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/coding-team-working-on-laptops-9wgpFwM5MuKQ-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/coding-team-working-on-laptops-9wgpFwM5MuKQ-640w.jpeg&quot; alt=&quot;Coding team working on laptops&quot; width=&quot;928&quot; height=&quot;619&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/coding-team-working-on-laptops-9wgpFwM5MuKQ-640w.jpeg 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/coding-team-working-on-laptops-9wgpFwM5MuKQ-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;figcaption&gt;Photo by &lt;a href=&quot;https://www.shopify.com/stock-photos/@shopifyphotos?utm_campaign=photo_credit&amp;amp;utm_content=Free+Stock+Photo+of+Coding+Team+Working+On+Laptops+%E2%80%94+HD+Images&amp;amp;utm_medium=referral&amp;amp;utm_source=credit&quot;&gt;Nicole De Khors&lt;/a&gt; from &lt;a href=&quot;https://www.shopify.com/stock-photos/education?utm_campaign=photo_credit&amp;amp;utm_content=Free+Stock+Photo+of+Coding+Team+Working+On+Laptops+%E2%80%94+HD+Images&amp;amp;utm_medium=referral&amp;amp;utm_source=credit&quot;&gt;Burst&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Today, Shopify &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://shopify.dev/&quot;&gt;relaunched its developer documentation&lt;/a&gt;. This was a long, complex project that required pulling on every UX and development discipline working inside the company, and it’s great to see it finally out in the world. I had the unique privilege of working on this project from beginning to end, so I wanted to share some of the lessons we learned on the way.&lt;/p&gt;&lt;p&gt;The final verdict is, of course, up to the community of thousands of developers who turn to our docs every day. We won’t know for some time whether we’ve truly hit the mark, and as we continue to gather feedback, we’d love to hear from you.&lt;/p&gt;&lt;p&gt;I can, however, provide some background on the origins of this change: why we decided it needed to be done, and how we decided on the changes we’ve made.&lt;/p&gt;&lt;p&gt;As with just about everything we launch at Shopify, our developer docs will continue to evolve as we learn more about how people use them. But the core of this project was about trying to understand what makes a great developer experience.&lt;/p&gt;&lt;p&gt;So how do we know? Well, we asked.&lt;/p&gt;&lt;h2 id=&quot;why-change-the-docs%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/#why-change-the-docs%3F&quot;&gt;Why change the docs?&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;First, let’s rewind. What’s the problem we were trying to solve here?&lt;/p&gt;&lt;p&gt;More than 1,000,000 businesses now run on Shopify, and each one has unique needs. Shopify solves many of their hardest commerce problems out of the box — but it can’t be all things to all people. One of Shopify’s secret weapons has always been the large community of third-party developers who build apps, themes, and other integrations to solve problems for merchants. Their hard work makes Shopify flexible and adaptable to just about any special case you can think of.&lt;/p&gt;&lt;p&gt;Shopify has grown a lot over the last few years, and we were finding that our developer documentation wasn’t scaling at the same pace. The growing pains showed up in subtle but increasingly troublesome ways:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Tutorial and how-to content had become mixed in with reference documentation. My analogy would be like using a recipe that doesn’t separate the list of ingredients from the directions. Sometimes this is the right choice! But in our case, it was leading to growing complexity that was making the docs harder to maintain. We haven’t totally solved this problem yet, but we have a clear path forward with this new site.&lt;/li&gt;&lt;li&gt;The old site structure was very deeply nested, which made it hard for developers to browse and discover new and useful features spontaneously. Take something like &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://shopify.dev/docs/kit&quot;&gt;Kit&lt;/a&gt;, the conversational “virtual employee” that merchants can install on their stores to automate common tasks. This is a powerful feature of Shopify and developers can extend it in all sorts of ways. But if you didn’t already know about Kit, you were fairly unlikely to stumble over it, since it only showed up three levels deep in the “Embedded Apps” menu. (Why was it put there? No one could quite remember.)&lt;/li&gt;&lt;li&gt;The app and theme developer docs were struggling in the &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://help.shopify.com/en&quot;&gt;Shopify Help Center&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The Help Center’s primary job is to assist business owners who are running Shopify stores. Developer content, focused on interacting with the underlying Shopify APIs, was sometimes an awkward fit. Shopify merchants and developers sometimes have related needs — and plenty of merchants do their own coding! — but there are genuine differences between the types of documentation each audience needs. We wanted to set up our developer documentation platform for future growth and evolution by separating these concerns.&lt;/p&gt;&lt;p&gt;Some of these pain points were visible when browsing the docs. Others were internal organizational issues — the little behind-the-scenes dramas and stresses of any fast-growing company. Both types were making it harder and harder to maintain the docs, let alone grow them for the future.&lt;br&gt;Our developer docs were, in fact, rated fairly well in our annual surveys of the community. There was no crisis that demanded an instant fix. But we could see trouble on the horizon. Documentation that is hard to maintain is documentation that soon gets outdated, duplicated, and disorganized. So we knew we wanted to address these problems sooner rather than later. We knew we needed to do…something. But what, exactly?&lt;/p&gt;&lt;h2 id=&quot;the-content-audit&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/#the-content-audit&quot;&gt;The content audit&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I started by simply trying to get an idea of how much stuff we were dealing with. Our goal was to get the lay of the land, understand the potential scope of the work, and start narrowing the focus to a manageable, shippable project.&lt;/p&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/content-strategy-for-the-web-OhXoz2AfruHX-640w.avif 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/content-strategy-for-the-web-OhXoz2AfruHX-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/content-strategy-for-the-web-OhXoz2AfruHX-640w.webp 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/content-strategy-for-the-web-OhXoz2AfruHX-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/content-strategy-for-the-web-OhXoz2AfruHX-640w.jpeg&quot; alt=&quot;A copy of the book “Content Strategy for the Web” sitting on a desk.&quot; width=&quot;928&quot; height=&quot;635&quot; srcset=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/content-strategy-for-the-web-OhXoz2AfruHX-640w.jpeg 640w, https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/content-strategy-for-the-web-OhXoz2AfruHX-928w.jpeg 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;p&gt;I pulled out my dog-eared copy of &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://www.contentstrategy.com/content-strategy-for-the-web&quot;&gt;Content Strategy for the Web&lt;/a&gt;, by Kristina Halvorson and Melissa Rach. It’s one of the definitive books about the craft, and it contains great advice on &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://ux.shopify.com/6-steps-to-product-content-audit-perfection-fc89d3b637f4&quot;&gt;how to do a thorough content audit&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There’s not enough space here to get into all the gory details of my audit methodology, but it was a combination of manually combing through our codebase, some brute-force web and API scraping, and trawling through our historical analytics. It took a few weeks, but in the end I had an enormous spreadsheet that could answer many of our quantitative content questions quickly.&lt;/p&gt;&lt;p&gt;In all, my initial audit turned up 2,868 pages of material that would be relevant to Shopify’s developer community. It included things like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Hundreds of pages of instructional documentation in our &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://help.shopify.com/&quot;&gt;Help Center&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Hundreds more pages of API reference docs. (For the most part, &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://shopify.dev/docs/admin-api/graphql/reference/object/product&quot;&gt;this type of page&lt;/a&gt;, describing a particular API resource in detail, is automatically generated straight from the actual API, not manually authored by our technical writing teams)&lt;/li&gt;&lt;li&gt;More than 800 posts on our &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://www.shopify.ca/partners/blog&quot;&gt;Partner Blog&lt;/a&gt;, many of them covering development topics&lt;/li&gt;&lt;li&gt;Shopify’s &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://www.shopify.ca/partners/academy&quot;&gt;Partner Academy&lt;/a&gt;, the online learning platform that offers courses and certification, and which contains hundreds of pages of educational material&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20210308130234/https://polaris.shopify.com/&quot;&gt;Polaris&lt;/a&gt;, Shopify’s open-source design system&lt;/li&gt;&lt;li&gt;Around 500 &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://github.com/Shopify&quot;&gt;open-source GitHub repos&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;By correlating individual URLs with analytics data and git commit histories, I was able to fill out the spreadsheet with more detail, and we began to see some pretty clear patterns: which parts of our docs were being revised most actively; which sections were most popular with readers; which ones were performing well in search. I also categorized pages by type (auto-generated API reference pages; human-authored instructional content) and by broad topic (themes, apps, custom storefronts).&lt;/p&gt;&lt;p&gt;At this point, we felt like we had a good map of the territory, but a quantitative content audit can only really give you a snapshot of how things stand today. To figure out what changes we needed to make, we’d need to talk to users.&lt;/p&gt;&lt;h2 id=&quot;the-research-phase&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/#the-research-phase&quot;&gt;The research phase&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;To puzzle through our questions in a more structured way, I turned to our &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://ux.shopify.com/tagged/ux-research&quot;&gt;User Experience Research&lt;/a&gt; team. One of the great privileges of working at Shopify is having access to world-class UX researchers who love untangling exactly these kinds of thorny, ambiguous questions.&lt;/p&gt;&lt;p&gt;For this project we did two types of research: one-on-one interviews, and a moderated card-sort exercise. In both cases, the goal was to figure out a common mental model for the Shopify ecosystem. If we know how people think about our platform, we can design an information architecture that matches people’s understanding, reinforce the things they already know, and, hopefully, fill any knowledge gaps.&lt;/p&gt;&lt;h2 id=&quot;user-interviews&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/#user-interviews&quot;&gt;User interviews&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Andrew Rajaram, our UX researcher on this project, designed the program and conducted most of the interviews. We recruited two panels: existing Shopify partners who were already experienced with the platform; and developers who had never used Shopify before. We also balanced the list for gender representation, a mix of international markets, and business size, from solo freelancers to people who worked at large firms or agencies.&lt;/p&gt;&lt;p&gt;Andrew drew up a script to run through with each interviewee, so that we’d ask all the key questions consistently. We scheduled the interviews for an hour each by video chat. In each one, there was the interviewee, the interviewer, and a note-taker to observe.&lt;/p&gt;&lt;p&gt;The interviews were instructive. There were a few clear patterns that came through:&lt;/p&gt;&lt;h3 id=&quot;1.-most-developers-wanted-to-learn-by-doing&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/#1.-most-developers-wanted-to-learn-by-doing&quot;&gt;1. Most developers wanted to learn by doing&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;While there’s definitely a segment of developers who thoroughly read over documentation before starting their development process, a majority of the ones we interviewed like to start something quickly and see how far they can get, consulting references and tutorials as they go.&lt;/p&gt;&lt;p&gt;There’s &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://sigdoc.acm.org/cdq/how-developers-use-api-documentation-an-observation-study/&quot;&gt;fascinating pre-existing research on this topic&lt;/a&gt;, identifying developers as taking either “systematic” or “opportunistic” approaches to navigating software documentation. The systematic ones like to read first, then code. The opportunists start writing code as quickly as they can. It’s worth noting that neither style is associated with “better” code or a more successful outcome; but it does mean that documentation needs to be accessible and useful to both types of learners.&lt;/p&gt;&lt;h3 id=&quot;2.-interest-in-legal-documentation-scaled-with-firm-size&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/#2.-interest-in-legal-documentation-scaled-with-firm-size&quot;&gt;2. Interest in legal documentation scaled with firm size&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;At mid-sized agencies and up, the interviewees typically asked more about the API terms of service, privacy policies, service-level agreements, and other legal or contractual documentation. This is especially important for those types of companies, because they’re often working on behalf of clients who have their own requirements around licensing and user privacy.&lt;/p&gt;&lt;p&gt;This was one of the valuable new insights we learned from our interview process — one that hadn’t been suggested by earlier research. Once we heard it, of course, it made perfect sense. As a result, the new developer docs foreground things like &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://www.shopify.com/legal/api-terms&quot;&gt;Shopify’s API Terms of Service&lt;/a&gt;, and &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://shopify.dev/concepts/trust-and-security&quot;&gt;concepts around user privacy and legislation&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&quot;3.-documentation-needs-change-over-time&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/#3.-documentation-needs-change-over-time&quot;&gt;3. Documentation needs change over time&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;This may sound obvious, but this insight is what steered us away from organizing our documentation around &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://www.interaction-design.org/literature/article/personas-why-and-how-you-should-use-them&quot;&gt;user personas&lt;/a&gt;. Often, UX practitioners bucket users by common personas: for instance, if you were building an app for running a car dealership, you’d probably design different activity flows for the people selling the cars out on the lot (a “salesperson persona”), compared to the mechanics working in the service bay (a “mechanic persona”). This technique allows you to make informed generalizations that address common workflows and user needs.&lt;/p&gt;&lt;p&gt;This clear-cut division of labor is less prevalent in software development. At a larger shop, there may be a vice-president who needs to know about the business opportunity, a chief product officer who needs to know about the platform’s high-level technical capabilities, and a team of devs who need to know the API references in detail. But often, those information needs overlap and blur; sometimes, all those “personas” are, in fact, a single individual — just one who’s progressing through the different stages in their development process.&lt;/p&gt;&lt;p&gt;This was what convinced us that organizing docs around personas was a dead end. People aren’t “beginners” or “experts” — they’re people with a problem to solve. We can’t read their minds or pigeonhole them based on (likely erroneous) assumptions about what they want to do at any given time.&lt;/p&gt;&lt;p&gt;This finding led us toward a flatter information hierarchy, increasing the number of topics at higher levels of navigation. Our hypothesis is that this makes it easier for casual users to browse and discover, while motivated users can more quickly drill down to answer their specific questions.&lt;/p&gt;&lt;h2 id=&quot;card-sorting&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/#card-sorting&quot;&gt;Card-sorting&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Our next step was a &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://www.nngroup.com/articles/card-sorting-definition/&quot;&gt;moderated card-sort exercise&lt;/a&gt;. This involved giving developers a stack of about 50 “cards”, each with the name of a documentation concept or topic on it. We asked them to sort these cards into piles that made sense to them, and then label the piles. We wanted to see if we could find common mental models in how people arranged information about Shopify’s platform.&lt;/p&gt;&lt;p&gt;What emerged was surprisingly consistent. Very often the arrangement of piles came out looking something like this:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Basic information about the platform&lt;/li&gt;&lt;li&gt;Legal requirements and responsibilities&lt;/li&gt;&lt;li&gt;Thorough reference documentation&lt;/li&gt;&lt;li&gt;How-to guides, instructional material, tutorials&lt;/li&gt;&lt;li&gt;Software tools, libraries, and SDKs&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;What was really interesting about this result is that it matched some other research we’d been doing in parallel about &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://www.xml.com/articles/2017/01/19/what-dita/&quot;&gt;DITA&lt;/a&gt;, which is an XML-based format for managing documentation, originally developed by IBM in the early 2000s and now an open standard. DITA categorizes instructional material into three broad categories:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Concepts (contextual information you need to know)&lt;/li&gt;&lt;li&gt;References (a detailed description of the thing you’re working with)&lt;/li&gt;&lt;li&gt;Tasks (actions you can take, based on your knowledge of 1 and 2)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The overlap between theory and behavior was striking. People were consistently looking for:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Background information and context (Concepts)&lt;/li&gt;&lt;li&gt;A comprehensive “dictionary” of Shopify’s capabilities (References)&lt;/li&gt;&lt;li&gt;Guides to implementing common workflows (Tasks)&lt;/li&gt;&lt;li&gt;Tools to help them do their work faster&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;DITA’s core specification doesn’t address “tools”, but the categories were clear enough that we decided we could start prototyping and try revamping the information architecture around these ideas. The result, many months later, is what you see today.&lt;/p&gt;&lt;h2 id=&quot;the-result&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs/#the-result&quot;&gt;The result&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;As you can see, the architecture of the site pretty closely mirrors what we learned from the user research. The five top-level “zones” are &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://shopify.dev/concepts&quot;&gt;Concepts&lt;/a&gt;, &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://shopify.dev/docs&quot;&gt;Docs&lt;/a&gt;, &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://shopify.dev/tutorials&quot;&gt;Tutorials&lt;/a&gt;, &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://shopify.dev/tools&quot;&gt;Tools&lt;/a&gt;, and Community (the last of which, for now, only links out to other parts of Shopify, like our &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://community.shopify.com/c/App-Partner-Platform/ct-p/appdev&quot;&gt;developer forums&lt;/a&gt; and &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://www.shopify.ca/partners/academy&quot;&gt;Partner Academy&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;With today’s launch, &lt;a href=&quot;https://web.archive.org/web/20210308130234/https://shopify.dev/&quot;&gt;Shopify.dev&lt;/a&gt; has become the primary home of all our developer-facing documentation and educational content, and we see lots of room for further growth and innovation in Shopify’s developer experience.&lt;/p&gt;&lt;p&gt;We feel pretty confident that this new information architecture sets us up to continue growing in an organized and scalable way. But whether it’s useful? Intuitive? Quick to navigate? Clear? That judgement is ultimately up to you.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href=&quot;https://gfscott.com/blog/how-we-rebuilt-shopify-developer-docs-again/&quot;&gt;How we rebuilt Shopify’s developer docs (again)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;</content>
  </entry>
  <entry>
    <title>Rebasing against a feature branch with Git cherry-pick</title>
    <link href="https://gfscott.com/blog/git-rebase-using-git-cherry-pick/" />
    <updated>2020-01-23T02:00:00Z</updated>
    <id>https://gfscott.com/blog/git-rebase-using-git-cherry-pick/</id>
    <content type="html">&lt;p&gt;I’ve been working on a project recently where we occasionally deal with some pretty gnarly rebases. I recently learned how to recover from them much more easily.&lt;/p&gt;&lt;p&gt;This is our setup&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/#fn1&quot; id=&quot;fnref1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;, which is &lt;a href=&quot;https://www.atlassian.com/agile/software-development/branching&quot;&gt;quite common&lt;/a&gt;:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The master branch runs in production&lt;/li&gt;&lt;li&gt;The team contributes to a very active and long-lived feature branch.&lt;/li&gt;&lt;li&gt;We branch off the feature branch and merge contributions back into it.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Anytime there’s an update to master, we have to rebase the feature branch against it. (We &lt;a href=&quot;https://i.imgur.com/VoQBiet.gif&quot;&gt;rebase on Wednesdays&lt;/a&gt; in order not to fall too far behind.) That means any personal working branch checked out before the rebase winds up with a ton of extraneous commits and merge conflicts when making a pull request on GitHub:&lt;/p&gt;&lt;p&gt;&lt;picture&gt;&lt;source type=&quot;image/avif&quot; srcset=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/rebase-hell-kaaE-vMsJpew-640w.avif 640w, https://gfscott.com/blog/git-rebase-using-git-cherry-pick/rebase-hell-kaaE-vMsJpew-928w.avif 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;source type=&quot;image/webp&quot; srcset=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/rebase-hell-kaaE-vMsJpew-640w.webp 640w, https://gfscott.com/blog/git-rebase-using-git-cherry-pick/rebase-hell-kaaE-vMsJpew-928w.webp 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;img loading=&quot;lazy&quot; decoding=&quot;async&quot; src=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/rebase-hell-kaaE-vMsJpew-640w.png&quot; alt=&quot;Image of a GitHub pull request with a bunch of post-rebase commits that shouldn’t be there&quot; width=&quot;928&quot; height=&quot;716&quot; srcset=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/rebase-hell-kaaE-vMsJpew-640w.png 640w, https://gfscott.com/blog/git-rebase-using-git-cherry-pick/rebase-hell-kaaE-vMsJpew-928w.png 928w&quot; sizes=&quot;100vw&quot;&gt;&lt;/picture&gt;&lt;/p&gt;&lt;p&gt;This is a screenshot of a real pull request. I’ve obscured my colleagues to protect the innocent.&lt;/p&gt;&lt;p&gt;Sometimes I’ve slogged through and reconciled the conflicts, but it took a long time and felt like a waste of effort. Other times I’ve just given up, deleted my local working branch, recreated the work on a fresh branch, and kept going.&lt;/p&gt;&lt;p&gt;A few weeks ago, my colleague &lt;a href=&quot;https://twitter.com/tiffany_tse&quot;&gt;Tiffany Tse&lt;/a&gt; taught me a technique that sidesteps this mess with &lt;code&gt;git cherry-pick&lt;/code&gt;. Note that this really only works if you pushed up your local working branch to remote (in my case, GitHub).&lt;/p&gt;&lt;h3 id=&quot;the-method&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/#the-method&quot;&gt;The method&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Here’s how it works:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Check out the rebased feature branch and do a pull so it’s up to date and includes all the rebase changes:&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout feature/branch
Switched to branch &lt;span class=&quot;token string&quot;&gt;&#39;feature/branch&#39;&lt;/span&gt;
$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; pull
Already up to date.&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&quot;2&quot;&gt;&lt;li&gt;Delete your local working branch that’s been caught in the rebase:&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; branch &lt;span class=&quot;token parameter variable&quot;&gt;-D&lt;/span&gt; my-working-branch
Deleted branch my-working-branch &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;was 478fe9e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&quot;3&quot;&gt;&lt;li&gt;Checkout a new branch with the same name as the old one:&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout &lt;span class=&quot;token parameter variable&quot;&gt;-b&lt;/span&gt; my-working-branch
Switched to a new branch &lt;span class=&quot;token string&quot;&gt;&#39;my-working-branch&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ol&gt;&lt;li&gt;This is where the magic happens. On GitHub&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/#fn2&quot; id=&quot;fnref2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt; find the commit(s) you need to rescue from rebase hell. In this example, it’s &lt;a href=&quot;https://github.com/gfscott/rebase-cherry-pick-example/commit/478fe9e68a71fd706149f95e0719cdd9bdc4197d&quot;&gt;this one&lt;/a&gt;. Use its hash to &lt;code&gt;git cherry-pick&lt;/code&gt; just that commit for your fresh branch:&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; cherry-pick 478fe9e68a71fd706149f95e0719cdd9bdc4197d
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;my-working-branch 77d97fc&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; adding a feature
 Date: Sat Jan &lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt;:39:23 &lt;span class=&quot;token number&quot;&gt;2020&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-0500&lt;/span&gt;
 &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;file&lt;/span&gt; changed, &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; insertions&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;+&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&quot;5&quot;&gt;&lt;li&gt;Finally, force-push the new branch to remote. Force-pushing wipes out all the rebased junk and re-applies the smaller batch of changes you were making:&lt;/li&gt;&lt;/ol&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push origin +my-working-branch&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(The plus-sign &lt;code&gt;+branchname&lt;/code&gt; shortcut was also new to me. It’s &lt;a href=&quot;https://stackoverflow.com/a/25937833&quot;&gt;functionally equivalent&lt;/a&gt; to &lt;code&gt;-f&lt;/code&gt;.)&lt;/p&gt;&lt;p&gt;In just a few days this has definitely saved me time and hassle, and it was something I’d never seen in many months of periodically searching for how to make this process less painful. Maybe it’ll save you some time too.&lt;/p&gt;&lt;h3 id=&quot;try-it-out-on-a-toy-repo&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/#try-it-out-on-a-toy-repo&quot;&gt;Try it out on a toy repo&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;To help understand the problem and this particular solution, I created a GitHub repo that recreates the problem:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/gfscott/rebase-cherry-pick-example/commits/master&quot;&gt;&lt;code&gt;master&lt;/code&gt;&lt;/a&gt; is up to date.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/gfscott/rebase-cherry-pick-example/commits/feature/branch&quot;&gt;&lt;code&gt;feature/branch&lt;/code&gt;&lt;/a&gt; was two commits ahead of master, but has been rebased against master.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/gfscott/rebase-cherry-pick-example/commits/my-working-branch&quot;&gt;&lt;code&gt;my-working-branch&lt;/code&gt;&lt;/a&gt; is one commit ahead of the non-rebased feature branch. A &lt;a href=&quot;https://github.com/gfscott/rebase-cherry-pick-example/compare/feature/branch...my-working-branch&quot;&gt;pull request against the feature branch&lt;/a&gt; produces merge conflicts.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Feel free to clone the repo to sandbox the problem and test the &lt;code&gt;git cherry-pick&lt;/code&gt; approach to rebasing.&lt;/p&gt;&lt;hr class=&quot;footnotes-sep&quot;&gt;&lt;section class=&quot;footnotes&quot;&gt;&lt;ol class=&quot;footnotes-list&quot;&gt;&lt;li id=&quot;fn1&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://rollout.io/blog/pitfalls-feature-branching/&quot;&gt;Some argue&lt;/a&gt; this whole scheme is a bad idea; I take the point but we deal with the world as it is. &lt;a href=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/#fnref1&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;li id=&quot;fn2&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;...or whatever service you use, of course. You could also comb through &lt;code&gt;git reflog&lt;/code&gt; if that’s your thing. &lt;a href=&quot;https://gfscott.com/blog/git-rebase-using-git-cherry-pick/#fnref2&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Content strategy on the command line</title>
    <link href="https://gfscott.com/blog/content-strategy-on-the-command-line/" />
    <updated>2019-11-21T13:10:00Z</updated>
    <id>https://gfscott.com/blog/content-strategy-on-the-command-line/</id>
    <content type="html">&lt;p&gt;Recently at work I had the opportunity to work on &lt;a href=&quot;https://github.com/Shopify/shopify-app-cli&quot;&gt;Shopify App CLI&lt;/a&gt;, a command-line tool to accelerate the development process for developers building on Shopify’s platform.&lt;/p&gt;&lt;p&gt;For a product content strategist, an all-text user interface is both intriguing and intimidating. I got to write about the process &lt;a href=&quot;https://www.shopify.com/partners/blog/content-strategy&quot;&gt;for Shopify’s Partner Blog&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Normally, content strategy is one piece of a much larger design puzzle: content strategists typically collaborate with UX researchers, designers, front end developers, and product managers to ship a new feature. Building a complex graphical user interface requires this kind of collaboration across a wide variety of UX practices.&lt;/p&gt;&lt;p&gt;But what happens when the content is the interface?&lt;/p&gt;&lt;p&gt;That’s the question we’ve been grappling with as we work on the Shopify App CLI, the open-source command-line tool we officially launched last week. The process raised some fascinating content strategy questions and while we haven’t answered them all yet, I’d like to share some of what we’ve learned so far.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://www.shopify.com/partners/blog/content-strategy&quot;&gt;Read the full post here.&lt;/a&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Create a dark mode with CSS Variables</title>
    <link href="https://gfscott.com/blog/dark-mode-with-css-variables/" />
    <updated>2019-04-19T21:54:00Z</updated>
    <id>https://gfscott.com/blog/dark-mode-with-css-variables/</id>
    <content type="html">&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;February 2022:&lt;/strong&gt; I finally got around to &lt;a href=&quot;https://gfscott.com/blog/dark-mode-for-real/&quot;&gt;implementing this method for real&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Apple introduced “dark mode” for macOS in late 2018 and now designers are scrambling to &lt;a href=&quot;https://www.wired.co.uk/article/google-chrome-dark-mode-design&quot;&gt;duplicate the look everywhere&lt;/a&gt;. While Apple’s &lt;a href=&quot;https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/dark-mode/&quot;&gt;own guidelines&lt;/a&gt; say dark mode is strictly an aesthetic preference, others cite benefits like &lt;a href=&quot;https://www.theverge.com/2018/11/8/18076502/google-dark-mode-android-battery-life&quot;&gt;improved battery life&lt;/a&gt; and &lt;a href=&quot;https://www.wired.com/story/give-yourself-to-dark-mode-side/&quot;&gt;reduced eye strain&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I&#39;m currently dabbling in my own site redesign, and trying to decide on an updated colour scheme. I was curious how complicated it would be to create my own dark mode.&lt;/p&gt;&lt;p&gt;It turns out: not that hard! But the approach I took relies on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/--*&quot;&gt;CSS Variables&lt;/a&gt;, so it requires a little planning. In this post I’ll explain how this minimal proof of concept works. You can &lt;a href=&quot;https://codepen.io/gfscott/pen/bJEBPe&quot;&gt;skip to the final product&lt;/a&gt; if you like.&lt;/p&gt;&lt;h3 id=&quot;1.-define-the-defaults&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-with-css-variables/#1.-define-the-defaults&quot;&gt;1. Define the defaults&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;I’m going to use light mode as the default, and let people switch to dark mode if they want. That means setting the default colours on the &lt;code&gt;:root&lt;/code&gt; element:&lt;/p&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorBackground&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; white&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorForeground&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; darkslategray&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorLink&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; royalblue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; crimson&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For now, I&#39;m setting colours for just a few elements. (For a more complex project, this list would surely get longer.)&lt;/p&gt;&lt;h3 id=&quot;2.-define-a-dark-colour-scheme&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-with-css-variables/#2.-define-a-dark-colour-scheme&quot;&gt;2. Define a dark colour scheme&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The benefit of CSS Variables is right there in the name: they’re &lt;em&gt;variable&lt;/em&gt;. You can change them any time and everything that references them changes too. So what we’ll do is change the colour values when they’re inside an element with a &lt;code&gt;.dark&lt;/code&gt; class:&lt;/p&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.dark&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorBackground&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; black&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorForeground&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lightgray&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorLink&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cornflowerblue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;--colorCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lightpink&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;3.-use-those-colour-variables-in-your-design&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-with-css-variables/#3.-use-those-colour-variables-in-your-design&quot;&gt;3. Use those colour variables in your design&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Now you simply use these variables in the rest of your CSS. Instead of, say, &lt;code&gt;color: darkslategray&lt;/code&gt;, you’d write &lt;code&gt;color: var(--colorForeground)&lt;/code&gt;. For example:&lt;/p&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--colorBackground&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;--colorForeground&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That may feel more cumbersome, but it allows you to be flexible in other ways. Want to change your look a year from now? Just edit those colour names.&lt;/p&gt;&lt;p&gt;In this case, the important part is that it also lets you use code to change colours on the fly.&lt;/p&gt;&lt;h3 id=&quot;4.-add-a-dark-mode-toggle&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-with-css-variables/#4.-add-a-dark-mode-toggle&quot;&gt;4. Add a dark mode toggle&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We already defined our dark mode colour scheme above. To activate it, we just have to add that &lt;code&gt;.dark&lt;/code&gt; class to the page. Since it’s an on-or-off value, let’s use a checkbox to toggle it on or off:&lt;/p&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;darkmode&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;checkbox&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;darkmode&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Dark Mode&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;By default, it will start unchecked. With Javascript, we can tell when that changes and toggle the &lt;code&gt;.dark&lt;/code&gt; class on the page’s &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; element:&lt;/p&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Find the checkbox by its ID&lt;/span&gt;
document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#darkmode&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// listen for when the box’s “checked” status changes&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;change&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// toggle the `.dark` class on the page’s body element&lt;/span&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;classList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toggle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;dark&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that’s it. When the user checks that box — either by tapping on a touch screen, clicking with a mouse, or navigating with their keyboard — the colour scheme switches to dark mode.&lt;/p&gt;&lt;h3 id=&quot;the-full-working-example&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-with-css-variables/#the-full-working-example&quot;&gt;The full working example&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://codepen.io/gfscott/pen/bJEBPe&quot;&gt;This Codepen&lt;/a&gt; demonstrates the principle in full.&lt;/p&gt;&lt;p class=&quot;codepen&quot; data-height=&quot;500&quot; data-theme-id=&quot;dark&quot; data-default-tab=&quot;html,result&quot; data-user=&quot;gfscott&quot; data-slug-hash=&quot;bJEBPe&quot; style=&quot;box-sizing:border-box;border:2px solid #000;justify-content:center;align-items:center;height:500px;margin:1em 0;padding:1em;display:flex&quot; data-pen-title=&quot;Simple Dark Mode with CSS Variables and 3 lines of Javascript&quot;&gt;&lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/gfscott/pen/bJEBPe/&quot;&gt;Simple Dark Mode with CSS Variables and 3 lines of Javascript&lt;/a&gt; by Graham F. Scott (&lt;a href=&quot;https://codepen.io/gfscott&quot;&gt;@gfscott&lt;/a&gt;) on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;&lt;/p&gt;&lt;script async defer=defer src=&quot;https://static.codepen.io/assets/embed/ei.js&quot;&gt;&lt;/script&gt;&lt;h3 id=&quot;getting-more-elaborate&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-with-css-variables/#getting-more-elaborate&quot;&gt;Getting more elaborate&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;This is a very minimal example, but you could extend it in several ways:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Use cookies to remember the user’s choice of colour scheme.&lt;/li&gt;&lt;li&gt;Create &lt;a href=&quot;https://codepen.io/bbodine1/pen/novBm&quot;&gt;more complex checkbox styles&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Offer more than two looks by changing the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; class to, say, &lt;code&gt;high-contrast&lt;/code&gt; or &lt;code&gt;bubblegum&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme&quot;&gt;Use a media query&lt;/a&gt; to detect the user’s preference at the OS/browser level.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;notes-and-caveats&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/dark-mode-with-css-variables/#notes-and-caveats&quot;&gt;Notes and caveats&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.caniuse.com/#feat=css-variables&quot;&gt;Support for CSS Variables&lt;/a&gt; is pretty good. As always, use progressive enhancement to make sure your site remains accessible to everyone.&lt;/li&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>Getting Cron jobs to actually run</title>
    <link href="https://gfscott.com/blog/getting-cron-jobs-to-actually-run/" />
    <updated>2019-03-18T18:43:39Z</updated>
    <id>https://gfscott.com/blog/getting-cron-jobs-to-actually-run/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Cron&quot;&gt;Cron&lt;/a&gt; is the utility that allows you to schedule repetitive tasks on Unix-like systems. Mostly I use it to schedule backups: I have a little script that sends a copy of my database to Amazon S3 every day. It seems like it should be straightforward, but there’s a gotcha that I didn’t understand for a long time, and I think I’m not alone.&lt;/p&gt;&lt;p&gt;You control Cron by editing your Crontab file with this command:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;$ &lt;span class=&quot;token function&quot;&gt;crontab&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Crontab’s &lt;a href=&quot;https://crontab.guru/&quot;&gt;syntax&lt;/a&gt; is terse: numbers separated by spaces to indicate time, and then a command to run:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token number&quot;&gt;35&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;22&lt;/span&gt; * * * &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;command&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For instance, that means “Run this command every day at 10:35 PM”. Not difficult to grasp conceptually. But I always had trouble getting cron jobs to run properly because almost no one tells beginners that the &lt;code&gt;&amp;lt;command&amp;gt;&lt;/code&gt; you invoke must be an &lt;em&gt;absolute&lt;/em&gt; path, not a &lt;em&gt;relative&lt;/em&gt; one.&lt;/p&gt;&lt;p&gt;Here’s a common situation to illustrate what I mean:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;You’re logged in to your machine as, say, a user named &lt;code&gt;webmaster&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;You have your backup script, &lt;code&gt;backup.sh&lt;/code&gt;, saved in your home directory.&lt;/li&gt;&lt;li&gt;You type &lt;code&gt;~/backup.sh&lt;/code&gt; into the command line.&lt;/li&gt;&lt;li&gt;The script runs successfully!&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;So intuitively, it feels like this Crontab line &lt;em&gt;should&lt;/em&gt; work:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token number&quot;&gt;35&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;22&lt;/span&gt; * * * ~/backup.sh&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;...but it won’t, because when Cron runs, it doesn’t assume that user’s identity. It’s not starting its operation from their home directory, so the relative path doesn’t work.&lt;/p&gt;&lt;p&gt;Instead, you have to use the full, &lt;a href=&quot;https://www.cs.bu.edu/teaching/unix/reference/vocab.html#fullpath&quot;&gt;absolute path&lt;/a&gt; to the script, starting from the root directory:&lt;/p&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token number&quot;&gt;35&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;22&lt;/span&gt; * * * /home/webmaster/backup.sh&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I was confused for, literally, years about why I couldn’t get cron jobs to run, and this was almost always why. Many of the popular Crontab tutorials and blog posts out there, even &lt;a href=&quot;https://www.cyberciti.biz/faq/how-do-i-add-jobs-to-cron-under-linux-or-unix-oses/&quot;&gt;otherwise fairly thorough ones&lt;/a&gt;, use absolute file paths in their examples — but they very rarely explain &lt;em&gt;why&lt;/em&gt;, or that this is effectively a requirement, not just a convention.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Theatre in 2030</title>
    <link href="https://gfscott.com/blog/theatre-in-2030/" />
    <updated>2015-07-29T08:17:00Z</updated>
    <id>https://gfscott.com/blog/theatre-in-2030/</id>
    <content type="html">&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;February 2021:&lt;/strong&gt; A few years ago I was involved in a digital theatre project called &lt;a href=&quot;https://spiderwebshow.ca/&quot;&gt;SpiderWebShow&lt;/a&gt;, which is still going today. In early 2015, the project members were each asked to articulate a vision of theatre in 2030. Our answers appeared as part of Sarah Garton Stanley’s article “&lt;a href=&quot;https://ctr.utpjournals.press/doi/full/10.3138/ctr.163.003&quot;&gt;Spinning a National Imaginary&lt;/a&gt;” in &lt;em&gt;Canadian Theatre Review&lt;/em&gt;. I recently stumbled over my answers again and thought it was fun, so here it is.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Theatre keeps fracturing and forking, so there is no coherent vision for theatre in 2030. Instead a thousand new genres are flourishing. Including:&lt;/p&gt;&lt;h3 id=&quot;recap-theatre&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/theatre-in-2030/#recap-theatre&quot;&gt;Recap theatre&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Actors re-enact episodes of popular television shows, incorporating audience suggestions for script improvements, character motivations and alternate outcomes. Over time these theatrical versions are deviating so far from their source material as to bear almost no resemblance.&lt;/p&gt;&lt;h3 id=&quot;exotheatre&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/theatre-in-2030/#exotheatre&quot;&gt;Exotheatre&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Preparations are underway for the first theatre performance to be produced on another planet. It takes the format of a ritualized memorial to the Soviet Mars 2 lander, the first human artifact to reach the surface of the Red Planet. The performance has an audience of three humans and five robots.&lt;/p&gt;&lt;h3 id=&quot;singularity-theatre&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/theatre-in-2030/#singularity-theatre&quot;&gt;Singularity theatre&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Artificial intelligences that have ingested the entire known cultural corpus from the &lt;em&gt;Epic of Gilgamesh&lt;/em&gt; to &lt;em&gt;The Real Housewives of Taipei&lt;/em&gt; have become prolific authors of theatrical works, with the leading programs having written billions of plays, most of which remain unperformed and unread by humans.&lt;/p&gt;&lt;h3 id=&quot;fake-theatre&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/theatre-in-2030/#fake-theatre&quot;&gt;Fake theatre&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The audience receives a weekly email describing to them a piece of theatre they could have seen but did not attend. It describes the production in broad strokes, provides a list of specific moments or telling details for credibility, and a unique, algorithmically generated opinion about the production you can share with friends.&lt;/p&gt;&lt;h3 id=&quot;xeritheatre&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/theatre-in-2030/#xeritheatre&quot;&gt;Xeritheatre&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;A movement of touring companies which thrive throughout the western North American dust bowl, mounting quasi-religious revivalist passion plays about water conservation and ecological hubris, and distributing desperately needed bottled water and iodine pills.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Podcast: Web analytics for magazines</title>
    <link href="https://gfscott.com/blog/podcast-magazine-analytics/" />
    <updated>2015-02-23T09:00:00Z</updated>
    <id>https://gfscott.com/blog/podcast-magazine-analytics/</id>
    <content type="html">&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;July 2022&lt;/strong&gt;: I recently ran across this recording again and wanted to make sure it was saved in my archive. Some of this advice still holds, although, listening today, it definitely shows its age.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Recently I &lt;a href=&quot;https://magazinescanada.ca/audiomag/ep-82-graham-scott/&quot;&gt;spoke about web analytics&lt;/a&gt; with Tina Pittaway, host of the &lt;a href=&quot;https://magazinescanada.ca/&quot;&gt;Magazines Canada&lt;/a&gt; podcast, &lt;a href=&quot;https://magazinescanada.ca/audiomag/&quot;&gt;AudioMag&lt;/a&gt;. We talked about some of the ways magazines can measure their success online, distinguishing signal from noise, and making sure you’re using metrics to answer the right questions.&lt;/p&gt;&lt;h2 id=&quot;audio&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/podcast-magazine-analytics/#audio&quot;&gt;Audio&lt;/a&gt;&lt;/h2&gt;&lt;audio controls src=&quot;https://obj.gfscott.com/audiomag-20150223-graham-scott-web-analytics.mp3&quot;&gt;&lt;a href=&quot;https://obj.gfscott.com/audiomag-20150223-graham-scott-web-analytics.mp3&quot; download&gt;Download MP3 file&lt;/a&gt;&lt;/audio&gt;&lt;h2 id=&quot;transcript&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;https://gfscott.com/blog/podcast-magazine-analytics/#transcript&quot;&gt;Transcript&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;em&gt;I’ve lightly edited this transcript for better readability.&lt;/em&gt;&lt;/p&gt;&lt;style&gt;span.timestamp{color:var(--colorFgSecondary);padding-left:1em;font-family:JetBrains Mono,monospace}&lt;/style&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;0:04&lt;/span&gt;&lt;br&gt;Today on audio mag:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;&lt;span class=&quot;timestamp&quot;&gt;0:06&lt;/span&gt;&lt;/span&gt;&lt;br&gt;We&#39;re discovering that some of the things that we think audiences should be interested in, they&#39;re just not. And that can be really hard to hear for publishers. But the truth is better than not knowing. Because if you if you don&#39;t know, you can&#39;t fix it.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;0:21&lt;/span&gt;&lt;br&gt;&lt;em&gt;Canadian Business&lt;/em&gt;’s Graham Scott on what you need to know about web analytics.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;0:25&lt;/span&gt;&lt;br&gt;There’s nowhere to hide.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;0:26&lt;/span&gt;&lt;br&gt;That’s next.&lt;/p&gt;&lt;p&gt;Welcome to AudioMag from Magazines Canada, I&#39;m Tina Pittaway.&lt;/p&gt;&lt;p&gt;Understanding the analytics behind your digital offerings is crucial in trying to better understand how your readers are connecting with you. Analytics lets you find clear answers on what people are reading, how far down they&#39;re scrolling and who the frequent sharers are.&lt;/p&gt;&lt;p&gt;But how often should you dive into the data? And just because something doesn&#39;t perform well online, what can the data reveal that might point you towards doing it better next time? Sometimes it&#39;s as simple as creating a different kind of headline in the one offered on the printed page.&lt;/p&gt;&lt;p&gt;Graham Scott understands a lot about analytics. He&#39;s the Managing Editor, Digital at &lt;em&gt;Canadian Business&lt;/em&gt;, and teaches &lt;a href=&quot;https://continuing.torontomu.ca/search/publicCourseSearchDetails.do?method=load&amp;amp;courseId=26167&quot;&gt;The Online Publishing Toolkit&lt;/a&gt; at Ryerson University’s Chang School for Continuing Education. I spoke with him following a two-part webinar he presented on this very subject. I began by asking him why it&#39;s important for publishers to have a good understanding of analytics.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;1:32&lt;/span&gt;&lt;br&gt;Analytics gives you a way to answer questions. I think when you&#39;re doing it, right, you know, so much of the analytics we have now we, you know, we can measure all kinds of things. And in a way, we can measure too many things. Like, when I sit there, and I open up our analytics suite for CanadianBusiness.com, we literally have three or four different analytics platforms that measure everything that happens on the site.&lt;/p&gt;&lt;p&gt;So one of them measures in real time. So I can see what are people reading on the site right now; we have one that is kind of demographics, comScore, which measures things so we can sell the site to advertisers. We have Omniture, which is meant to be kind of the big official one — it&#39;s what we base decision-making on and all of that stuff.&lt;/p&gt;&lt;p&gt;And, like, honestly, you can measure pageviews, you can measure the the amount of time that people spend on the page, you can measure how many visitors and how many pages each one goes to. And there are more of these numbers being added all the time. Like, if you if you want it, it is there.&lt;/p&gt;&lt;p&gt;But that&#39;s not necessarily helpful if it&#39;s just a blizzard of numbers. And what I think the the usefulness of analytics is, is when you have a hypothesis as a publisher, and analytics is a way to help you test that hypothesis and make business decisions.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;2:54&lt;/span&gt;&lt;br&gt;What is the really sweet spot for what you&#39;re trying to engage the reader with? Or does it depend on the platform that they&#39;re accessing? What guides that?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;3:05&lt;/span&gt;&lt;br&gt;I mean, the platform definitely matters. And I think app measurement is a whole other kettle of fish. And one, frankly, that I don&#39;t have a lot of experience with. So my experience is related to measuring people&#39;s engagement on websites. And on the open web, I would say.&lt;/p&gt;&lt;p&gt;And I mean, the platform matters there because people behave differently if they&#39;re at a desktop computer, at their desk, or their laptop. And that&#39;s different from how they behave on a tablet or a smartphone. So it is important to keep track of all of that information so that you have it.&lt;/p&gt;&lt;p&gt;The thing that&#39;s happening right now is that we&#39;re actually kind of in a period of change for what publishers are looking to prove to advertisers in terms of who is on their site. And this is largely driven by advertisers and publishers wanting to show not only that people land and look at a page on your website, but that they are repeat visitors, that they are people who will share your content with their friends on Facebook or Twitter, that they are a desirable demographic group that is reading your site and that they&#39;re they&#39;re sticking around for a long time.&lt;/p&gt;&lt;p&gt;And the way that the web started, or the way that the &lt;em&gt;monetization&lt;/em&gt; of the web started, was with the banner ad, and that was a pretty crude measure, because all we had was: “Did someone look at the site, or did they not look at the site?” And what a lot of publishers are finding now is that they&#39;re having to prove engagement at a deeper level. And so they&#39;re looking for things like “how far down the page did someone scroll?” and “How long did they spend on the site, and how often do they come back?” And “What is their social relationship to the site, in terms of social media?”&lt;/p&gt;&lt;p&gt;All of those things together are proving not just awareness of a site, but trying to prove that the people, you know that your audience is really an &lt;em&gt;audience&lt;/em&gt;, and not just a bunch of random people clicking on on random links. And I think that&#39;s particularly important for magazine publishers.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;5:22&lt;/span&gt;&lt;br&gt;Why are advertisers interested in those heavier users? Why aren&#39;t they just interested in reaching that eyeball for three seconds?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;5:33&lt;/span&gt;&lt;br&gt;Well, I mean, for advertisers, especially brand advertisers, they want to be in a high-quality environment. And that&#39;s something the basis of which we&#39;ve sold print magazines for years: that a subscriber who has paid up for your magazine is considered by many advertisers to be more valuable than someone who is just getting it for free. And magazines that have that kind of premium audience are able to charge a premium.&lt;/p&gt;&lt;p&gt;And the same is true on the web: that people want, you know, they want access to the &lt;em&gt;right&lt;/em&gt; audience. And, you know, the promise of the web always was we know exactly who was on the site, like you can go much deeper; you can get much more granular as well and have a clearer idea of what is happening on a website than you necessarily can in the pages of a magazine that has gone to your subscriber.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;6:31&lt;/span&gt;&lt;br&gt;I know one of the statistics that you put up in one of your presentations was that something like 40% of the articles at the &lt;em&gt;Globe and Mail&lt;/em&gt; are read by only 1,000 people.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;6:42&lt;/span&gt;&lt;br&gt;There&#39;s nowhere to hide here. And that&#39;s, one of the things that is scary about it — is that for a long time, you know, we were able to talk about these things in fairly abstract ways, like who the audience was, how valuable they were, what their interests were. And now we are getting kind of a warts-and-all picture of the readership in a way that, you know, was not quite possible before. And there are publishers who are certainly finding that, you know, the things that they thought were successful for them, are not necessarily successful on the web.&lt;/p&gt;&lt;p&gt;Part of this has to do with the fact that the kinds of things that work well in a print magazine don&#39;t always work well on the web, and it takes a different strategy. And if you just take something from a magazine and put it online, it is unlikely to perform really well.&lt;/p&gt;&lt;p&gt;The other part of this is that we&#39;re discovering that some of the things that we &lt;em&gt;think&lt;/em&gt; audiences should be interested in, they&#39;re just not. And that can be really hard to hear for publishers. But the truth is better than not knowing. Because if you if you don&#39;t know you can&#39;t fix it. Once you kind of steel yourself to look with a fairly clear view at what is actually working on your site and what is not working on your site, you can have a very clear picture of that. You know what people find engaging, what they love clicking on, what they love sharing with their friends, and that can teach you a lot about your your audience.&lt;/p&gt;&lt;p&gt;I think there&#39;s a huge amount of opportunity there. It can be really painful as well. The sensation of opening something up and seeing that it just has not connected with the audience, that they are not interested — that can shatter some some illusions that we clung to, and still do cling to, for totally understandable reasons.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;8:34&lt;/span&gt;&lt;br&gt;What are some of the challenges of just too much data?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;8:39&lt;/span&gt;&lt;br&gt;That is a huge challenge. So one of the experts that I&#39;ve quoted several times on this issue is a guy named Stijn Debrouwere, who is a developer who has worked on the &lt;em&gt;Guardian&lt;/em&gt; and several other high profile projects. And the saying of his, that I really love and that I quote as often as I can is: “&lt;a href=&quot;http://debrouwere.org/2012/08/24/metrics-are-for-doing-not-for-staring/&quot;&gt;Analytics are for doing, not for staring&lt;/a&gt;.”&lt;/p&gt;&lt;p&gt;And this is something that I&#39;ve absolutely done: open up the the analytics site, check out the numbers, and if the graph has ticked up that day, I feel great. And if the graph has ticked down, I feel bad. And that is, like a &lt;em&gt;totally natural&lt;/em&gt; thing to do, but it&#39;s not much use.&lt;/p&gt;&lt;p&gt;What I think people need to focus on is to find a couple of metrics that make sense for their business goals, monitor those, unclench a bit, and calm down about the day-to-day noise in that data. You want to stay on top of it, but it&#39;s really hard to think about this stuff in a sensible way when you&#39;re captive to every twitch and jerk in the numbers. You need to kind of smooth it out. Look at it, you know, not day-to-day, but look at it week-to-week, month-to-month, quarter-to-quarter. And in that way, you can start to get a better picture, and a more &lt;em&gt;actionable&lt;/em&gt; picture, where you can look at what the big trends are, what the big opportunities are for you as a publisher, and then make a decision that&#39;s going to actually have a material effect on your business.&lt;/p&gt;&lt;p&gt;That could be all kinds of things. It could be to do with the functionality of the site. It could be to do with the editorial content. There are lots of different knobs you can turn and dials you can adjust in terms of what you are doing day-to-day. And if you&#39;ve got a question that you want answered, those analytics can give you those answers.&lt;/p&gt;&lt;p&gt;To give you a concrete example: say you have a &amp;quot;Subscribe Now&amp;quot; button on the site, and you&#39;ve got one at the top of the page, and you&#39;ve got one at the bottom of the page. And at the moment, it&#39;s red, and you want to try it in green, and you have a suspicion that green is going to be a more clickable color. And you&#39;ll sell more subscriptions as a result. And that is a hypothesis that you can test. And you can see the results in analytics by, you know, making some changes to the site, observing how people react to that, and seeing whether it produces an uptick in subscriptions.&lt;/p&gt;&lt;p&gt;That is not something that you&#39;re going to stumble across by just opening up analytics and staring at the line. What you need is a question, and it can help you find an answer.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;11:36&lt;/span&gt;&lt;br&gt;There&#39;s also you when you brought up that notion of publishers realizing that readers just aren&#39;t interested in certain areas. Could it also help inform publishers that they&#39;re just telling a story the wrong way? Or on certain platforms? Like they&#39;re not using images or visualization well enough? Is it one of those things where you abandon those kinds of stories? Or whatever it is that that isn&#39;t working? Or you try to figure out if you&#39;re just telling those stories wrong?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;12:11&lt;/span&gt;&lt;br&gt;I mean, I think that is kind of the kind of question that you need to put to your analytics. Say Story X is not performing to your expectations; you have an editorial hunch that there is an interest out there in it, and you put it up and it kind of dies. It doesn&#39;t get a lot of readers spending time on it, it doesn&#39;t get socially shared, any of that kind of stuff.&lt;/p&gt;&lt;p&gt;There are so many ways that you could experiment with changing that story itself, or changing the type of story that you do. Maybe it’s as simple as you need to change the style of your headline writing. That is a huge thing that lots of publishers are still figuring out: that the kind of headline writing that does gangbusters in print just dies online. So how could you try selling the story? How is that headline promoting that story? And is it doing the work that you need it to do?&lt;/p&gt;&lt;p&gt;It could also be: are you getting it to your readers in the way that they want it? Is it that it&#39;s just not catching on on Twitter, but it&#39;s going great on Facebook? If you need to put a slightly different spin on it in on one platform versus another when you&#39;re doing that key promotion?&lt;/p&gt;&lt;p&gt;A lot of people struggle with their homepage. If I go to CanadianBbusiness.com and see all the stories that are there, do people land on that page, and then click on something and find something that interests them? Or do they land on the page, and then throw up their hands? Are they finding content that that speaks to them when they get there?&lt;/p&gt;&lt;p&gt;Could you experiment with better images on the homepage? Is there a tweak that you can make to the design of the homepage that will help that reader who gets there get that second click, who will go and read the article, and share the article, and love the article, and sign up for your newsletter, and subscribe and give me money, and do all that great stuff that follows on from it.&lt;/p&gt;&lt;p&gt;And those are the kind of questions that that analytics can help you with in terms of saying like, you know, if you make the images bigger, and maybe illustration works for you better on your homepage, that&#39;s a question that you can ask and that I think analytics can can help you get some clarity on.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;14:40&lt;/span&gt;&lt;br&gt;For those smaller publishers who don&#39;t have a huge staff. What are some of the tools that they can tap into? You mentioned plugins, what are they and how can they help?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;14:51&lt;/span&gt;&lt;br&gt;So something that I&#39;ve done a little bit of writing on recently is plugins that are designed to serve related content on your site.&lt;/p&gt;&lt;p&gt;You see these all over the place, like at the end of articles, often you&#39;ll scroll down and there&#39;ll be a couple of tiles, and they will say, you know, “Recommended for You” or, you know, “Other Stories You May Like”. In many cases, these are third-party plugins that are quite inexpensive to implement, sometimes free, and, usually, technically, not terrifically difficult to implement on a site.&lt;/p&gt;&lt;p&gt;In many cases, they can help you in a way that is low effort on your part, increase the time that people spend on a site. So say someone gets the end of your article, you know, it&#39;s a, it&#39;s a great piece of writing, and they really enjoy what it what it has to say, and they might be open to reading more on your site, if you can get a couple of links down at the bottom to stuff that is relevant to them. So they&#39;ve read, they&#39;ve just read something really great, and they want more, it will help you kind of automatically find related articles on your site, and it will suggest them to them.&lt;/p&gt;&lt;p&gt;And second thing it will do is it will find the things that are already popular on your site. So the things that attract the most pageviews. And it&#39;s likely that those are quite appealing. And it&#39;ll suggest those as well.&lt;/p&gt;&lt;p&gt;And so a couple of services, you know, one is Outbrain; if you&#39;re a larger publisher, Taboola is one, that, that I believe that a paid product, Contextly is another one. And there&#39;s even something as simple as the Facebook recommended box, which takes copying and pasting one little piece of code. And it will start recommending things that your friends have recommended on Facebook to every visitor who shows up.&lt;/p&gt;&lt;p&gt;That kind of thing can have a really measurable effect on the amount of time people spend on your site, how often they come back, and making sure that they find something they like when they get there.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;17:12&lt;/span&gt;&lt;br&gt;It&#39;s both exciting and terrifying all at once.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Graham Scott&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;17:15&lt;/span&gt;&lt;br&gt;It certainly is. But you know, the nice thing about it is that if you have a question, you can implement a change fairly quickly, and see results fairly quickly.&lt;/p&gt;&lt;p&gt;And, you know, the best publishers who are doing this are doing it all the time — just a constant series of optimizations that they&#39;re doing. And sometimes, you know, it produces, you know, half a percent more pageviews or more unique visitors or things like that. If you do 10 of those things in a row, 10 Little fixes that make a measurable increase, that starts to add up to something real. And not all of them are going to work. But that&#39;s okay.&lt;/p&gt;&lt;p&gt;One of the nice things about the web, that was not necessarily true in print, is that, with a monthly or bimonthly magazine that comes out six to 12 times a year, you&#39;ve got to get it right, and you&#39;ve got to get it right every single time. Whereas on the web, you know, you&#39;ve got another chance the next day — there&#39;s always another train coming. The stakes, in many ways, are higher, but they can be lower, in terms of the individual actions that you can take. If something you try doesn&#39;t stick, stop doing it.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tina Pittaway&lt;/strong&gt; &lt;span class=&quot;timestamp&quot;&gt;18:38&lt;/span&gt;&lt;br&gt;Absolutely. That&#39;s there&#39;s so much there to chew on. Thanks so much, Graham. All right. Thank you. Graham Scott, Managing Editor, Digital at &lt;em&gt;Canadian Business&lt;/em&gt;.&lt;/p&gt;</content>
  </entry>
</feed>