<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
	<title>Mike Fallows</title>
	<subtitle>Full feed of posts on mikefallows.com</subtitle>
	
	<link href="https://mikefallows.com/feed/feed.xml" rel="self"/>
	<link href="https://mikefallows.com/"/>
	<updated>2023-10-25T09:00:00Z</updated>
	<id>https://mikefallows.com/</id>
	<author>
		<name>Mike Fallows</name>
	</author>
	
	<entry>
		<title>Adding an SVG favicon with dark mode support</title>
		<link href="https://mikefallows.com/posts/adding-an-svg-favicon-with-dark-mode-support/"/>
		<updated>Wed, 25 Oct 2023 09:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/adding-an-svg-favicon-with-dark-mode-support/</id>
		<content type="html">&lt;p&gt;When I first set this site up I used &lt;a href=&quot;https://css-tricks.com/emoji-as-a-favicon/&quot;&gt;this technique&lt;/a&gt; to add a favicon with the 🥶 (cold face) emoji. This was a great way to have a favicon present without having to stress about designing one, or resorting to my usual default of a black square 🥱. Good enough for now, I thought, I can worry more about it later.&lt;/p&gt;
&lt;p&gt;Here&#39;s the code I used for that.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;link rel=&amp;quot;shortcut icon&amp;quot; href=&amp;quot;data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%3Ctext%20y%3D%22.9em%22%20font-size%3D%2290%22%3E%F0%9F%A5%B6%3C%2Ftext%3E%3C%2Fsvg%3E&amp;quot; type=&amp;quot;image/svg+xml&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That was fine until I noticed that a search result for my site in &lt;a href=&quot;https://duckduckgo.com/&quot;&gt;DuckDuckGo&lt;/a&gt; (my primary search engine at time of writing) didn&#39;t support emojis as a shortcut icon. I think partially because it (or Bing or whichever system it uses for it) tries to generate a small self-hosted image for the short cut icon. Drat. Time to do something about it.&lt;/p&gt;
&lt;p&gt;I still didn&#39;t really want to have to &lt;em&gt;design&lt;/em&gt; anything. I can tackle that later if I ever get around to seriously thinking about the visual identity of this site. For now I wanted a small improvement, a &lt;em&gt;kaizen&lt;/em&gt;[^1] if you will. So I thought about whether it was possible to get raw SVG versions of emoji. In my search I came across &lt;a href=&quot;https://openmoji.org/&quot;&gt;OpenMoji&lt;/a&gt; which I&#39;d seen mentioned before. It provides open source versions of emoji that are available as coloured or outlined SVG files. Perfect!&lt;/p&gt;
&lt;p&gt;I decided to switch from 🥶 (cold face) to 👾 (space invader/alien monster) as I really liked the image by Antonia Wagner, and it worked well in outline. I decided to go for outline so that I didn&#39;t have to commit to a colour. I also wanted to implement dark mode support to the SVG and I though it would make doing that simpler.&lt;/p&gt;
&lt;h2&gt;Dark mode support&lt;/h2&gt;
&lt;p&gt;Adding dark mode support was pretty simple. First I replaced all hardcoded colour references to &lt;code&gt;currentColor&lt;/code&gt; so I could control the colour more reliably through CSS. eg.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff-svg&quot;&gt;- &amp;lt;rect ... stroke=&amp;quot;#000000&amp;quot; ... /&amp;gt;
+ &amp;lt;rect ... stroke=&amp;quot;currentColor&amp;quot; ... /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I added some CSS in the body of the SVG file by using a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag inside a &lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt; tag. That CSS simply applied a black stroke to all the visual elements in the SVG, and using an &lt;code&gt;@media&lt;/code&gt; rule, switched that to white when a visitor is in dark mode.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-svg&quot;&gt;&amp;lt;svg ... &amp;gt;
  &amp;lt;defs&amp;gt;
    &amp;lt;style&amp;gt;
      rect, path, line, polyline {
        stroke: black;
        color-scheme: light dark;
      }
      @media (prefers-color-scheme:dark) {
        rect, path, line, polyline {
          stroke: white;
        }
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/defs&amp;gt;
  ...
&amp;lt;/svg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it! And here&#39;s the cute little critter in all its dark-mode-detecting[^2] glory.&lt;/p&gt;
&lt;style&gt;
  @keyframes cycle-background {
    0% { background-color: hsl(0deg 100% 30% / 50%); }
    20% { background-color: hsl(72deg 100% 30% / 50%); }
    40% { background-color: hsl(144deg 100% 30% / 50%); }
    60% { background-color: hsl(216deg 100% 30% / 50%); }
    80% { background-color: hsl(288deg 100% 30% / 50%); }
    100% { background-color: hsl(360deg 100% 30% / 50%); }
  }

  #alien-demo {
    padding: 18px;
    width: calc(72px + (18px * 2));
    aspect-ratio: 1;
    border-radius: 100%;
    margin-inline: auto;
    color-scheme: light dark;
    animation: cycle-background 12s infinite;
  }
&lt;/style&gt;
&lt;div id=&quot;alien-demo&quot;&gt;
  &lt;svg id=&quot;emoji&quot; viewBox=&quot;0 0 72 72&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
    &lt;defs&gt;
      &lt;style&gt;
        rect, path, line, polyline {
          stroke: black;
          color-scheme: light dark;
        }
        @media (prefers-color-scheme: dark) {
          rect, path, line, polyline {
            stroke: white;
          }
        }
      &lt;/style&gt;
    &lt;/defs&gt;
    &lt;g id=&quot;line&quot;&gt;
      &lt;rect x=&quot;25.175&quot; y=&quot;31&quot; width=&quot;3.6&quot; height=&quot;6&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;/rect&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;22,45 16,45 16,39&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;22.583,25 22.583,20 26,20&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;48.708,25 48.708,20 45.292,20&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;13,35 10,35 10,20 16,20 16,35&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;56,35 56,20 62,20 62,35 59,35&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;26,20 26,14 32,14 32,20&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;39,20 39,14 45,14 45,20&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;16,35 19,35 19,38 13,38 13,35&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;59,35 59,38 53,38 53,35 56,35&quot;&gt;&lt;/polyline&gt;
      &lt;rect x=&quot;16&quot; y=&quot;51&quot; width=&quot;6&quot; height=&quot;6&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;/rect&gt;
      &lt;rect x=&quot;50&quot; y=&quot;51&quot; width=&quot;6&quot; height=&quot;6&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;/rect&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;28,45 28,51 22,51 22,45&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;50,45 56,45 56,39&quot;&gt;&lt;/polyline&gt;
      &lt;polyline fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; points=&quot;44,45 44,51 50,51 50,45&quot;&gt;&lt;/polyline&gt;
      &lt;rect x=&quot;43.425&quot; y=&quot;31&quot; width=&quot;3.6&quot; height=&quot;6&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;/rect&gt;
      &lt;path fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M44,45L44,45z&quot;&gt;&lt;/path&gt;
      &lt;path fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M39,20L39,20z&quot;&gt;&lt;/path&gt;
      &lt;path fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M16,25L16,25z&quot;&gt;&lt;/path&gt;
      &lt;path fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M49,25L49,25z&quot;&gt;&lt;/path&gt;
      &lt;line x1=&quot;28&quot; x2=&quot;44&quot; y1=&quot;45&quot; y2=&quot;45&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;/line&gt;
      &lt;line x1=&quot;32&quot; x2=&quot;39&quot; y1=&quot;20&quot; y2=&quot;20&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;/line&gt;
      &lt;line x1=&quot;16&quot; x2=&quot;22&quot; y1=&quot;25&quot; y2=&quot;25&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;/line&gt;
      &lt;line x1=&quot;49&quot; x2=&quot;56&quot; y1=&quot;25&quot; y2=&quot;25&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot;&gt;&lt;/line&gt;
    &lt;/g&gt;
  &lt;/svg&gt;
&lt;/div&gt;
&lt;p&gt;And here&#39;s the code in full.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-svg&quot;&gt;&amp;lt;svg id=&amp;quot;emoji&amp;quot; viewBox=&amp;quot;0 0 72 72&amp;quot; xmlns=&amp;quot;http://www.w3.org/2000/svg&amp;quot;&amp;gt;
  &amp;lt;defs&amp;gt;
    &amp;lt;style&amp;gt;
      rect, path, line, polyline {
        stroke: black;
        color-scheme: light dark;
      }
      @media (prefers-color-scheme:dark) {
        rect, path, line, polyline {
          stroke: white;
        }
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/defs&amp;gt;
  &amp;lt;g id=&amp;quot;line&amp;quot;&amp;gt;
    &amp;lt;rect x=&amp;quot;25.175&amp;quot; y=&amp;quot;31&amp;quot; width=&amp;quot;3.6&amp;quot; height=&amp;quot;6&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;22,45 16,45 16,39&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;22.583,25 22.583,20 26,20&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;48.708,25 48.708,20 45.292,20&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;13,35 10,35 10,20 16,20 16,35&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;56,35 56,20 62,20 62,35 59,35&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;26,20 26,14 32,14 32,20&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;39,20 39,14 45,14 45,20&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;16,35 19,35 19,38 13,38 13,35&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;59,35 59,38 53,38 53,35 56,35&amp;quot;/&amp;gt;
    &amp;lt;rect x=&amp;quot;16&amp;quot; y=&amp;quot;51&amp;quot; width=&amp;quot;6&amp;quot; height=&amp;quot;6&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot;/&amp;gt;
    &amp;lt;rect x=&amp;quot;50&amp;quot; y=&amp;quot;51&amp;quot; width=&amp;quot;6&amp;quot; height=&amp;quot;6&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;28,45 28,51 22,51 22,45&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;50,45 56,45 56,39&amp;quot;/&amp;gt;
    &amp;lt;polyline fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; points=&amp;quot;44,45 44,51 50,51 50,45&amp;quot;/&amp;gt;
    &amp;lt;rect x=&amp;quot;43.425&amp;quot; y=&amp;quot;31&amp;quot; width=&amp;quot;3.6&amp;quot; height=&amp;quot;6&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot;/&amp;gt;
    &amp;lt;path fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; d=&amp;quot;M44,45L44,45z&amp;quot;/&amp;gt;
    &amp;lt;path fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; d=&amp;quot;M39,20L39,20z&amp;quot;/&amp;gt;
    &amp;lt;path fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; d=&amp;quot;M16,25L16,25z&amp;quot;/&amp;gt;
    &amp;lt;path fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot; d=&amp;quot;M49,25L49,25z&amp;quot;/&amp;gt;
    &amp;lt;line x1=&amp;quot;28&amp;quot; x2=&amp;quot;44&amp;quot; y1=&amp;quot;45&amp;quot; y2=&amp;quot;45&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot;/&amp;gt;
    &amp;lt;line x1=&amp;quot;32&amp;quot; x2=&amp;quot;39&amp;quot; y1=&amp;quot;20&amp;quot; y2=&amp;quot;20&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot;/&amp;gt;
    &amp;lt;line x1=&amp;quot;16&amp;quot; x2=&amp;quot;22&amp;quot; y1=&amp;quot;25&amp;quot; y2=&amp;quot;25&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot;/&amp;gt;
    &amp;lt;line x1=&amp;quot;49&amp;quot; x2=&amp;quot;56&amp;quot; y1=&amp;quot;25&amp;quot; y2=&amp;quot;25&amp;quot; fill=&amp;quot;none&amp;quot; stroke=&amp;quot;currentColor&amp;quot; stroke-linecap=&amp;quot;round&amp;quot; stroke-linejoin=&amp;quot;round&amp;quot; stroke-width=&amp;quot;2&amp;quot;/&amp;gt;
  &amp;lt;/g&amp;gt;
&amp;lt;/svg&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&#39;s the code I needed to add to the head. As I was switching to using an external SVG file I noticed that I needed to add the &lt;code&gt;sizes=&amp;quot;any&amp;quot;&lt;/code&gt; to get it to show up properly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;link rel=&amp;quot;shortcut icon&amp;quot; href=&amp;quot;/public/alien.svg&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; sizes=&amp;quot;any&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All in all, a neat little way to get a dark mode supporting favicon!&lt;/p&gt;
&lt;p&gt;[^1]: To &lt;em&gt;&#39;change for better&#39;&lt;/em&gt;. A business concept that refers to small incremental improvements. &lt;a href=&quot;https://en.wikipedia.org/wiki/Kaizen&quot;&gt;Wiki article&lt;/a&gt;.
[^2]: Note: this only responds to the system dark mode, not the personal setting for the site because the colour scheme depends on the browser&#39;s chrome not the site&#39;s design.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Optimising CSS minification in Liquid</title>
		<link href="https://mikefallows.com/posts/optimising-css-minification-in-liquid/"/>
		<updated>Tue, 24 Oct 2023 23:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/optimising-css-minification-in-liquid/</id>
		<content type="html">&lt;div class=&quot;note&quot;&gt;
&lt;p&gt;2025-09-22 Update: &lt;a href=&quot;https://ellodave.dev/blog/article/inline-critical-css-in-liquid/&quot;&gt;David Warrington posted about&lt;/a&gt; using the recently released &lt;code&gt;inline_asset_content&lt;/code&gt; filter to liquid (&lt;a href=&quot;https://shopify.dev/docs/api/liquid/filters/inline_asset_content&quot;&gt;see docs&lt;/a&gt;) to minify assets. This filter will automatically minify CSS (and JavaScript) files when they are inlined so I&#39;m updating to use that technique instead.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;One of the mailing lists I subscribe to is &lt;em&gt;I Only Speak Liquid&lt;/em&gt;, where they regularly publish emails from their stable of developers with some great insights into working on Shopify themes and more generally working as a freelancer/consultant in the Shopify ecosystem. If you&#39;re looking for good content on those subjects, I definitely recommend &lt;a href=&quot;https://ionlyspeakliquid.beehiiv.com/subscribe&quot;&gt;subscribing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One tip mentioned in &lt;a href=&quot;https://ionlyspeakliquid.beehiiv.com/p/speak-liquid-33-minify-css-liquid&quot;&gt;an email by Billy Noyes&lt;/a&gt; was a way to use Liquid to minify CSS. The technique was provided by a community member in &lt;a href=&quot;https://community.shopify.com/c/technical-q-a/tool-to-minify-css-liquid/m-p/1123337&quot;&gt;this forum post&lt;/a&gt;. It&#39;s a great way to remove unnecessary characters (like whitespace and comments) from a CSS file without the need for an additional build tool, so it&#39;s a low effort way to increase a site&#39;s performance by reducing its page weight.&lt;/p&gt;
&lt;h2&gt;Original&lt;/h2&gt;
&lt;p&gt;Here is the initial implementation (rewritten slightly to make comparison easier, and added some comments to explain what&#39;s happening):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{% capture bloated %}
/* CSS ... */
{% endcapture %}

&amp;lt;!-- CSS {{ bloated.size }} chars --&amp;gt;
{%- liquid
  assign original = &#39;&#39;
  # remove line breaks,
  # multiple whitespace characters and
  # split on closing comment tags
  assign chunks = bloated | strip_newlines | split: &#39; &#39; | join: &#39; &#39; | split: &#39;*/&#39;
  # iterate over each chunk of CSS
  for chunk in chunks
    # remove comments and whitespace around syntax characters
    assign mini = chunk | split: &#39;/*&#39; | first | strip | replace: &#39;; &#39;, &#39;;&#39; | replace: &#39;} &#39;, &#39;}&#39; | replace: &#39;{ &#39;, &#39;{&#39; | replace: &#39; {&#39;, &#39;{&#39;
    assign original = original | append: mini
  endfor
  # calculate characters saved
  assign change = original.size | times: 1.0 | divided_by: bloated.size | times: 100.0 | round: 1
%}
&amp;lt;!-- original    -{{ bloated.size | minus: original.size }}   {{ 100 | minus: change }}%  --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This alone was providing me with a 10% reduction across most of the CSS I tried it on. However, having looked at the code, I thought I could see an opportunity to improve on it &lt;em&gt;very slightly&lt;/em&gt;, and wanted to see if there were any meaningful gains to be made. I noticed that the final semicolon in a ruleset was being preserved, but it can &lt;a href=&quot;https://css-tricks.com/css-basics-syntax-matters-syntax-doesnt/#aa-mostly-important-semicolons&quot;&gt;safely be removed&lt;/a&gt;. If you have only 100 rulesets that could still save up to 100 characters. That extra bit of code was &lt;code&gt;replace: &#39;;}&#39;, &#39;}&#39;&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Test&lt;/h2&gt;
&lt;p&gt;I wanted to find out how much of a difference this made on a real codebase, and also test that nothing would break as a result, so I set up the following test. I used a 5,000+ lines CSS file from one of my projects to test the results.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{% capture bloated %}
// 5000+ lines of CSS
{% endcapture %}

&amp;lt;!-- CSS {{ bloated.size }} chars --&amp;gt;
{%- liquid
  assign original = &#39;&#39;
  assign chunks = bloated | strip_newlines | split: &#39; &#39; | join: &#39; &#39; | split: &#39;*/&#39;
  for chunk in chunks
    assign mini = chunk | split: &#39;/*&#39; | first | strip | replace: &#39;; &#39;, &#39;;&#39; | replace: &#39;} &#39;, &#39;}&#39; | replace: &#39;{ &#39;, &#39;{&#39; | replace: &#39; {&#39;, &#39;{&#39;
    assign original = original | append: mini
  endfor
  assign change = original.size | times: 1.0 | divided_by: bloated.size | times: 100.0 | round: 1
%}
&amp;lt;!-- original    -{{ bloated.size | minus: original.size }}   {{ 100 | minus: change }}%  --&amp;gt;

{%- liquid
  assign optimised = &#39;&#39;
  assign chunks = bloated | strip_newlines | split: &#39; &#39; | join: &#39; &#39; | split: &#39;*/&#39;
  for chunk in chunks
    assign mini = chunk | split: &#39;/*&#39; | first | strip | replace: &#39;: &#39;, &#39;:&#39; | replace: &#39;; &#39;, &#39;;&#39; | replace: &#39;} &#39;, &#39;}&#39; | replace: &#39;{ &#39;, &#39;{&#39; | replace: &#39; {&#39;, &#39;{&#39; | replace: &#39;;}&#39;, &#39;}&#39;
    assign optimised = optimised | append: mini
  endfor
  assign change = optimised.size | times: 1.0 | divided_by: bloated.size | times: 100.0 | round: 1
%}
&amp;lt;!-- optimised   -{{ bloated.size | minus: optimised.size }}   {{ 100 | minus: change }}%  --&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;Here are the results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- CSS 121066 chars --&amp;gt;
&amp;lt;!-- original    -12829   10.6%  --&amp;gt;
&amp;lt;!-- optimised   -16091   13.3%  --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nice! An extra 300+ characters removed equating to over 2.5% more of a reduction. The output CSS worked perfectly still. For very little effort or intervention, a 10%+ reduction is a great result, and will benefit almost any project.&lt;/p&gt;
&lt;h2&gt;Extracting to a snippet&lt;/h2&gt;
&lt;p&gt;Finally to wrap it up I created a snippet called &lt;code&gt;minify-css.liquid&lt;/code&gt; so that I can easily add this feature to any large CSS files as well as to snippets and sections that may also contain large chunks of CSS that could benefit from minification.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{%- liquid
  assign chunks = input | strip_newlines | split: &#39; &#39; | join: &#39; &#39; | split: &#39;*/&#39;
  for chunk in chunks
    assign mini = chunk | split: &#39;/*&#39; | first | strip | replace: &#39;: &#39;, &#39;:&#39; | replace: &#39;; &#39;, &#39;;&#39; | replace: &#39;} &#39;, &#39;}&#39; | replace: &#39;{ &#39;, &#39;{&#39; | replace: &#39; {&#39;, &#39;{&#39; | replace: &#39;;}&#39;, &#39;}&#39;
    echo mini
  endfor
%}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So it can even be used like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;&amp;lt;style&amp;gt;
{%- capture bloated %}
// Unminified CSS
{%- endcapture %}

{%- render &#39;minify-css&#39;, input: bloated -%}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I can maintain beautiful, well documented, human-readable CSS code across an entire theme and still have it optimised for performance without any additional build tools.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Avoiding a DOMContentLoaded gotcha with readyState</title>
		<link href="https://mikefallows.com/posts/avoiding-a-domcontentloaded-gotcha-with-readystate/"/>
		<updated>Mon, 23 Oct 2023 15:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/avoiding-a-domcontentloaded-gotcha-with-readystate/</id>
		<content type="html">&lt;p&gt;I hit one of those strange issues where a script failed &lt;em&gt;sometimes&lt;/em&gt; and only in Safari.&lt;/p&gt;
&lt;p&gt;It was on a Shopify theme - in this case I was modifying some of the HTML in the header of a site via JavaScript, but didn&#39;t realise that in some cases the theme&#39;s JavaScript was also modifying some of the same parts of the DOM. I had also moved the code from being inline to an asychronously loaded script, and had dutifully wrapped it in a &lt;code&gt;DOMContentLoaded&lt;/code&gt; event listener, patted myself on the back and got on with the day.&lt;/p&gt;
&lt;p&gt;That was until, the inevitable &lt;em&gt;it&#39;s not working&lt;/em&gt; email landed. It turned out that Safari, and possibly only certain versions of Safari, would call the &lt;code&gt;DOMContentLoaded&lt;/code&gt; event in an unreliable way (compared to other browsers) and was therefore executing my script unpredictably. The end result was that the modifications I wanted to make to the DOM weren&#39;t taking place, and I think it was down to the &lt;code&gt;DOMContentLoaded&lt;/code&gt; event being triggered before all scripts were loaded. A little hunting on &lt;a href=&quot;https://stackoverflow.com/a/39993724&quot;&gt;Stack Overflow&lt;/a&gt; and I managed to find this trick (which I&#39;m recording here as a &lt;em&gt;note to self&lt;/em&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function init() {
  // code to run on load...
}

if (document.readyState === &#39;loading&#39;) {
  // document still loading...
  document.addEventListener(&#39;DOMContentLoaded&#39;, () =&amp;gt; {
    init();
  });
} else {
  // already loaded, chocs away!
  init();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It essentially checks to see if the browser reports that it&#39;s still loading via &lt;code&gt;document.readyState&lt;/code&gt; (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState&quot;&gt;docs&lt;/a&gt;) and if so, registers the &lt;code&gt;DOMContentLoaded&lt;/code&gt; event listener. If not, then it can safely be assumed that the event has already been fired, in which case, we run the code immediately. For convenience you can wrap all the code you wanted to run in a function (in this example, &lt;code&gt;init&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;I&#39;m not sure if this only relates to Safari (although it seems to be more commonly reported that way) and/or I&#39;ve just been lucky enough that I&#39;ve not run into it before, but it&#39;s handy to have in the toolbox to keep things more robust.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Prefill a Shopify discount with the Fetch API</title>
		<link href="https://mikefallows.com/posts/prefill-a-shopify-discount-with-the-fetch-api/"/>
		<updated>Mon, 23 Oct 2023 10:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/prefill-a-shopify-discount-with-the-fetch-api/</id>
		<content type="html">&lt;p&gt;Shopify has a method for &lt;a href=&quot;https://help.shopify.com/en/manual/discounts/managing-discount-codes#promote-a-discount-using-a-shareable-link&quot;&gt;prefilling discounts at checkout&lt;/a&gt;. It&#39;s designed to allow a merchant to share a link for marketing purposes so that visitors using the link will conveniently have the discount applied at checkout rather than having to remember the spelling of the code, or get frustrated with typos. The discount link takes the format &lt;code&gt;&amp;lt;SITE URL&amp;gt;/discount/&amp;lt;DISCOUNT CODE&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Visiting that link will store a discount code in a browser Cookie (to be read later at the checkout), and then redirects the visitor to the homepage[^1] so that the whole process feels seamless.&lt;/p&gt;
&lt;h2&gt;Why not use an Automatic Discount?&lt;/h2&gt;
&lt;p&gt;Shopify also supports &lt;a href=&quot;https://help.shopify.com/en/manual/discounts/discount-types#automatic-discounts&quot;&gt;Automatic Discounts&lt;/a&gt; that don&#39;t rely on having a specific code applied at checkout. Unfortunately, there are some limitations to Automatic Discounts, so certain discounts can only be achieved when requiring a code.&lt;/p&gt;
&lt;h2&gt;Just use &lt;code&gt;fetch&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;There are also some cases where traffic may already be inbound to a specific page and there&#39;s no opportunity to use the &lt;code&gt;/discount/&lt;/code&gt; link directly. For that case (and the one above) I set up a simple section that a client can add to any page to make a &#39;visit&#39; invisibly with a &lt;code&gt;fetch&lt;/code&gt; request and have the discount prefilled. I chose not to directly manipulate or create the Cookie myself as that would involve a lot more code and as the format of the Cookie is not documented, it&#39;s also more likely to change without any notice and silently break. So this simple one-liner does the job and should be well supported.&lt;/p&gt;
&lt;p&gt;Here&#39;s the code with section settings that allow it to be added to your OS2 templates.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;{%- if section.settings.discount_code != blank -%}
&amp;lt;script&amp;gt;fetch(&#39;/discount/{{ section.settings.discount_code }}&#39;);&amp;lt;/script&amp;gt;
{%- endif -%}

{% schema %}
{
  &amp;quot;name&amp;quot;: &amp;quot;Prefill discount&amp;quot;,
  &amp;quot;class&amp;quot;: &amp;quot;shopify-section--prefill-discount&amp;quot;,
  &amp;quot;tag&amp;quot;: &amp;quot;section&amp;quot;,
  &amp;quot;settings&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;text&amp;quot;,
      &amp;quot;id&amp;quot;: &amp;quot;discount_code&amp;quot;,
      &amp;quot;label&amp;quot;: &amp;quot;Discount code&amp;quot;
    }
  ],
  &amp;quot;presets&amp;quot;: [
    {
      &amp;quot;name&amp;quot;: &amp;quot;Prefill discount&amp;quot;
    }
  ]
}
{% endschema %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;p&gt;Shopify will only store and apply one prefilled discount at checkout, so keep in mind that if a visitor already has another discount stored in a Cookie, that will then be overridden if they visit a page with the section prefilling the discount. So be careful not to leave expired or unwanted versions of these sections live on the site as you may unintentionally overwrite another code!&lt;/p&gt;
&lt;p&gt;[^1]: Actually, it&#39;s even better, you can add additional parameters to the link to redirect to another part of your site besides the homepage, as you can see &lt;a href=&quot;https://help.shopify.com/en/manual/discounts/managing-discount-codes#promote-a-discount-using-a-shareable-link&quot;&gt;in the docs&lt;/a&gt;.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Implement a low stock notice in a Shopify theme</title>
		<link href="https://mikefallows.com/posts/implement-a-low-stock-notice-a-shopify-theme/"/>
		<updated>Thu, 19 Oct 2023 23:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/implement-a-low-stock-notice-a-shopify-theme/</id>
		<content type="html">&lt;p&gt;I recently needed to implement a small feature on a Shopify theme to display a low stock notice.&lt;/p&gt;
&lt;h2&gt;Requirements&lt;/h2&gt;
&lt;p&gt;This was a quick task that simply needed to display a low stock notice when the selected variant was below a certain threshold, and reflect the change when another variant was selected. For example, given the following scenario:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;low stock threshold is 5&lt;/li&gt;
&lt;li&gt;product has two variants: &lt;em&gt;Standard&lt;/em&gt; and &lt;em&gt;Deluxe&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Standard&lt;/em&gt; has 20 items in stock&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Deluxe&lt;/em&gt; has 3 items in stock&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The notice should only show when the &lt;em&gt;Deluxe&lt;/em&gt; item is selected and it should include the number of units left in stock, i.e. &amp;quot;3 in stock&amp;quot;.&lt;/p&gt;
&lt;h2&gt;Demo&lt;/h2&gt;
&lt;p&gt;Here&#39;s a demo (try selecting &lt;em&gt;Deluxe&lt;/em&gt;):&lt;/p&gt;
&lt;form id=&quot;low-stock-demo&quot;&gt;
  &lt;input type=&quot;hidden&quot; id=&quot;input&quot; value=&quot;123&quot; /&gt;
  &lt;span&gt;Choose&lt;/span&gt;
  &lt;select name=&quot;id&quot; id=&quot;options&quot;&gt;
    &lt;option value=&quot;123&quot;&gt;Standard&lt;/option&gt;
    &lt;option value=&quot;456&quot;&gt;Deluxe&lt;/option&gt;
  &lt;/select&gt;
&lt;/form&gt;
&lt;style&gt;
  #low-stock-demo {
    border: 1px dashed;
    padding: 1rem;
    line-height: 2;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
  }

  #low-stock-demo select {
    color: #333;
    background: #fff;
    outline: 0.25em solid #fff;
    font-family: sans-serif;
    font-size: 1rem;
  }
&lt;/style&gt;
&lt;script&gt;
(() =&gt; {
  const threshold = 5;
  const inventory = [
    { id: &#39;123&#39;, quantity: 20 },
    { id: &#39;456&#39;, quantity: 3 },
  ];
  const demo = document.querySelector(&#39;#low-stock-demo&#39;);
  const input = demo.querySelector(&#39;#input&#39;);
  const select = demo.querySelector(&#39;#options&#39;)
  select.insertAdjacentHTML(&#39;afterend&#39;, `&lt;div id=&quot;stock-notice&quot;&gt;&lt;/div&gt;`);
  const notice = demo.querySelector(&#39;#stock-notice&#39;);
  select.addEventListener(&#39;change&#39;, (e) =&gt; {
    input.value = e.target.value;
    input.dispatchEvent(new Event(&#39;change&#39;))
  });
  function updateMessage(id) {
    let message = &#39;&#39;;
    const variant = inventory.find(i =&gt; i.id === id);
    if (variant &amp;&amp; (variant.quantity &gt; 0 &amp;&amp; variant.quantity &lt;= threshold)) {
      message = `${variant.quantity} in stock`;
    }
    notice.textContent = message;
  }
  updateMessage(&#39;123&#39;);
  input.addEventListener(&#39;change&#39;, (e) =&gt; updateMessage(e.target.value));
})();
&lt;/script&gt;
&lt;h2&gt;Considerations&lt;/h2&gt;
&lt;p&gt;Because this theme had the option to display variants as a &lt;code&gt;select&lt;/code&gt; dropdown, or as &lt;code&gt;radio&lt;/code&gt; options that acted like buttons to select the variant, I decided to look for a way I could make this feature generic enough to work with both. I noticed both versions would dynamically update a hidden variant input to reflect the chosen variant&#39;s ID. This made it easy to observe changes to that ID and to know when to update the notice and identify the currently selected variant.&lt;/p&gt;
&lt;h2&gt;The code&lt;/h2&gt;
&lt;p&gt;As I wanted to enable this as a section (so that it could be used conditionally on different templates), I could use Liquid to store the variant stock quantities in a JavaScript variable[^1].&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const inventory = [
  {%- for v in product.variants %}
    { id: &#39;{{ v.id }}&#39;, quantity: {{ v.inventory_quantity | json }} },
  {%- endfor %}
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Assuming that the HTML structure of the page is something that could be boiled down to a simplified structure like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&amp;quot;product&amp;quot;&amp;gt;
  &amp;lt;form&amp;gt;
    &amp;lt;input type=&amp;quot;hidden&amp;quot; class=&amp;quot;product-variant-id&amp;quot; value=&amp;quot;123&amp;quot;&amp;gt;
    &amp;lt;select name=&amp;quot;id&amp;quot; class=&amp;quot;options&amp;quot;&amp;gt;
      &amp;lt;option value=&amp;quot;123&amp;quot;&amp;gt;Standard&amp;lt;/option&amp;gt;
      &amp;lt;option value=&amp;quot;456&amp;quot;&amp;gt;Deluxe&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
  &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the script would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script&amp;gt;
  window.addEventListener(&#39;DOMContentLoaded&#39;, () =&amp;gt; {

    // Initial config
    const threshold = 5;
    const productSelector = &#39;.product&#39;;
    const optionsSelector = &#39;.options&#39;;
    const hiddenVariantIdInputSelector = &#39;.product-variant-id&#39;;
    const initialVariantId = &#39;{{ product.selected_or_first_available_variant.id }}&#39;;

    // Store stock quantities
    const inventory = [
      {%- for v in product.variants %}
        { id: &#39;{{ v.id }}&#39;, quantity: {{ v.inventory_quantity | json }} },
      {%- endfor %}
    ];

    // Find the root product element
    const productEl = document.querySelector(productSelector);
    if (!productEl) return;

    // Find the options selector (we will append our low stock notice container to this)
    const optionsEl = productEl.querySelector(optionsSelector);
    if (!optionsEl) return;

    // Append the notice container
    optionsEl.insertAdjacentHTML(&#39;afterend&#39;, `&amp;lt;div class=&amp;quot;stock-notice&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;`);

    // Method to update the content of the notice container
    // if the stock is not above the threshold
    function updateMessage(variantId) {
      const stockNoticeEl = productEl.querySelector(&#39;.stock-notice&#39;);
      if (!stockNoticeEl) return;
      let message = &#39;&#39;;
      const variant = inventory.find(i =&amp;gt; i.id === variantId);
      if (variant &amp;amp;&amp;amp; (variant.quantity &amp;gt; 0 &amp;amp;&amp;amp; variant.quantity &amp;lt;= threshold)) {
        message = `${variant.quantity} in stock`;
      }
      stockNoticeEl.textContent = message;
    }

    // Update the message on first page load
    updateMessage(initialVariantId);

    // Find the input that is updated with the selected id
    const hiddenVariantIdInputEl = productEl.querySelector(hiddenVariantIdInputSelector);
    if (!hiddenVariantIdInputEl) return;

    // Watch the input for changes and update the message in response
    hiddenVariantIdInputEl.addEventListener(&#39;change&#39;, (e) =&amp;gt; updateMessage(e.target.value));

  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Other considerations&lt;/h2&gt;
&lt;p&gt;I ended up adding the inventory threshold as a section setting so that it could be populated through a value stored on the product as a metafield. I also added some styling to the message, and made the message customisable, ie. the message could take the format &amp;quot;Hurry, only {count} left in stock!&amp;quot;, where &lt;code&gt;{count}&lt;/code&gt; would be dynamically replaced by the remaining stock quantity.&lt;/p&gt;
&lt;h2&gt;This is the way&lt;/h2&gt;
&lt;p&gt;What I particularly liked about this implementation was that it didn&#39;t require editing a single existing theme file. It&#39;s an approach that I try to prioritise as much as possible as it has multiple benefits for working within a codebase.&lt;/p&gt;
&lt;p&gt;In the past I might have been tempted to update the theme&#39;s JavaScript to insert this functionality where the code dynamically changes the selected variant. Or add it into the variant selector&#39;s Liquid file. Certainly that might have been a more direct approach. However, the maintenance burden is likely to be much greater whenever you want to upgrade the theme (especially if you do a lot of point upgrades) when having to diff/re-apply these types of changes every time. Worse when it&#39;s to a large file like the theme&#39;s main JavaScript file.&lt;/p&gt;
&lt;p&gt;By isolating this feature to its own file, it makes it easier to delete it completely if the theme implements its own version of the feature, or identify how that feature works or needs to be changed, fixed or improved. Even if a new version of the theme handles quantity changes differently, the only thing that is likely to change is one of the selectors, or how to observe the selected variant (maybe through a &lt;code&gt;MutationObserver&lt;/code&gt; or detecting changes to parameters in the uri) which is a much smaller change to make.&lt;/p&gt;
&lt;p&gt;I&#39;ve found life much easier customising themes with this approach.&lt;/p&gt;
&lt;p&gt;[^1]: Note that I store the id as a string for comparison later (thanks to &lt;a href=&quot;https://github.com/ThomasAndrewMacLean&quot;&gt;@ThomasAndrewMacLean&lt;/a&gt;). I&#39;ve also used the &lt;code&gt;json&lt;/code&gt; filter to help make sure that a value suitable for a JavaScript object should always be output (probably overkill as it&#39;s unlikely that Shopify will ever change this API, but I try to do this as a best practise).&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Quick error reporting with Val Town</title>
		<link href="https://mikefallows.com/posts/quick-error-reporting-with-val-town/"/>
		<updated>Mon, 21 Aug 2023 12:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/quick-error-reporting-with-val-town/</id>
		<content type="html">&lt;div class=&quot;note italic&quot;&gt;
&lt;p&gt;This post was updated on 24th October 2023 to reflect the changes in Val Town v3.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Recently I needed to quickly inspect a script running in the &#39;Additional Checkout Scripts&#39; portion of the order thank you / status page for a Shopify client. These are copy-paste scripts and there&#39;s no easy way to manage them, and it&#39;s particularly difficult to test them properly without running real orders. The client was seeing some odd data in their reporting and suspected that there might be some occasional errors with one of the scripts, but it was hard to work out when or why this might be happening.&lt;/p&gt;
&lt;h2&gt;Welcome to Val Town&lt;/h2&gt;
&lt;p&gt;I&#39;d recently heard about the features of &lt;a href=&quot;https://val.town/&quot;&gt;val.town&lt;/a&gt; – a new project that enables you to quickly create javascript functions with a public API endpoint, and some neat features like a small amount of storage and an email notification system, even on their free plan.&lt;/p&gt;
&lt;p&gt;I was honestly blown away by how quickly I could add some observability to a script this way. At the heart of it, Val Town lets you build some serverless functions via the web browser using an interface that will be familiar if you&#39;ve used GitHub gists.&lt;/p&gt;
&lt;p&gt;All I wanted to do was be able to log a minimal amount of data whenever the script runs and let Val Town notify me by email if an error was detected in the script so I can investigate it.&lt;/p&gt;
&lt;h2&gt;Creating a store&lt;/h2&gt;
&lt;p&gt;First, let&#39;s create a store:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export let store = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yep, that&#39;s literally it. Val Town will let you add up to 100kb of data to that value (on its very reasonable free tier), at which point it will begin to truncate the data. This is plenty for my purposes as I&#39;m only storing an order id and at most I&#39;m only interested in a few dozen of the latest records.&lt;/p&gt;
&lt;p&gt;Still, it might be useful to have a way to empty our store (let&#39;s say you potentially fill the store up with lots of dummy data while preparing to write a blog post 👀), so you can create an additional Val with the following snippet. Note that we&#39;re importing &lt;code&gt;set&lt;/code&gt; from Val&#39;s standard library to update the value of one of our other Val&#39;s by reference. Also note that it&#39;s constructed as an IIFE to make sure that the function is invoked when you &#39;run&#39; the Val.&lt;/p&gt;
&lt;p&gt;Just beware that when you run this Val, the results may not appear instantly (ie. your store won&#39;t be cleared out straight away so wait a few moments to see the results).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { set } from &amp;quot;https://esm.town/v/std/set&amp;quot;;

export const clear = (async () =&amp;gt; {
  await set(&amp;quot;store&amp;quot;, []);
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Creating an endpoint&lt;/h2&gt;
&lt;p&gt;Finally, the cool part: a Val that serves as our web endpoint to receive the data from our script. Here, we&#39;re accepting three arguments, &lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt; and an optional &lt;code&gt;error&lt;/code&gt;. The subject and message will be added to the store, and if an error is present it will also use the &lt;code&gt;email&lt;/code&gt; function from the standard library to send me an email. Val will only send emails to the email address of the account. This is a totally acceptable limitation, but I guess if you needed to notify others you could set up a forwarding filter.&lt;/p&gt;
&lt;p&gt;You should also note that I&#39;m importing my &lt;code&gt;store&lt;/code&gt; Val so that I can append to it and then use the &lt;code&gt;set&lt;/code&gt; function to update the Val with the new contents.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { email } from &amp;quot;https://esm.town/v/std/email&amp;quot;;
import { set } from &amp;quot;https://esm.town/v/std/set&amp;quot;;
import { store } from &amp;quot;https://esm.town/v/&amp;lt;user&amp;gt;/store&amp;quot;;

export async function report(subject, message, error = false) {
  store.push({ subject, message });
  await set(&amp;quot;store&amp;quot;, store);
  if (error) {
    await email({
      subject: `Error: ${subject}`,
      html: `&amp;lt;pre&amp;gt;${message}&amp;lt;/pre&amp;gt;&amp;lt;hr&amp;gt;&amp;lt;pre&amp;gt;${error}&amp;lt;/pre&amp;gt;`,
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember to click the lock icon on the Val to convert from a &#39;private&#39; Val. Private Val&#39;s need an API key that you wouldn&#39;t want to leak publicly.&lt;/p&gt;
&lt;h3&gt;A note on security&lt;/h3&gt;
&lt;p&gt;By making the Val unlisted (or public) you can send unauthorised web requests. For the purpose of knocking up some temporary logging on a protected part of the internet (a checkout thank you page) you have &lt;em&gt;some&lt;/em&gt; security through obscurity, but it&#39;s not enough for general purpose, so just keep this in mind. I&#39;m certainly not storing or transferring any sensitive data (only an order id reference), and I&#39;m aware that the endpoint could be abused to spam me at which point I&#39;d have to take measures to defend against that if it were the case (like, change the Val&#39;s endpoint?). If you make the Val &#39;public&#39; (as opposed to unlisted) you make it available to other Val users to reference inside their Vals.&lt;/p&gt;
&lt;h2&gt;Sending data to Val Town&lt;/h2&gt;
&lt;p&gt;With that in place, you will need to add some code to format your data and send the request to the API endpoint that Val Town provides. Here I&#39;m storing the strings for the subject and message data I want to store (Shopify&#39;s Liquid syntax will replace the &lt;code&gt;{{ shop.url }}&lt;/code&gt; and &lt;code&gt;{{ order_id }}&lt;/code&gt; dynamically). I append any error that is thrown in the script that I want to test and finally, use the &lt;code&gt;fetch&lt;/code&gt; method to send this data to Val Town. Anything you include in the &lt;code&gt;args&lt;/code&gt; parameter will be passed as arguments to the Val.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let report = [
  `Order on {{ shop.url }}`,
  `order_id: {{ order_id }}`,
];

try {
  // main script to test
} catch (error) {
  report.push(error);
}

fetch(&amp;quot;https://api.val.town/v1/run/&amp;lt;username&amp;gt;.report?args=&amp;quot; + JSON.stringify(report));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this tool you can have a simple mechanism for reporting how and when a script runs and an email notification for errors. I can see plenty of ways this could be augmented to capture more information or to create additional Vals to inspect the store, like looking for duplicate order IDs being logged. As Val Town has a built-in &#39;Interval&#39; system you could even periodically run Vals to clear your log, or aggregate the data and email yourself a summary.&lt;/p&gt;
&lt;p&gt;Val Town is very cool.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Links #5</title>
		<link href="https://mikefallows.com/posts/links-5/"/>
		<updated>Wed, 16 Aug 2023 23:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/links-5/</id>
		<content type="html">&lt;p&gt;Worthy of note.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://pudding.cool/2023/06/groove/&quot;&gt;Wonky, an audio story by Michelle McGhee&lt;/a&gt;
It&#39;s always great to find truly interactive content that takes full advantage of being on the web. As a huge fan of Dilla and someone that has next to no musical education, I found this a wonderful explanation of why his music hits differently.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gradient.style/&quot;&gt;CSS HD Gradients&lt;/a&gt;
Killer tool to let you build out high definition gradients leveraging the &lt;code&gt;oklch&lt;/code&gt; colour scheme. Anyone who remembers working on optimising GIFs and battling &#39;strobing&#39; effects in certain gradient colour combinations, will appreciate that &lt;code&gt;oklch&lt;/code&gt; provides much more natural (to the human eye) gradation points between colours.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://martinfowler.com/articles/dont-compare-averages.html&quot;&gt;Don&#39;t Compare Averages&lt;/a&gt;
Great article by Martin Fowler detailing the flaw in the (often tempting) method of comparing sets of averages, and how misleading the results can be.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=7_GsiAf2qj8&quot;&gt;Alan Braxe in studio&lt;/a&gt;
As someone who&#39;s musical awakening came at the height of French House this is a fascinating glimpse of how those tracks were made and how much the style was defined by the technology they were using. A reminder that cruder tools can be limiting, but are often more fun to play with! The impact of personal computers on DIY music soon ushered in almost unlimited abilities to create and source sounds, but so often the friction and imeadiacy inspires much greater creativity in some.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.amygoodchild.com/blog/midjourney-sol-lewitt&quot;&gt;Midjouney takes on Sol LeWitt&#39;s Wall Drawings&lt;/a&gt;
I remember being fascinated by the concept of LeWitt&#39;s wall drawing instructions and years ago having a conversation with a technician that installed a LeWitt at a gallery in Manchester. There&#39;s something really interesting in the boundaries established in the instructions that allow a little bit of personal interpretation so that the outcome isn&#39;t entirely predictable, but you can still see all the drawings as both the same and entirely unique. A great task for an AI prompt. Also, lol at it utterly refusing to draw wavy vertical lines.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.jim-nielsen.com/2023/subscribe-wherever-you-get-your-content/&quot;&gt;Subscribe Wherever You Get Your Podcasts&lt;/a&gt;
Great observation on how podcasts have delivered on the principle of an open web, and maybe how else that can be expanded to other aspects of the online experience.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Observing cart changes in a Shopify theme</title>
		<link href="https://mikefallows.com/posts/observing-cart-changes-in-a-shopify-theme/"/>
		<updated>Mon, 06 Mar 2023 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/observing-cart-changes-in-a-shopify-theme/</id>
		<content type="html">&lt;p&gt;Recently I needed to write some code to monitor and respond to changes made to a Shopify cart. The script would need to be added to several themes and be independent of the specific theme the site used or potential apps that were (or would be) installed.&lt;/p&gt;
&lt;p&gt;This presented a challenge as cart interactions can take many forms and vary across themes. For example, as well as a dedicated cart page most sites have some sort of dynamic &#39;Ajax&#39; cart. Many can also have apps integrated with the theme that enable products to be added to the cart in different ways.&lt;/p&gt;
&lt;p&gt;I&#39;ve often seen crude attempts to solve this by adding event listeners to the &#39;Add to cart&#39; button on the product page. But this falls down once you consider things like &#39;Quick Buy&#39; or &#39;Upsell&#39; features, or take into account that simply clicking an &#39;Add to cart&#39; button doesn&#39;t guarantee a product is &lt;em&gt;actually&lt;/em&gt; added to the cart. Problems like network request errors, or items becoming sold out come into play and it quickly becomes complicated.&lt;/p&gt;
&lt;h2&gt;Detecting changes to the cart&lt;/h2&gt;
&lt;p&gt;To solve this I opted to record requests to both the cart page, and the &lt;code&gt;/cart/*&lt;/code&gt; endpoints used to dynamically mutate the cart. This would mean that regardless of which part of the theme or an app was updating the cart, I would be able to detect and react to it.&lt;/p&gt;
&lt;p&gt;Determining if a request is made to the cart page is easy enough as you can just inspect the location or type of the current page.&lt;/p&gt;
&lt;p&gt;In Liquid you can access the type of page from the &lt;code&gt;request&lt;/code&gt; object.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const page_type = {{ request.page_type | json }};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s a bit more complicated to watch for dynamic requests, but you can do that with a &lt;code&gt;PerformanceObserver&lt;/code&gt; that filters on &lt;code&gt;resource&lt;/code&gt;-based performance activity, and within that modern &lt;code&gt;fetch&lt;/code&gt; requests as well as the older &lt;code&gt;xmlhttprequest&lt;/code&gt; type.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;observeCartChanges&lt;/code&gt; function below&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;loops over entries to the &lt;code&gt;PerformanceObserver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;checks if the entry is an Ajax request&lt;/li&gt;
&lt;li&gt;checks if the endpoint of the request contains &lt;code&gt;/cart/&lt;/code&gt;[^1]&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, it initiates the observer restricted to only &lt;code&gt;resource&lt;/code&gt; entries.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function observeCartChanges() {
  const cartObserver = new PerformanceObserver((list) =&amp;gt; {
    list.getEntries().forEach((entry) =&amp;gt; {
      const isValidRequestType = [&#39;xmlhttprequest&#39;, &#39;fetch&#39;].includes(entry.initiatorType);
      const isCartChangeRequest = /&#92;/cart&#92;//.test(entry.name);
      if (isValidRequestType &amp;amp;&amp;amp; isCartChangeRequest) {
        // handle cart request
      }
    });
  });
  cartObserver.observe({ entryTypes: [&amp;quot;resource&amp;quot;] });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Determine the changes to the cart&lt;/h2&gt;
&lt;p&gt;Now that we have a way to observe &lt;em&gt;when&lt;/em&gt; the cart is changed we next need to determine &lt;em&gt;how&lt;/em&gt; the cart has changed.&lt;/p&gt;
&lt;p&gt;At first, I considered inspecting the endpoint that was requested, eg. &lt;code&gt;cart/add.js&lt;/code&gt; and assume that the items in the payload had been added to the cart. However, just because the request has been made is no guarantee that it was successful. It could be an invalid request such as an out-of-stock item or use badly formed data.&lt;/p&gt;
&lt;p&gt;I decided the better approach is to track the current state of the cart and, given a request to mutate the cart, get the latest version and figure out what changes have been made. I chose to use a fetch request to get the latest cart state and track it in local storage for reference.&lt;/p&gt;
&lt;h3&gt;Retrieving and storing cart state&lt;/h3&gt;
&lt;p&gt;Here&#39;s a quick example of fetching the current cart, as well as storing and then retrieving it from local storage:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const response = await fetch(&#39;/cart.js&#39;);
const cart = response.json();
localStorage.setItem(&#39;cart&#39;, JSON.stringify(cart));
const storedCart = JSON.parse(localStorage.getItem(&#39;cart&#39;));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Identifying items that have been added or removed&lt;/h3&gt;
&lt;p&gt;Next, to work out whether items have been added or removed from the cart, I created a function that accepts the old cart items (eg. retrieved from local storage) and some new cart items (eg. retrieved from a fetch request) and returns a tuple of the added and removed items. To do this, I find the items present in the new cart that are not present in the old cart and stored these in the &lt;code&gt;added&lt;/code&gt; array. Next, I do the inverse and check for items in the old cart that no longer exist in the new cart to determine what has been removed, and stored them in the &lt;code&gt;removed&lt;/code&gt; array.&lt;/p&gt;
&lt;p&gt;It starts getting a bit complicated to read here as I&#39;ve used some quite generic terminology like &lt;code&gt;l&lt;/code&gt; and &lt;code&gt;r&lt;/code&gt; for &lt;em&gt;left&lt;/em&gt; and &lt;em&gt;right&lt;/em&gt;, and used &lt;code&gt;li&lt;/code&gt; to represent the &lt;em&gt;items&lt;/em&gt; on the left-hand side. I try to avoid this type of naming, but as this is just a simple algorithm for comparing arrays, the terseness is okay for me. Fortunately, because Shopify applies a unique &lt;code&gt;key&lt;/code&gt; property to each line item in the cart, it&#39;s easy to check whether the current line exists in both carts. The &lt;code&gt;onlyInLeft&lt;/code&gt; function takes the two carts and filters out any items in the left cart that exist in the right cart.&lt;/p&gt;
&lt;p&gt;Here&#39;s an example of how that function could look:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function findCartChanges(oldCart, newCart) {
  const onlyInLeft = (l, r) =&amp;gt; l.filter(li =&amp;gt; !r.some(ri =&amp;gt; li.key == ri.key));
  return {
    added: onlyInLeft(newCart.items, oldCart.items),
    removed: onlyInLeft(oldCart.items, newCart.items),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Identifying items that have been updated&lt;/h3&gt;
&lt;p&gt;So far, so good. But this still doesn&#39;t take into account quantities of exiting line items being increased or decreased.&lt;/p&gt;
&lt;p&gt;For example, if a customer already has &lt;em&gt;Product X&lt;/em&gt; in their cart and then adds another &lt;em&gt;Product X&lt;/em&gt; then I want to record that item in the &lt;code&gt;added&lt;/code&gt; array with a quantity of 1 (to represent the quantity &lt;em&gt;added&lt;/em&gt; to the previous state of the cart). I considered adding a third property to the returned object with a key of &lt;code&gt;updated&lt;/code&gt;, but the distinction wasn&#39;t necessary for my use case. Quantity changes could be recorded in the relevant &lt;code&gt;added&lt;/code&gt; and &lt;code&gt;removed&lt;/code&gt; properties.&lt;/p&gt;
&lt;p&gt;To do that, I assign the original calculations to a &lt;code&gt;result&lt;/code&gt; variable. That allows me to iterate over the new cart&#39;s items and look for matching lines (by &lt;code&gt;key&lt;/code&gt;) that exist in the old cart, but have a different &lt;code&gt;quantity&lt;/code&gt; value. I can then calculate the difference between the two quantities – a negative value means the quantity was reduced and a positive value means increased. So I can make a copy of the line item, update its &lt;code&gt;quantity&lt;/code&gt; to an absolute number of the calculated value (so that negative numbers become positive), and then &lt;code&gt;push&lt;/code&gt; that item onto the correct property of the &lt;code&gt;result&lt;/code&gt; object.&lt;/p&gt;
&lt;p&gt;Here&#39;s how that looks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function findCartChanges(oldCart, newCart) {
  const onlyInLeft = (l, r) =&amp;gt; l.filter(li =&amp;gt; !r.some(ri =&amp;gt; li.key == ri.key));
  let result = {
    added: onlyInLeft(newCart.items, oldCart.items),
    removed: onlyInLeft(oldCart.items, newCart.items),
  };

  oldCart.items.forEach(oi =&amp;gt; {
    const ni = newCart.items.find(i =&amp;gt; i.key == oi.key &amp;amp;&amp;amp; i.quantity != oi.quantity);
    if (!ni) return;
    let quantity = ni.quantity - oi.quantity;
    let item = { ...ni };
    item.quantity = Math.abs(quantity);
    quantity &amp;gt; 0
      ? result.added.push(item)
      : result.removed.push(item)
  });

  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Here&#39;s an example of wrapping it all up into a &lt;code&gt;CartWatcher&lt;/code&gt; class with an &lt;code&gt;init&lt;/code&gt; method to handle fetching current cart details in case there have been changes between requests and setting up the observer for dynamic requests. The &lt;code&gt;emitCartChanges&lt;/code&gt; method fires a custom event with the details of the changes whenever the cart is updated. This allows me to build several features that can independently listen and respond to changes in the cart without having to repeat this code. If I needed more flexibility I could add the ability to configure things through the constructor such as the keys used in the events and local storage (if I was worried about collisions).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;class CartWatcher {

  init() {
    this.emitCartChanges().then(() =&amp;gt; {
      this.observeCartChanges();
    });
  }

  async fetchCart() {
    const response = await fetch(&#39;/cart.js&#39;);
    return response.json();
  }

  storeCart(cart) {
    localStorage.setItem(&#39;cart&#39;, JSON.stringify(cart));
  }

 storedCart() {
    return JSON.parse(localStorage.getItem(&#39;cart&#39;)) || { items: [] };
  }

 findCartChanges(oldCart, newCart) {
    const onlyInLeft = (l, r) =&amp;gt; l.filter(li =&amp;gt; !r.some(ri =&amp;gt; li.key == ri.key));
    let result = {
      added: onlyInLeft(newCart.items, oldCart.items),
      removed: onlyInLeft(oldCart.items, newCart.items),
    };

    oldCart.items.forEach(oi =&amp;gt; {
      const ni = newCart.items.find(i =&amp;gt; i.key == oi.key &amp;amp;&amp;amp; i.quantity != oi.quantity);
      if (!ni) return;
      let quantity = ni.quantity - oi.quantity;
      let item = { ...ni };
      item.quantity = Math.abs(quantity);
      quantity &amp;gt; 0
        ? result.added.push(item)
        : result.removed.push(item)
    });

    return result;
  }

  async emitCartChanges() {
    const newCart = await this.fetchCart();
    const oldCart = this.storedCart();
    const changes = this.findCartChanges(oldCart, newCart);

    const event = new CustomEvent(&amp;quot;cart_changed&amp;quot;, { detail: changes });
    window.dispatchEvent(event);

    this.storeCart(newCart);
  }

 observeCartChanges() {
    const cartObserver = new PerformanceObserver((list) =&amp;gt; {
      list.getEntries().forEach((entry) =&amp;gt; {
        const isValidRequestType = [&#39;xmlhttprequest&#39;, &#39;fetch&#39;].includes(entry.initiatorType);
        const isCartChangeRequest = /&#92;/cart&#92;//.test(entry.name);
        if (isValidRequestType &amp;amp;&amp;amp; isCartChangeRequest) {
          this.emitCartChanges();
        }
      });
    });
    cartObserver.observe({ entryTypes: [&amp;quot;resource&amp;quot;] });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&#39;s a quick example of newing up the class and adding a listener for the &lt;code&gt;cart_changed&lt;/code&gt; event that will log out the changes to the cart whenever it is updated.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const myCartWatcher = new CartWatcher;
myCartWatcher.init();
window.addEventListener(&amp;quot;cart_changed&amp;quot;, e =&amp;gt; console.log(e.detail));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You could of course use this to provide some feedback to the customer, log some data in analytics or provide an offer based on the change to the cart.&lt;/p&gt;
&lt;p&gt;[^1]: According to the &lt;a href=&quot;https://shopify.dev/docs/api/ajax/reference/cart&quot;&gt;Cart API docs&lt;/a&gt;, regardless of the locale, the endpoints that mutate the cart all contain &lt;code&gt;/cart/&lt;/code&gt; eg. &lt;code&gt;/{locale}/cart/add.js&lt;/code&gt;, &lt;code&gt;/{locale}/cart/update.js&lt;/code&gt;, etc.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Add a custom domain to CloudFront with Cloudflare</title>
		<link href="https://mikefallows.com/posts/add-a-custom-url-to-cloudfront-with-cloudflare/"/>
		<updated>Tue, 28 Feb 2023 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/add-a-custom-url-to-cloudfront-with-cloudflare/</id>
		<content type="html">&lt;p&gt;After trying and failing to get this to work several times I thought I&#39;d record the particular setup that worked for me. The main thing I&#39;ve learned is that there are a lot of moving parts and layers of caching so sometimes it just takes some patience. Now I&#39;ve finally succeeded, I intend to apply this to several sites mainly to decouple myself from the default AWS CloudFront domain.&lt;/p&gt;
&lt;p&gt;With some of the sites I look after, static assets such as images are stored in an S3 bucket and I&#39;ve set up CloudFront as a CDN, but I want my assets served from a nice domain name that I control, rather than the default one supplied by CloudFront.&lt;/p&gt;
&lt;h2&gt;Pick a domain for the CDN&lt;/h2&gt;
&lt;p&gt;Pick the domain you want to use. Ideally, you should use a subdomain of the site&#39;s primary domain. If there are some access issues with managing the DNS of the site&#39;s primary domain, then you may want to reserve another generic domain for this purpose. Using the same top-level domain might help you operate a stricter CORS setup if you need it. A prefix like &lt;code&gt;cdn.&lt;/code&gt; is a sensible option. For this post, let&#39;s assume it&#39;s &lt;code&gt;cdn.project.tld&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Set up a new SSL certificate in ACM&lt;/h2&gt;
&lt;p&gt;Assuming that you already have an S3 bucket set up and are using CloudFront, then the next step is to create a custom SSL certificate with AWS Certificate Manager (ACM). You can access this via the AWS console. There are a couple of things that I read were important to keep in mind when setting this up. Regardless of which region your bucket is located in, the SSL certificate should be located in the &lt;code&gt;us-east-1&lt;/code&gt; region.&lt;/p&gt;
&lt;p&gt;In ACM choose to request a certificate. The Certificate type should be set to &#39;Public&#39;, and use the domain you picked earlier, eg. &lt;code&gt;cdn.project.tld&lt;/code&gt;. Use the DNS validation method so the certificate can auto-renew, and pick the default/recommended &#39;Key algorithm&#39; (I don&#39;t know if this matters).&lt;/p&gt;
&lt;h2&gt;Validate the CDN via Cloudflare&lt;/h2&gt;
&lt;p&gt;When you view the new certificate in ACM, it will provide you with a CNAME key-value pair. Log into Cloudflare and create this new DNS record. The CNAME record should &lt;em&gt;not&lt;/em&gt; be proxied. Note that the name that you copy from AWS will include the top-level domain, so make sure that you correct that before creating the record on Cloudflare (it&#39;s quite long so the end can be obscured in the input field). I like to add a note here in Cloudflare for the record too, something like &#39;AWS CloudFront SSL Certificate&#39;.&lt;/p&gt;
&lt;p&gt;Once you&#39;ve created the record, check back on ACM and see if the certificate has updated its status from &#39;Pending&#39; to &#39;Issued&#39;. It might take some time, but in my experience, it worked pretty much instantly.&lt;/p&gt;
&lt;h2&gt;Assign the SSL certificate&lt;/h2&gt;
&lt;p&gt;Now you can assign the SSL Certificate to your CloudFront distribution, so head over to CloudFront in AWS, and locate the relevant distribution. Edit the General Settings. This is where you can assign the alternate domain name you want to use instead of the default CloudFront-assigned URL. In this case, it&#39;s &lt;code&gt;cdn.project.tld&lt;/code&gt;. Now select the SSL certificate you just created. Choose the recommended Security policy and add the additional supported HTTP versions (I don&#39;t see any harm). I keep the logging setting off, and IPv6 on. I also like to add a description eg. &lt;em&gt;CDN for project.tld&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Create the DNS records for the custom domain&lt;/h2&gt;
&lt;p&gt;In Cloudflare, create the CNAME record for custom CDN domain (in this case &lt;code&gt;cdn.project.tld&lt;/code&gt;) and set the target as the CloudFront distribution domain that was used previously. It looks something like &lt;code&gt;abc123.cloudfront.net&lt;/code&gt;. Don&#39;t proxy this domain and let CloudFront handle all the caching[^1].&lt;/p&gt;
&lt;h2&gt;Test, test again and be patient&lt;/h2&gt;
&lt;p&gt;To test this, try accessing a CloudFront asset through the new domain e.g. where it might have been &lt;code&gt;https://abc123.cloudfront.net/image.png&lt;/code&gt;, try &lt;code&gt;https://cdn.project.tld/image.png&lt;/code&gt;. Again, this could take a few minutes to work, and sometimes it might stop working for a bit, and then work again, so I usually leave it overnight to propagate (48 hours if you really want to feel safe).&lt;/p&gt;
&lt;p&gt;If it&#39;s still not working, double-check that you haven&#39;t set up the last step as a proxied domain. This seems to be the part that takes the longest to resolve if you&#39;ve made a mistake, so you may need to check back in a few hours. Sometimes when I&#39;m finding DNS records I like to try and &lt;a href=&quot;https://developers.google.com/speed/public-dns/cache&quot;&gt;flush the cache of the problem record in Google&#39;s public DNS&lt;/a&gt;. I honestly can&#39;t tell whether it makes a difference but sometimes it&#39;s just nice to experience the placebo effect from doing it 😉.&lt;/p&gt;
&lt;p&gt;[^1]: I&#39;m sure there&#39;s a way to set this up which uses CloudFlare to proxy requests to CloudFront or even directly to the S3 bucket. That might be useful particularly if you&#39;re serving things like your CSS or JS from S3 and want the benefits of Brotli compression that Cloudflare provides. Cloudflare also gives you a global cache on the free tier, whereas a global cache in CloudFront is more expensive than limiting it to North America and Europe. For my needs it&#39;s fine.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Detect subscription orders in the Shopify checkout</title>
		<link href="https://mikefallows.com/posts/detect-subscription-orders-in-shopify-checkout/"/>
		<updated>Fri, 17 Feb 2023 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/detect-subscription-orders-in-shopify-checkout/</id>
		<content type="html">&lt;p&gt;This week I needed to help a client distinguish which orders contained subscriptions so they can measure the results of their PPC[^1] advertising. As their business model is heavily geared towards subscriptions and subscriptions will typically indicate a much higher LTV[^2], measuring which adverts are more likely to generate subscription purchases is a key metric.&lt;/p&gt;
&lt;p&gt;It was a bit harder than I expected, and that&#39;s down to the limited amount of data available on the &#39;Thank You&#39; page of Shopify&#39;s checkout.&lt;/p&gt;
&lt;h2&gt;The result&lt;/h2&gt;
&lt;p&gt;Here&#39;s the final logic that I used in the additional scripts part of the checkout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# track subscriptions
assign includes_subscription = false
for line_item in line_items
  if line_item.selling_plan_allocation.selling_plan.name contains &#39;Delivery every&#39;
    assign includes_subscription = true
  endif
endfor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That will check if any of the line items on an order include a subscription purchase and give you a nice variable you can use to conditionally fire an event, or pass as a value to Google Tag Manager.&lt;/p&gt;
&lt;p&gt;For example, to fire a Meta pixel &#39;Subscribe&#39; event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;{%- if includes_subscription == true %}
  fbq(&#39;track&#39;, &#39;Subscribe&#39;);
{%- endif -%}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To include as a &lt;code&gt;dataLayer&lt;/code&gt; variable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;dataLayer.push({
  event: &#39;purchase&#39;,
  ecommerce: {
    // ...
    &#39;includes_subscription&#39;: {{ includes_subscription | default: false }},
    // ...
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why the hacky inspection of selling_plan.name?&lt;/h2&gt;
&lt;p&gt;As I mentioned earlier, you&#39;re a bit limited with the data you can access on the initial &#39;Thank You&#39; page that you&#39;re presented with immediately after placing an order. That&#39;s because the full order hasn&#39;t been created yet due to the potential delays in building up the order in a background task.&lt;/p&gt;
&lt;p&gt;It&#39;s also very hard to test the Thank You page, as to my knowledge you can&#39;t simulate it, so you have to place a live order to test each code change. Initially, I ran my tests by visiting the status page of an order that had already been placed. The &#39;Order Status&#39; page has access to the entire order object which, you can use to find out if &lt;code&gt;selling_plan.recurring_deliveries&lt;/code&gt; is set to &lt;code&gt;true&lt;/code&gt;. That appeared to be the most robust way to identify subscription orders. Unfortunately, the only field available on the &#39;Thank You&#39; page is &lt;code&gt;selling_plan.name&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As far as I could tell, at least in my client&#39;s case (English language only checkout), the selling plan name for a line item would always begin &#39;Delivery every&#39; as in, &lt;em&gt;&#39;Delivery every 1 week&#39;&lt;/em&gt;, &lt;em&gt;&#39;Delivery every 2 weeks&#39;&lt;/em&gt;, etc. The other types of selling plan names would not.&lt;/p&gt;
&lt;h2&gt;Why not use Customer Events?&lt;/h2&gt;
&lt;p&gt;There certainly was the option of using a &lt;a href=&quot;https://shopify.dev/docs/api/pixels/customer-events#checkout_completed&quot;&gt;Checkout Completed Customer Event&lt;/a&gt; instead of the checkout additional scripts. And while I welcome the introduction of this feature, I&#39;ve found it much harder to test as the delay between making changes and seeing those updates reflected in the shop have been unpredictable.&lt;/p&gt;
&lt;p&gt;[^1]: Pay Per Click: display advertising through platforms like Google or Meta where you pay for the amount of traffic the advert drives to your website.
[^2]: Life Time Value: the total amount spent by an individual customer over their lifetime.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Adding search to an Eleventy site</title>
		<link href="https://mikefallows.com/posts/adding-search-to-eleventy-site/"/>
		<updated>Wed, 08 Feb 2023 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/adding-search-to-eleventy-site/</id>
		<content type="html">&lt;p&gt;TL;DR I went with &lt;a href=&quot;https://pagefind.app/&quot;&gt;Pagefind&lt;/a&gt; and followed &lt;a href=&quot;https://rknight.me/using-pagefind-with-eleventy-for-search/&quot;&gt;this solution by Robb Knight&lt;/a&gt; which I had running locally after about 10 mins. Sure, I lost an hour or so deploying it because I&#39;d got my node versions out of sync, but that&#39;s to be expected.&lt;/p&gt;
&lt;h2&gt;Finding my search solution&lt;/h2&gt;
&lt;p&gt;Weirdly, I&#39;ve found that I seem to read the posts on this site more than I write them. I say read, but I just mean referring to the stuff I&#39;ve written because it has left my head. For a long time I&#39;ve tried to keep some developer notes in my note-taking app &lt;em&gt;de jour&lt;/em&gt;, but those often tend to be quick copy-paste bits of code with perfunctory captions, and the habit of properly writing up technical notes on this blog has been more useful than I imagined. As there are only a couple dozen posts it&#39;s pretty easy to navigate to the information I&#39;m looking for from the homepage. Still, I&#39;m a developer, so that extra effort seems like something I can waste an afternoon resolving.&lt;/p&gt;
&lt;p&gt;It was also an academic exercise as I&#39;ve only ever had to implement very simple search solutions that query a database (hello &lt;code&gt;LIKE %%&lt;/code&gt;), or the platform I&#39;m working on has already implemented it. I wasn&#39;t sure how you would handle this on a static site. I assumed it would mean some sort of serverless function or using a third-party integration. The fact that there&#39;s a solution that just crawls your site, looks for HTML attributes to decide what to add to the file-based index, and then queries that client-side is so simple in its genius. That&#39;s essentially what Pagefind does. It also ships with a little Svelte-based search UI component that you can drop-in and customise with a few CSS variables. It makes it very easy to integrate with low performance footprint.&lt;/p&gt;
&lt;h2&gt;Honourable mention&lt;/h2&gt;
&lt;p&gt;When I was casting about for solutions I found some older posts by Raymond Camden, one on &lt;a href=&quot;https://www.raymondcamden.com/2019/10/20/adding-search-to-your-eleventy-static-site-with-lunr&quot;&gt;adding Lunr to an Eleventy site&lt;/a&gt; and one on &lt;a href=&quot;https://www.raymondcamden.com/2020/06/24/adding-algolia-search-to-eleventy-and-netlify&quot;&gt;using Algolia&lt;/a&gt;. Ultimately, both those posts came with caveats, but they both seem like viable solutions. &lt;a href=&quot;https://lunrjs.com/&quot;&gt;Lunr&lt;/a&gt; is not that different to PageFind, but it perhaps needs a bit more setup to install on Eleventy. Algolia has a great reputation, but I didn&#39;t see the need to use an external service given the modest size of this site.&lt;/p&gt;
&lt;p&gt;One of the things I&#39;m particularly enjoying about Eleventy is the ease of finding well written up solutions, and getting to tinker with them.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://mikefallows.com/search/&quot;&gt;search page&lt;/a&gt; is now live.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Links #4</title>
		<link href="https://mikefallows.com/posts/links-4/"/>
		<updated>Tue, 07 Feb 2023 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/links-4/</id>
		<content type="html">&lt;p&gt;Interesting bits from around the web.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ericwbailey.website/published/visit-for-a-surprise/&quot; title=&quot;An article about accessible link titles&quot;&gt;Visit for a surprise&lt;/a&gt;. A great example of thinking deeply about the nuances involved in ensuring the web is accessible to as many people as possible without just ticking &amp;quot;accessibility&amp;quot; checkboxes. Context is key.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://baremetrics.com/blog/aarrr-metrics-framework-what-is-it-and-how-to-use-it&quot;&gt;AARRR&lt;/a&gt;. A nice pirate-y acronym to help you remember &amp;quot;acquisition, activation, retention, referral, and revenue&amp;quot;. I find this useful when looking at a client&#39;s site and other touch-points to try and find opportunities to leverage improvements. You can be certain that there&#39;s always one point that can benefit from more attention.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.theatlantic.com/technology/archive/2022/09/youtube-homepage-editor-google-algorithm-book-excerpt/671339/&quot;&gt;Before YouTube&#39;s algorithm there were &amp;quot;coolhunters&amp;quot;&lt;/a&gt;. An article in &lt;em&gt;The Atlantic&lt;/em&gt; about the way the YouTube homepage was curated by hand. I think there&#39;s a lot to be said for having a strongly opinionated editorial direction on the web, I try to encourage this with homepages and merchandising online shops. There are some potential downsides (you can&#39;t please everyone and you run the risk of gatekeeping) but human elements in a digital world stand out.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.theverge.com/2019/5/4/18529381/google-youtube-internet-explorer-6-kill-plot-engineer&quot;&gt;How YouTube killed IE6&lt;/a&gt;. I was reminded of this article in &lt;em&gt;The Verge&lt;/em&gt; about the actions of a small group of engineers at YouTube in the early days that helped accelerate the demise of Internet Explorer 6, and I don&#39;t blame them one bit. Bonus, there&#39;s a picture of a client in the screen grab of YouTube&#39;s homepage. There&#39;s also a Twitter thread somewhere that I can&#39;t find for the life of me, but was an interesting tale of Pinterest&#39;s human curated homepage in the early days.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.stilldrinking.org/ai-is-not-the-problem&quot;&gt;AI is not the problem&lt;/a&gt;. Obviously, it&#39;s &lt;em&gt;the&lt;/em&gt; hot topic, and there&#39;s plenty of opinions around, but this one struck a chord. I recently listened to the &lt;a href=&quot;https://www.bbc.co.uk/programmes/m001216k/episodes/player&quot;&gt;2021 Reith Lectures on the BBC&lt;/a&gt; which had some interesting discussion around the ethics of AI from a time before ChatGPT and Stable Diffusion become part of the discourse.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Hello World with Lit</title>
		<link href="https://mikefallows.com/posts/hello-world-with-lit/"/>
		<updated>Mon, 24 Oct 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/hello-world-with-lit/</id>
		<content type="html">&lt;p&gt;The world of Web Components seems to be getting more attention recently so I&#39;ve been exploring some of the frameworks emerging around it. Here are some notes on my initial experience with &lt;a href=&quot;https://lit.dev/&quot;&gt;Lit&lt;/a&gt; (specifically version 2).&lt;/p&gt;
&lt;h2&gt;Aims&lt;/h2&gt;
&lt;p&gt;Build a small web component that can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;render some HTML&lt;/li&gt;
&lt;li&gt;accept some data as a property&lt;/li&gt;
&lt;li&gt;have a button that can mutate that data&lt;/li&gt;
&lt;li&gt;style the component&lt;/li&gt;
&lt;li&gt;expose some styling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&#39;s pretty much 90% of what I&#39;ve been doing with Web Components.&lt;/p&gt;
&lt;h2&gt;Getting set up&lt;/h2&gt;
&lt;p&gt;The first thing I did was head over to the docs. They provide some &lt;a href=&quot;https://lit.dev/tutorials/&quot;&gt;interactive tutorials&lt;/a&gt; in a REPL which look great but for whatever reason, was behaving slowly when I visited. I also wasn&#39;t sure if Lit required a build step so I decided I&#39;d rather try to get something running locally (like the boomer I am).&lt;/p&gt;
&lt;p&gt;This turned out to be the most confusing part for me because my preferred way of learning involves watching a screencast and hopefully copy-pasting or re-typing some code into my editor and viewing the results. The Lit &lt;a href=&quot;https://www.youtube.com/watch?v=QBa1_QQnRcs&amp;amp;t=302s&quot;&gt;introductory screencast&lt;/a&gt; I found on YouTube used TypeScript and I&#39;m not all aboard the TypeScript train yet. I wanted a more apples-to-apples approach to the way I currently write my Web Components so I realised I was going to have to bumble through on my own a bit.&lt;/p&gt;
&lt;p&gt;This is not to criticise Lit, I think it&#39;s marketing itself as an alternative to developers coming from the React ecosystem and that seems perfectly reasonable. I&#39;m personally coming from a &amp;quot;sprinkles&amp;quot;[^1] background so there are some gaps I have to acknowledge.&lt;/p&gt;
&lt;h2&gt;Rendering a component&lt;/h2&gt;
&lt;p&gt;After some back and forth trying to make things much more complicated than I needed to, I ended up with a minimal HTML page that registered a Lit component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;
  &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;
  &amp;lt;title&amp;gt;Lit Test&amp;lt;/title&amp;gt;
  &amp;lt;script type=&amp;quot;module&amp;quot;&amp;gt;
    import { html, LitElement } from &#39;https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js&#39;

    class HelloWorld extends LitElement {
      render() {
        return html`&amp;lt;p&amp;gt;Hello world&amp;lt;/p&amp;gt;`;
      }
    }

    customElements.define(&#39;hello-world&#39;, HelloWorld)
  &amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;hello-world&amp;gt;&amp;lt;/hello-world&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Great, I have a page that renders a &lt;code&gt;&amp;lt;p&amp;gt;Hello world&amp;lt;/p&amp;gt;&lt;/code&gt; in the shadow DOM of the &lt;code&gt;&amp;lt;hello-world&amp;gt;&lt;/code&gt; element. By the way, that &lt;a href=&quot;https://cdn.jsdelivr.net/gh/lit/dist@2.4.0/core/&quot;&gt;Lit Core library&lt;/a&gt; comes in at just over 16kb at the time of writing – time to find out what affordances that payload gets me!&lt;/p&gt;
&lt;p&gt;Of note here is that the &lt;code&gt;render&lt;/code&gt; function and the &lt;code&gt;html&lt;/code&gt; helper are taking care of a few things under the hood and allowing for the use of &lt;a href=&quot;https://www.digitalocean.com/community/tutorials/js-tagged-template-literals&quot;&gt;tagged template literals&lt;/a&gt;. For comparison, here&#39;s the boilerplate you would need to write the same functionality without Lit.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;class HelloWorld extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: &amp;quot;open&amp;quot; });
    let p = document.createElement(&#39;p&#39;);
    p.innerText = &#39;Hello world&#39;;
    this.shadowRoot.append(p);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Passing properties&lt;/h2&gt;
&lt;p&gt;I found the Lit docs a bit confusing here, mainly because the script examples only included the JS portion without the HTML element and attribute I was looking for. It also opens with examples using decorators which require a build step (they are still only a proposal). I&#39;m unfamiliar with the syntax and I wasn&#39;t sure whether this was a Lit requirement or something to do with TypeScript but it turns out you can achieve the same thing by defining a &lt;code&gt;static properties&lt;/code&gt; object.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;class HelloWorld extends LitElement {
  static properties = {
    name: { type: String }
  }
  render() {
    return html`&amp;lt;p&amp;gt;Hello ${this.name || &#39;world&#39;}&amp;lt;/p&amp;gt;`;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Allows you to do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- output: Hello world --&amp;gt;
&amp;lt;hello-world&amp;gt;&amp;lt;/hello-world&amp;gt;
&amp;lt;!-- output: Hello you --&amp;gt;
&amp;lt;hello-world name=&amp;quot;you&amp;quot;&amp;gt;&amp;lt;/hello-world&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mutating the data&lt;/h2&gt;
&lt;p&gt;That&#39;s all good, but how about adding some behaviour to the component? This is where Lit&#39;s use of tagged template literals helps to make things so much easier. Using &lt;code&gt;@click&lt;/code&gt; allows you to create an event listener and remove the need for a whole host of boilerplate to register elements, insert them into the DOM, attach events to them, as well as managing the component&#39;s state. I made a simple button to add exclamation marks to the &lt;code&gt;name&lt;/code&gt; property with a couple of lines of code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;class HelloWorld extends LitElement {
  static properties = {
    name: { type: String }
  }
  constructor() {
    super();
    this.name = &amp;quot;world&amp;quot;;
  }
  makeLouder() {
    this.name = this.name + &#39;!&#39;
  }
  render() {
    return html`
    &amp;lt;p&amp;gt;Hello ${this.name}&amp;lt;/p&amp;gt;
    &amp;lt;button @click=${this.makeLouder}&amp;gt;Louder&amp;lt;/button&amp;gt;
    `;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Styling&lt;/h2&gt;
&lt;p&gt;Because Lit uses the Shadow DOM, it isolates your styling from the rest of the document by default. Making it easy to style your component without worrying about parent styles leaking in, or clashing with other styles. Lit offers a handy &lt;code&gt;css&lt;/code&gt; function to help manage these styles. This may initially break some syntax highlighting in your editor, but there&#39;s a selection of VS Code extensions that add syntax support for Lit.&lt;/p&gt;
&lt;p&gt;You can apply styling to the root component element using the &lt;code&gt;:host&lt;/code&gt; selector. You can even set styles conditionally based on the state of the element. You can interpolate variables into the &lt;code&gt;css&lt;/code&gt; function eg. the &lt;code&gt;fontFamily&lt;/code&gt; variable set outside the component, and you can expose certain values by declaring CSS variables.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { css, html, LitElement } from &#39;https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js&#39;

const fontFamily = css`sans-serif`;

class HelloWorld extends LitElement {
  static styles = css`
    :host {
      display: block;
      padding: 1rem;
      background: var(--background, #eee);
    }
    :host(.blue) {
      background: aliceblue;
    }
    p {
      font-family: ${fontFamily};
    }
  `;
  render() {
    return html`&amp;lt;p&amp;gt;Hello world&amp;lt;/p&amp;gt;`;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- Background #eee --&amp;gt;
&amp;lt;hello-world&amp;gt;&amp;lt;/hello-world&amp;gt;
&amp;lt;!-- Background aliceblue --&amp;gt;
&amp;lt;hello-world class=&amp;quot;blue&amp;quot;&amp;gt;&amp;lt;/hello-world&amp;gt;
&amp;lt;!-- Background goldenrod --&amp;gt;
&amp;lt;style&amp;gt;
hello-world {
  --background: goldenrod;
}
&amp;lt;/style&amp;gt;
&amp;lt;hello-world&amp;gt;&amp;lt;/hello-world&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Thoughts&lt;/h2&gt;
&lt;p&gt;So far my use case for Web Components often means creating one-off, quite simple components that need to be portable (ie. dropped into Shopify themes), where it doesn&#39;t necessarily make sense to add Lit as a dependency. However, for future projects that might need to make use of multiple Web Components, the small penalty of including Lit will surely be worth it for the reduced boilerplate and, I believe easier to read, code.&lt;/p&gt;
&lt;p&gt;[^1]: The JavaScript &amp;quot;sprinkles&amp;quot; approach is a largely undefined term that tends to refer to a backend-driven site or app that includes a small amount of JavaScript added to the front end for client-side activity (as opposed to a fully-fledged SPA). It usually implies using small bits of custom JS, or micro-frameworks like Stimulus, jQuery or Alpine.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Links #3</title>
		<link href="https://mikefallows.com/posts/links-3/"/>
		<updated>Mon, 05 Sep 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/links-3/</id>
		<content type="html">&lt;p&gt;A recent reading list.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://jvns.ca/blog/good-questions/&quot;&gt;How to ask good questions&lt;/a&gt;. A great reminder of how to seek help successfully. The focus is on getting help on technical subjects in a team environment, but it could easily be extrapolated to any subject or situation with plenty of useful lessons about good communication in general.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.andy.works/words/the-most-satisfying-checkbox&quot;&gt;The World&#39;s Most Satisfying Checkbox&lt;/a&gt;. A neat example of how small interactions can be elevated. When working on transactional sites, micro-interactions can be a great opportunity to explore identity can be expressed beyond colours, fonts and messaging. It&#39;s also a good reminder of how much attention these interactions automatically receive in the world of game design.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.minethatdata.com/2022/05/prioritize-what-you-do.html&quot;&gt;Prioritize What You Do&lt;/a&gt;. This is a fantastic list for retailers on where to prioritise efforts. It&#39;s easy to become distracted by novel techniques and forget to give precedence to the basic principles of selling online. Personalisation is the only thing I would caveat because, in my experience, it can create unneeded complexity and make customer experiences more opaque and harder to empathise with (although this largely depends on the nature of your products).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ishadeed.com/article/conditional-border-radius/&quot;&gt;Conditional Border Radius In CSS&lt;/a&gt;. A neat trick to toggle &lt;code&gt;border-radius&lt;/code&gt; in CSS based purely on a container element&#39;s width. Something to tide us over until &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries&quot;&gt;Container Queries&lt;/a&gt; are supported.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Adding a `robots.txt` file to an Eleventy site</title>
		<link href="https://mikefallows.com/posts/adding-robots-txt-to-eleventy-site/"/>
		<updated>Thu, 01 Sep 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/adding-robots-txt-to-eleventy-site/</id>
		<content type="html">&lt;p&gt;Having set up a fair few sites over the years, I periodically get a chunk of emails from Google&#39;s Search Console notifying me of indexing errors. Usually, these are pretty small potatoes caused by things like old products being unpublished and will resolve themselves on the next crawl.&lt;/p&gt;
&lt;p&gt;I realised I hadn&#39;t yet set up a &lt;code&gt;robots.txt&lt;/code&gt; file for this site when a couple of errors popped up in Search Console that I wouldn&#39;t have expected. The errors I had were for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;https://mikefallows.com/cdn-cgi/l/email-protection
https://mikefallows.com/admin/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I had already set up a &lt;code&gt;sitemap.xml&lt;/code&gt; file but somehow overlooked creating a &lt;code&gt;robots.txt&lt;/code&gt; file at the time. The &lt;code&gt;/admin/&lt;/code&gt; URL was easy for me to identify, that is for my Forestry integration which I use as a CMS. That page has a &lt;code&gt;noindex&lt;/code&gt; meta tag in the head.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;meta name=&amp;quot;robots&amp;quot; content=&amp;quot;noindex&amp;quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;I was mistaken&lt;/h2&gt;
&lt;p&gt;I was under the impression that would be enough to signal to Search Console that it should be ignored, but it turns out that it&#39;s being included in my &lt;code&gt;sitemap.xml&lt;/code&gt; so it&#39;s (quite rightly) marked as invalid.&lt;/p&gt;
&lt;p&gt;The other URL that started &lt;code&gt;/cdn-cgi/l/email-protection&lt;/code&gt; was more of a mystery. I hadn&#39;t added anything in a &lt;code&gt;/cdn-cgi/&lt;/code&gt; folder!  The fact that it contained a reference to a CDN was a clue, so I wondered if it was related to Netlify, but I couldn&#39;t think of an obvious reason why it would have any reference to emails. After a bit of quick research, I realised this was related to Cloudflare which I&#39;d recently set up for the site. As I had activated their proxy in front of the site, it explained the unknown folder and it appears to be a part of their bot protection.&lt;/p&gt;
&lt;p&gt;So to fix these validation errors in Search Console I needed to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add a &lt;code&gt;robots.txt&lt;/code&gt; file that disallows &lt;code&gt;/admin/&lt;/code&gt; and &lt;code&gt;/cdn-cgi/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;exclude &lt;code&gt;/admin/&lt;/code&gt; from my &lt;code&gt;sitemap.xml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Adding a &lt;code&gt;robots.txt&lt;/code&gt; file&lt;/h2&gt;
&lt;p&gt;This is super-easy in Eleventy. I created a file: &lt;code&gt;src/robots.txt&lt;/code&gt;; and added the following to my &lt;code&gt;.eleventy.js&lt;/code&gt; config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Put robots.txt in root
eleventyConfig.addPassthroughCopy({ &#39;src/robots.txt&#39;: &#39;/robots.txt&#39; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;addPassthroughCopy&lt;/code&gt; method will just copy the file &amp;quot;as is&amp;quot; into the generated &lt;code&gt;_site&lt;/code&gt; folder. Great.&lt;/p&gt;
&lt;p&gt;My &lt;code&gt;robots.txt&lt;/code&gt; file looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;User-agent: *
Allow: /
Disallow: /admin/
Disallow: /cdn-cgi/

Host: https://mikefallows.com/

Sitemap: https://mikefallows.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important parts were the two &lt;code&gt;Disallow&lt;/code&gt; rules that tell bots that they shouldn&#39;t try to crawl or index those paths in their results.&lt;/p&gt;
&lt;p&gt;You can also &lt;a href=&quot;https://mikefallows.com/robots.txt&quot;&gt;view whatever the current version is&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Excluding pages from &lt;code&gt;sitemap.xml&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;My sitemap is generated by a single &lt;code&gt;sitemap.xml.njk&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;
&amp;lt;urlset xmlns=&amp;quot;http://www.sitemaps.org/schemas/sitemap/0.9&amp;quot;&amp;gt;
{%- for page in collections.all %}
  {%- set adminUrl = r/^&#92;/admin&#92;//i.test(page.url) %}
  {%- set draft = page.data.draft %}
  {%- if not adminUrl and not draft  %}
    {%- set absoluteUrl %}{{ page.url | url | absoluteUrl(metadata.url) }}{% endset %}
    &amp;lt;url&amp;gt;
      &amp;lt;loc&amp;gt;{{ absoluteUrl }}&amp;lt;/loc&amp;gt;
      &amp;lt;lastmod&amp;gt;{{ page.date | htmlDateString }}&amp;lt;/lastmod&amp;gt;
      &amp;lt;changefreq&amp;gt;{{ page.data.changeFreq if page.data.changeFreq else &amp;quot;monthly&amp;quot; }}&amp;lt;/changefreq&amp;gt;
    &amp;lt;/url&amp;gt;
  {%- endif %}
{%- endfor %}
&amp;lt;/urlset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generates &lt;a href=&quot;https://mikefallows.com/sitemap.xml&quot;&gt;an XML file for all pages&lt;/a&gt;, excluding any pages where the URL begins &lt;code&gt;/admin/&lt;/code&gt; or is marked as a draft. I usually default my posts to draft until I&#39;m ready to publish them. Draft posts are excluded by checking if the frontmatter has &lt;code&gt;draft&lt;/code&gt; value set to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The key bit was writing the Regex to test that a URL begins &lt;code&gt;/admin/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;set adminUrl = r/^&#92;/admin&#92;//i.test(page.url)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just to break that down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;r/&lt;/code&gt; regular expressions in Nunjucks need to be prefixed with &lt;code&gt;r&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;^&lt;/code&gt; indicates we&#39;re only matching the start of the string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&#92;/admin&#92;/&lt;/code&gt; literally matches &lt;code&gt;/admin/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/i&lt;/code&gt; makes the test case insensitive (y&#39;know, just in case)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I find most enjoyable about having my own site is having the time to tinker and dig through these types of issues. When they&#39;re low pressure like this one, it&#39;s great to spend a little time polishing and learning in a way that I often miss in client work. For such a small task, I solidified my knowledge just a little bit more about Eleventy, Sitemaps, Regex, and Robots files and that&#39;s mostly due to taking the time to write it up.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Compiling `.kit` files with Grunt</title>
		<link href="https://mikefallows.com/posts/compiling-kit-files-with-grunt/"/>
		<updated>Thu, 11 Aug 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/compiling-kit-files-with-grunt/</id>
		<content type="html">&lt;p&gt;This week I needed to hand over some files to another development team. I was originally commissioned to create some email templates way back in early 2013 for &lt;a href=&quot;https://www.campaignmonitor.com/&quot;&gt;Campaign Monitor&lt;/a&gt; and, apart from some small tweaks and additions, the templates have hardly changed since. You can view an &lt;a href=&quot;http://oipolloi.createsend1.com/t/ViewEmail/r/03E42A0A780B70022540EF23F30FEDED/54DE0C796051573CF351F20C80B74D5E?alternativeLink=False&quot;&gt;archived version of one of the earliest&lt;/a&gt;. But &lt;a href=&quot;http://oipolloi.cmail1.com/t/ViewEmail/r/411C187586824B302540EF23F30FEDED/C378788E19EDA1B76707B176AE29F890?alternativeLink=False&quot;&gt;this is a better one&lt;/a&gt;. Design credit to my good friend &lt;a href=&quot;https://www.eoinmac.com/&quot;&gt;Eóin MacManus&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The templates made some heavy use of &lt;a href=&quot;https://www.campaignmonitor.com/create/editable-content/#repeater&quot;&gt;&lt;code&gt;&amp;lt;repeater&amp;gt;&lt;/code&gt; regions&lt;/a&gt;, a feature of Campaign Monitor&#39;s templates that I utilised to allow multiple sections with different colour backgrounds. Because this was email, it required a lot of inline CSS and a lot of code repetition to achieve the desired result. I was using CodeKit a lot at the time and the app had just introduced the &lt;a href=&quot;https://codekitapp.com/help/kit/&quot;&gt;&lt;code&gt;.kit&lt;/code&gt; file feature&lt;/a&gt;. Kit files added variables and imports to HTML via CSS-style comments which reduced the amount of code I needed to write substantially.&lt;/p&gt;
&lt;p&gt;This worked great for the best part of a decade as the sole developer on the project: I could easily make a small adjustment and compile the files quickly with my copy of CodeKit.&lt;/p&gt;
&lt;p&gt;Now that I needed to pass the files on to another team, I wanted to do so without the requirement of a proprietary app to compile the templates. A quick search and I discovered there was an open source &lt;a href=&quot;https://github.com/jeremyworboys/node-kit&quot;&gt;node-based compiler&lt;/a&gt; for &lt;code&gt;.kit&lt;/code&gt; files and a &lt;a href=&quot;https://github.com/fatso83/grunt-codekit&quot;&gt;Grunt plugin&lt;/a&gt; that leveraged it. I was aware of &lt;a href=&quot;https://gruntjs.com/&quot;&gt;Grunt&lt;/a&gt; as an alternative task runner to something like CodeKit and it had a reputation for having a straightforward build pipeline so it seemed like a good solution.&lt;/p&gt;
&lt;p&gt;First I installed Grunt and the CodeKit plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i -D grunt grunt-codekit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I set up a simple &lt;code&gt;Gruntfile.js&lt;/code&gt; config file that would compile my newsletter template to HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;module.exports = function(grunt) {

  grunt.initConfig({
    codekit: {
      &#39;build/newsletter/newsletter.html&#39;: &#39;src/newsletter/newsletter.kit&#39;
    },
  });

  grunt.loadNpmTasks(&#39;grunt-codekit&#39;);

  grunt.registerTask(&#39;default&#39;, [&#39;codekit&#39;]);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now all I needed to do was run &lt;code&gt;grunt&lt;/code&gt; at the root of the project and my &lt;code&gt;src/newsletter/newsletter.kit&lt;/code&gt; file would be converted to HTML at &lt;code&gt;build/newsletter/newsletter.html&lt;/code&gt;. Easy as that!&lt;/p&gt;
&lt;p&gt;When you add a new template to Campaign Monitor you need to upload the HTML template and also any images the template uses as a separate zip file. Now that I&#39;d got this far I figured I could make that final step easier too. First I installed the &lt;code&gt;grunt-zip&lt;/code&gt; plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i -D grunt-zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I added the &lt;code&gt;zip&lt;/code&gt; task to my &lt;code&gt;Gruntfile&lt;/code&gt; so that my images were zipped into my build folder ready to be uploaded.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;module.exports = function(grunt) {

  grunt.initConfig({
    codekit: {
      &#39;build/newsletter/newsletter.html&#39; : &#39;src/newsletter/newsletter.kit&#39;
    },
    zip: {
      &#39;build/newsletter/img.zip&#39;: &#39;src/newsletter/img/*&#39;
    }
  });

  grunt.loadNpmTasks(&#39;grunt-codekit&#39;);
  grunt.loadNpmTasks(&#39;grunt-zip&#39;);

  grunt.registerTask(&#39;default&#39;, [&#39;codekit&#39;, &#39;zip&#39;]);

};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are potentially a bunch more steps I could add like optimising the images and minifying the HTML but as Campaign Monitor already applies its own optimisations there&#39;s no need to overengineer this solution (as much as I&#39;d love to).&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Laravel Sail, Vite and SSL with a custom domain</title>
		<link href="https://mikefallows.com/posts/laravel-sail-vite-ssl-custom-domain/"/>
		<updated>Fri, 29 Jul 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/laravel-sail-vite-ssl-custom-domain/</id>
		<content type="html">&lt;p&gt;The Shopify apps I build are typically powered by Laravel. Version 8 introduced &lt;a href=&quot;https://laravel.com/docs/master/sail&quot;&gt;Sail&lt;/a&gt;[^1] as an alternative to Valet and version 9 introduced &lt;a href=&quot;https://vitejs.dev/&quot;&gt;Vite&lt;/a&gt;[^2] to replace Laravel Mix (Webpack). I ran into an issue developing my Shopify apps locally when I tried to switch to using these new features.&lt;/p&gt;
&lt;h2&gt;Using a custom domain&lt;/h2&gt;
&lt;p&gt;Giving each project its own development domain is a great feature of Valet. Sail assumes a default &lt;code&gt;laravel.test&lt;/code&gt; domain but allows you to override that in the Docker config. If you wanted to use a domain such as &lt;code&gt;myproject.test&lt;/code&gt; you can make a couple of changes to achieve that.&lt;/p&gt;
&lt;p&gt;In your &lt;code&gt;.env&lt;/code&gt; file set the &lt;code&gt;APP_URL&lt;/code&gt; value and add a new Sail-specific &lt;code&gt;APP_URL&lt;/code&gt; value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;APP_URL=&amp;quot;http://myproject.test&amp;quot;
APP_SERVICE=&amp;quot;myproject.test&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then update the &lt;code&gt;docker-compose.yml&lt;/code&gt; file to reflect the new domain, changing &lt;code&gt;laravel.test&lt;/code&gt; to &lt;code&gt;myproject.test&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &#39;3&#39;
services:
  myproject.test:
    build:
    # etc...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Securing the domain with Caddy&lt;/h2&gt;
&lt;p&gt;Because Shopify requires secure URLs for its Oauth integration, I followed &lt;a href=&quot;https://gilbitron.me/blog/enabling-https-ssl-for-laravel-sail-using-caddy/&quot;&gt;this guide by Gilbert Pellegrom&lt;/a&gt; to enable HTTPS for Sail using Caddy. I won&#39;t repeat the steps here, but it involves adding a Caddy service to Docker, creating a custom &lt;code&gt;Caddyfile&lt;/code&gt; and setting up an endpoint for Caddy. Gilbert&#39;s post kindly &lt;a href=&quot;https://gist.github.com/gilbitron/36d48eb751875bebdf43da0a91c9faec&quot;&gt;provides a gist&lt;/a&gt; with some example code you can drop in. The only changes you would need to make is replacing any references to &lt;code&gt;laravel.test&lt;/code&gt; with &lt;code&gt;myproject.test&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You will also want to update the &lt;code&gt;APP_URL&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt; file to use &lt;code&gt;https&lt;/code&gt; now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;APP_URL=&amp;quot;https://myproject.test&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also found I needed to add a line to the &lt;code&gt;AppServiceProvider&lt;/code&gt;&#39;s &lt;code&gt;boot&lt;/code&gt; method to force all links to use HTTPS. Without that, at least on my machine, routes or URLs generated with the &lt;code&gt;url()&lt;/code&gt; helper would use the HTTP scheme instead.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function boot()
{
    &#92;Illuminate&#92;Support&#92;Facades&#92;URL::forceScheme(&#39;https&#39;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now running &lt;code&gt;sail up&lt;/code&gt; should give you a server you can access at &lt;code&gt;https://myproject.test&lt;/code&gt;. However, you may find that the certificate is untrusted. If you don&#39;t want the warnings to keep appearing and you&#39;re on a Mac, you can add the certificate to your Keychain Access and mark it trusted. Details on how to do that are also included in Gilbert&#39;s guide.&lt;/p&gt;
&lt;h2&gt;Securing Vite&#39;s dev server&lt;/h2&gt;
&lt;p&gt;The last issue I bumped up against was that, by default, Vite will run its dev server on HTTP, which means that the browser wouldn&#39;t load the styles and scripts on my pages. It&#39;s possible to set Vite to load its server under HTTPS, but it won&#39;t have a certificate. You can use the &lt;a href=&quot;https://github.com/vitejs/vite-plugin-basic-ssl&quot;&gt;@vitejs/plugin-basic-ssl&lt;/a&gt; plugin to generate untrusted certificates which will allow you to access the page, but you will need to first dismiss a warning. You can follow the steps in this &lt;a href=&quot;https://stackoverflow.com/a/73148121/15761479&quot;&gt;Stack Overflow answer&lt;/a&gt; to set your Vite config&#39;s host settings to allow it to run on the Docker container. This is how I set it up at first, but dismissing the warning and seeing the red badge in the address bar bothered me.&lt;/p&gt;
&lt;p&gt;I also tried to figure out a way to get the Vite server to use the Caddy certificates but I couldn&#39;t work it out. That is probably the best solution so if &lt;em&gt;you&lt;/em&gt; figure it out – please let me know!&lt;/p&gt;
&lt;p&gt;The alternative I went with was to use the &lt;a href=&quot;https://github.com/liuweiGL/vite-plugin-mkcert&quot;&gt;vite-plugin-mkcert&lt;/a&gt; package. This allows you to generate a trusted certificate locally on-the-fly. The one compromise is that you have to run the Vite server outside the Docker container (ie use &lt;code&gt;npm run dev&lt;/code&gt; rather than &lt;code&gt;sail npm run dev&lt;/code&gt;). In my case, that is acceptable as I&#39;m not using it to build the production assets and the Vite server is probably going to run faster outside of Docker too. What I decided to do was to use a dedicated subdomain of my project eg. &lt;code&gt;vite.myproject.test&lt;/code&gt; for the Vite server so that I didn&#39;t have any conflicts with existing certificates or other projects.&lt;/p&gt;
&lt;p&gt;My final &lt;code&gt;vite.config.js&lt;/code&gt; file looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import laravel from &#39;laravel-vite-plugin&#39;
import { defineConfig } from &#39;vite&#39;
import mkcert from&#39;vite-plugin-mkcert&#39;

export default defineConfig({
    server: {
      https: true,
      host: &#39;vite.myproject.test&#39;,
    },
    plugins: [
        mkcert(),
        laravel({
          input: [
            &#39;resources/css/app.css&#39;,
            &#39;resources/js/app.js&#39;,
          ],
          refresh: true,
        }),
    ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when I start work on a project I can run &lt;code&gt;sail up -d &amp;amp;&amp;amp; npm run dev&lt;/code&gt; and have my app running locally with trusted HTTPS along with all the speed and Hot Module Reloading goodness of Vite.&lt;/p&gt;
&lt;p&gt;[^1]: &amp;quot;A light-weight command-line interface for interacting with Laravel&#39;s default Docker development environment&amp;quot;. Recently I&#39;ve preferred to use Docker over Laravel Valet as I&#39;m regularly switching between projects with different PHP versions and database engines, as well as working locally on either Intel or Silicon hardware.
[^2]: &amp;quot;A modern frontend build tool that provides an extremely fast development environment&amp;quot;. I&#39;ve never really been comfortable with Webpack configs. Mix was a much simpler wrapper, but with esbuild under the hood, Vite seems to be so much faster.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Bulk deleting Cloudflare DNS records</title>
		<link href="https://mikefallows.com/posts/bulk-deleting-cloudflare-dns-records/"/>
		<updated>Mon, 25 Jul 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/bulk-deleting-cloudflare-dns-records/</id>
		<content type="html">&lt;p&gt;I&#39;ve recently been trying to consolidate some of the services that I use, a sort of technical spring clean if you will. In summer.&lt;/p&gt;
&lt;p&gt;As well as transferring domains that I own to a single provider, I wanted to have a single point of management that would also work for domains that I don&#39;t own, but happen to manage on behalf of clients. I&#39;ve long been wanting to tinker with Cloudflare as it offers a chunk of nice features like &lt;a href=&quot;https://en.wikipedia.org/wiki/Denial-of-service_attack&quot;&gt;DDoS&lt;/a&gt; protection, asset compression, HTTPS and email routing, all on a pretty generous free plan. So far I&#39;ve found it easy to work with, and after migrating a dozen or so domains to it, I&#39;ve had a pretty good opportunity to try it out.&lt;/p&gt;
&lt;p&gt;When you set up a new &#39;site&#39; in Cloudlflare, you give it a domain and it will try to detect all the A records and create those for you. It does also seem to detect a couple of other records, but in most cases I&#39;ve had to manually add some records myself, usually to do with email and third-party verification (&lt;a href=&quot;https://en.wikipedia.org/wiki/DMARC&quot;&gt;DMARC&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/Sender_Policy_Framework&quot;&gt;SPF&lt;/a&gt;, etc.). You can also import records from a text file, but for my needs adding a couple of extra records helped me review any obsolete records that existed anyway.&lt;/p&gt;
&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;The only real issue I ran into was that for a couple of domains, on importing them to Cloudflare, it detected and created dozens of A records for standard terms eg. &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt; ... &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt;, &lt;code&gt;apps&lt;/code&gt; ... &lt;code&gt;www1&lt;/code&gt;, &lt;code&gt;www2&lt;/code&gt;, etc. Nearly 200 unused records in total on one domain. The Cloudflare UI doesn&#39;t currently offer any ways to remove DNS records in bulk, so the process of removing so many manually, and on several domains made me look for a solution.&lt;/p&gt;
&lt;h2&gt;A fix&lt;/h2&gt;
&lt;p&gt;Fortunately one of the other benefits of Cloudflare is that it offers a Rest API which allows you to delete &lt;a href=&quot;https://en.wikipedia.org/wiki/Domain_Name_System&quot;&gt;DNS&lt;/a&gt; records programmatically. I was able to put together a script to delete the unused records on one of my domains pretty quickly. I already knew I was going to be repeating this task on other domains, and that there would be some different requirements for each, so of course, I &lt;s&gt;wasted some time&lt;/s&gt; &lt;em&gt;did the sensible thing&lt;/em&gt; and &lt;a href=&quot;https://github.com/mikenewbuild/cloudflare&quot;&gt;put together a little repo&lt;/a&gt; that could be configured. I haven&#39;t done much PHP recently so it was also an excuse to flex that muscle. It&#39;s still my favourite scripting language for putting something together quickly.&lt;/p&gt;
&lt;p&gt;I honestly couldn&#39;t figure out why this happened on some domains and not others, there wasn&#39;t anything I could identify as the root cause. Maybe you&#39;re here because you&#39;ve encountered the same problem? If so hopefully &lt;a href=&quot;https://github.com/mikenewbuild/cloudflare&quot;&gt;this script might help you out&lt;/a&gt;!&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Handling dark mode in HTML emails</title>
		<link href="https://mikefallows.com/posts/handling-dark-mode-in-html-emails/"/>
		<updated>Sat, 16 Jul 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/handling-dark-mode-in-html-emails/</id>
		<content type="html">&lt;p&gt;Recently I needed to customise an HTML email template written in &lt;a href=&quot;https://pugjs.org/&quot;&gt;Pug&lt;/a&gt;. This was my first time using Pug and I was pleasantly surprised to find it intuitive and with a few useful features to help organise some reusable components. Its terse syntax is also handy for email templates, as supporting current email clients (even in 2022!) means that you still need to work with deeply nested tables to achieve even the simplest of layouts.&lt;/p&gt;
&lt;p&gt;Although CSS support in email is improving, there&#39;s still a range of mail clients that are hard to test, so resources like &lt;a href=&quot;https://www.caniemail.com/&quot;&gt;Can I email...&lt;/a&gt; are invaluable in helping identify which features are widely supported. For example, my first thought was to use SVG and then define the colours in CSS. Unfortunately, the support at this point is still lacking, so it does require creating multiple versions of images that you need inverting for your design.&lt;/p&gt;
&lt;p&gt;I&#39;ve noticed that some email clients (particularly on mobile), will do their best to apply a dark theme to emails, and sometimes the results can be less than satisfactory. In this case, the company&#39;s branding was monochrome, so it gave me an opportunity to easily adopt a dark mode alternative as the colour scheme was so simple.&lt;/p&gt;
&lt;h2&gt;Declaring dark mode support&lt;/h2&gt;
&lt;p&gt;The first thing to do is indicate that the email supports both light and dark mode. You can declare this in a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag within the head of a document.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root {
  color-scheme: light dark;
  supported-color-schemes: light dark;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s then possible to use a media query to detect the mode that mode.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@media (prefers-color-scheme: dark) {
  /* styles for dark mode... */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Providing alternative colours&lt;/h2&gt;
&lt;p&gt;As a fallback for clients that may not support alternative colour schemes, I set my light styles first and then used the media query to override those styles for dark mode. Pug allows you to store values in a &lt;a href=&quot;https://pugjs.org/language/interpolation.html&quot;&gt;variable&lt;/a&gt; that can be interpolated into strings. Normally on a web page, I would use CSS Variables (Custom Properties) but support for that is still patchy for emails.&lt;/p&gt;
&lt;p&gt;I created a set of variables for my &amp;quot;default&amp;quot; colours, and then a matching set of alternative &amp;quot;inverted&amp;quot; colours. This helped keep everything symmetrical and made it easier not to miss any elements that I needed to account for in dark mode.&lt;/p&gt;
&lt;p&gt;Here&#39;s a basic example declaring some default colours for the element with a class of &lt;code&gt;.document&lt;/code&gt; that I used for the root of my email, and then declaring the inverted colours for dark mode.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.document {
  background-color: #{defaultBgColor};
  color: #{defaultTextColor};
}

@media (prefers-color-scheme: dark) {
  .document {
    background-color: #{invertedBgColor} !important;
    color: #{invertedTextColor} !important;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Handling alternate images&lt;/h2&gt;
&lt;p&gt;There are only some images that need to be handled for dark mode, and these were graphic elements that essentially match the text and background colours of the design.&lt;/p&gt;
&lt;p&gt;The strategy I used to handle these was to wrap the light and dark versions of images in a &lt;code&gt;span&lt;/code&gt; element with a class to identify which mode the image was supposed to appear in. I used the wrapping &lt;code&gt;span&lt;/code&gt; to declare some width and height values which had the added bonus of keeping these graphic images sharp on high-density screens.&lt;/p&gt;
&lt;p&gt;Using a Pug &lt;a href=&quot;https://pugjs.org/language/mixins.html&quot;&gt;mixin&lt;/a&gt; you can create a function to return the HTML with both images - note the &lt;code&gt;span&lt;/code&gt; with a class of &lt;code&gt;.light-mode-images&lt;/code&gt; wraps &lt;code&gt;logo.png&lt;/code&gt; and &lt;code&gt;.dark-mode-images&lt;/code&gt; wraps &lt;code&gt;logo-inverted.png&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;mixin logo()

  span.light-mode-image(style=&#39;width:180px;height:64px;&#39;)
    img(src=&#39;#{assetUrl}/logo.png&#39; alt=&#39;Acme&#39; width=&#39;360&#39; height=&#39;128&#39;)
  span.dark-mode-image(style=&#39;width:180px;height:64px;&#39;)
    img(src=&#39;#{assetUrl}/logo-inverted.png&#39; alt=&#39;Acme&#39; width=&#39;360&#39; height=&#39;128&#39;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can use some generic CSS rules to handle the correct display of images by toggling between &lt;code&gt;display: block&lt;/code&gt; and &lt;code&gt;display: none&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;span.light-mode-image {
  display: block;
}

span.dark-mode-image {
  display: none;
}

@media (prefers-color-scheme: dark) {
  span.light-mode-image {
    display: none !important;
  }

  span.dark-mode-image {
    display: block !important;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Other considerations&lt;/h2&gt;
&lt;p&gt;For this relatively simple template, I only had to generate one other &lt;code&gt;mixin&lt;/code&gt; function for a graphic element (an arrow) and handle some link colours. In this case, the template was defaulting to system fonts. If it was using a custom font then it might be worth adjusting weights as light-on-dark text can often appear heavier than dark-on-light.&lt;/p&gt;
&lt;p&gt;Because the design was essentially black and white, it was easy to decide which colours needed swapping and what the alternative would be. Once you start getting into lighter colours the decision about the alternatives becomes a bit harder to decide, so testing and image generation can increase the workload, and accessibility concerns may require more attention.&lt;/p&gt;
&lt;p&gt;In the end, I was pleased I managed to get an effective result with a relatively low number of lines of code. Better still, I got an appreciation of how much potential work would be involved in a more complicated design!&lt;/p&gt;
&lt;p&gt;You can check out a finished email for &lt;a href=&quot;https://static.common-ground.io/shops/70/campaigns/11/campaign-11.html&quot;&gt;Australia&#39;s best record shop&lt;/a&gt;.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Links #2</title>
		<link href="https://mikefallows.com/posts/links-2/"/>
		<updated>Thu, 12 May 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/links-2/</id>
		<content type="html">&lt;p&gt;A list of articles which have recently grabbed my attention.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://csswizardry.com/2022/03/optimising-largest-contentful-paint/&quot;&gt;Optimising Largest Contentful Paint&lt;/a&gt;. The really interesting part of this for me is that pop-ups (aka &lt;a href=&quot;https://en.wikipedia.org/wiki/Alien_(creature_in_Alien_franchise)#Facehugger&quot;&gt;Facehuggers&lt;/a&gt;) which typically prompt you for your email, if big enough, will be considered the LCP. A lot of these are delayed on purpose, but the speed test won&#39;t distinguish that so you are artificially signalling to Google that your page is slow.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs&quot;&gt;Favicons in 2021&lt;/a&gt;. The plethora of shortcut icon formats that have been introduced, has tended to make me nope out of providing anything beyond a simple 64x png and cross my fingers. With wide support for SVG  things seemed to have calmed down, so this seems like a manageable amount of work to do on new projects.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://paulgraham.com/words.html&quot;&gt;Putting Ideas into Words&lt;/a&gt;. I started this blog as an experiment to see if I could improve my writing, and help me to clarify some of my nebulous ideas and document things that I&#39;ve learnt (so that maybe it will stick better). One of the best side effects is that writing about something forces me to test my assumptions before committing them to the world wide web.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.theregister.com/2013/09/02/compact_cassette_supremo_lou_ottens_talks_to_el_reg/&quot;&gt;Lou Ottens Interview&lt;/a&gt;. A very technical interview from 2013 with the inventor of the compact cassette who passed away in 2021. There&#39;s an interesting parallel to the web world in terms of developing standards and how they are often trumped by popularity. Lou on the &lt;a href=&quot;https://en.wikipedia.org/wiki/Dolby_noise-reduction_system&quot;&gt;Dolby B&lt;/a&gt; format: &lt;em&gt;&amp;quot;I was afraid that it would cause a lot of confusion in the market. And I think I was right. But being right is not always good enough. Ray Dolby came to visit me and I gave my arguments, but he went on. Lesson: you cannot stop progress...&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://omnichord.jake.fun/&quot;&gt;Omnichord OM-27&lt;/a&gt;. Not an article, but a fun little experiment by Jake Albaugh that makes use of the Web Audio API. &lt;a href=&quot;https://www.discogs.com/release/51656-Sirconical-Make-The-Music-With-Your-Mouse&quot;&gt;Make the music with your mouse&lt;/a&gt;, Biz.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Making a Shopify Theme App Extension for Google Site Verification</title>
		<link href="https://mikefallows.com/posts/making-a-shopify-theme-app-extension-for-google-site-verification/"/>
		<updated>Wed, 23 Feb 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/making-a-shopify-theme-app-extension-for-google-site-verification/</id>
		<content type="html">&lt;p&gt;Given that I&#39;m &lt;em&gt;all in&lt;/em&gt; on Shopify&#39;s OS2 themes, I&#39;ve been looking for a way to experiment with building the recently introduced &amp;quot;app extensions&amp;quot; for themes. These extensions provide a way to add functionality to Shopify themes without the need to change the underlying theme code itself.&lt;/p&gt;
&lt;p&gt;Assuming you already have a Shopify Partner account and the &lt;a href=&quot;https://shopify.dev/apps/tools/cli&quot;&gt;Shopify CLI&lt;/a&gt; installed, then it&#39;s incredibly simple to get started with theme app extensions. As a small project to get familiar with them, I set myself the task of creating an app that allows you to insert a tag to verify a site with Google. Although I tend to recommend verifying via DNS records, sometimes it&#39;s more suitable to include a meta tag in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of the site.&lt;/p&gt;
&lt;h2&gt;Theme app extension benefits&lt;/h2&gt;
&lt;p&gt;Normally adding a verification tag would mean editing all of the &lt;code&gt;/layout&lt;/code&gt; files in a theme like &lt;code&gt;theme.liquid&lt;/code&gt; and &lt;code&gt;password.liquid&lt;/code&gt; . Once those files are edited it also means maintaining those changes any time you update the theme. Using a theme app extension reduces this burden.&lt;/p&gt;
&lt;p&gt;Shopify hosts the code for the extensions on their servers so performance is great and, as a developer, you don&#39;t have to worry about standing up servers or deployments.&lt;/p&gt;
&lt;h2&gt;The verification tag&lt;/h2&gt;
&lt;p&gt;The tag in question is very simple and has the following format:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;meta name=&amp;quot;google-site-verification&amp;quot; content=&amp;quot;UNIQUE_ID&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I want to be able to have a theme block that allows adding the client&#39;s &lt;code&gt;UNIQUE_ID&lt;/code&gt; as a setting in the theme&#39;s customisation UI, and once set rendering a valid tag in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; portion of the site.&lt;/p&gt;
&lt;h2&gt;Creating the app&lt;/h2&gt;
&lt;p&gt;Extensions must be associated with an app so you need to create an app first if you don&#39;t have one set up already. The easiest way is to use the CLI&#39;s &lt;code&gt;app create&lt;/code&gt; command. You can pick which starter template to work with, eg. Node, PHP, etc. I went for Node, but it&#39;s not particularly important because I&#39;m not worried about creating any UI for the app right now.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;shopify app create node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, I supplied a name for the app. I just picked something generic: &#39;Theme Blocks&#39;. It will also ask you which type of app you want to create, for simplicity I picked &#39;Custom&#39; which means it can only be installed on one specific store.&lt;/p&gt;
&lt;p&gt;That created an app and output a link to it in the Partner dashboard. It also provided a link to a page in the dashboard where I could install the app in my chosen store. I didn’t do that though (yet).&lt;/p&gt;
&lt;h2&gt;Creating the extension&lt;/h2&gt;
&lt;p&gt;There are two types of &lt;a href=&quot;https://shopify.dev/apps/online-store/theme-app-extensions/extensions-framework&quot;&gt;theme app extensions&lt;/a&gt;. Standard “App blocks” and “App embed blocks”. It’s the embed blocks that I’m interested in as they allow you to insert code into the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; portion of the document. You can learn a bit more in Shopify&#39;s &lt;a href=&quot;https://shopify.dev/apps/online-store/theme-app-extensions/getting-started&quot;&gt;Getting Started Guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A theme app extension should be created within the app project itself. This isn&#39;t actually that explicit in the documentation so, at first, I wasn&#39;t sure if it should be treated as a separate project in its own repo.  Don&#39;t make the same mistake. At the root of the project I ran:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;shopify extension create
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was prompted to choose the app I wanted to associate the extension with and I, of course, picked the one I was currently working on. The next question was about which type of extension I wanted to create for which I selected “Theme App Extension”.&lt;/p&gt;
&lt;p&gt;The project now contained a folder called &lt;code&gt;theme-app-extension&lt;/code&gt; scaffolded with empty &lt;code&gt;assets&lt;/code&gt;, &lt;code&gt;blocks&lt;/code&gt;, &lt;code&gt;locales&lt;/code&gt; and a &lt;code&gt;snippets&lt;/code&gt; folder, along with a &lt;code&gt;.env&lt;/code&gt; file and &lt;code&gt;.shopify-cli.yml&lt;/code&gt; file.&lt;/p&gt;
&lt;h2&gt;Building a block&lt;/h2&gt;
&lt;p&gt;Okay, now for the block code itself. I created a &lt;code&gt;blocks/google-verification.liquid&lt;/code&gt; file with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;{%- if block.settings.google_verification_id != blank %}
&amp;lt;meta name=&amp;quot;google-site-verification&amp;quot; content=&amp;quot;{{ block.settings.google_verification_id }}&amp;quot;&amp;gt;
{%- endif %}

{% schema %}
{
  &amp;quot;name&amp;quot;: &amp;quot;Theme Customisations&amp;quot;,
  &amp;quot;target&amp;quot;: &amp;quot;head&amp;quot;,
  &amp;quot;settings&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;text&amp;quot;,
      &amp;quot;id&amp;quot;: &amp;quot;google_verification_id&amp;quot;,
      &amp;quot;label&amp;quot;: &amp;quot;Google verification ID&amp;quot;
    }
  ]
}
{% endschema %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should make sense if you&#39;re familiar with Shopify&#39;s theme sections. Essentially I&#39;m defining some settings that can capture the information I need and dynamically insert that data where I need to.&lt;/p&gt;
&lt;p&gt;Note the &lt;code&gt;target&lt;/code&gt; is set to &lt;code&gt;head&lt;/code&gt; and I’m rendering the tag only on the condition that a &lt;code&gt;google_verification_id&lt;/code&gt; is provided.&lt;/p&gt;
&lt;h2&gt;Publishing the extension&lt;/h2&gt;
&lt;p&gt;Now I can &lt;code&gt;cd&lt;/code&gt; into the &lt;code&gt;theme-app-extension&lt;/code&gt; folder and run&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;shopify extension push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following dialogue allowed me to register the extension and update the &lt;code&gt;.env&lt;/code&gt; file, then run any validation before pushing it to Shopify. Once it’s pushed, it outputs a link to the app in my Partner dashboard where I can version and publish it. Here I can also enable a development store preview so I can test the changes.&lt;/p&gt;
&lt;p&gt;I can now go to a theme in my development store, and in the theme customiser under &lt;em&gt;Theme Settings &amp;gt; App Embeds&lt;/em&gt; I can see my new app block sitting there waiting to be enabled. Genuine thrill when stuff like this just works™.&lt;/p&gt;
&lt;p&gt;To test it, I enabled the block, added a dummy verification ID, hit save and checked the output theme code. Sure enough, in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; tag, before the standard Shopify &lt;code&gt;content_for_header&lt;/code&gt; output, I can see the verification tag output using the data I added.&lt;/p&gt;
&lt;p&gt;Nice. I’ve managed to modify a theme without having to change any of the underlying theme templates themselves.&lt;/p&gt;
&lt;h2&gt;Updating the extension&lt;/h2&gt;
&lt;p&gt;Shopify lets you work in &#39;draft&#39; mode, and has a workflow to publish versions of your extension in production so you can push as many changes as you want, and safely test updates in your development store.&lt;/p&gt;
&lt;h2&gt;Connecting the extension&lt;/h2&gt;
&lt;p&gt;To simulate collaborating on an extension I checked out the project repository on another machine and was able to populate the &lt;code&gt;.env&lt;/code&gt; file by running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;shopify extension connect
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Some notes&lt;/h2&gt;
&lt;p&gt;It’s worth mentioning that changing the filename of any of the extensions will lose any settings previously created in a theme. So will changing any of the ids in the schema settings in the same way that they would if you were using theme sections. I would want to give this some consideration if I was building a public app.&lt;/p&gt;
&lt;p&gt;You can only have one theme extension per app, but I&#39;m not sure if there&#39;s a limit to the number of blocks it can have. I might want to be conservative with the number of blocks I created in a public app, but the blocks themselves might be more complex as a result. For custom extensions, I might prefer to have more, simpler blocks to make it easy to add new blocks and make them more portable between clients too.&lt;/p&gt;
&lt;p&gt;Overall, I&#39;m super impressed with the experience of creating my first theme app extension, and I intend to use it a lot as a tool to add theme customisations in future.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Using PostCSS and Autoprefixer with esbuild</title>
		<link href="https://mikefallows.com/posts/using-postcss-and-autoprefixer-with-esbuild/"/>
		<updated>Tue, 15 Feb 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/using-postcss-and-autoprefixer-with-esbuild/</id>
		<content type="html">&lt;p&gt;In &lt;a href=&quot;https://mikefallows.com/posts/shopify-theme-development-with-esbuild/&quot;&gt;a previous post&lt;/a&gt;, I described how to use &lt;a href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt; as a bundler for Shopify themes.&lt;/p&gt;
&lt;p&gt;In this post, I want to document an additional step to use a tool called &lt;a href=&quot;https://github.com/postcss/autoprefixer&quot;&gt;Autoprefixer&lt;/a&gt; (a &lt;a href=&quot;https://github.com/postcss/postcss&quot;&gt;PostCSS plugin&lt;/a&gt;) which will automatically add browser vendor prefixes to your compiled CSS so that features are supported in older browsers.&lt;/p&gt;
&lt;h2&gt;Why add vendor prefixes?&lt;/h2&gt;
&lt;p&gt;Even in the days of modern &amp;quot;evergreen&amp;quot; browsers, users don&#39;t always upgrade in a timely fashion or are on older versions of operating systems which are limited to certain versions of browsers (👋 Safari). &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Vendor_Prefix&quot;&gt;Vendor prefixes&lt;/a&gt; were initially a way for browsers to ship their own implementations of features which developers could opt into before standards had been agreed but I think it&#39;s largely abandoned as an approach now. I don&#39;t want to keep track of which vendor prefixes I should include where but when it comes to e-commerce, a broken-looking site can reduce credibility, and ultimately sales.&lt;/p&gt;
&lt;p&gt;Wouldn&#39;t it be nice if there was a tool that keeps track of which vendor prefixes I should still be including, and like, y&#39;know, add them for me at build time? Could it please compile my old Sass files too? Really &lt;em&gt;fast&lt;/em&gt;? Oh goody.&lt;/p&gt;
&lt;h2&gt;Adding PostCSS and Autoprefixer&lt;/h2&gt;
&lt;p&gt;Fortunately, the &lt;a href=&quot;https://github.com/glromeo/esbuild-sass-plugin&quot;&gt;esbuild-sass-plugin&lt;/a&gt; package by Gianluca Romeo I was already using makes integrating PostCSS super easy and &lt;a href=&quot;https://github.com/glromeo/esbuild-sass-plugin#--postcss&quot;&gt;documents a simple example&lt;/a&gt; that includes Autoprefixer.&lt;/p&gt;
&lt;p&gt;First, install the necessary packages from the command line.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i -D postcss autoprefixer postcss-preset-env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we can update our &lt;code&gt;esbuild.config.js&lt;/code&gt; to use these plugins.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import esbuild from &#39;esbuild&#39;;
import { sassPlugin } from &#39;esbuild-sass-plugin&#39;;
import postcss from &#39;postcss&#39;;
import autoprefixer from &#39;autoprefixer&#39;;
import postcssPresetEnv from &#39;postcss-preset-env&#39;;

esbuild.build({
  //...
  plugins: [
    sassPlugin({
      async transform(source, resolveDir) {
        const {css} = await postcss([autoprefixer, postcssPresetEnv({stage: 0})]).process(source, {from: undefined})
        return css
      }
    }),
    //...
  ],
  //...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I ended up adding &lt;code&gt;{from: undefined}&lt;/code&gt; to the options of the &lt;code&gt;process&lt;/code&gt; function, just to quieten a warning about the way the source maps are generated. It&#39;s true that with this config the source maps aren&#39;t really very useful, but I haven&#39;t taken the time to look into getting that set up correctly. For now, I&#39;m just glad to have the benefit of having the vendor prefixes added as part of the build and a single entry point.&lt;/p&gt;
&lt;p&gt;I could probably improve this, if I felt the need/pain, by defining a separate config for CSS eg. an &lt;code&gt;esbuild.css.config.js&lt;/code&gt; with just the rules for the CSS compilation and with the option to run those independently or in parallel.&lt;/p&gt;
&lt;h2&gt;Defining supported browsers&lt;/h2&gt;
&lt;p&gt;The last part is specifying which browsers you want to support, to define which vendor prefixes are required. There is a couple of ways to handle this, but I prefer to add a &lt;code&gt;browserslist&lt;/code&gt; property to my &lt;code&gt;package.json&lt;/code&gt; as it&#39;s pretty universally supported, and fairly explicit.&lt;/p&gt;
&lt;p&gt;The current default is to specify that usage is greater than 0.5% and at least the last two versions are supported which, will make sure that the most commonly used browser versions are included.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  //...
  &amp;quot;browserslist&amp;quot;: &amp;quot;&amp;gt; 0.5%, last 2 versions, Firefox ESR, not dead&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can &lt;a href=&quot;https://github.com/browserslist/browserslist#queries&quot;&gt;learn more about Browserslist queries&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With that, my built CSS includes the necessary vendor prefixes. Plus I now also have a config that&#39;s easily portable between other Shopify themes (&lt;a href=&quot;https://gist.github.com/mikenewbuild/2eaddfb3f5bbca0d1d07908c61725daf&quot;&gt;Gist&lt;/a&gt;) where I&#39;m bundling Sass and using the Shopify CLI.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Links #1</title>
		<link href="https://mikefallows.com/posts/links-1/"/>
		<updated>Sun, 06 Feb 2022 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/links-1/</id>
		<content type="html">&lt;p&gt;I&#39;ve never really been good at keeping bookmarks, so I&#39;m going to try tracking interesting things I&#39;ve found on the web in posts for future me. I&#39;m hoping this also provides some lower-effort content that I can publish more easily, and get less hung up about writing &amp;quot;proper&amp;quot; posts. Not entirely uninspired by &lt;a href=&quot;https://brucelawson.co.uk/category/accessibility-web-standards/reading-list/&quot;&gt;Bruce Lawson&#39;s Reading Lists&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.theatlantic.com/ideas/archive/2021/04/nfts-werent-supposed-end-like/618488/&quot;&gt;NFTs weren&#39;t supposed to end like this&lt;/a&gt;. Anil Dash on the unfulfilled promise of technology benefitting artists foremost.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nippon.com/en/japan-topics/g02027/&quot;&gt;Okuda Hiroko: behind the Sleng Teng rhythm&lt;/a&gt;. Fascinating how culture was transferred via pre-internet age technology with unexpected consequences.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.newstatesman.com/business/companies/2021/04/ceos-are-hugely-expensive-why-not-automate-them&quot;&gt;CEOs are hugely expensive – why not automate them?&lt;/a&gt; Indeed. Via the New Statesman.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://thoughtbot.com/blog/shell-script-suggestions-for-speedy-setups&quot;&gt;Shell script suggestions for speedy setups&lt;/a&gt;. A great post on scripting your project setup to reduce the friction when jumping into a project.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cognition.happycog.com/article/monkey-patching-in-craft&quot;&gt;Monkey Patching&lt;/a&gt;. Although Rene Merino&#39;s article uses Craft CMS for its example, it&#39;s a great piece on when and why Monkey Patching is a useful technique. It for sure comes in handy when customising Shopify themes.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://onelook.com/&quot;&gt;One Look Dictionary&lt;/a&gt;. A useful writing tool.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=az-MX_M11lg&quot;&gt;How to read complex code&lt;/a&gt;. A talk, by Dr Felienne Hermans, which covers the challenges of encountering unfamiliar code. It provides some insight on ways to improve cognition of unfamiliar syntax. Plenty to consider when it&#39;s code you might encounter, or considering the reader of the code you write.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Responsive images in Shopify themes</title>
		<link href="https://mikefallows.com/posts/responsive-images-in-shopify-themes/"/>
		<updated>Wed, 22 Dec 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/responsive-images-in-shopify-themes/</id>
		<content type="html">&lt;p&gt;Performance is a useful metric for most cases on the web. None more so than in e-commerce where performance can have a dramatic impact on conversion rates and therefore profits.&lt;/p&gt;
&lt;p&gt;One of the benefits of using a platform like Shopify is that you can leverage their margins of scale to achieve high-volume global performance for a fraction of the cost you would need to spend to do it yourself. Although the operating costs of servers have been shrinking in previous years, the complexity and pace of change have increased. As someone that would just about describe themselves as a full-stack[^1] developer, I feel like I offer my clients more value with my front-end expertise than back-end.&lt;/p&gt;
&lt;p&gt;Outside of video, images are likely to be the heaviest assets on a web page in terms of sheer file size. This can be particularly true on e-commerce sites, where high-quality images are a key factor in sales conversion. This post covers my current best practices for handling responsive images in Shopify themes when you have access to the &lt;code&gt;image&lt;/code&gt; object.[^2]&lt;/p&gt;
&lt;h2&gt;tl;dr&lt;/h2&gt;
&lt;p&gt;Skip ahead to the new &lt;a href=&quot;https://mikefallows.com/posts/responsive-images-in-shopify-themes/#the-new-image_tag-filter&quot;&gt;image_tag&lt;/a&gt; section, if you don&#39;t want to wade through my ruminations on the old ways.&lt;/p&gt;
&lt;h2&gt;Legacy solution&lt;/h2&gt;
&lt;p&gt;Before the &lt;code&gt;picture&lt;/code&gt; element, &lt;code&gt;srcset&lt;/code&gt; and &lt;code&gt;loading=&amp;quot;lazy&amp;quot;&lt;/code&gt; attributes had enough cross-browser support, detecting which images to load – and when – relied on using JavaScript. The technique I used allowed me to effectively query whether an image was within the browser&#39;s viewport, and based on the containing element, identify the most appropriate sized image to load in.&lt;/p&gt;
&lt;p&gt;This was possible due to Shopify&#39;s CDN that allowed you to store a single large image, but also fetch different versions of the image by adjusting certain values in the filename. For example, a large (2000×2000px) image named &lt;code&gt;arthur.jpg&lt;/code&gt; could be scaled down to 100×100px by requesting &lt;code&gt;arthur_100x.jpg&lt;/code&gt;. In Liquid that could be achieved with the &lt;code&gt;img_url&lt;/code&gt; filter like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{{ settings.banner | img_url: &#39;100x&#39; }}
// Outputs: //cdn.shopify.com/full/path/to/arthur_100x.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;figcaption&gt;
Please note: I&#39;m excluding the cache-busting part of the query strings in my examples for simplicity&#39;s sake.
&lt;/figcaption&gt;
&lt;p&gt;This meant that even with significant design changes, there was no need to worry about replacing existing images (as long as the originals were already stored at a large enough size). Later Shopify introduced more features to the CDN like the ability to &lt;code&gt;crop&lt;/code&gt; images or convert images to the progressive jpeg format (&lt;code&gt;pjpg&lt;/code&gt;), which was then superceded by more modern formats like &lt;code&gt;webp&lt;/code&gt; (update: and now &lt;code&gt;avif&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Later Shopify made it possible to request image transformations by appending query parameters to the request rather than by mutating the filename itself. For example, the equivalent file of &lt;code&gt;arthur_100x.jpg&lt;/code&gt; could be requested instead with &lt;code&gt;arthur.jpg?width=100&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{{ settings.banner | image_url: width: 100 }}
// Outputs: //cdn.shopify.com/full/path/to/arthur.jpg?width=100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This also makes programmatically generating image URLs (for example in JavaScript) easier, because it doesn&#39;t rely on a regular expression to check whether the passed filename already includes an image transformation eg. &lt;code&gt;arthur_480x.jpg&lt;/code&gt; that needs to be accounted for. But we&#39;re not covering that here.&lt;/p&gt;
&lt;h2&gt;Lazy loading&lt;/h2&gt;
&lt;p&gt;The concept of lazy loading is that you needn&#39;t load images that the visitor can&#39;t immediately see anyway – perhaps because they are outside of the browser&#39;s viewport (lower down the page), or in an unreached slide in a slideshow component.&lt;/p&gt;
&lt;p&gt;Originally you needed to rely on the browser&#39;s &lt;code&gt;resize&lt;/code&gt; and &lt;code&gt;scroll&lt;/code&gt; events to detect when and which images might be required. These events could have their performance penalty, particularly on image-heavy pages and so meant leveraging things like &lt;code&gt;debounce&lt;/code&gt; to minimise calculations. The tricks were frankly quite gnarly and I found no &lt;em&gt;perfect&lt;/em&gt; solution, even after several years of refinements. This was exacerbated with the introduction of Apple&#39;s retina display on iPhones creating the additional matrix of serving higher resolution images at each size.&lt;/p&gt;
&lt;p&gt;Some of these issues were mitigated with the introduction of the browser&#39;s &lt;code&gt;IntersectionObserver&lt;/code&gt; API, which allowed you to more easily detect images entering (or more usefully: about to enter) the viewport with a much lower performance cost.&lt;/p&gt;
&lt;p&gt;Two things have largely removed the need for JavaScript solutions. The first: cross-browser support for &lt;code&gt;srcset&lt;/code&gt;, a feature which allows you to define a set of images and let the browser decide which is most appropriate, based on a much broader set of variables such as device, network speeds, and battery level; the second: a &lt;code&gt;loading=&amp;quot;lazy&amp;quot;&lt;/code&gt; attribute, which lets the browser use similar factors to optimise when the browser should start loading in an image with minimum interruption to the visitor&#39;s experience.&lt;/p&gt;
&lt;p&gt;In a template, a responsive image might be constructed like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;&amp;lt;img
    src=&amp;quot;{{ settings.banner | img_url: &#39;2000x&#39; }}&amp;quot;
    srcset=&amp;quot;{{ settings.banner | img_url: &#39;320x&#39; }} 320w,
      		{{ settings.banner | img_url: &#39;640x&#39; }} 640w,
      		{{ settings.banner | img_url: &#39;1000x&#39; }} 1000w&amp;quot;
    alt=&amp;quot;Let&#39;s go swimming!&amp;quot;
    width=&amp;quot;2000&amp;quot;
    height=&amp;quot;2000&amp;quot;
    loading=&amp;quot;lazy&amp;quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are surprisingly few places where you need to define image outputs in a theme, but I tried to avoid some duplication by creating a generic snippet for images and passing the image object into it.&lt;/p&gt;
&lt;h2&gt;The new &lt;code&gt;image_tag&lt;/code&gt; filter&lt;/h2&gt;
&lt;p&gt;Even more recently, Shopify has introduced the &lt;code&gt;image_tag&lt;/code&gt; filter which cleans up a lot of boilerplate and provides a consistent way to generate responsive images in theme code. Effectively you can generate the same thing with just the filter:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{{ settings.banner | image_url: width: 2000 | image_tag }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img
  src=&amp;quot;//cdn.shopify.com/full/path/to/arthur.jpg?width=2000&amp;quot;
  alt=&amp;quot;&amp;quot;
  srcset=&amp;quot;//cdn.shopify.com/full/path/to/arthur.jpg?width=352 352w,
          //cdn.shopify.com/full/path/to/arthur.jpg?width=832 832w,
          //cdn.shopify.com/full/path/to/arthur.jpg?width=1200 1200w,
          //cdn.shopify.com/full/path/to/arthur.jpg?width=1920 1920w&amp;quot;
  sizes=&amp;quot;(min-width: 1100px) 535px,
         (min-width: 750px) calc((100vw - 130px) / 2),
         calc((100vw - 50px) / 2)&amp;quot;
  width=&amp;quot;2000&amp;quot;
  height=&amp;quot;1600&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The generated image tag includes some good defaults for sizing the image based on the original image size. These can be overridden if you need to fine-tune the output.&lt;/p&gt;
&lt;p&gt;You can also pass other attributes to the &lt;code&gt;image_tag&lt;/code&gt; filter, such as &lt;code&gt;alt&lt;/code&gt; tags, &lt;code&gt;class&lt;/code&gt; contents or maybe even to add hooks like &lt;code&gt;data&lt;/code&gt; attributes for scripts. You would pass an &lt;code&gt;alt&lt;/code&gt; tag like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{{ settings.banner | image_url: width: 2000 | image_tag: alt: &amp;quot;It&#39;s a wild combination&amp;quot; }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Good performance optimisations&lt;/h2&gt;
&lt;p&gt;You&#39;re already getting some good performance benefits from using &lt;code&gt;srcset&lt;/code&gt; on large images, but what if you wanted to add that snazzy lazy loading feature? Easy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{{ settings.banner | image_url: width: 2000 | image_tag: loading: &amp;quot;lazy&amp;quot; }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a caveat to using the &lt;code&gt;loading=&amp;quot;lazy&amp;quot;&lt;/code&gt; attribute, in that it can potentially have a &lt;em&gt;negative&lt;/em&gt; performance impact if it is used on images that are already within the browser viewport on load. Therefore it&#39;s advised that you only use it on images lower down on the page.&lt;/p&gt;
&lt;p&gt;However, for key images, it may also be useful to &lt;em&gt;prioritise&lt;/em&gt; the loading of those images to reduce &amp;quot;layout shift&amp;quot; and display key content to the visitor faster. You can do that by setting a &lt;code&gt;preload&lt;/code&gt; attribute on the &lt;code&gt;image_tag&lt;/code&gt; filter with a value of &lt;code&gt;true&lt;/code&gt;. Behind the scenes, Shopify will send a &lt;code&gt;Link&lt;/code&gt; header in the response to the browser with a &lt;code&gt;rel&lt;/code&gt; attribute of &lt;code&gt;preload&lt;/code&gt; that contains the relevant &lt;code&gt;srcset&lt;/code&gt; and &lt;code&gt;sizes&lt;/code&gt; values. That case would look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{{ settings.banner | image_url: width: 2000 | image_tag: preload: true }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You want to be careful about how many preload hints you give to the browser, as too many may render the benefits moot. It&#39;s ideal for the first image on a product page, or a large banner image on the homepage, for example.&lt;/p&gt;
&lt;p&gt;More details about the &lt;code&gt;image_tag&lt;/code&gt; filter can be found &lt;a href=&quot;https://shopify.dev/api/liquid/filters/html-filters#image_tag&quot;&gt;in the docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It&#39;s one of the better feelings in development when you can revisit some code and easily remove workarounds, and it&#39;s an added bonus if you get some quick performance wins at the same time!&lt;/p&gt;
&lt;p&gt;[^1]: The term &amp;quot;full-stack&amp;quot; seems to have quite a fluid definition depending on who you ask, and the term has certainly expanded over the years. Originally it might mean someone that knows how to use a MySQL database and write some jQuery. These days you might be expected to understand a frontend framework like Nextjs and write your own Docker config.&lt;/p&gt;
&lt;p&gt;[^2]:  In some cases, you may only have the URL of the image on Shopify&#39;s CDN (typically if you&#39;re adding images into the body of the rich text editor). In those cases, images can still be optimised, but the strategy is a bit more complicated and I hope to cover that in a later post.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Using tags to render related content in Shopify themes</title>
		<link href="https://mikefallows.com/posts/using-tags-to-render-related-content-in-shopify-themes/"/>
		<updated>Tue, 21 Dec 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/using-tags-to-render-related-content-in-shopify-themes/</id>
		<content type="html">&lt;p&gt;With the recent launch of Shopify&#39;s OS2 themes, I wanted to reflect on a technique that has been extremely useful as a way to add richer layouts to themes. OS2 introduced a way to add Metafields natively and easily generate new templates via the theme editor, including fields that can also pull in data based on those Metafields.&lt;/p&gt;
&lt;p&gt;This latest combination has opened up a native way to build rich templates without the need to edit code directly or resort to third-party apps. The ability to add features via third-party apps is one of the killer features of Shopify. That said, I&#39;ve always had huge reservations about adding apps that change, or require changes to the theme code. As well as introducing a potential maintenance burden when updating or switching themes, it also seems to carry a high risk in terms of breaking the website in a very consumer-facing and hard-to-debug way. It&#39;s not unusual for store owners to contact me to resolve issues caused by the (un)installation of apps that may not even necessarily be the fault of the app developers.&lt;/p&gt;
&lt;h2&gt;Benefits of tags&lt;/h2&gt;
&lt;p&gt;The big benefit I see of using tags is how well they are already supported in Shopify&#39;s admin. They are already exposed in the admin when editing resources, it&#39;s possible to group resources by tag and either add or remove tags in bulk making it easy for a store owner to manipulate tags, or find and change resources with a given tag.&lt;/p&gt;
&lt;p&gt;The only real downsides are the lack of tags on resources like collections or pages. But most use cases I&#39;ve encountered have applied to products and articles anyway.&lt;/p&gt;
&lt;h2&gt;Using tags as a key-value store&lt;/h2&gt;
&lt;p&gt;By defining a structure for your tags, it&#39;s possible to use them as a type of key-value store. I tend to do this by defining a separator for namespace, key and value. eg. &lt;code&gt;namespace:key:value&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Using that structure means that within liquid we can look for tags with the given namespace, or namespace and key, then extract the value. Let&#39;s say we want to be able to associate a related collection with a product. If our namespace was &lt;code&gt;theme&lt;/code&gt; and our &lt;code&gt;key&lt;/code&gt; was &lt;code&gt;related-collection&lt;/code&gt;, and the value could be the handle of the collection we wanted to identify, then a tag might look like &lt;code&gt;theme:related-collection:bestsellers&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If that tag was assigned to an article then in a template with access to the &lt;code&gt;article&lt;/code&gt; object, we could access the collection like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{% liquid

assign needle = &#39;theme:related-collection:&#39;

for tag in product.tags
	if tag contains needle
		assign handle = tag | remove: needle
        break
    endif
endfor

assign related_collection = collections[handle]

%}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then, for example, display the products from that collection below the article.&lt;/p&gt;
&lt;h2&gt;Why the namespace?&lt;/h2&gt;
&lt;p&gt;Although it&#39;s not necessary to use the namespace in the example above, the namespace has a couple of useful benefits. First, it protects you from potential collisions with tags created by apps or that a shop owner wants to apply. Second, you can use the presence of the namespace to quickly exclude those tags in the case that you want to use and expose tags for their &amp;quot;traditional&amp;quot; use, i.e. to filter for a related topic.&lt;/p&gt;
&lt;p&gt;Here&#39;s a simple example of excluding tags by detecting the presence of the namespace:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;&amp;lt;ul class=&amp;quot;tags&amp;quot;&amp;gt;
{%- for tag in product.tags %}
	{%- unless tag contains &#39;theme:&#39; %}
		&amp;lt;li&amp;gt;{{ tag }}&amp;lt;/li&amp;gt;
    {%- endunless %}
{%- endfor %}
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use cases&lt;/h2&gt;
&lt;p&gt;Along with the ability to associate a group of products with an article, other use cases I&#39;ve found have included storing rich content in a single blog post or page, and then associating that with several products. This could be videos, animation, or a gallery of images that can then be displayed on multiple product pages, without the need to duplicate the content or update it on each product.&lt;/p&gt;
&lt;p&gt;The value doesn&#39;t need to be a handle either, it can be used to store meaningful strings like a colour, or a date that can be used in the layout. You could then use those to define the colour of elements on the page, or a countdown timer.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Hiding Shopify pages from search results </title>
		<link href="https://mikefallows.com/posts/hiding-shopify-pages-from-search-results/"/>
		<updated>Mon, 20 Dec 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/hiding-shopify-pages-from-search-results/</id>
		<content type="html">&lt;h2&gt;Url structure&lt;/h2&gt;
&lt;p&gt;Shopify has a defined URL structure, for example, all product URLs include &lt;code&gt;/products/&lt;/code&gt;. If you&#39;re used to having complete control over your URL structure, this can feel quite restrictive at first. I tend to quite like working within constraints and the consistent and immutable nature of the URL structure can be a benefit.&lt;/p&gt;
&lt;p&gt;Besides anything else, it reduces the surface area for potential bikeshedding[^1] opportunities.&lt;/p&gt;
&lt;h2&gt;Reasons for hiding content&lt;/h2&gt;
&lt;p&gt;Sometimes it&#39;s useful to create collections, or posts in Shopify that aren&#39;t meant to be customer-facing, at least on their own. In the past, I&#39;ve used the collections feature to group &amp;quot;sibling&amp;quot; products or to conform to certain tax overrides. I&#39;ve used blog posts as a source of shared content to augment product descriptions.&lt;/p&gt;
&lt;p&gt;In these cases, the collection or post was never meant to be crawled. In addition, because the content is designed to be pulled through to different areas of the site, it can fall foul of duplicate content penalties from search engines.&lt;/p&gt;
&lt;h2&gt;Using &lt;code&gt;robots.txt&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Recently, Shopify has added the ability to customise a store&#39;s &lt;code&gt;robots.txt&lt;/code&gt; file by adding a &lt;code&gt;robots.txt.liquid&lt;/code&gt; &lt;a href=&quot;https://shopify.dev/themes/architecture/templates/robots-txt-liquid&quot;&gt;template&lt;/a&gt; to your theme. A &lt;code&gt;robots.txt&lt;/code&gt; file is designed to provide instructions to a search engine&#39;s web crawler on how to crawl your site. It&#39;s mostly used to indicate which URLs should not be indexed. Previously, you weren&#39;t able to do that as themes only shipped with a Shopify-generated &lt;code&gt;robots.txt&lt;/code&gt; file that you weren&#39;t able to influence as a theme developer (or shop owner).&lt;/p&gt;
&lt;p&gt;The only reliable way I was able to prevent URLs from being indexed was by using a &lt;code&gt;robots&lt;/code&gt; meta tag with a &lt;code&gt;content=&amp;quot;noindex&amp;quot;&lt;/code&gt;. This is a meta tag you can add to the &lt;code&gt;head&lt;/code&gt; of the HTML document that tells web crawlers not to add the current page to the index. A full tag would be inserted between the &lt;code&gt;head&lt;/code&gt; tags like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;head&amp;gt;
  &amp;lt;meta name=&amp;quot;robots&amp;quot; content=&amp;quot;noindex&amp;quot;&amp;gt;
  //...
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using the &lt;code&gt;seo.hidden&lt;/code&gt; metafield&lt;/h2&gt;
&lt;p&gt;There is an, apparently, undocumented feature that will add this meta tag to pages for you. To do so you need to add a Metafield to a resource with a namespace of &lt;code&gt;seo&lt;/code&gt;, a key of &lt;code&gt;hidden&lt;/code&gt; and a truthy value, eg. &lt;code&gt;1&lt;/code&gt;. At the time of writing Shopify has introduced a native way of adding Metafields to products via the admin, but it&#39;s not available for other resources such as collections, or articles (yet!).&lt;/p&gt;
&lt;p&gt;Metafields can be added to other resources via Shopify&#39;s API or through an app. But there is another way to edit Metafields directly in Shopify&#39;s admin. You can create and modify Metafields by appending values to the query string generated by the bulk editor. For example, you could begin bulk editing some collections and add &lt;code&gt;&amp;amp;edit=metafields.seo.hidden&lt;/code&gt; to the address in the URL bar, and it will expose a field you can populate with a &lt;code&gt;1&lt;/code&gt; for collections you don&#39;t want to be indexed.&lt;/p&gt;
&lt;h2&gt;Using a custom Metafield&lt;/h2&gt;
&lt;p&gt;Before I discovered that Shopify had already defined a Metafield for this purpose, I had used a similar approach of assigning a custom Metafield and adding some logic to the theme code to determine whether the page should output a &lt;code&gt;noindex&lt;/code&gt; meta tag. In fact, I also used a similar tag to output custom &lt;code&gt;canonical&lt;/code&gt; URLs and other data to the head.&lt;/p&gt;
&lt;h2&gt;Alternative solutions&lt;/h2&gt;
&lt;p&gt;Quite often I resort to the presence of a tag to define some custom functionality because they are exposed readily in the admin and easy to change. The issue is that, unlike products and articles, resources like collections and pages can&#39;t be tagged.&lt;/p&gt;
&lt;p&gt;Normally I try to expose as many options as possible via the theme editor using section settings, but sections need an &lt;code&gt;id&lt;/code&gt; in the HTML and are by default wrapped in an HTML tag. There are ways to circumvent that, but it feels less robust overall as a solution because I find theme customisations are better for providing visual feedback rather than having hidden consequences.&lt;/p&gt;
&lt;p&gt;Another solution which can work when you have a simple on/off state is to rely on a template, eg. applying a &lt;code&gt;collection.noindex.liquid&lt;/code&gt; template to the relevant collections. This is relatively easy to implement and control in the admin. However, if you&#39;re already using multiple templates, then creating additional &lt;code&gt;noindex&lt;/code&gt; templates for each one becomes somewhat problematic to maintain.&lt;/p&gt;
&lt;h2&gt;Trade-offs&lt;/h2&gt;
&lt;p&gt;As with most solutions to tricky problems with a platform like Shopify, there are trade-offs. Often it&#39;s a case of trying to predict which solution will be the least likely to fail, have the lowest maintenance burden and is within a client&#39;s technical abilities to manage. This tends to come down to experience, but I often look for options that will be the easiest to change in future, as with the introduction of the ability to customise the &lt;code&gt;robots.txt&lt;/code&gt; file (like so many improvements) previously difficult to achieve features become much easier.&lt;/p&gt;
&lt;p&gt;[^1]: A metaphor to illuminate &lt;a href=&quot;https://en.wikipedia.org/wiki/Parkinson%27s_Law_of_Triviality&quot; title=&quot;w:Parkinson&#39;s Law of Triviality&quot;&gt;Parkinson’s Law of Triviality&lt;/a&gt;. It describes the act of spending a majority of a project&#39;s time on trivial but easier-to-grasp details rather than more important and difficult to criticise tasks. The original example was focussing on the materials used to build the bike shed of a nuclear power plant.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Refactoring a Shopify upsell widget to a Custom Element</title>
		<link href="https://mikefallows.com/posts/refactoring-a-shopify-upsell-widget-to-a-custom-element/"/>
		<updated>Tue, 26 Oct 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/refactoring-a-shopify-upsell-widget-to-a-custom-element/</id>
		<content type="html">&lt;p&gt;Clients often ask me for a good way to upsell a product at the point of purchase. A good example might be a florist wanting to sell a vase with a bouquet. The main product they&#39;re selling is the bouquet, but it&#39;s probable that the customer may also like to add a vase to their order at the same time.&lt;/p&gt;
&lt;p&gt;One solution would be to add a &amp;quot;fake&amp;quot; variant to the product that acts as a combo of the bouquet + vase, but this quickly falls down with stock, reporting, etc as the upsell vase is its own product with inventory that should really exist separately. It&#39;s also then necessary to create all the extra &amp;quot;fake&amp;quot; variants on each product. What if there&#39;s a requirement for more than just the vase, or different styles of vase?&lt;/p&gt;
&lt;p&gt;The simplest solution is to add a prompt in the description that directs customers to the upsell product(s), but this is not as convenient as being able to add the product without leaving the page.&lt;/p&gt;
&lt;p&gt;There are apps that offer this functionality, but they&#39;re not always the most elegant which is an issue for design-led sites.&lt;/p&gt;
&lt;p&gt;I recently tackled this issue for a client and my initial solution accepted a Shopify Collection and used vanilla javascript to display the products with +/- buttons to set the quantity added to the cart. This first solution worked great, but there was some complexity in tracking the state of each product&#39;s quantity and having to continually filter DOM elements to match a reference.&lt;/p&gt;
&lt;p&gt;Here&#39;s a simplified example of the type of code I was using just to add a product to the cart:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&amp;quot;custom-upsell-products&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;custom-upsell-products-item&amp;quot;&amp;gt;
    &amp;lt;button data-decrease data-id=&amp;quot;123&amp;quot;&amp;gt;-&amp;lt;/button&amp;gt;
    &amp;lt;input data-quantity data-id=&amp;quot;123&amp;quot; value=&amp;quot;0&amp;quot;&amp;gt;
    &amp;lt;button data-increase data-id=&amp;quot;123&amp;quot;&amp;gt;+&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;!-- more products --&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const increaseButtons = document.querySelectorAll(&#39;.custom-upsell-products button[data-increase]&#39;);
const quantityInputs = document.querySelectorAll(&#39;.custom-upsell-products input[data-quantity]&#39;);

function enableLoading(el) {
  el.closest(&#39;.custom-upsell-products-item&#39;).classList.add(&#39;loading&#39;);
}

function disableLoading(el) {
  const quantity = Array.from(quantityInputs)
    .find(quantityInput =&amp;gt; quantityInput.dataset.id == el.dataset.id)?.value || 0;
  handleChange(el.dataset.id, quantity);
  el.closest(&#39;.custom-upsell-products-item&#39;).classList.remove(&#39;loading&#39;);
}

function handleChange(id, quantity) {
  //  set button state, etc
}

function increaseCart(id) {
  // get current quantity
  // send post request to cart
}

increaseButtons.forEach(increaseButton =&amp;gt; increaseButton.addEventListener(&#39;click&#39;, () =&amp;gt; {
 enableLoading(increaseButton);
 increaseCart(increaseButton.dataset.id)
   .then(qty =&amp;gt; {
     Array.from(quantityInputs)
       .filter(quantityInput =&amp;gt; quantityInput.dataset.id === increaseButton.dataset.id)
       .forEach(quantityInput =&amp;gt; quantityInput.value = qty);
    })
    .catch(err =&amp;gt; console.error(err))
    .finally(() =&amp;gt; disableLoading(increaseButton));
  })
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You don&#39;t have to particularly understand in detail what&#39;s going on in the example above, but you&#39;ll notice that there&#39;s a lot of occasions where the code needs to iterate over the DOM elements to get a reference to the relevant product. It&#39;s possible that it could be done with a different DOM traversal strategy, or maybe there&#39;s a better way to store references (quite possible!) that I&#39;m not aware of. In this instance, the code needed to account for an unknown number of upsell products, so it has to rely on a lot of assumptions about how the code is set up too. There&#39;s a trade-off between storing the &lt;code&gt;id&lt;/code&gt; on a parent element (more complex traversing) or adding a reference to the &lt;code&gt;id&lt;/code&gt; on all the interactive elements. It&#39;s much harder for me to feel confident about where the boundaries should be.&lt;/p&gt;
&lt;p&gt;I often have to write code like this for Shopify themes, and I know it&#39;s a smell when I have to start querying elements for their state, passing around references (elements, ids) or trying to make unique-enough selectors to avoid leaking styles, etc. Traditionally, I would just suck it up and hope that I can still understand it if I ever have to come back to it to make changes.&lt;/p&gt;
&lt;h2&gt;Enter Custom HTML Elements&lt;/h2&gt;
&lt;p&gt;More often I&#39;m encountering widgets that are implemented using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements&quot;&gt;Custom HTML Elements&lt;/a&gt; (sometimes referred to as Web Components) that encapsulate some code into a defined HTML element, usually with some bespoke interactivity. This sounded like it might be a solution for my upsell widget... (Spoiler: it was!)&lt;/p&gt;
&lt;p&gt;Here&#39;s the same functionality implemented with a Custom HTML Element.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;custom-upsell-product data-id=&amp;quot;123&amp;quot;&amp;gt;
  &amp;lt;button data-decrease&amp;gt;-&amp;lt;/button&amp;gt;
  &amp;lt;input data-quantity value=&amp;quot;0&amp;quot;&amp;gt;
  &amp;lt;button data-increase&amp;gt;+&amp;lt;/button&amp;gt;
&amp;lt;/custom-upsell-product&amp;gt;
&amp;lt;!-- more products --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;class CustomUpsellProduct extends HTMLElement {
  constructor() {
    super();

    this.id = this.getAttribute(&#39;data-id&#39;);
    this.quantityInput = this.querySelector(&#39;input[data-quantity]&#39;);
    this.increaseButton = this.querySelector(&#39;button[data-increase]&#39;);

    this.increaseButton.addEventListener(&#39;click&#39;, () =&amp;gt; this.quantity++);
  }

  get quantity() {
    return this.quantityInput.value;
  }

  set quantity(value) {
    this.enableLoading();
    this.updateCart(value)
    .then(qty =&amp;gt; this.quantityInput.value = qty)
    .catch(err =&amp;gt; console.error(err))
    .finally(() =&amp;gt; this.disableLoading());
  }

  handleChange() {
    // set button state, etc
  }

  updateCart(quantity) {
    // send post request to cart
  }

  enableLoading() {
    this.classList.add(&#39;loading&#39;);
  }

  disableLoading() {
    this.handleChange();
    this.classList.remove(&#39;loading&#39;);
  }
}

customElements.define(&#39;custom-upsell-product&#39;, CustomUpsellProduct);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is so much complexity from the implementation that has now been removed. Each product has its own internal state eliminating the need to filter through elements, pass around ids, or traverse the DOM. In this case I&#39;ve used accessors (&lt;code&gt;get&lt;/code&gt;/&lt;code&gt;set&lt;/code&gt;) to simplify the code further, and easily been able to use a more generic &lt;code&gt;updateCart&lt;/code&gt; method but these are really just implementation details (although they were certainly easier to reach for in this version).&lt;/p&gt;
&lt;p&gt;Just the fact that the line lengths are shorter and a huge amount of visual noise has been removed from both the Javascript and the HTML makes it much easier to reason about. I&#39;m confident that returning to this code 12 months from now to add a feature would be much less onerous than the original implementation too.&lt;/p&gt;
&lt;p&gt;I&#39;m pretty pleased to discover how well Custom HTML Elements solve common problems I&#39;ve experienced when adding features to Shopify themes, and I&#39;m looking forward to being able to use them more in future and even refactor older code towards a simpler – and ultimately more portable – implementation.&lt;/p&gt;
&lt;p&gt;Check out &lt;a href=&quot;https://gist.github.com/mikenewbuild/797eeb136b762ebc4a935d87bcfedf31&quot;&gt;my starter gist of a full implementation&lt;/a&gt; that can be modified and integrated into any Shopify theme.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Adding VAT to 31,522 Shopify prices as quickly as possible</title>
		<link href="https://mikefallows.com/posts/adding-vat-to-31-522-shopify-prices-as-quickly-as-possible/"/>
		<updated>Wed, 20 Oct 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/adding-vat-to-31-522-shopify-prices-as-quickly-as-possible/</id>
		<content type="html">&lt;p&gt;I built my first Shopify theme in 2012, and after years of custom-built PHP carts, and third-party self-hosted products, Shopify was a breath of fresh air. No servers to worry about, a pleasant templating language in Liquid, great support and an API that gave you advanced capabilities when you needed it.&lt;/p&gt;
&lt;h2&gt;Shopify and the VAT problem&lt;/h2&gt;
&lt;p&gt;I only had one major gripe: VAT support. Shopify took a very North American-centric approach to tax, that assumed that you expected to see pre-tax prices with the tax added at the checkout. European tax laws require prices to be displayed inclusive of tax, and that proved to be a big issue. Customers outside the EEC weren&#39;t required to pay tax on products (they would often be subject to import duties instead) so it was common - particularly with high ticket items - to offer the net price to customers outside the EEC. This was difficult with Shopify, as it had no way to allow you to &lt;em&gt;deduct&lt;/em&gt; the domestic tax from a product&#39;s price at checkout.&lt;/p&gt;
&lt;h2&gt;The hack&lt;/h2&gt;
&lt;p&gt;The solution for years was to store all prices as their net value and customise themes to display a VAT calculated price. This is essentially a simple solution, but caused no amount of headaches when integrating Shopify with third parties or trying to build complex functionality, not to mention odd penny rounding issues.&lt;/p&gt;
&lt;p&gt;It meant I became quite the expert on all the ways that this approach could break, and spent a huge amount of my professional time working around these limitations.&lt;/p&gt;
&lt;h2&gt;The eventual fix&lt;/h2&gt;
&lt;p&gt;Almost ten years later, in early 2021 the fix I had been waiting for finally arrived. Ironically this may be the one upside of Brexit for me as it seemed to coincide with Shopify making a host of changes to accommodate the UK leaving the EU. There was now a checkbox in the settings to deduct VAT at checkout. Hallelujah.&lt;/p&gt;
&lt;h2&gt;The easy way&lt;/h2&gt;
&lt;p&gt;Soon it became time to start taking advantage of this on client sites. For smaller stores with relatively fixed catalogue sizes, this was a pretty simple process of removing any price customisations in the theme and updating the prices and the setting. For a few dozen products a quick import of new prices updated in a spreadsheet was sufficient.&lt;/p&gt;
&lt;h2&gt;The hard way&lt;/h2&gt;
&lt;p&gt;However, some of my clients have been on Shopify for years and were consistently publishing new products weekly. In one client&#39;s case, I believe they were one of the first Shopify Plus shops in the UK and had a catalogue of 8,187 products, totalling 31,522 individual prices that needed updating! Such a fundamental change required closing the shop while prices were updated, and as a high-traffic site, minimising downtime was a priority.&lt;/p&gt;
&lt;p&gt;Running that amount of data through a spreadsheet risked being slow and extremely error-prone. Some prices needed to be preserved because they&#39;re not subject to VAT like books and magazines. A slight mistake could overwrite the data on over 8,000 products.&lt;/p&gt;
&lt;p&gt;I realised I could use my &lt;a href=&quot;https://github.com/mikenewbuild/shopify-csv-export&quot;&gt;Shopify CSV Export&lt;/a&gt; app to optimise the process. I set up a development store and imported the existing catalogue to safely test against (it took over 12 hours for the initial import to complete!). This doubled to test any theme updates against too, so I could be confident that prices would be displayed properly after the switch - some of the code in that theme hadn&#39;t been touched for years.&lt;/p&gt;
&lt;p&gt;I then set about writing a script that would create an optimised CSV of just the minimal data needed to modify prices. Then I allowed for it to split the export into a backup of the existing prices (so I could reset if anything went wrong) and the new modified prices that we would want to import.&lt;/p&gt;
&lt;h2&gt;Optimising imports&lt;/h2&gt;
&lt;p&gt;Importing the full 31,522 updated prices took over 80 minutes - but the bulk of those products aren&#39;t even published on the online store. The total number of prices for published products was 4,300 which took less than 10 minutes to import. Nice! We would only need to close the store for a few minutes to update the online prices, publish the theme and make any changes to settings and apps we needed to. We could import the rest of the prices for the physical store and historical products separately.&lt;/p&gt;
&lt;p&gt;I updated the script so that it would create 4 files on export:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;em&gt;original&lt;/em&gt; prices of &lt;em&gt;published&lt;/em&gt; products&lt;/li&gt;
&lt;li&gt;the &lt;em&gt;original&lt;/em&gt; prices of &lt;em&gt;unpublished&lt;/em&gt; products&lt;/li&gt;
&lt;li&gt;the &lt;em&gt;updated&lt;/em&gt; prices of &lt;em&gt;published&lt;/em&gt; products&lt;/li&gt;
&lt;li&gt;the &lt;em&gt;updated&lt;/em&gt; prices of &lt;em&gt;unpublished&lt;/em&gt; products&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The exports were always much faster, taking only seconds to generate the full 31,522 prices, even when split into existing and new. This allowed me to take an up-to-the-minute snapshot of the prices as we closed the site, meaning there was no impact on business operations in preparation either.&lt;/p&gt;
&lt;p&gt;In the end, even with manual acceptance testing and some last-minute decisions, the site was closed for less than 20 minutes. A few moments after re-opening it was a relief to see an order placed with correctly calculated tax and the rest of the prices completed updating about an hour later.&lt;/p&gt;
&lt;h2&gt;It pays to test&lt;/h2&gt;
&lt;p&gt;Taking the time to set up the development store and run the tests may have saved hours of problems, and possibly even some disastrous outcomes. It gave me the chance to test some hypotheses, measure real-word examples and safely experiment with solutions without impacting the day-to-day business. Changing prices, and updating the entire catalogue has such a high risk of going wrong that perhaps the biggest benefit of tests is providing the confidence to be able to safely make the switch when the time came.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Uploading Source Maps to Honeybadger with GitHub Actions</title>
		<link href="https://mikefallows.com/posts/uploading-source-maps-to-honeybadger-with-github-actions/"/>
		<updated>Wed, 08 Sep 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/uploading-source-maps-to-honeybadger-with-github-actions/</id>
		<content type="html">&lt;p&gt;I&#39;ve been a happy user of &lt;a href=&quot;https://www.honeybadger.io/&quot;&gt;Honeybadger&lt;/a&gt; for well over a year now. For the small apps I&#39;ve built it has often been enough to rely on users reporting bugs, as I usually have a direct line of contact with the user (AKA the client).&lt;/p&gt;
&lt;h2&gt;Error Tracking Shopify apps&lt;/h2&gt;
&lt;p&gt;As the apps I&#39;ve been building have become more complex and the number of users increased, it seemed prudent to add an error reporting service to my stack. I&#39;d heard good things about Honeybadger, and I liked that they promoted good company ethics and had seen a lot of support in the development communities that I subscribe to. Their free tier seemed like it would cover my requirements comfortably, and allow me to scale as necessary. I was pleasantly surprised at how quickly I had error reporting set up in my apps, along with automatically generating GitHub issues for errors that were detected.&lt;/p&gt;
&lt;p&gt;This was particularly the case for the backend of the apps that I typically develop in PHP with Laravel. They have a &lt;a href=&quot;https://docs.honeybadger.io/lib/php/integration/laravel/&quot;&gt;Laravel-specific integration&lt;/a&gt; which is a cinch to install.&lt;/p&gt;
&lt;p&gt;Some of my most heavily used apps are integrated with Shopify and utilise Shopify&#39;s React-based framework, &lt;a href=&quot;https://polaris.shopify.com/&quot;&gt;Polaris&lt;/a&gt;. The JavaScript is minified for production so by default errors reported in JS can be obscured by the minification process. Often the nature of the error can reveal its origin, but that&#39;s not always the case.&lt;/p&gt;
&lt;h3&gt;Using Source Maps&lt;/h3&gt;
&lt;p&gt;Fortunately, it&#39;s possible to provide Source Maps to Honeybadger which can then interpret the errors and identify their location in the source files. Much easier to debug!&lt;/p&gt;
&lt;p&gt;Honeybadger can try to detect your Source Maps automatically, but by its nature, can be unreliable. A better solution is to upload the latest source maps to Honeybadger on each deployment.&lt;/p&gt;
&lt;p&gt;This can be done in any number of ways, including a Webpack plugin or cURL.&lt;/p&gt;
&lt;h3&gt;Using GitHub Actions&lt;/h3&gt;
&lt;p&gt;In the end, I decided to explore doing it via GitHub Actions. I have been keen to find an excuse to try them out since they launched but haven&#39;t really needed to, or had the impetus to dig into it.&lt;/p&gt;
&lt;p&gt;There is already a &lt;a href=&quot;https://github.com/honeybadger-io/github-upload-sourcemap-action&quot;&gt;GitHub Package&lt;/a&gt; provided that handles the hard part of uploading the files to Honeybadger via their API. All I had to do was configure it correctly.&lt;/p&gt;
&lt;p&gt;I clumsily added the Action via GitHub&#39;s web interface, and after a couple of false starts (YAML is unforgiving), I had my first successful run and Source Maps uploaded. It took less than 20 mins in the end.&lt;/p&gt;
&lt;p&gt;The final action looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Install npm packages
      run: npm install --prefer-offline --no-audit

    - name: Build production assets
      run: npm run production

    - name: Upload source maps to Honeybadger
      uses: honeybadger-io/github-upload-sourcemap-action@master
      with:
        api_key: ${{ secrets.HONEYBADGER_API_KEY }}
        minified_url: ${{ secrets.PRODUCTION_URL }}/js/app.js
        minified_file: public/js/app.js
        source_map: public/js/app.js.map
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I stored the API key for the project as a GitHub Secret to keep it secure, and I also stored the URL of the production site to make this action more portable. This works for me as I try to enforce building my apps in a consistent way.&lt;/p&gt;
&lt;p&gt;Now each time the main branch is updated, the npm packages are installed, the production assets (including Source Maps) are generated and the latest Source Maps are uploaded to Honeybadger with a reference to the commit. So it&#39;s much easier for me to track down the source of any JS errors that users generate in production (and of course, fix them 😉).&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Archiving a Perch CMS site by statically rendering it with Wget</title>
		<link href="https://mikefallows.com/posts/archiving-a-perch-cms-site-by-statically-rendering-it-with-wget/"/>
		<updated>Fri, 27 Aug 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/archiving-a-perch-cms-site-by-statically-rendering-it-with-wget/</id>
		<content type="html">&lt;p&gt;I started working with the &lt;a href=&quot;https://www.shortfilms.org.uk/&quot;&gt;London Short Film Festival&lt;/a&gt; in 2007, creating the brochure and website for the 2008 festival that takes place in early January. If you&#39;ve ever been involved in the organisation of festivals you&#39;ll know that, due to the sheer number of parties involved, no amount of preparation can entirely prevent information arriving at the last minute.&lt;/p&gt;
&lt;p&gt;After getting that first brochure signed off and delivered to the printers just before Christmas, I quickly put together a website with Wordpress that was little more than a glorified page of the event listings.&lt;/p&gt;
&lt;h3&gt;12 years of Perch&lt;/h3&gt;
&lt;p&gt;The following year I used the relatively new &lt;a href=&quot;https://grabaperch.com/&quot;&gt;Perch CMS&lt;/a&gt; that I found much more developer friendly than Wordpress and could still be hosted cheaply. The site would always get a lot of traffic in the short period of time up to and during the festival, but much less outside of it, so keeping annual hosting costs low in the days before autoscaling was a big bonus.&lt;/p&gt;
&lt;p&gt;Year by year, I would update the site for a sustained period adding new features and evolving the design to match the look of the printed promotion. I took great care to preserve access to old events and film listings (typically 300+ each year) along with articles from festivals past. Consequently, the site went through several data migrations.&lt;/p&gt;
&lt;p&gt;The site went from being a supplement to the listings brochure to becoming the primary source of truth, allowing me to generate listings content for the brochure from data initially added to the CMS. Using the CMS first made getting content approved by third parties much easier, and meant a lot of previously tedious aspects of the job could be automated.&lt;/p&gt;
&lt;p&gt;The (generally) annual cadence of the work on the site meant there would always be some updates to apply to the CMS, versions of PHP or MySQL, current best practices and often dropping feature support for obsolete browsers.&lt;/p&gt;
&lt;p&gt;In 2020 the festival took on a new director and due to the pandemic took place online and employed a new technical team, so with a little sadness and relief, I&#39;m no longer spending my December days scrambling to put the site together. I still had (felt?) the responsibility to archive the existing content. Initially, that was just a case of swapping to an archive subdomain, but I knew that keeping the site on the LAMP stack would not make sense permanently.&lt;/p&gt;
&lt;h3&gt;Legacy LAMP sites&lt;/h3&gt;
&lt;p&gt;Anyone who&#39;s spent time as a general web developer in the 2000s has probably stood up a few PHP sites that are still ticking along on an old shared server running on some now obsolete version of PHP. They&#39;re probably running fine (and I&#39;m sure a lot are still generating a good profit!) but as always there are potential security risks running on versions of software that no longer receive security fixes, and the chances of being able to fix something on a site rotting on some crusty version of PHP5 doesn&#39;t sound very appetising.&lt;/p&gt;
&lt;p&gt;Even though the site was running on PHP 7.4 (the latest at the time) I didn&#39;t want the technical burden of having to maintain and pay for a server or applying security updates to the CMS software for a site that doesn&#39;t need to be dynamically rendered or even updated. By taking a static snapshot of the site and hosting the files (effectively for free) on a CDN, I could reduce the technical burden to essentially zero. It also meant it would require a lot less energy and overhead to run the site - it seemed like the responsible thing to do.&lt;/p&gt;
&lt;p&gt;Of course, there&#39;s always the potential that the site will need to be updated in some way, so I decide a good solution would be for me to be able to run the site locally and then automate the process of snapshotting and deploying the site to something like &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Only running the site locally&lt;/h3&gt;
&lt;p&gt;I&#39;ve always been able to run the site locally for development purposes, initially using things like &lt;a href=&quot;https://www.mamp.info/&quot;&gt;MAMP&lt;/a&gt; but more recently using &lt;a href=&quot;https://laravel.com/docs/valet&quot;&gt;Laravel Valet&lt;/a&gt; (with a &lt;a href=&quot;https://github.com/mikenewbuild/valet-drivers/blob/main/PerchRunwayValetDriver.php&quot;&gt;custom driver&lt;/a&gt;). I relied on taking copies of the production database to the staging server to get an up-to-date version of the production state that I can work on safely. The only change I made was to store a SQL dump in the repo so that I could always restore a working copy of the site from just the repo. I considered adding scripts to restore the database, run composer, set up a Docker file etc as well as one to dump changes to the database, but in the end, I just left some notes for myself in the README. There&#39;s a good chance I&#39;ll never need to use this repo again, but if I do that will be the time to solve that problem.&lt;/p&gt;
&lt;h3&gt;Generating the static site&lt;/h3&gt;
&lt;p&gt;Although it would probably make sense to have the static sites generated and hosted within the same repo, for simplicity I decided that all the static files could be generated in a separate repo. It&#39;s possibly something I would change, but for now, it feels much &#39;lighter&#39; to separate out the repos.&lt;/p&gt;
&lt;p&gt;I decided to use &lt;a href=&quot;https://www.gnu.org/software/wget/&quot;&gt;Wget&lt;/a&gt; to clone the locally running version of the site and store it in a folder that can then be set to serve the files, in this case, &lt;code&gt;/public&lt;/code&gt; which conveniently allowed me to serve the files locally via Valet for testing purposes. It took me scouring a few tutorials and some trial and error to get the configuration of arguments that I wanted.&lt;/p&gt;
&lt;p&gt;I ended up with something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wget -mpckEnH -P ./public https://lsff.test
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-m&lt;/code&gt; creates a mirror of the site&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt; get all images, etc. needed to display an HTML page&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-c&lt;/code&gt; resume getting a partially-downloaded file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-k&lt;/code&gt; make links in downloaded HTML or CSS point to local files&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-E&lt;/code&gt; save HTML/CSS documents with proper extensions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-nH&lt;/code&gt; won&#39;t create host directories&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-P&lt;/code&gt; define a directory to save files to&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This worked a treat, and the only issue was that absolute urls would refer to the local domain &lt;code&gt;https://lsff.test&lt;/code&gt; but the files would eventually be hosted at a different domain &lt;code&gt;https://lsff.newbuild.studio&lt;/code&gt; so I realised I would need to do a find and replace on the url. There&#39;s probably some solution for this built into Wget, but I couldn&#39;t figure it out. A couple of trips to Stack Overflow (plus some more trial and error) later and I decided the best solution for me would be to use a combination of the &lt;code&gt;find&lt;/code&gt; &lt;a href=&quot;https://en.wikipedia.org/wiki/Find_(Unix)&quot;&gt;file finding command&lt;/a&gt; and the &lt;a href=&quot;https://en.wikipedia.org/wiki/Sed&quot;&gt;stream editor command&lt;/a&gt;, &lt;code&gt;sed&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;On Mac OS you need to create backup files when you run &lt;code&gt;sed&lt;/code&gt; to preserve the integrity of the filesystem or something. Eventually, I got the following to work.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find ./public -name &amp;quot;*.html&amp;quot; -type f -exec sed -i &#39;.bak&#39; &#39;s/lsff&#92;.test/lsff&#92;.newbuild&#92;.studio/gI&#39; {} &#92;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Great! I could now generate a fully working static reproduction of the site 🙌&lt;/p&gt;
&lt;p&gt;The interesting outcome was that now the generated files were being stored in a repo, I could easily see how small changes to the original site were reflected in the static output and therefore which files were directly effected. Reviewing the files helped me spot a couple of small errors, pages that needed to be removed and other improvements that could be made quickly. It was satisfying being able to see the resulting changes in a git diff in a way that you don&#39;t when dynamically generating the content and only working with the templates.&lt;/p&gt;
&lt;p&gt;I noticed a couple of oversights, like needing to empty the &lt;code&gt;/public&lt;/code&gt; directory before running &lt;code&gt;wget&lt;/code&gt;, so that pages I wanted to be removed didn&#39;t remain. Although I could add the &lt;code&gt;.bak&lt;/code&gt; files I was generating to &lt;code&gt;.gitignore&lt;/code&gt; it added a lot of noise to the file explorer so I decided to clean those up on each run too. In the end, I had a &lt;code&gt;build.sh&lt;/code&gt; file I could run that looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rm -rf ./public
wget -mpckEnH -P ./public https://lsff.test
find ./public -name &amp;quot;*.html&amp;quot; -type f -exec sed -i &#39;.bak&#39; &#39;s/lsff&#92;.test/lsff&#92;.newbuild&#92;.studio/gI&#39; {} &#92;;
find ./public -name &amp;quot;*.bak&amp;quot; -type f -delete
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Deploying to Netlify&lt;/h3&gt;
&lt;p&gt;All that remained was setting up a new site in Netlify linked to the repo on GitHub that served the &lt;code&gt;./public&lt;/code&gt; folder and pointing the domain. I can&#39;t see &lt;a href=&quot;https://lsff.newbuild.studio/&quot;&gt;this archive of the London Short Film Festival&lt;/a&gt; ever generating enough traffic to escape the free tier and, if necessary, there&#39;s a few useful goodies like &lt;a href=&quot;https://docs.netlify.com/routing/redirects/&quot;&gt;redirects&lt;/a&gt; and &lt;a href=&quot;https://docs.netlify.com/site-deploys/post-processing/snippet-injection/&quot;&gt;snippet injection&lt;/a&gt; I can take advantage of if I ever need to.&lt;/p&gt;
&lt;p&gt;So that&#39;s it – with just a handful of commands – I can pull down both repos make a change in the dynamic one and generate and publish a new set of static files to the web. I now have a solid solution for archiving and hosting static versions of LAMP sites without losing the ability to still make changes via the CMS.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Installing Perch Runway via Composer</title>
		<link href="https://mikefallows.com/posts/installing-perch-runway-via-composer/"/>
		<updated>Mon, 16 Aug 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/installing-perch-runway-via-composer/</id>
		<content type="html">&lt;p&gt;When I&#39;ve needed to provide a CMS for brochure sites, my first port of call has been &lt;a href=&quot;https://grabaperch.com/&quot;&gt;Perch CMS&lt;/a&gt;. I&#39;ve been using it since 2010 (when it was still v1.2!) as a great solution to adapt static sites to be updated by a client (eg. adding a news section), or building out highly customised views. Perch uses PHP and MySQL so it was familiar and easy to set up locally and host on a budget.&lt;/p&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;As some of the sites grew in their volume of content and feature complexity, I upgraded them to Perch&#39;s more advanced developer version, &lt;a href=&quot;https://perchrunway.com/&quot;&gt;Perch Runway&lt;/a&gt; (released 2014). Runway has features like collections and dynamic routes, making it more akin to popular MVC frameworks like &lt;a href=&quot;https://laravel.com/&quot;&gt;Laravel&lt;/a&gt;, and the types of dynamic regions you get in &lt;a href=&quot;https://craftcms.com/&quot;&gt;Craft&lt;/a&gt;. Unlike Laravel and Craft it didn&#39;t have the convenience of &lt;a href=&quot;https://getcomposer.org/&quot;&gt;Composer&lt;/a&gt; to pull in useful packages and facilitate the types of deploy pipelines I had become accustomed to.&lt;/p&gt;
&lt;p&gt;It&#39;s expected with Perch that to upgrade to new versions you would replace the &lt;code&gt;core&lt;/code&gt; folder with new files downloaded from your account and then manually uploaded to the client&#39;s server. To use a deployment pipeline meant either committing all of Perch&#39;s core files to version control (yuk), or manually updating multiple sites and local development machines with each release, which even with a small number of projects quickly becomes onerous and error-prone.&lt;/p&gt;
&lt;h3&gt;The ideal&lt;/h3&gt;
&lt;p&gt;What I wanted to be able to do is pull in specific versions of Perch, or easily update to the latest version consistently across projects and machines. Because Perch doesn&#39;t provide this feature I decided to roll my own. To do this I realised I could host my own, private repository of Perch with versioned releases in one place. The issue was that because Perch&#39;s files need to be accessible in a publicly accessible directory, I would need to figure out how to set up Composer to install the files outside the &lt;code&gt;/vendor&lt;/code&gt; folder.&lt;/p&gt;
&lt;h3&gt;Installers&lt;/h3&gt;
&lt;p&gt;Fortunately, there&#39;s a &lt;a href=&quot;https://github.com/composer/installers&quot;&gt;Composer tool&lt;/a&gt; designed just for this purpose. It&#39;s specifically designed to allow packages to be installed outside the &lt;code&gt;/vendor&lt;/code&gt; directory (useful for things like Wordpress). Normally you would define within the package where the files would be installed, and although Perch is typically installed in a  &lt;code&gt;/perch&lt;/code&gt; directory, it doesn&#39;t have to be and for some projects I have it installed in &lt;code&gt;/cms&lt;/code&gt; for example. That&#39;s okay because the &lt;a href=&quot;https://github.com/oomphinc/composer-installers-extender&quot;&gt;Installers Extender&lt;/a&gt; allows the client &lt;code&gt;composer.json&lt;/code&gt; file to define where the Perch core files should be installed to.&lt;/p&gt;
&lt;h3&gt;Setting up Perch as a package&lt;/h3&gt;
&lt;p&gt;In my case, I created a private repository on GitHub called &lt;code&gt;perch-core&lt;/code&gt; - but it could be called anything - and committed the latest version of Perch&#39;s core files.&lt;/p&gt;
&lt;p&gt;To set up the repository as an installer, I first needed to add a &lt;code&gt;composer.json&lt;/code&gt; to the project which I did by running: &lt;code&gt;composer init&lt;/code&gt; and following the instructions.&lt;/p&gt;
&lt;p&gt;Next, I installed the two packages I needed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require composer/installers oomphinc/composer-installers-extender
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After committing the changes to &lt;code&gt;composer.json&lt;/code&gt;, I pushed them to GitHub and was now ready to tag the repo with the current release of Perch (at the time v3.1.5). This would allow me to specify the version of Perch I required in my client projects.&lt;/p&gt;
&lt;h3&gt;Installation on a Perch project&lt;/h3&gt;
&lt;p&gt;Now I&#39;m able to install Perch as well as any other composer dependencies in my project such as a helper file, or a package to support the use of &lt;code&gt;.env&lt;/code&gt; files. It&#39;s important to make sure that the &lt;code&gt;/vendor&lt;/code&gt; directory is not publicly accessible on the web server, so my Perch projects are typically set up with the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;public/
├─ perch/
│  ├─ core/
├─ favicon.png
├─ .htaccess
vendor/
composer.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use my private &lt;code&gt;perch-core&lt;/code&gt; repository and have the contents be installed to &lt;code&gt;/public/perch/core&lt;/code&gt; I need to add the following to my &lt;code&gt;composer.json&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  ...
  &amp;quot;require&amp;quot;: {
    &amp;quot;mikenewbuild/perch-core&amp;quot;: &amp;quot;^3.1&amp;quot;
  },
  &amp;quot;repositories&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;vcs&amp;quot;,
      &amp;quot;url&amp;quot;: &amp;quot;git@github.com:mikenewbuild/perch-core.git&amp;quot;
    }
  ],
  &amp;quot;extra&amp;quot;: {
    &amp;quot;installer-types&amp;quot;: [
      &amp;quot;perch-core&amp;quot;
    ],
    &amp;quot;installer-paths&amp;quot;: {
      &amp;quot;public/perch/core/&amp;quot;: [
        &amp;quot;mikenewbuild/perch-core&amp;quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &lt;code&gt;repositories&lt;/code&gt; section, I&#39;ve defined the location of my private repo on GitHub, and in &lt;code&gt;extra&lt;/code&gt; I define the package that should be &amp;quot;installed&amp;quot; and the directory where the package should be installed.&lt;/p&gt;
&lt;p&gt;It&#39;s possible to define multiple packages and locations, which is useful for applying the same technique to eg. &lt;a href=&quot;https://addons.perchcms.com/&quot;&gt;Perch Add Ons&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;One last thing&lt;/h3&gt;
&lt;p&gt;Because I don&#39;t want to make the Perch core files public I also needed to set up a &lt;a href=&quot;https://github.com/settings/tokens&quot;&gt;Personal Access Token&lt;/a&gt; in GitHub to grant access to the private repository. This can be done with either an SSH key or by adding an &lt;code&gt;auth.json&lt;/code&gt; file to the root of the project with the generated token:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;github-oauth&amp;quot;: {
    &amp;quot;github.com&amp;quot;: &amp;quot;github-token-here&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Needless to say, this file should not be committed to the repo as it includes sensitive information.&lt;/p&gt;
&lt;p&gt;Running &lt;code&gt;composer install&lt;/code&gt; in my client project will now replace the &lt;code&gt;/public/perch/core&lt;/code&gt; folder with the contents of my &lt;code&gt;perch-core&lt;/code&gt; repository, and other packages in the vendor directory. The version will be stored in the &lt;code&gt;composer.lock&lt;/code&gt; file so I can quickly replicate my project in any environment.&lt;/p&gt;
&lt;p&gt;This small addition has made my development workflow much more robust and enables me to leverage Composer in my Perch projects. I can also track the changes to Perch core, and if necessary apply my own patches to enable support for newer versions of PHP before an official version of Perch is released.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Using Jest tests to replace jQuery in a Shopify theme</title>
		<link href="https://mikefallows.com/posts/using-jest-tests-to-replace-jquery-in-a-shopify-theme/"/>
		<updated>Mon, 26 Jul 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/using-jest-tests-to-replace-jquery-in-a-shopify-theme/</id>
		<content type="html">&lt;p&gt;I started building custom Shopify themes in 2015 when it was still quite painful dealing with different browsers and lots of elements of Shopify relied on jQuery by default. In 2020, the browser landscape is much less problematic and so I decided to stop supporting IE and only target modern browsers instead. Although most sites still get a tiny percentage of traffic from older browsers, the number is dwindling and actual sales are non-existent so the cost of extra development is no longer justified.&lt;/p&gt;
&lt;p&gt;This has given me the opportunity to do all my new feature development with modern JavaScript techniques using newer frameworks like &lt;a href=&quot;https://alpinejs.dev/&quot;&gt;Alpine.js&lt;/a&gt;. Still, there is a lot of previous code knocking around that relies on jQuery and a lot of it is fundamental to the way the theme works. I&#39;m generally fine with leaving working code alone, but jQuery is quite a heavy dependency, so there are definite performance gains from removing it.&lt;/p&gt;
&lt;p&gt;Also, it should go without saying, that the code I wrote in 2015 is much more &lt;em&gt;naïve&lt;/em&gt; than the code I would write today. Although I&#39;m sure I will have a similar attitude in the future to the code I write now :)&lt;/p&gt;
&lt;p&gt;The difficulty is how to replace such a fundamental part of the codebase (relatively) painlessly without risking breaking something?&lt;/p&gt;
&lt;h3&gt;Benefits of tests&lt;/h3&gt;
&lt;p&gt;Something that I&#39;ve come to rely on a lot in backend development is automated testing. Sadly I&#39;ve struggled so far to develop a good testing workflow with JavaScript. So I wanted to use upgrading a legacy codebase as a good opportunity to work on that.&lt;/p&gt;
&lt;p&gt;Ultimately I believe Shopify themes would benefit the most from end-to-end testing tools like &lt;a href=&quot;https://www.cypress.io/&quot;&gt;Cypress&lt;/a&gt;, but due to the nature of theme development on Shopify, this seems quite daunting. What I want primarily is to be able to quickly set up and run tests to give me a quick feedback loop so I can confidently refactor code.&lt;/p&gt;
&lt;p&gt;Fortunately, most of the code I would be updating is limited to DOM traversal, toggling a few classes and some very simple DOM insertion. More complex code (mainly around cart interaction) I would want to manually test anyway. I figured this simpler code might be a good candidate for a testing framework like &lt;a href=&quot;https://jestjs.io/&quot;&gt;Jest&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Install &amp;amp; set up&lt;/h3&gt;
&lt;p&gt;This was my first experience setting up Jest in a project and credit to the maintainers, it was much easier than I expected and thanks to the helpful error messages I had it set up to my specific requirements in minutes.&lt;/p&gt;
&lt;p&gt;I installed Jest with the command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ npm i jest --save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now any files ending &lt;code&gt;.test.js&lt;/code&gt; will be treated as a test when running &lt;code&gt;jest&lt;/code&gt; from the root of the project.&lt;/p&gt;
&lt;p&gt;You can also keep all your files in a dedicated test folder, which I would typically do when using things like &lt;a href=&quot;https://pestphp.com/&quot;&gt;Pest&lt;/a&gt;, but in this case, I want the files to be easily transferrable between projects so I opted to colocate my tests with my implementation code.&lt;/p&gt;
&lt;h3&gt;Running tests&lt;/h3&gt;
&lt;p&gt;The first snag I hit was using import statements in my tests. I wanted to use imports as I&#39;m &lt;a href=&quot;https://mikefallows.com/posts/shopify-theme-development-with-esbuild/&quot;&gt;in the future&lt;/a&gt; now. At the time of writing, this is only supported as an &lt;a href=&quot;https://jestjs.io/docs/ecmascript-modules&quot;&gt;experimental feature in Jest,&lt;/a&gt; but as I&#39;m not planning to do anything crazy in my tests it has worked fine for me. It does mean adding a flag to the command, so it&#39;s easier to add to an npm script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  scripts: {
    &amp;quot;test&amp;quot;: &amp;quot;node --experimental-vm-modules node_modules/.bin/jest&amp;quot;
  },
  &amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Module imports are now automatically supported when I run &lt;code&gt;npm test&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Testing the DOM&lt;/h3&gt;
&lt;p&gt;Pretty much all of the work I&#39;m doing with JS is to do with DOM manipulation, certainly the code I had written with jQuery. Jest has some features that make it easy to test the basic DOM manipulation which I was performing. I struggled a bit when it came to things like using &lt;code&gt;setTimeout&lt;/code&gt; so I decided to dig into that another day and focus on the simple stuff and test anything more complicated manually.&lt;/p&gt;
&lt;p&gt;There is one thing you need to do to allow your tests to use the DOM, and that is to include a doc block at the beginning of your tests.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/**
 * @jest-environment jsdom
 */
 test(&#39;...&#39;, () =&amp;gt; {
   // test DOM stuff...
 })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&#39;s a full example of a test for a dynamic gallery component. It&#39;s a quick test that given the right HTML structure, clicking a thumbnail will update the main image with the relevant &lt;code&gt;src&lt;/code&gt; attribute and apply an &lt;code&gt;active&lt;/code&gt; class to the corresponding thumbnail.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/**
 * @jest-environment jsdom
 */

import dynamicGalleries from &amp;quot;./dynamicGallery&amp;quot;

test(&#39;it can dynamically update a main image by clicking thumbnails&#39;, () =&amp;gt; {

  document.body.innerHTML = `
    &amp;lt;div class=&amp;quot;gallery&amp;quot;&amp;gt;
      &amp;lt;img src=&amp;quot;1.jpg&amp;quot; class=&amp;quot;main&amp;quot;&amp;gt;

      &amp;lt;img src=&amp;quot;thumb-1.jpg&amp;quot; class=&amp;quot;thumb&amp;quot; data-gallery-img-src=&amp;quot;1.jpg&amp;quot;&amp;gt;
      &amp;lt;img src=&amp;quot;thumb-2.jpg&amp;quot; class=&amp;quot;thumb&amp;quot; data-gallery-img-src=&amp;quot;2.jpg&amp;quot;&amp;gt;
      &amp;lt;img src=&amp;quot;thumb-3.jpg&amp;quot; class=&amp;quot;thumb&amp;quot; data-gallery-img-src=&amp;quot;3.jpg&amp;quot;&amp;gt;
    &amp;lt;/div&amp;gt;
  `;

  const main = document.querySelector(&#39;.main&#39;);
  const thumbs = document.querySelectorAll(&#39;.thumb&#39;);

  dynamicGalleries(&#39;.gallery&#39;, { main: &#39;.main&#39;, thumb: &#39;.thumb&#39; });

  expect(thumbs[0].classList.contains(&#39;active&#39;)).toBe(true);

  thumbs[1].click();

  expect(thumbs[0].classList.contains(&#39;active&#39;)).toBe(false);
  expect(thumbs[1].classList.contains(&#39;active&#39;)).toBe(true);
  expect(main.src).toBe(&#39;http://localhost/2.jpg&#39;);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allowed me to refactor away jQuery from the implementation of the &lt;code&gt;dynamicGalleries()&lt;/code&gt; function using the instant feedback from a test and giving me the confidence that I haven&#39;t broken anything. All this without having to wait for files to bundle and sync with Shopify and a browser refresh. 🔥&lt;/p&gt;
&lt;h3&gt;Helpful utilities&lt;/h3&gt;
&lt;p&gt;As I mentioned earlier, I rarely do anything that complicated with jQuery anyway, but its killer feature has always been the elegance of accessing the power of the &lt;a href=&quot;https://github.com/jquery/sizzle&quot;&gt;Sizzle&lt;/a&gt; selector engine with the &lt;code&gt;$()&lt;/code&gt; function. Thanks to jQuery a lot of the power of that engine is now available in the browser&#39;s native APIs with &lt;code&gt;querySelector&lt;/code&gt;, &lt;code&gt;querySelectorAll&lt;/code&gt;, &lt;code&gt;nextSibling&lt;/code&gt;, &lt;code&gt;closest&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;However, these aren&#39;t quite as terse, and having my code littered with &lt;code&gt;document.querySelectorAll(&#39;div&#39;).forEach()&lt;/code&gt; isn&#39;t as nice as &lt;code&gt;$(&#39;div&#39;).each()&lt;/code&gt; . Inspired by a great series of posts on taking a &lt;a href=&quot;https://sebastiandedeyne.com/javascript-framework-diet/&quot;&gt;JavaScript Framework Diet&lt;/a&gt; by &lt;a href=&quot;https://sebastiandedeyne.com/&quot;&gt;Sebastien De Dyne&lt;/a&gt;, I&#39;ve been able to replace a lot of my jQuery use with some simple utility functions.&lt;/p&gt;
&lt;p&gt;These two functions alone do most of the heavy lifting, which meant in most cases I could replace &lt;code&gt;$(&#39;div&#39;).each()&lt;/code&gt;  with &lt;code&gt;_$$(&#39;div&#39;).forEach()&lt;/code&gt; .&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function _$(selector, scope = document) {
  return scope.querySelector(selector);
}

function _$$(selector, scope = document) {
  return Array.from(scope.querySelectorAll(selector));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course, with Jest, it&#39;s easy to test this functionality too!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;test(&#39;_$ can select an element&#39;, () =&amp;gt; {
  document.body.innerHTML = `
  &amp;lt;div&amp;gt;
    &amp;lt;div id=&amp;quot;test&amp;quot; class=&amp;quot;yo&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
  `;

  const el = _$(&#39;div#test&#39;);

  expect(el).toBeInstanceOf(HTMLElement);
  expect(el.classList.contains(&#39;yo&#39;)).toBe(true);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As I worked my way through the code I was able to find other opportunities to add utilities for frequently used jQuery features like toggling classes, or wrapping elements in another element. With the tests as backup, it greatly reduced the amount of time it took to replace jQuery, and in many cases allowed me to significantly improve the simplicity and readability of the code I was refactoring.&lt;/p&gt;
&lt;p&gt;I often find that making code easier to test improves the code itself.&lt;/p&gt;
&lt;h3&gt;Next steps&lt;/h3&gt;
&lt;p&gt;With these tests in place, I can now take steps towards a CI/CD pipeline which will allow me to run tests automatically when merging new features or fixes. This will reduce both the chances of regressions being introduced, as well as the time spent testing manually.&lt;/p&gt;
&lt;p&gt;For now, I&#39;m just happy to be able to write tests easily and get super quick feedback.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Shopify theme development with esbuild</title>
		<link href="https://mikefallows.com/posts/shopify-theme-development-with-esbuild/"/>
		<updated>Wed, 21 Jul 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/shopify-theme-development-with-esbuild/</id>
		<content type="html">&lt;p&gt;Shopify recently released a new CLI that makes it easier to develop themes locally.&lt;/p&gt;
&lt;h2&gt;The old way: Theme Kit + CodeKit&lt;/h2&gt;
&lt;p&gt;Previously I had developed a workflow using Bryan Jones&#39; &lt;a href=&quot;https://codekitapp.com/&quot;&gt;CodeKit&lt;/a&gt; app[^1] and Shopify&#39;s old &lt;a href=&quot;https://github.com/Shopify/themekit&quot;&gt;Theme Kit&lt;/a&gt; tool.&lt;/p&gt;
&lt;p&gt;Using Shopify&#39;s old Theme Kit tool, I could bundle and minify assets with CodeKit, have them synced to a development theme on Shopify and then CodeKit could refresh the page for me. This gave me the closest thing to an ideal local development setup. There were some[^2] niggles[^3] though.&lt;/p&gt;
&lt;h2&gt;Shopify CLI for themes&lt;/h2&gt;
&lt;p&gt;With the &lt;a href=&quot;https://www.shopify.com/partners/blog/shopify-online-store&quot;&gt;announcement of Shopify&#39;s Online Store 2.0&lt;/a&gt; came a new CLI (Command Line Interface) tool to manage themes with faster syncing and a local dev server out of the box. Along with being a much faster native solution for a local dev server, it has removed the need for managing API keys and config files for multiple themes (a particular nuisance when working across multiple machines).&lt;/p&gt;
&lt;p&gt;Another difference with the &lt;a href=&quot;https://shopify.dev/themes/tools/cli&quot;&gt;Shopify CLI for themes&lt;/a&gt; over Theme Kit is that there is no feature for preventing syncing certain files. For example, I would often avoid syncing the &lt;code&gt;settings_data.json&lt;/code&gt; files and keep these out of version control to avoid overwriting client customisations. However, with the changes to the structure of the new themes, the sync speed and the deprecation of automatic &lt;a href=&quot;https://sass-lang.com/&quot;&gt;SASS&lt;/a&gt; compilation by Shopify, it makes less sense to not track all theme files.&lt;/p&gt;
&lt;h2&gt;Bundling assets with esbuild&lt;/h2&gt;
&lt;p&gt;With less need for third-party tooling and increased browser support for modern CSS and JavaScript, it&#39;s easy to imagine building new themes without the need for bundlers at all (unless I adopt things like &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;However, there are multiple themes I maintain that will take a lot of time to safely convert to the new store structure. In the meantime, I need a fast and simple way to bundle &lt;code&gt;.scss&lt;/code&gt; and &lt;code&gt;.js&lt;/code&gt; files. I could of course continue to use CodeKit for this purpose, but it feels like overkill for such a simple task. I also want to make it easier to run build steps as part of a deployment pipeline or work with other developers on a theme, so free open-source command line tools have some additional benefits.&lt;/p&gt;
&lt;p&gt;Fortunately, &lt;a href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt; is perfect for the task. It&#39;s fast, and although it requires some config I only needed less than 20 lines of code to replace my CodeKit setup.&lt;/p&gt;
&lt;p&gt;My esbuild setup needed to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bundle &lt;code&gt;.js&lt;/code&gt; imports for browsers (and support some older browsers)&lt;/li&gt;
&lt;li&gt;bundle &lt;code&gt;.scss&lt;/code&gt; files to a &lt;code&gt;.css&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;minify the output&lt;/li&gt;
&lt;li&gt;watch and bundle files on change&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the simplest set up you can just run esbuild from the command line without the need for a &lt;code&gt;node_modules&lt;/code&gt; folder, but to handle &lt;code&gt;.scss&lt;/code&gt; files and support older browsers requires a build config.&lt;/p&gt;
&lt;h3&gt;Set up&lt;/h3&gt;
&lt;p&gt;Here&#39;s a typical setup when converting a theme from CodeKit. My file structure typically looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;assets/
...
src/
├─ scripts/
│  ├─ modules/
│  ├─ theme.js
├─ styles/
│  ├─ modules/
│  ├─ theme.scss
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only thing I would need to change is to import styles at the top of  &lt;code&gt;src/scripts/theme.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import &#39;../scss/theme.scss&#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Install&lt;/h3&gt;
&lt;p&gt;I can install esbuild and a plugin to compile &lt;code&gt;.scss&lt;/code&gt; files as npm packages via the command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ npm i esbuild esbuild-sass-plugin --save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As esbuild relies on module imports &lt;code&gt;&amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;&lt;/code&gt; needs to be present in the &lt;code&gt;package.json&lt;/code&gt; file.&lt;/p&gt;
&lt;h3&gt;Build config&lt;/h3&gt;
&lt;p&gt;The convention is to set up a build file as &lt;code&gt;esbuild.config.js&lt;/code&gt;. The example below takes the &lt;code&gt;theme.js&lt;/code&gt; file as the entry point and compiles the minified output to the &lt;code&gt;assets&lt;/code&gt; directory in a format for browsers that support the es2018 syntax. This allows for some slightly older versions of browsers to be supported, but not legacy versions like IE11 which I no longer actively support.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import esbuild from &#39;esbuild&#39;
import { sassPlugin } from &#39;esbuild-sass-plugin&#39;

esbuild.build({
  entryPoints: {
    theme: &#39;src/scripts/theme.js&#39;,
  },
  bundle: true,
  outdir: &#39;assets&#39;,
  target: [&#39;es2018&#39;],
  plugins: [sassPlugin()],
  minify: true,
  watch: true,
}).then(() =&amp;gt; {
  console.log(&#39;Watching files...&#39;)
}).catch((e) =&amp;gt; console.error(e.message))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;node esbuild.config.js&lt;/code&gt; will bundle the files to the assets folder and watch for any changes to imported files. I usually add that to a &amp;quot;start&amp;quot; script so a boilerplate &lt;code&gt;package.json&lt;/code&gt; might look like this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;esbuild&amp;quot;: &amp;quot;^0.12.15&amp;quot;,
    &amp;quot;esbuild-sass-plugin&amp;quot;: &amp;quot;^1.4.8&amp;quot;
  },
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;start&amp;quot;: &amp;quot;node esbuild.config.js&amp;quot;
  },
  &amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Next steps&lt;/h3&gt;
&lt;p&gt;I now have a setup that allows me to organise my code in a way that suits me best. I can leverage the features of Sass and npm with instant browser updates, generate minified assets and see the results of my changes almost instantly in the browser.&lt;/p&gt;
&lt;p&gt;I&#39;m still looking at ways to improve my process and I plan to introduce things like type safety, automated testing and code linting. I feel positive that this is all a step in the right direction.&lt;/p&gt;
&lt;h4&gt;Update&lt;/h4&gt;
&lt;p&gt;I have added a post on how to update your config to &lt;a href=&quot;https://mikefallows.com/posts/using-postcss-and-autoprefixer-with-esbuild/&quot;&gt;automatically add vendor prefixes&lt;/a&gt; to the compiled CSS using PostCSS and the Autoprefixer plugin.&lt;/p&gt;
&lt;p&gt;[^1]: CodeKit provides a GUI for bundling assets with great defaults without having to resort to using a complex &lt;a href=&quot;https://webpack.js.org/&quot;&gt;Webpack&lt;/a&gt; config like Shopify&#39;s &lt;a href=&quot;https://github.com/Shopify/slate&quot;&gt;Slate Theme&lt;/a&gt;. It also allowed me to set up a local server that could proxy to a specific development theme on the store.&lt;/p&gt;
&lt;p&gt;[^2]: One drawback was that the bundling and syncing weren&#39;t instant, so I would need to set a 2-3 second delay on the refresh to allow for the bundled files to be uploaded to Shopify. Kudos to CodeKit for including that functionality though.&lt;/p&gt;
&lt;p&gt;[^3]: Another was that at some point CodeKit released a fix for a bug where some URLs weren&#39;t being encoded. Unfortunately, I realised I had been relying on that bug to make it easier for me to quickly launch a development server. This is not a knock on CodeKit. It must have saved me hundreds of hours over the years I&#39;ve been using it and it&#39;s still my go-to for lots of projects. There just has never been a great out-of-the-box developer experience for managing themes on Shopify.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>An interview by Sam Waller for Nativve</title>
		<link href="https://mikefallows.com/posts/an-interview-by-sam-waller-for-nativve/"/>
		<updated>Thu, 15 Jul 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/an-interview-by-sam-waller-for-nativve/</id>
		<content type="html">&lt;h2&gt;Building the information super-highway&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;The internet… it’s pretty big these days, isn’t it? The so-called information superhighway has gone from being a quiet country lane to a fully-fledged six-lane autobahn, and is showing no signs of slowing down.&lt;/p&gt;
&lt;p&gt;But this rapid development didn’t just happen on its own—behind all those fancy websites there’s a slew of hard-working web designers, building the brave new world one line of code at a time. Mike Fallows is one of them.&lt;/p&gt;
&lt;p&gt;We met up with him in the physical world to discuss the place they call cyberspace.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Read the &lt;a href=&quot;https://www.nativve.com/people/an-interview-with-web-designer-mike-fallows/&quot;&gt;full interview on nativve.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Photos and words by &lt;a href=&quot;https://blog.thecentrallibrary.com/&quot;&gt;Sam Waller&lt;/a&gt;.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Quickly export Shopify data to a CSV</title>
		<link href="https://mikefallows.com/posts/quickly-export-shopify-data-to-a-csv/"/>
		<updated>Mon, 12 Jul 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/quickly-export-shopify-data-to-a-csv/</id>
		<content type="html">&lt;p&gt;Shopify provides some simple tools to export your data from the admin. There are also plenty of apps available that enable you to manage complex data exports.&lt;/p&gt;
&lt;p&gt;But sometimes I just want to export some data into a CSV file, either from a resource that Shopify doesn&#39;t expose in the admin or that includes a field like the resource ID, that&#39;s not present in the standard exports. It would be nice to be able to do it without the cost or hassle of installing an app.&lt;/p&gt;
&lt;p&gt;I&#39;ve found a couple of simple scripts that have been published that do something similar, but they were either unmaintained and needed updating or required you to hardcode values to configure the resources you wanted to export.&lt;/p&gt;
&lt;p&gt;So I decided this would be a good excuse to try and learn some more &lt;a href=&quot;https://nodejs.org/&quot;&gt;Node.js&lt;/a&gt; basics and leverage a handy &lt;a href=&quot;https://www.npmjs.com/package/shopify-api-node&quot;&gt;Node wrapper for the Shopify API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My end goal was to have a script where I could just set up some API access credentials, plug some variables into a &lt;code&gt;.env&lt;/code&gt; file and quickly kick out a CSV from the command line. Referencing some of the existing implementations, I surprised myself by putting something together that did that in less than an hour and fewer than 100 lines of code.&lt;/p&gt;
&lt;p&gt;It made me realise how easy and accessible using Node is for running small tasks locally where previously I might have used a shell script or full-blown app.&lt;/p&gt;
&lt;p&gt;You can check out and use my simple &lt;a href=&quot;https://github.com/mikenewbuild/shopify-csv-export&quot;&gt;Shopify CSV Exports&lt;/a&gt; script on GitHub.&lt;/p&gt;
</content>
	</entry>
	
	<entry>
		<title>Making this website</title>
		<link href="https://mikefallows.com/posts/making-this-website/"/>
		<updated>Sun, 11 Jul 2021 00:00:00 GMT</updated>
		<id>https://mikefallows.com/posts/making-this-website/</id>
		<content type="html">&lt;p&gt;I have planned to have a personal site for a long time and in this (my first!) post I&#39;m going to cover how I selected the technology used to build this site and why.&lt;/p&gt;
&lt;p&gt;The main roadblock to having my own site over the years has been a mixture of &amp;quot;analysis paralysis&amp;quot;, the commitment to maintaining it (cobbler&#39;s shoes) and for a large part the fear of it being - well - a bit rubbish.&lt;/p&gt;
&lt;p&gt;I&#39;m not a great writer, nor prolific, but I do document a lot of thoughts in emails. Email can be a great resource to dig into, but I hope a personal site of writing will serve as a way to distil those thoughts that are more important to me. I also want somewhere I can experiment freely, without the commercial considerations of professional work.&lt;/p&gt;
&lt;p&gt;Left to my own devices, I&#39;m pretty sure I would have put it off indefinitely. But as is so often the way, a casual conversation with &lt;a href=&quot;https://kierankelly.net/&quot;&gt;a friend&lt;/a&gt; turned out to be the motivation I needed.&lt;/p&gt;
&lt;p&gt;Thinking about what would be the best solution for him, helped me better consider what &lt;em&gt;I&lt;/em&gt; would want. I wanted something that would be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mikefallows.com/posts/making-this-website/#low-friction-to-update&quot;&gt;low friction to update&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mikefallows.com/posts/making-this-website/#low-cost&quot;&gt;low cost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mikefallows.com/posts/making-this-website/#portable&quot;&gt;portable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mikefallows.com/posts/making-this-website/#easy-to-maintain&quot;&gt;easy to maintain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mikefallows.com/posts/making-this-website/#fun-to-tinker-with&quot;&gt;fun to tinker with&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Low friction to update&lt;/h3&gt;
&lt;p&gt;I don&#39;t have a habit of writing a blog, and I know enough about myself that any substantial barrier to publishing new content will result in failure. I want it to be as easy as copy-pasting an email I&#39;ve written. I want to be able to write in &lt;a href=&quot;https://en.wikipedia.org/wiki/Markdown&quot;&gt;Markdown&lt;/a&gt;, and I don&#39;t want an editor that&#39;s going to start injecting cruft into the document. I want to be able to write directly in my code editor if it suits me and other times be able to use a GUI if I want to and still have version control.&lt;/p&gt;
&lt;h3&gt;Low cost&lt;/h3&gt;
&lt;p&gt;These days a few static pages of a personal blog needn&#39;t come with running costs to host. I don&#39;t want the &amp;quot;gym subscription&amp;quot; effect that contributes to feeling guilty about not writing more often. I&#39;ve had such a good experience with Netlify hosting simple landing pages, I was looking for something easy to host there, but not necessarily tied to that service.&lt;/p&gt;
&lt;h3&gt;Portable&lt;/h3&gt;
&lt;p&gt;This basically means the freedom to switch tools easily whenever I need, or want to, or even archive the whole thing if I change my mind. I don&#39;t want an all-in-one solution, but pieces that can easily be swapped out for building, editing, hosting, etc.&lt;/p&gt;
&lt;h3&gt;Easy to maintain&lt;/h3&gt;
&lt;p&gt;I don&#39;t want to commit to maintaining a server and the evolving languages or database versions that come with that choice. I want to be able to have purple patches where I can work on it a lot, but also have the freedom to just leave it alone for long stretches.&lt;/p&gt;
&lt;h3&gt;Fun to tinker with&lt;/h3&gt;
&lt;p&gt;Part of the appeal of having a personal site is having a place where I can just try things out (in public). I&#39;m fortunate enough to have clients with a lot of confidence in me, so I get plenty of creative opportunities in my professional work. Yet there are still times I want to be able to quickly try out a new idea free from commercial constraints, but may also serve as a demonstration of an idea that I can share when necessary.&lt;/p&gt;
&lt;h3&gt;Picking a stack&lt;/h3&gt;
&lt;p&gt;In 2021 there are a lot of options to choose from: headless CMS providers like Contentful or Sanity; hosted platforms like Tumblr or Substack; self-hosting a Wordpress or Ghost site. But these fail on either having to maintain a server, being tied to a service, being restricted in how much you can customise them, potentially incurring too high a financial cost or just subjectively being unappealing.&lt;/p&gt;
&lt;p&gt;It would also be true to say that my experience with Netlify was a huge influence. I&#39;ve been so impressed with it that it is my first consideration when hosting any static sites.&lt;/p&gt;
&lt;p&gt;So this steered me towards static site generators. I already had some experience building client sites with tools like &lt;a href=&quot;https://jigsaw.tighten.co/&quot;&gt;Jigsaw&lt;/a&gt; (PHP), &lt;a href=&quot;https://www.gatsbyjs.com/&quot;&gt;Gatsby&lt;/a&gt; (React) and &lt;a href=&quot;https://routify.dev/&quot;&gt;Routify&lt;/a&gt; (Svelte). I had success with all of them, and consider them great tools. I also considered &lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt; (Go) and &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; (Ruby), but in the end, I felt like they all required either too much code out of the box, were subject to change, or meant committing to a single language.&lt;/p&gt;
&lt;p&gt;I had been aware of Eleventy being a relative newcomer and had read some glowing reviews but still wasn&#39;t sure I &amp;quot;got it&amp;quot;. Then I watched the creator, &lt;a href=&quot;https://www.zachleat.com/&quot;&gt;Zach Leatherman&lt;/a&gt;, demo it and its simplicity frankly blew me away. It felt easy to grasp, very flexible and lightweight and I was excited to try it out. You could literally start your site from a single file with zero config. It also supported Liquid syntax which I use regularly on Shopify themes so the learning curve was much less steep.&lt;/p&gt;
&lt;p&gt;I could see that it was simple to deploy to Netlify (and others) via Git, and had support for Netlify CMS (which I&#39;d used) as well as &lt;s&gt;Forestry&lt;/s&gt;[^1] TinaCMS[^2] which I was interested to try out.&lt;/p&gt;
&lt;p&gt;So &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt; it is, with &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; and &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;[^1]: Update: I went with &lt;a href=&quot;https://forestry.io/&quot;&gt;Forestry&lt;/a&gt; and I can recommend it.
[^2]: Further update: Forestry is now &lt;a href=&quot;https://tina.io/&quot;&gt;TinaCMS&lt;/a&gt; and still recommending it.&lt;/p&gt;
</content>
	</entry>
</feed>
